skip to content

A bash wrapper script forwards its arguments with `mytool $@`, and a user who passes an argument containing a space finds that mytool sees two arguments. What do `"$@"`, `$@`, `"$*"` and `$*` each expand to, and which belongs in that wrapper?

level: middleimportance: must knowfreq 62%

answer

  1. one of the four preserves boundaries
  2. quoting changes list versus single word
  3. the star form joins on IFS
  4. check what happens with zero arguments
  5. only one form is safe to forward

basics

~10 s

Only "$@" forwards arguments faithfully: it expands to one word per positional parameter with boundaries intact. $@ and $* are both re-split on IFS and globbed, and "$*" joins everything into a single word.

solid answer

~40 s

`"$@"` is the only correct forwarding form: it expands to exactly as many words as there are positional parameters, each preserved verbatim, so `mytool "$@"` passes `My Report.txt` through as one argument. Unquoted `$@` expands first and then the results are word-split on `IFS` and glob-expanded, which is why an argument with a space arrives as two. `"$*"` joins all parameters into one single word, separated by the first character of `IFS` (a space by default) — occasionally useful for building a log message, never for forwarding. Unquoted `$*` behaves the same as unquoted `$@`. The edge case worth knowing: with zero arguments `"$@"` expands to nothing at all, while `"$*"` still produces one empty argument, so `cmd "$*"` with no arguments passes an unwanted empty string.

code

bash · 10 lines
bash
show() { echo "count=$#"; printf '[%s]' "$@"; echo; }

set -- 'one two' three
show "$@"    # count=2  [one two][three]
show $@      # count=3  [one][two][three]
show "$*"    # count=1  [one two three]

set --       # no arguments at all
show "$@"    # count=0
show "$*"    # count=1  []

go deeper

for a junior

Memorise the rule and the shape: forwarding arguments is always cmd "$@", with the quotes. Being able to say that unquoted $@ breaks on arguments containing spaces is enough at this level.

for a middle

Explain the mechanics — that "$@" is special-cased into one word per parameter, that the unquoted forms are re-split on IFS and globbed, and that "$*" joins on the first character of IFS.

for a senior

Bring the production failures: an empty argument silently vanishing and shifting the rest, a * argument expanding against the working directory, and the zero-argument difference that leaves "$*" passing a stray empty string.

for a principal

Own the wrapper-script pattern as a design concern — thin exec mytool "$@" wrappers, arrays for anything that assembles a command, and a lint rule (SC2068) in CI so entrypoint scripts cannot regress this.

## The four forms Bash has two special parameters that stand for "all of the positional parameters", and each has a quoted and an unquoted form. They are not four ways to spell the same thing; they produce four different argument lists. Assume a script has been called with two arguments, `one two` (a single argument containing a space) and `three`: ```bash show() { printf '[%s]' "$@"; echo; } set -- 'one two' three show "$@" # [one two][three] 2 arguments show $@ # [one][two][three] 3 arguments show "$*" # [one two three] 1 argument show $* # [one][two][three] 3 arguments ``` **`"$@"`** is special-cased by the shell: it expands to one word per positional parameter, and each word is taken verbatim — spaces, newlines, tabs, glob characters and all. This is the only form that round-trips an argument list. **`$@` unquoted** first expands to the parameters and then, because the expansion was unquoted, each resulting word goes through word splitting on `IFS` and pathname expansion. An argument containing a space becomes two words; an argument that is literally `*` becomes a list of filenames. That is exactly the failure in the question. **`"$*"`** joins all the parameters into a *single* word, using the first character of `IFS` as the separator. With the default `IFS` that is a space. If you change `IFS`, the join character changes with it: ```bash set -- one two three ( IFS=,; echo "$*" ) # one,two,three ``` That makes `"$*"` genuinely useful for building a human-readable string — a log line, an error message, a comma-separated list — and useless for passing arguments on. **`$*` unquoted** joins on `IFS` and then splits the result back apart on `IFS`, which in practice gives the same word list as unquoted `$@`. There is essentially no reason to write it. ## The empty case The difference nobody remembers until it bites shows up with zero arguments: ```bash set -- # no positional parameters count() { echo "$#"; } count "$@" # 0 count "$*" # 1 - one argument, the empty string ``` So `ssh host "$*"` on a script called with no arguments runs a command with a stray empty argument, while `"$@"` correctly disappears. This is also why `"$@"` is safe as the whole of a command's argument list, and why `set -- "$@"` is a no-op you can rely on. ## Why the wrapper is broken ```bash #!/usr/bin/env bash # broken exec mytool $@ # correct exec mytool "$@" ``` The broken form fails for any argument containing whitespace, for an empty-string argument (which vanishes entirely, shifting every later argument left by one), and for an argument containing a glob character in a directory where it happens to match. All three are user-supplied values, so the bug appears in production and not in your testing. The same rule applies to any place a script re-passes its arguments: `sudo "$@"`, `command "$@"`, `"$@" </dev/null`, and calling a shell function with `myfunc "$@"` — inside a function, `$@` refers to the function's own parameters, not the script's, which is what makes helper wrappers compose. ## Prefixing and suffixing A common need is "my fixed flags, then the user's arguments". You cannot glue text onto `"$@"` as a whole; the concatenation applies only to the first and last words: ```bash set -- a b c printf '[%s]' "pre-$@" # [pre-a][b][c] - rarely what you want mytool --verbose "$@" # the normal, correct shape ``` For anything more structured than "flags then arguments", build the command as an array and expand that — but the quoting rule is identical: the expansion must be quoted or the boundaries are lost. ## What to say in an interview State the rule first — `"$@"` is the only faithful forwarding form — then demonstrate that you know *why*: the quotes suppress the re-splitting and globbing that the unquoted forms undergo, `"$*"` is a join rather than a list, and the zero-argument behaviour differs between the two quoted forms. ShellCheck reports the unquoted forms as `SC2068`.

  • When would you deliberately reach for `"$*"` instead of `"$@"`?
    When you want a single string rather than an argument list — a log line, an error message, or a joined display of what the user typed: `log "invoked with: $*"`. Setting `IFS` first lets you choose the join character, as in `( IFS=,; echo "$*" )` for a comma-separated list. Never use it to pass arguments to a command.
  • Inside a shell function, what do the positional parameters refer to?
    The function's own arguments — a function call rebinds `$1`, `$2`, `$#` and `"$@"` for the duration of the call, and the script's arguments are restored when it returns. That is why `wrapper() { mytool "$@"; }` forwards the caller's arguments, and why `"$0"` is the exception: it still names the script, not the function.
  • What goes wrong with `$@` when a user passes an empty-string argument?
    It disappears completely. Unquoted, the empty expansion produces no word at all, so every later argument shifts one position left and `$#` drops by one — a command that expected a value in a fixed slot silently reads the wrong one. `"$@"` preserves the empty argument as a real, empty word.

saying these in an interview costs you the question

  • Says $@ and $* are the same thing
  • Thinks quoting $@ collapses the arguments into one string
  • Uses "$*" to forward arguments to another command
  • Believes quotes are only needed if an argument contains a space
  • Assumes "$@" and "$*" behave identically when there are no arguments

context