Why is unquoted $@ not a safe way to forward shell-script arguments, and what specifically makes "$@" different from "$…
Why is unquoted $@ not a safe way to forward shell-script arguments, and what specifically makes "$@" different from "$*"?
Why is unquoted $@ not a safe way to forward shell-script arguments, and what specifically makes "$@" different from "$*"?
Unquoted $@ is unsafe because, after it expands to your arguments, the shell still performs word splitting and pathname (glob) expansion on the result. So any argument that contains whitespace gets torn into multiple arguments, and any argument containing *, ?, or [...] may be replaced by matching filenames. The fix is to always quote it as "$@", which is the one construct that reproduces the original argument list exactly. "$*" is not a substitute: it collapses every argument into a single string.
For positional parameters 1 2 3 where, say, $1 is a b (contains a space):
| Form | Result | What happens |
|---|---|---|
$@ (unquoted) |
a b 2 3 |
expands to the args, then word-splits and globs each one |
$* (unquoted) |
a b 2 3 |
identical to unquoted $@ |
"$*" |
a b 2 3 (one arg) |
joins all args into a single field |
"$@" |
a b 2 3 |
each arg stays a separate field; boundaries preserved |
So unquoted $@ and unquoted $* are interchangeable and both broken for forwarding. The meaningful distinction only appears inside double quotes.
Per POSIX (and Bash matches this):
* — "When the expansion occurs in a context where field splitting will not be performed [i.e. inside double quotes], the initial fields shall be joined to form a single field with the value of each parameter separated by the first character of the IFS variable" (a space if IFS is unset, no separator if IFS is null). That is why "$*" produces one word.@ — inside double quotes, "the initial fields shall be retained as separate fields." So "$@" is equivalent to "$1" "$2" "$3" … — each argument is its own quoted word, with no splitting and no globbing applied. This is the only form that round-trips an arbitrary argument list.Two consequences worth knowing:
IFS affects "$*" but not "$@". Because "$*" joins on the first character of IFS, changing IFS changes its output; "$@" is immune. (Unquoted, both are subject to splitting by IFS afterward.)'@' shall generate zero fields, even when '@' is within double-quotes." So "$@" correctly disappears entirely when there are no args. "$*" expands to a single empty string. This is exactly why cmd "$@" is safe with zero arguments while older idioms like cmd "$*" or cmd ${1+"$@"} workarounds were once needed for buggy shells.The canonical wrapper:
#!/bin/sh
# forward every argument, unchanged, to the real command
exec realcmd "$@"
Run as wrapper "file with spaces.txt" '*.log', this passes exactly two arguments through. With $@ unquoted, file with spaces.txt becomes three args and *.log is glob-expanded against the current directory. With "$*", the whole thing becomes the single argument file with spaces.txt *.log. Only "$@" preserves the count, the embedded spaces, and the literal glob characters.
Rule of thumb: to forward or iterate over arguments, use "$@" and nothing else. Reach for "$*" only when you deliberately want a single joined string (e.g. building a message), and remember it joins on IFS[0].
Sources: