How to pass a Bash command to `entr`, quoting to guard against filenames with spaces?

My Goal

I'm writing a small Bash script, which uses entr, which is a utility to re-run arbitrary commands when it detects file-system events. My immediate goal is to pass entr a command which converts a given markdown file to HTML. entr will run this command every time the markdown file changes. A simplified but working script looks like:

# script 1
echo "$in" | entr pandoc "${in}" -o "${out}"

This works fine. The filename to be watched is supplied to entr on stdin. On detecting changes in that file, entr runs the command specified by its args. In this example that is pandoc, and all the args after it, to convert the markdown file to an HTML file.

For future reference, set -x shows that entr was invoked as we'd expect. (Throughout, lines starting with + show the output from set -x):

+ entr pandoc 'READ' -o 'READ ME.html'

The problem

I want to look-up the command given to entr depending on the file-type of the given input file. So the file-conversion command ends up in a variable, and I want to use that variable as the command-line args to entr. But I can't get the quoting right. Again, simplified:

# script 2
cmd="pandoc \"${in}\" -o \"${out}\""
echo "$in" | entr "$cmd"

( detects no issues on the above)

This fails. Because "$cmd" in the final line is in quotes, the entirety of $cmd is treated as a single arg to entr:

+ entr 'pandoc "READ" -o "READ ME.html"'

entr tries to interpret the whole thing as the name of an executable, which it cannot find:

entr: exec pandoc "READ" -o "READ ME.html": No such file or directory

So how should I modify script 2, to use the content of $cmd as the args to entr?

What have I tried?

  1. Check that $cmd is being formed as I expect? If I echo "$cmd" right after it is defined in script 2, it looks exactly how I'd hope:

    pandoc "READ" -o "READ ME.html"

    I tried messing around with alternate ways of constructing cmd, such as:

    cmd='pandoc "'"${in}"'" -o "'"${out}"'"'

    but variations like this produce identical values of $cmd, and identical behavior as script2.

  2. Try not quoting the use of $cmd?

    Since the final line of script 2 erroneously treats the whole of "$cmd" as a single arg, and we want it to split up the words into seprate args instead, maybe removing the quotes and using a bare $cmd is a step in the right direction?

    echo "$in" | entr $cmd

    Predictably enough though, this splits $cmd up on every space, even the ones inside our double-quotes:

    + entr pandoc '"READ' '"' -o '"READ' 'ME.html"'

    This makes Pandoc try, and fail, to open a file called "READ:

    pandoc: "READ: openBinaryFile: does not exist (No such file or directory)
  3. Try constructing $cmd using printf?

    I notice printf -v can store output in a variable. How about using that instead of assiging to cmd?

    printf -v cmd 'pandoc "%s" -o "%s"' "$in" "$out"

    Predictably enough, this produces the same results as script2. I tried some speculative variations, such as %q in the format string, or using $in and $out directly in the format string, but didn't stumble on anything that seemed to help.

  4. Try using the ${var@Q} form of parameter expansion.

    echo "$in" | entr ${cmd@Q}

    Tried with and without double quotes around the use of ${cmd@q}. No joy, I guess I'm misunderstanding what @Q is for.

    + entr ''\''pandoc' '"READ' '"' -o '"READ' 'ME.html"'\'''
    entr: exec 'pandoc: No such file or directory


I'm using Bash v5.1.16, in Pop!_OS 22.04, derived from Ubuntu 22.04 (Jammy).

The current 'apt' version of entr (v5.1) in Ubuntu Jammy (22.04) is too old for my needs (e.g. the -z flag doesn't work.) so I'm compiling my own from the latest v5.3 source release.

I know there are a lot of questions about quoting in Bash, but I don't see any that seem to match this. Apologies if I'm wrong.


  • Assemble the command as an array, instead of a string.

    I read somewhere that maybe $@ might do what I need, so I put the parts of $cmd into an array:

    cmd=(pandoc "$in" -o "$out")
    echo "$in" | entr "${cmd[@]}"

    This correctly quotes the items in ${cmd[@]} which require it (e.g. have spaces in.)

    + entr pandoc 'READ' -o 'READ ME.html'

    So ‘entr’ successfully calls ‘pandoc’, which successfully converts the documents. It works! I confess I did not expect that.

    This approach seems viable for other similar situations, not just when invoking entr.

    So I have a solution. It doesn't seem completely ideal for my future plans. I had visions of these 'file conversion commands' being configurable, and hence defined in a text file somewhere, so that users (==me, probably) could override them and define their own, and I'm not fluent enough with Bash to be sure how to go about that when commands are defined as arrays instead of strings.

    I can't help but feel I've overlooked something simpler.