Bash – Unexpected outcome of a=“$@”

arrayassignmentbashshellcheckstring

I'm struggling with this situation:

$ set -- 1 2 3
$ a="$@"
$ echo "$a"
1 2 3

What I find unexpected is the assignment itself.

man bash says this about the "$@" expansion:

When the expansion occurs within double quotes, each parameter expands
to a separate word.

So this should be analogous to:

b="1" "2" "3"
bash: 2: command not found

And that's what the "$*" expansion is for, I gather:

When the expansion occurs within double quotes, it expands to a single
word with the value of each parameter separated by the first
character of the IFS special variable. That is, "$*" is equivalent to
"$1c$2c…", where c is the first character of the value of the IFS
variable. If IFS is unset, the parameters are separated by spaces. If
IFS is null, the parameters are joined without intervening
separators.

And so this should be correct:

$ set -- 1 2 3
$ a="$*"
$ echo "$a"
1 2 3

So why does "$@" yield the same? They're supposed to differ in this point. Is this a Bash problem or my misunderstanding?

Shellcheck detects this as SC2124. I can also provide an example that triggers SC2145.

It's observed on:

GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
4.9.0-6-amd64 #1 SMP Debian 4.9.82-1+deb9u3 (2018-03-02) x86_64 GNU/Linux

Best Answer

As far as I can tell, POSIX leaves $@ in an assignment undefined, so it's not really a bug, except maybe in Bash's documentation. $@ is defined in two cases:

  • When the expansion occurs in a context where field splitting will be performed...
  • When the expansion occurs within double-quotes, the behavior is unspecified unless [...] Field splitting would be performed [...] (*)

But,

In all other contexts the results of the expansion are unspecified.

Field splitting doesn't happen in an assignment, and wouldn't happen even without the double quotes, so that's undefined.


Now, I assume that a="$@" acts about in the same way as a="$*" because expanding to multiple words wouldn't really make any sense here. You can't assign multiple words to a regular variable as distinct entities, and assigning one but using the rest as command arguments would be confusing and prone to bugs.

As that shellcheck page says, the behaviour of "$@" in an assignment differs between shells. Bash and ksh join the positional parameters with spaces, zsh and dash with the first letter of IFS (exactly like "$*" would do).

$ bash -c 'set -- x y z; IFS=.; a="$@"; printf "<%s>\n" "$a"'
<x y z>
$ ksh93 -c 'set -- x y z; IFS=.; a="$@"; printf "<%s>\n" "$a"'
<x y z>
$ zsh -c 'set -- x y z; IFS=.; a="$@"; printf "<%s>\n" "$a"'
<x.y.z>
$ dash -c 'set -- x y z; IFS=.; a="$@"; printf "<%s>\n" "$a"'
<x.y.z>

It's probably best to use a="$*" if you want to join to a single string, or otherwise explicitly write what you want.

(* or another case involving ${parameter:-word} expansions)

Related Question