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'
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
?
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 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 $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' '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 $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.
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' 'ME.md"' -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:
in="$1"
out="${in%.md}.html"
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 ME.md' -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.