What's the right way to quote $(command $arg)?

6,208

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

Share:
6,208

Related videos on Youtube

Admin
Author by

Admin

Updated on September 18, 2022

Comments

  • Admin
    Admin almost 2 years

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

    • Admin
      Admin about 6 years
      Quote all substitutions.
    • Admin
      Admin about 6 years
      Just like this: "$(dirname "$0")"/stop.bash? It seems to work... What's the story?
    • Admin
      Admin about 6 years
      bash is smart enough to know when the context has changed.
    • Admin
      Admin about 6 years
      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.
    • Admin
      Admin about 6 years
      "Shellcheck can't make up its mind too." - The two runs have two different messages and point at different places. It wants both.
  • javaamtho
    javaamtho about 6 years
    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.