Argument Handling - OR: What's the deal with $* and $@?

Every once in a while, people come around with code like this:

foo() { command foo $*; }

…or…

foo() { command foo $@; }

And what we'll tell them is to just use:

foo() { command foo "$@"; }

until they understand the subtle differences between $*, "$*", $@ and "$@" are. And when they do, they will see that "$@" is actually the correct expression to use when they want to process all arguments that the script/function was given.

Finding out what these subtle differences are can be a little hard if you don't know what you're looking for, so let's take a look at some code to see what's going on.

function f0() { local i; print -- "f0 (\$@ quoted): $#"; for i in "$@"; do print -- \'"$i"\'; done; }
function f1() { local i; print -- "f1 (\@ unquoted): $#"; for i in $@; do print -- \'"$i"\'; done; }
function f2() { local i; print -- "f2 (\$* quoted): $#"; for i in "$*"; do print -- \'"$i"\'; done; }
function f3() { local i; print -- "f3 (\$* unquoted): $#"; for i in $*; do print -- \'"$i"\'; done; }

So there, four functions. One for each way to use these special variables - quoted and unquoted.

And now call them with a number of arguments. With default zsh options set. In particular, sh_word_split isn't set. Also, look at what happens to the empty argument.

f0 foo " " "" "bar baz" "z  m"
print -- --------------------------------------
f1 foo " " "" "bar baz" "z  m"
print -- --------------------------------------
f2 foo " " "" "bar baz" "z  m"
print -- --------------------------------------
f3 foo " " "" "bar baz" "z  m"

And here's the output.

f0 ($@ quoted): 5
'foo'
' '
''
'bar baz'
'z  m'
--------------------------------------
f1 ($@ unquoted): 5
'foo'
' '
'bar baz'
'z  m'
--------------------------------------
f2 ($* quoted): 5
'foo    bar baz z  m'
--------------------------------------
f3 ($* unquoted): 5
'foo'
' '
'bar baz'
'z  m'

So, can you spot the expression, that got it right? - Right.

Now let's see what happens, when we call those functions without arguments. You can imagine the code. Here is the output:

f0 ($@ quoted): 0
f1 ($@ unquoted): 0
f2 ($* quoted): 0
''
f3 ($* unquoted): 0

So, yeah. "$*" always expands to something, even if there were no arguments given.

Above using $* $@ unquoted looks the same. They both missed the empty argument, but they look the same. Now, let's turn on the sh_word_split option and see how that turns out…

setopt shwordsplit
f0 foo " " "" "bar baz" "z  m"
print -- --------------------------------------
f1 foo " " "" "bar baz" "z  m"
print -- --------------------------------------
f2 foo " " "" "bar baz" "z  m"
print -- --------------------------------------
f3 foo " " "" "bar baz" "z  m"

Here's the output:

f0 ($@ quoted): 5
'foo'
' '
''
'bar baz'
'z  m'
--------------------------------------
f1 ($@ unquoted): 5
'foo'
'bar'
'baz'
'z'
'm'
--------------------------------------
f2 ($* quoted): 5
'foo    bar baz z  m'
--------------------------------------
f3 ($* unquoted): 5
'foo'
'bar'
'baz'
'z'
'm'

Yeah, yeah. We all already know who won the game. We also see the effects of sh_word_split on the unquoted versions of our functions. Really not what you'd usually want to when you're processing arguments, is it?

As I said the differences can be subtle. For zsh it even depends on option settings. What always gets it right is "$@". If you use another version, which you sometimes need to, you should really try to understand what's happening above.

If you care what happens if you run the functions with sh_word_split set, and no arguments… The same as with the option off. Don't believe me? Try it.

And for zsh that's basically the story. If you're using other shells, too, or you're generally concerned about shell portability, you may want to take a look at this posting by Stéphane Chazelas in comp.unix.shell, where Stéphane plays more games with $* and $@ in a number of different shells.

And finally, Sven Mascheck - on his excellent page about bourne family shells - tells us why in some scripts ${1+"$@"} is used instead of just "$@" (preview: the reason are very old versions of traditional bourne shells that expand "$@" weirdly).

 
scripting/args.txt · Last modified: 2010/01/05 09:20 (external edit)