skip to content

A bash wrapper script must forward every argument it received on to another command unchanged. What is the difference between `"$@"` and `"$*"`, and which one does that job?

level: middleimportance: must knowfreq 76%

answer

  1. one word each, or one word total
  2. the quotes are not decoration
  3. IFS joins, but only for one of them
  4. argv in equals argv out
  5. unquoted, both are the same mistake

basics

~10 s

"$@" expands to one separate word per positional parameter, preserving arguments that contain spaces. "$*" joins them all into a single word separated by the first character of IFS. Forwarding arguments always uses "$@".

solid answer

~50 s

Both are special parameters holding the script's positional parameters, and the difference only shows up in how many words they expand to. Quoted, `"$@"` is special-cased by bash to produce one word per parameter, so an argument like `my file.txt` survives as one argument on the other side. `"$*"` concatenates every parameter into a single string, separated by the first character of `IFS` — a space by default — so three arguments arrive as one. That is why a wrapper writes `exec realcmd "$@"` and never `realcmd "$*"`. Unquoted, the distinction collapses: both `$@` and `$*` are subject to word splitting and globbing, so an argument with a space becomes two arguments and one containing `*` may be replaced by filenames. `"$*"` is legitimately useful when you deliberately want one joined string, for example in a log line. `$#` gives the count of parameters.

code

bash · 19 lines
bash
#!/usr/bin/env bash
show() {
  printf 'received %d args:\n' "$#"
  printf '  [%s]\n' "$@"
}

set -- "my file.txt" second "a b"

echo '--- "$@" ---'
show "$@"

echo '--- "$*" ---'
show "$*"

echo '--- $@ unquoted ---'
show $@

echo '--- "$*" with IFS=, ---'
( IFS=,; echo "$*" )

go deeper

for a junior

Remember the practical rule: to pass a script's arguments on to another command, write "$@" with the quotes. Know that $# is the number of arguments and $1 is the first.

for a middle

Explain the word-count difference precisely — "$@" yields one word per parameter, "$*" yields one word joined by the first character of IFS — and show that unquoted, the two behave identically and get split and globbed.

for a senior

Demonstrate why this is a correctness issue in production glue: filenames with spaces, empty arguments and leading dashes all survive "$@" and are destroyed by the alternatives. Mention the empty-argument-list difference and how you would lint for it.

for a principal

Own the standard: pick "$@" plus arrays as the house convention for building command lines, and make unquoted expansion a lint failure rather than a review comment. Be ready to say when argument plumbing has grown complex enough to leave shell.

## What the positional parameters are When a bash script runs, the words after the script name become the *positional parameters*: `$1`, `$2`, `$3` and so on, with `$0` holding the name the script was invoked with. `$#` is how many positional parameters there are — note that `$0` is not counted. Rather than index them one at a time, bash gives you two special parameters that stand for the whole list: `$@` and `$*`. They hold exactly the same values. What differs is how many *words* they turn into during expansion, and in a shell the word count is everything, because words are what a command receives as its argv. ## Quoted: the only two forms you should write ```bash set -- "my file.txt" second "a b" printf '[%s]\n' "$@" # [my file.txt] [second] [a b] -> three words printf '[%s]\n' "$*" # [my file.txt second a b] -> one word ``` `"$@"` is a deliberate special case in the grammar: although it sits inside double quotes, it expands to one quoted word *per parameter*. It is the only expansion in bash that can produce a different number of words than the one it started as while quoted. That property is exactly what argument forwarding needs — the callee sees the same argv the wrapper saw, spaces, empty strings, leading dashes and all. `"$*"` produces a single word, formed by joining the parameters with the *first character of `IFS`*. With the default `IFS=$' \t\n'` that separator is a space. If `IFS` is set to a comma, `"$*"` joins with commas; if `IFS` is empty, the parameters are concatenated with nothing between them. That is occasionally useful: ```bash ( IFS=,; echo "args: $*" ) # args: my file.txt,second,a b ``` Note it is only the *first* character; `IFS=', '` still joins with a comma alone. ## Unquoted: both are wrong for forwarding Unquoted `$@` and `$*` behave identically and badly. Each expands, and the result then goes through word splitting on `IFS` and pathname expansion. An argument `my file.txt` arrives at the callee as two arguments; an argument `*.log` may be replaced by matching filenames from the current directory; an empty argument disappears entirely. A script that works in testing and breaks the first time someone passes a path with a space almost always has an unquoted `$@` in it. ```bash run() { printf '%d args\n' "$#"; } set -- "a b" c run $@ # 3 args -- wrong run "$@" # 2 args -- right ``` ## Empty argument lists With zero positional parameters, `"$@"` expands to *nothing at all* — not to one empty word. So `cmd "$@"` with no arguments runs `cmd` with no arguments, which is what you want. `"$*"` with zero parameters expands to one empty string, so `cmd "$*"` runs `cmd` with a single empty argument. That difference bites in wrappers that pass optional flags through. ## The related slices The same `@` versus `*` distinction runs through the other forms built on the positional parameters. `"${@:2}"` is every parameter from the second onward as separate words, which is the idiom for consuming a subcommand name and forwarding the rest: ```bash subcmd=$1 shift "handler_$subcmd" "$@" ``` `shift` drops `$1` and renumbers the rest, and `set -- ...` replaces the whole list, which is how you inject or rewrite arguments before forwarding. ## Where each one belongs Use `"$@"` whenever the values are going to be *executed* as arguments — a wrapper, a `exec` handoff, a retry loop that re-runs a command, an array copy `args=("$@")`. Use `"$*"` only when you genuinely want one human-readable string: a log message, an error line quoting what the user typed, a checksum key. Never use the unquoted forms; if you find yourself wanting word splitting on purpose, that intent is clearer with an explicit array. ShellCheck's `SC2068` flags unquoted `$@` for exactly this reason.

  • With no arguments at all, how do `cmd "$@"` and `cmd "$*"` differ?
    `"$@"` with zero positional parameters expands to nothing, so `cmd` runs with no arguments. `"$*"` expands to a single empty string, so `cmd` runs with one empty argument. A wrapper that forwards optional flags with `"$*"` therefore passes a spurious empty argument, which many programs reject or misinterpret as an empty filename.
  • How would you forward everything except the first argument?
    `shift` then `"$@"`, or the slice `"${@:2}"` directly. The usual dispatcher pattern is `sub=$1; shift; handler_"$sub" "$@"`. Both preserve one word per remaining argument; the slice form is handy when you also still need `$1` afterwards, since `shift` discards it.
  • When is `"$*"` the right choice?
    When you want one string rather than an argument list: an error message echoing what the user typed, a log line, or a joined key where you set `IFS` deliberately to pick the separator. The rule of thumb is that anything destined to become argv uses `"$@"`, and anything destined for human eyes or a single string may use `"$*"`.

saying these in an interview costs you the question

  • Says $@ and $* are interchangeable
  • Forwards arguments with an unquoted $@
  • Thinks "$*" preserves separate arguments
  • Believes $# counts the script name too
  • Assumes "$@" with no args passes one empty argument

context