When they are not quoted, $*
and $@
are the same. You shouldn't use either of these, because they can break unexpectedly as soon as you have arguments containing spaces or wildcards.
"$*"
expands to a single word "$1c$2c..."
. c
was a space in the Bourne shell but is now the first character of IFS
in modern Bourne-like shells (from ksh and specified by POSIX for sh), so it can be anything¹ you choose.
The only good use I've ever found for it is:
join arguments with comma (simple version)
function join1 {
typeset IFS=, # typeset makes a local variable in ksh²
print -r -- "$*" # using print instead of unreliable echo³
}
join1 a b c # => a,b,c
join arguments with the specified delimiter (better version)
function join2 {
typeset IFS="$1"
shift
print -r -- "$*"
}
join2 + a b c # => a+b+c
"$@"
expands to separate words: "$1"
"$2"
...
This is almost always what you want. It expands each positional parameter to a separate word, which makes it perfect for taking command line or function arguments in and then passing them on to another command or function. And because it expands using double quotes, it means things don't break if, say, "$1"
contains a space or an asterisk (*
)4.
Let's write a script called svim
that runs vim
with sudo
. We'll do three versions to illustrate the difference.
svim1
#!/bin/sh
sudo vim $*
svim2
#!/bin/sh
sudo vim "$*"
svim3
#!/bin/sh
sudo vim "$@"
All of them will be fine for simple cases, e.g. a single file name that doesn't contain spaces:
svim1 foo.txt # == sudo vim foo.txt
svim2 foo.txt # == sudo vim "foo.txt"
svim2 foo.txt # == sudo vim "foo.txt"
But only $*
and "$@"
work properly if you have multiple arguments.
svim1 foo.txt bar.txt # == sudo vim foo.txt bar.txt
svim2 foo.txt bar.txt # == sudo vim "foo.txt bar.txt" # one file name!
svim3 foo.txt bar.txt # == sudo vim "foo.txt" "bar.txt"
And only "$*"
and "$@"
work properly if you have arguments containing spaces.
svim1 "shopping list.txt" # == sudo vim shopping list.txt # two file names!
svim2 "shopping list.txt" # == sudo vim "shopping list.txt"
svim3 "shopping list.txt" # == sudo vim "shopping list.txt"
So only "$@"
will work properly all the time.
¹ though beware in some shells, it doesn't work for multibyte characters.
² typeset
which is used to set types and attributes of variables also makes a variable local in ksh
4 (in ksh93, that's only for functions defined with the Korn function f {}
syntax, not the Bourne f() ...
syntax ). It means here IFS
will be restored to its previous value when the function returns. This is important, because the commands you run afterward might not work as expected if IFS
is set to something non-standard and you forgot to quote some expansions.
³ echo
will or may fail to print its arguments properly if the first starts with -
or any contain backslashes, print
can be told not to do backslash processing with -r
and to guard against argument starting with -
or +
with the --
(or -
) option delimiter. printf '%s\n' "$*"
would be the standard alternative but note that ksh88 and pdksh and some of its derivatives still don't have printf
builtin.
4 Note that "$@"
didn't work properly in the Bourne shell and ksh88 when $IFS
didn't contain the space character, as effectively it was implemented as the positional parameter being joined with "unquoted" spaces and the result subject to $IFS
splitting. Early versions of the Bourne shell also had that bug that "$@"
was expanded to one empty argument when there was no positional parameter, which is one of the reasons why you sometimes see ${1+"$@"}
in place of "$@"
. Neither of those bugs affect modern Bourne-like shells.
5 The Almquist shell and bosh
have local
for that instead. bash
, yash
and zsh
also have typeset
, aliased to local
(also to declare
in bash and zsh) with the caveat that in bash
, local
can only be used in a function.