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?
answer
- one word each, or one word total
- the quotes are not decoration
- IFS joins, but only for one of them
- argv in equals argv out
- 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 sBoth 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#!/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
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.
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.
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.
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