Proper Quoting for Command Substitution in Bash

bashcommand-substitutionquoting

It's high time to solve this conundrum that's been bothering me for years…

I've been meeting this from time to time and thought this is the way to go:

$(comm "$(arg)")

And thought my view was strongly supported by experience. But I'm not so sure anymore. Shellcheck can't make up its mind too. It's both:

"$(dirname $0)"/stop.bash
           ^-- SC2086: Double quote to prevent globbing and word splitting.

And:

$(dirname "$0")/stop.bash
^-- SC2046: Quote this to prevent word splitting.

What's the logic behind?

(It's Shellcheck version 0.4.4, btw.)

Best Answer

You need to use "$(somecmd "$file")".

Without the quotes, a path with a space will be split in the argument to somecmd, and it will target the wrong file. So you need quotes on the inside.

Any spaces in the output of somecmd will also cause splitting, so you need quotes on the outside of the whole command substitution.

Quotes inside the command substitution have no effect on the quotes outside of it. Bash's own reference manual isn't too clear on this, but BashGuide explicitly mentions it. The text in POSIX also requires it, as "any valid shell script" is allowed inside $(...):

With the $(command) form, all characters following the open parenthesis to the matching closing parenthesis constitute the command. Any valid shell script can be used for command, except a script consisting solely of redirections which produces unspecified results.


Example:

$ file="./space here/foo"

a. No quotes, dirname processes both ./space and here/foo:

$ printf "<%s>\n" $(dirname $file)
<.>
<here>

b. Quotes inside, dirname processes ./space here/foo, giving ./space here, which is split in two:

$ printf "<%s>\n" $(dirname "$file")
<./space>
<here>

c. Quotes outside, dirname processes both ./space and here/foo, outputs on separate lines, but now the two lines form a single argument:

$ printf "<%s>\n" "$(dirname $file)"
<.
here>

d. Quotes both inside and outside, this gives the correct answer:

$ printf "<%s>\n" "$(dirname "$file")"
<./space here>

(that would possibly have been simpler if dirname only processed the first argument, but that wouldn't show the difference between cases a and c.)

Note that with dirname (and possibly others) you also need want to add --, to prevent the filename from being taken as an option in case it happens to start with a dash, so use "$(dirname -- "$file")".

Related Question