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?
answer
- one of the four preserves boundaries
- quoting changes list versus single word
- the star form joins on IFS
- check what happens with zero arguments
- only one form is safe to forward
basics
~10 sOnly "$@" 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 linesshow() { 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
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.
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.
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.
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