146

Consider the following code:

foo () {
    echo $*
}

bar () {
    echo $@
}

foo 1 2 3 4
bar 1 2 3 4

It outputs:

1 2 3 4

1 2 3 4

I am using Ksh88, but I am interested in other common shells as well. If you happen to know any particularity for specific shells, please do mention them.

I found the follwing in the Ksh man page on Solaris:

The meaning of $* and $@ is identical when not quoted or when used as a parameter assignment value or as a file name. However, when used as a command argument, $* is equivalent to ``$1d$2d...'', where d is the first character of the IFS variable, whereas $@ is equivalent to $1 $2 ....

I tried modifying the IFS variable, but it doesn't modify the output. Maybe I'm doing something wrong?

0

11 Answers 11

171

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 ksh4 (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.

1
50

Short answer: use "$@" (note the double quotes). The other forms are very rarely useful.

"$@" is a rather strange syntax. It is replaced by all the positional parameters, as separate fields. If there are no positional parameters ($# is 0), then "$@" expands to nothing (not an empty string, but a list with 0 elements), if there is one positional parameter then "$@" is equivalent to "$1", if there are two positional parameters then "$@" is equivalent to "$1" "$2", etc.

"$@" allows you to pass down the arguments of a script or function to another command. It is very useful for wrappers that do things like setting environment variables, preparing data files, etc. before calling a command with the same arguments and options that the wrapper was called with.

For example, the following function filters the output of cvs -nq update. Apart from the output filtering and the return status (which is that of grep rather than that of cvs), calling cvssm on some arguments behaves like calling cvs -nq update with these arguments.

cvssm () { cvs -nq update "$@" | egrep -v '^[?A]'; }

"$@" expands to the list of positional parameters. In shells that support arrays, there is a similar syntax to expand to the list of elements of the array: "${array[@]}" (the braces are mandatory except in zsh). Again, the double quotes are somewhat misleading: they protect against field splitting and pattern generation of the array elements, but each array element ends up in its own field.

Some ancient shells had what is arguably a bug: when there were no positional arguments, "$@" expanded to a single field containing an empty string, rather than into no field. This led to the workaround ${1+"$@"} (made famous via the Perl documentation). Only older versions of the actual Bourne shell and the OSF1 implementation are affected, none of its modern compatible replacements (ash, ksh, bash, …) are. /bin/sh is not affected on any system that was released in the 21st century that I know of (unless you count Tru64 maintenance release, and even there /usr/xpg4/bin/sh is safe so only #!/bin/sh script are affected, not #!/usr/bin/env sh scripts as long as your PATH is set up for POSIX compliance). In short, this is a historical anecdote that you don't need to worry about.


"$*" always expands to one word. This word contains the positional parameters, concatenated with a space in between. (More generally, the separator is the first character of the value of the IFS variable. If the value of IFS is the empty string, the separator is the empty string.) If there are no positional parameters then "$*" is the empty string, if there are two positional parameters and IFS has its default value then "$*" is equivalent to "$1 $2", etc.

$@ and $* outside quotes are equivalent. They expand to the list of positional parameters, as separate fields, like "$@"; but each resulting field is then split into separate fields which are treated as file name wildcard patterns, as usual with unquoted variable expansions.

For example, if the current directory contains three files bar, baz and foo, then:

set --         # no positional parameters
for x in "$@"; do echo "$x"; done  # prints nothing
for x in "$*"; do echo "$x"; done  # prints 1 empty line
for x in $*; do echo "$x"; done    # prints nothing
set -- "b* c*" "qux"
echo "$@"      # prints `b* c* qux`
echo "$*"      # prints `b* c* qux`
echo $*        # prints `bar baz c* qux`
for x in "$@"; do echo "$x"; done  # prints 2 lines: `b* c*` and `qux`
for x in "$*"; do echo "$x"; done  # prints 1 lines: `b* c* qux`
for x in $*; do echo "$x"; done    # prints 4 lines: `bar`, `baz`, `c*` and `qux`
1
34

Here is a simple script to demonstrates the difference between $* and $@:

#!/bin/bash

test_param() {
  echo "Receive $# parameters"
  echo Using '$*'

  echo
  for param in $*; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '"$*"'
  for param in "$*"; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '$@'
  for param in $@; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '"$@"';
  for param in "$@"; do
  printf '==>%s<==\n' "$param"
  done
}

IFS="^${IFS}"

test_param 1 2 3 "a b c"

Output:

% cuonglm at ~
% bash test.sh
Receive 4 parameters

Using $*
==>1<==
==>2<==
==>3<==
==>a<==
==>b<==
==>c<==

Using "$*"
==>1^2^3^a b c<==

Using $@
==>1<==
==>2<==
==>3<==
==>a<==
==>b<==
==>c<==

Using "$@"
==>1<==
==>2<==
==>3<==
==>a b c<==

In array syntax, there is no difference when using $* or $@. It only make sense when you use them with double quotes "$*" and "$@".

3
  • Excellent example! Can you explain the use of IFS="^${IFS}", though?
    – Russ
    Sep 16, 2016 at 12:47
  • @Russ: It show you how value is concat with the first character in IFS.
    – cuonglm
    Sep 16, 2016 at 13:03
  • In the same way that IFS="^xxxxx" would do? The trailing ${IFS} suffix made me think you were doing something trickier, like somehow automatically recovering the original IFS at the end (eg: first char automatically shifted out or something).
    – Russ
    Sep 16, 2016 at 14:48
11

The code you provided will give the same result. To understand it better, try this:

foo () {
    for i in "$*"; do
        echo "$i"
    done
}

bar () {
    for i in "$@"; do
        echo "$i"
    done
}

The output should now be different. Here's what I get:

$ foo() 1 2 3 4
1 2 3 4
$ bar() 1 2 3 4
1
2
3
4

This worked for me on bash. As far as I know, ksh should not differ much. Essentially, quoting $* will treat everything as one word, and quoting $@ will treat the list as separate words, as can be seen in the example above.

As an example of using the IFS variable with $*, consider this

fooifs () {
    IFS="c"            
    for i in "$*"; do
        echo "$i"
    done
    unset IFS          # reset to the original value
}

I get this as a result:

$ fooifs 1 2 3 4
1c2c3c4

Also, I've just confirmed it works the same in ksh. Both bash and ksh tested here were under OSX but I can't see how that would matter much.

5
  • "changed within a function - doesn't affect the global". Did you test that?
    – Mikel
    Jun 25, 2012 at 15:11
  • Yes, I've checked that. For the peace of mind, I'll add the unset IFS at the end to reset it to the original, but it worked for me no problem and doing an echo $IFS resulted in the standard output I get from it. Setting the IFS withing the curly brackets introduces a new scope, so unless you export it, it will not affect the outside IFS.
    – Wojtek
    Jun 25, 2012 at 16:00
  • echo $IFS doesn't prove anything, because the shell sees the ,, but then does word splitting using IFS! Try echo "$IFS".
    – Mikel
    Jun 25, 2012 at 16:43
  • good point. unsetting IFS should solve that.
    – Wojtek
    Jun 25, 2012 at 17:39
  • Unless IFS had a different custom value before calling the function. But yes, most of the time unsetting IFS will work.
    – Mikel
    Jun 25, 2012 at 20:51
7

The difference is important when writing scripts that should use the positional parameters in the right way...

Imagine the following call:

$ myuseradd -m -c "Carlos Campderrós" ccampderros

Here there are just 4 parameters:

$1 => -m
$2 => -c
$3 => Carlos Campderrós
$4 => ccampderros

In my case, myuseradd is just a wrapper for useradd that accepts the same parameters, but adds a quota for the user:

#!/bin/bash -e

useradd "$@"
setquota -u "${!#}" 10000 11000 1000 1100

Notice the call to useradd "$@", with $@ quoted. This will respect the parameters and send them as-they-are to useradd. If you were to unquote $@ (or to use $* also unquoted), useradd would see 5 parameters, as the 3rd parameter which contained a space would be split in two:

$1 => -m
$2 => -c
$3 => Carlos
$4 => Campderrós
$5 => ccampderros

(and conversely, if you were to use "$*", useradd would only see one parameter: -m -c Carlos Campderrós ccampderros)

So, in short, if you need to work with parameters respecting multi-word parameters, use "$@".

4
   *      Expands  to  the positional parameters, starting from one.  When
          the expansion occurs within double quotes, it expands to a  sin‐
          gle word with the value of each parameter separated by the first
          character of the IFS special variable.  That is, "$*" is equiva‐
          lent to "$1c$2c...", where c is the first character of the value
          of the IFS variable.  If IFS is unset, the parameters are  sepa‐
          rated  by  spaces.   If  IFS  is null, the parameters are joined
          without intervening separators.
   @      Expands to the positional parameters, starting from  one.   When
          the  expansion  occurs  within  double  quotes,  each  parameter
          expands to a separate word.  That is, "$@" is equivalent to "$1"
          "$2"  ...   If the double-quoted expansion occurs within a word,
          the expansion of the first parameter is joined with  the  begin‐
          ning  part  of  the original word, and the expansion of the last
          parameter is joined with the last part  of  the  original  word.
          When  there  are no positional parameters, "$@" and $@ expand to
          nothing (i.e., they are removed).

// man bash . is ksh, afair, similar behaviour.

4

File Name: try

#!/bin/bash

star() {
    echo
    echo '--- $* no quotes'
    for Field in $*; do
        echo $Field
    done
}

star_quote() {
    echo
    echo '--- $* with quotes'
    for Field in "$*"; do
        echo $Field
    done
}

dollar() {
    echo
    echo '--- $@ no quotes'
    for Field in $@; do
        echo $Field
    done
}

dollar_quote() {
    echo
    echo '--- $@ with quotes'
    for Field in "$@"; do
        echo $Field
    done
}


#-----------------------

echo
star $*
star_quote "$*"
dollar $@
dollar_quote "$@"
echo

exit

Command:

./try 1 2 3 "4 and 5"

Results:

--- $* no quotes
1
2
3
4
and
5

--- $* with quotes
1 2 3 4 and 5

--- $@ no quotes
1
2
3
4
and
5

--- $@ with quotes
1
2
3
4 and 5

1
  • You should be doing echo "$Field", otherwise your test might have some surprising results with different arguments.
    – muru
    Jun 21, 2020 at 15:00
3

Talking about differences between zsh and bash:

With quotes around $@ and $*, zsh and bash behave the same, and I guess the result is quite standard among all shells:

 $ f () { for i in "$@"; do echo +"$i"+; done; }; f 'a a' 'b' ''
 +a a+
 +b+
 ++
 $ f () { for i in "$*"; do echo +"$i"+; done; }; f 'a a' 'b' ''
 +a a b +

Without quotes, results are the same for $* and $@, but different in bash and in zsh. In this case zsh shows some odd behaviour:

bash$ f () { for i in $*; do echo +"$i"+; done; }; f 'a a' 'b' ''
+a+
+a+
+b+
zsh% f () { for i in $*; do echo +"$i"+; done; }; f 'a a' 'b' ''  
+a a+
+b+

(Zsh usually don't split textual data using IFS, unless explicitly requested, but notice that here the empty argument is unexpectedly missing in the list.)

1
  • 1
    In zsh, $@ isn't special in this respect: $x expands to at most one word, but empty variables expand to nothing (not an empty word). Try print -l a $foo b with foo empty or undefined. Jun 25, 2012 at 23:27
3

"$*" has few uses, the other answers cover why.

Here's a neat example of using "$*" to just "print everything" when doing error handling:

error () {
  printf '%s\n' "$*"
  exit 1
}

This script demonstrates the difference:

% cat foo.sh 
#!/usr/bin/env bash

error1 () {
  printf '%s\n' "$*"
  #exit 1
}

error2 () {
  printf '%s\n' "$@"
  #exit 1
}

error1 "aa" "bb"
echo "---"
error2 "aa" "bb"
% ./foo.sh  
aa bb
---
aa
bb
2

One of the answers says $* (which I think of as a "splat") is rarely useful.

I search google with G() { IFS='+' ; w3m "https://encrypted.google.com/search?q=$*" ; }

Since URL’s are often split with a +, but my keyboard makes   easier to reach than +, $* + $IFS feel worthwhile.

0

Most of the time you want $@. But a use I've found... Let's say you're creating a script that creates another script (e.g. to be executed on a remote server). You have an array with commands, and you want to pass $@ to the command:

f() {
    cmds+=("some-command ${@@Q}")
}
f a b c

But this way 3 elements are added to the array: some-command a, b, and c. What you want is:

cmds+=("some-command ${*@Q}")

Or even:

cmds+=("some-command${@+ ${*@Q}}")

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .