I was really surprised that a rather large amount of online tutorials or comments post code similar to this: dir=$(dirname $0) or dir=$(dirname $(readlink -f $0)).

On the surface, this code looks good, but there is a problem with it: what if $0 or the full path to $0 has a space in it? This raises the more general question for command substitution: what if the output of the command substitution (the $(...) or `...`) contains whitespaces, newlines or patterns?

Let's look at the POSIX1 specification for command substitution2 to find the answer:

The shell shall expand the command substitution by executing command in a subshell environment (see Shell Execution Environment) and replacing the command substitution (the text of command plus the enclosing "$()" or backquotes) with the standard output of the command, removing sequences of one or more characters at the end of the substitution.

In other words, the subcommand will be executed in a subshell, the output will be cleared of any trailing newlines and then it will replace the command substitution in the original command. This means that if the output has spaces and tabs it will be split into multiple tokens (arguments). In some cases this is the desired behavior, but most of the time it isn't.

Suppose that we are inside a directory that has some spaces in its full path:

d='/tmp/xyz/foo bar/baz bar'
mkdir -p "$d" && cd "$d"
bash -c 'for arg; do echo $arg; done' bash $(pwd)

The third line it's just a way of printing each argument on a separate line. So what do you thing it will print? It will just print:


which means that the string /tmp/xyz/foo bar/baz bar replaced the command substitution $(pwd) and was split and passed as three arguments.

To solve the problem, the subcommands must always be placed inside double quotes. The POSIX 7 1 specification says:

If a command substitution occurs inside double-quotes, field splitting and pathname expansion shall not be performed on the results of the substitution.

So the first two code snippets could be rewritten to be more robust:

dir="$(dirname "$0")"
dir="$(dirname "$(readlink -f "$0")")"