Bash – Quoting Within $(command substitution)

bashcommand-substitutionquotingshell

In my Bash environment I use variables containing spaces, and I use these variables within command substitution.

What is the correct way to quote my variables? And how should I do it if these are nested?

DIRNAME=$(dirname "$FILE")

or do I quote outside the substitution?

DIRNAME="$(dirname $FILE)"

or both?

DIRNAME="$(dirname "$FILE")"

or do I use back-ticks?

DIRNAME=`dirname "$FILE"`

What is the right way to do this? And how can I easily check if the quotes are set right?

Best Answer

In order from worst to best:

  • DIRNAME="$(dirname $FILE)" will not do what you want if $FILE contains whitespace (or whatever characters $IFS currently contains) or globbing characters \[?*.
  • DIRNAME=`dirname "$FILE"` is technically correct, but backticks are not recommended for command expansion because of the extra complexity when nesting them and the extra backslash processing that happens within them.
  • DIRNAME=$(dirname "$FILE") is correct, but only because this is an assignment to a scalar (not array) variable. If you use the command substitution in any other context, such as export DIRNAME=$(dirname "$FILE") or du $(dirname -- "$FILE"), the lack of quotes will cause trouble if the result of the expansion contain whitespace or globbing characters.
  • DIRNAME="$(dirname "$FILE")" (except for the missing --, see below) is the recommended way. You can replace DIRNAME= with a command and a space without changing anything else, and dirname receives the correct string.

To improve even further:

  • DIRNAME="$(dirname -- "$FILE")" works if $FILE starts with a dash.
  • DIRNAME="$(dirname -- "$FILE" && printf x)" && DIRNAME="${DIRNAME%?x}" || exit works even if $FILE's dirname ends with a newline, since $() chops off newlines at the end of output, both the one added by dirname and the ones that may be part of the actual data.

You can nest command expansions as much as you like. With $() you always create a new quoting context, so you can do things like this:

foo "$(bar "$(baz "$(ban "bla")")")"

You do not want to try that with backticks.

Related Question