skip to content

In a bash wrapper function that passes its arguments on to another command, what is the difference between forwarding them as "$@", as "$*", and as unquoted $@?

level: middleimportance: must knowfreq 72%

answer

  1. one form keeps the argument vector intact
  2. quotes change the word count, not the text
  3. one of them joins with a variable's first character
  4. the empty-list case tells them apart

basics

~20 s

"$@" expands to one word per argument, preserving spaces and empty arguments — it is the only correct way to forward. "$*" joins everything into a single word separated by IFS's first character. Unquoted $@ re-splits each argument on whitespace and globs the pieces.

solid answer

~50 s

`"$@"` is the forwarding form. Bash expands it to exactly as many words as the function received, each one intact, so an argument like `my file.txt` or an empty string survives the hop. `"$*"` expands to a *single* word: all the arguments concatenated with the first character of `IFS` (a space by default), so a wrapper that runs `cmd "$*"` passes one long argument no matter how many it got. Unquoted `$@` and `$*` are worse than either — the result goes through word splitting and pathname expansion, so `my file.txt` arrives as two arguments and an argument containing `*` may be replaced by matching filenames. There is also an empty-list special case: with no arguments, `"$@"` disappears entirely while `"$*"` still produces one empty argument. Use `"$@"` unless you deliberately want a joined string, for example in a log line.

code

bash · 9 lines
bash
#!/usr/bin/env bash
show()      { printf '[%s]' "$@"; echo; }
wrap_at()   { show "$@"; }
wrap_star() { show "$*"; }
wrap_bare() { show $@; }

wrap_at   "a b" "" c   # [a b][][c]  -> 3 arguments, intact
wrap_star "a b" "" c   # [a b  c]    -> 1 argument, joined by IFS
wrap_bare "a b" "" c   # [a][b][c]   -> re-split on whitespace

go deeper

for a junior

Know the rule of thumb and say it without hedging: forward arguments as "$@" with the quotes, and never as bare $@ or $*.

for a middle

Explain the mechanics — "$@" expands to one word per argument while "$*" joins with the first character of IFS, and an unquoted expansion is re-split and globbed afterwards.

for a senior

Demonstrate the consequences you have debugged: a wrapper that broke on a filename with a space, an empty argument that vanished, and the zero-argument case where "$@" disappears but "$*" leaves one empty argument.

for a principal

Own the standard: quoted "$@" forwarding enforced by ShellCheck in CI, an explicit convention for when a joined "$*" string is allowed, and a rule that any command line built as text rather than as an argument vector needs review.

## Three expansions that look interchangeable and are not A wrapper function is one of the most common shapes in shell scripting: add a default flag, add retries, add logging, then hand the arguments to the real command. ```bash run() { logger "running: $*"; command curl --fail "$@"; } ``` That one line uses both forms deliberately, and the difference between them is the single most-asked thing about bash functions. ## "$@" — one word per argument Inside double quotes, `$@` is special-cased by the shell: it expands to *n* separate words for *n* arguments, and each word is exactly the argument as received. Spaces, tabs, newlines, glob characters and empty strings all survive, because each expanded word is treated as if it had been individually quoted. This is the whole reason `"$@"` exists — no other expansion in the language does it. ```bash show() { printf '[%s]' "$@"; echo; } fwd() { show "$@"; } fwd "a b" "" c # prints [a b][][c] ``` The forwarded call is byte-for-byte the argument vector the wrapper was handed. ## "$*" — one word, joined by IFS `"$*"` produces a single word: the arguments concatenated, with the first character of the `IFS` variable between them (a space in a default shell). So `show "$*"` above prints `[a b c]` — one argument, and the empty argument has silently collapsed into a doubled separator. Any command receiving `"$*"` sees one long string, which is why `ssh host "$*"` and `docker run img "$*"` behave so differently from their `"$@"` counterparts: the remote or the container gets one argument that it must then re-parse. `"$*"` is not a mistake in itself; it is the right tool when you genuinely want a joined string: ```bash log() { printf '%s %s\n' "$(date +%FT%T)" "$*" >&2; } log deploy finished in 3s # one tidy message ``` Because the join character comes from `IFS`, you can pick it deliberately: `IFS=, ; joined="$*"` builds a comma-separated list. Do that in a subshell or restore `IFS` afterwards. ## Unquoted $@ and $* — the bug Without quotes, both first expand to the arguments separated by spaces and then the *result* goes through word splitting and pathname expansion, exactly like any other unquoted expansion. An argument `report 2024.txt` arrives as two arguments; an argument `*.log` may be replaced by the log files in the current directory; an empty argument vanishes entirely. This is the defect ShellCheck reports as SC2086, and in a wrapper it is especially nasty because the wrapper usually works during testing — the failure appears the first time a user passes a filename with a space. ## The empty-argument-list special case With zero arguments, `"$@"` expands to *nothing at all* — the word disappears, and `cmd "$@"` runs `cmd` with no arguments. `"$*"` under the same conditions expands to one empty word, so `cmd "$*"` runs `cmd` with a single empty argument. For commands that treat an empty argument as a path or a pattern, that difference is the difference between working and failing. ```bash count() { printf '%d\n' "$#"; } both() { count "$@"; count "$*"; } both # prints 0 then 1 ``` ## Forwarding a subset A dispatcher usually consumes the first argument and forwards the rest: ```bash dispatch() { local sub=$1; shift "cmd_$sub" "$@" } ``` `shift` then `"$@"` is the clearest spelling. The slice form `"${@:2}"` does the same without mutating the list, and `"${@:2:3}"` takes three arguments starting at the second — both keep the per-word quoting property, but only while quoted. ## Related idioms worth knowing `for x in "$@"` iterates arguments safely, and a bare `for x; do ...; done` means exactly that — the `in` list defaults to `"$@"`. Copying arguments into an array with `args=("$@")` and expanding it as `"${args[@]}"` follows the same quoting rule, which is why the two look so similar.

  • How do you forward every argument except the first?
    Either `shift` and then use `"$@"`, or expand the slice `"${@:2}"` — both keep one word per argument as long as they are quoted. `shift` mutates the function's list, which reads well in a dispatcher; the slice leaves it intact, which is handy when you need `$1` again later.
  • When is "$*" actually the right choice?
    When you want a single joined string rather than an argument vector: a log line, an error message, or a deliberate join with `IFS=,` to build a comma-separated list. The tell is that the value is going into text, not into a command's argument list.
  • What does ShellCheck SC2086 flag, and why does it fire on unquoted $@?
    SC2086 is "double quote to prevent globbing and word splitting". Unquoted `$@` lets the shell re-split each argument on IFS and then glob the pieces, so filenames with spaces break and arguments containing `*` can be replaced by matching files. Quoting is the fix; there is no case where unquoted `$@` is the intent.
  • Does `for f in "$@"` differ from writing a bare `for f; do`?
    No — when a `for` loop omits the `in` list, bash iterates over `"$@"` with exactly the same per-word quoting. The bare form is shorter and immune to someone later deleting the quotes, which is why it shows up in tightly written libraries.

"$@" hands over a stack of separately addressed parcels; "$*" tapes them all into one box with the labels written on the outside; unquoted $@ empties the parcels onto the floor and lets the sorter regroup them by whitespace.

saying these in an interview costs you the question

  • Says "$@" and "$*" are interchangeable
  • Thinks quoting $@ collapses it into one string
  • Claims unquoted $@ is fine when arguments have no spaces
  • Believes empty arguments cannot be forwarded at all
  • Says you must loop and rebuild the argument list manually

context