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
in="$1"
out="${in%.md}.html"
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 ME.md' -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
in="$1"
out="${in%.md}.html"
cmd="pandoc \"${in}\" -o \"${out}\""
echo "$in" | entr "$cmd"
(shellcheck.net 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 ME.md" -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 ME.md" -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?
Check that
$cmdis being formed as I expect? If Iecho "$cmd"right after it is defined in script 2, it looks exactly how I'd hope:pandoc "READ ME.md" -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.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$cmdis a step in the right direction?echo "$in" | entr $cmdPredictably enough though, this splits
$cmdup on every space, even the ones inside our double-quotes:+ entr pandoc '"READ' 'ME.md"' -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)Try constructing
$cmdusingprintf?I notice
printf -vcan store output in a variable. How about using that instead of assiging tocmd?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
%qin the format string, or using$inand$outdirectly in the format string, but didn't stumble on anything that seemed to help.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@Qis for.+ entr ''\''pandoc' '"READ' 'ME.md"' -o '"READ' 'ME.html"'\''' entr: exec 'pandoc: No such file or directory
Details
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$cmdinto an array:This correctly quotes the items in
${cmd[@]}which require it (e.g. have spaces in.)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.