15

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.)

  • 1
    Quote all substitutions. – Ignacio Vazquez-Abrams May 15 '18 at 18:20
  • 3
    Just like this: "$(dirname "$0")"/stop.bash? It seems to work... What's the story? – Tomasz May 15 '18 at 18:22
  • 1
    bash is smart enough to know when the context has changed. – Ignacio Vazquez-Abrams May 15 '18 at 18:22
  • 1
    think about it like this : shell_level_1 $(shell_level_2) shell_level_1 : when you are inside the $(....) you enter a "sublevel" of shell, BUT you can write in it as if you were on primary level (ie, you can directly write " instead of \" , etc). ex: touch "/tmp/a file" ; echo "its size is: $(find "/tmp/a file" -ls | awk '{print $5}) ..." : if you used backticks you'd have to find \"/tmp/a file\"` and print \$5. With $(...) no need: the shell adapts to the new level and you can write directly as if your interpreter now is at that level too. – Olivier Dulac May 16 '18 at 14:38
  • "Shellcheck can't make up its mind too." - The two runs have two different messages and point at different places. It wants both. – Izkata May 16 '18 at 16:57
44

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")".

  • 16
    Note: In general, quotes don't nest in bash; but in this case, the $( ) creates a new context, so the quotes inside it are independent of the quotes outside it. And you generally want quotes in both contexts. – Gordon Davisson May 15 '18 at 18:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.