skip to content

In bash, what is the difference between expanding an array as "${arr[@]}" and as "${arr[*]}", and what changes if you drop the double quotes from either one?

level: middleimportance: must knowfreq 62%

answer

  1. one is a list, one is a string
  2. quoting is what makes them differ
  3. joined on the first IFS character
  4. unquoted means split and glob again

basics

~20 s

Quoted "${arr[@]}" produces one word per element with contents preserved. Quoted "${arr[*]}" produces a single word, elements joined by the first character of IFS. Unquoted, both collapse to one string that is then word-split and glob-expanded.

solid answer

~40 s

`"${arr[@]}"` is the one you almost always want: it expands to exactly one word per element, so an element containing spaces or newlines still arrives as a single argument. `"${arr[*]}"` expands to one single word, with the elements joined by the first character of `IFS` — a space by default, so `"${arr[*]}"` is the way to build a display string, not an argument list. Without the quotes, the distinction largely disappears: both forms produce text that then goes through word splitting on `IFS` and pathname expansion, so `element with spaces` becomes several arguments and an element containing `*` can silently turn into filenames. That is why ShellCheck raises SC2068 on an unquoted array expansion. An empty array expands to zero words under `"${arr[@]}"`, while `"${arr[*]}"` still yields one empty word.

code

bash · 8 lines
bash
arr=(one "two three")

printf '[%s]\n' "${arr[@]}"   # [one] / [two three]  -> 2 arguments
printf '[%s]\n' "${arr[*]}"   # [one two three]      -> 1 argument
printf '[%s]\n' ${arr[@]}     # [one] / [two] / [three] -> re-split

IFS=,
printf '[%s]\n' "${arr[*]}"   # [one,two three]      -> joined on comma

go deeper

for a junior

Learn one rule and apply it every time: pass arrays as "${arr[@]}" with the quotes. Know that "${arr[*]}" makes a single string, which is for printing rather than for passing arguments.

for a middle

Explain the mechanics: quoted [@] expands to one word per element, quoted [*] joins on the first character of IFS, and unquoted either form is re-split and glob-expanded afterwards.

for a senior

Demonstrate the consequences on real data — filenames with spaces or globbing characters surviving intact, an empty array contributing zero arguments — and cite ShellCheck SC2068 as the automated guard in review.

for a principal

Own it as a convention rather than a case-by-case judgment: make quoted array expansion a lint-enforced house rule so the failure mode, which only appears on unusual data in production, cannot reach a shared script at all.

## The two subscripts An indexed array has two special subscripts that mean "all of it". They differ in how many *words* the expansion produces, and that difference only becomes visible once the expansion is quoted. ```bash arr=(one "two three") printf '[%s]\n' "${arr[@]}" # [one] [two three] printf '[%s]\n' "${arr[*]}" # [one two three] ``` `printf` reuses its format for each remaining argument, which makes it a good probe: two bracketed lines mean two arguments, one line means one. ## "${arr[@]}" — one word per element Inside double quotes, `[@]` is special-cased by the shell: instead of producing one quoted string, it produces a *list* of words, one per element, each individually protected from word splitting and pathname expansion. Whatever bytes are in an element — spaces, tabs, newlines, `*`, a leading `-` — arrive at the command as exactly one argument. This is the same rule that makes `"$@"` work for positional parameters; the array form is the general case of it. A useful corner: if the array is empty, `"${arr[@]}"` expands to *nothing at all* — zero words, not one empty word. So `mycmd "${opts[@]}" file` runs `mycmd file` when `opts` is empty, which is exactly what you want when building optional flags. ## "${arr[*]}" — one joined word `[*]` inside double quotes joins the elements into a single word, separated by the **first character** of `IFS`. With the default `IFS=$' \t\n'` the separator is a space, which is why the two forms look identical in casual `echo` output. Change `IFS` and the difference is obvious: ```bash arr=(a b c) IFS=, echo "${arr[*]}" # a,b,c ``` If `IFS` is unset, the join character is a space; if `IFS` is set to the empty string, the elements are concatenated with nothing between them. `"${arr[*]}"` is therefore the right tool for *display* and *serialisation* — a log line, an error message, a comma-separated field — and the wrong tool for passing arguments, because everything ends up in one argument. ## Dropping the quotes Unquoted, both forms first produce the elements as text and then hand the result to word splitting and pathname expansion, which is where data gets destroyed: ```bash arr=(one "two three") printf '[%s]\n' ${arr[@]} # [one] [two] [three] ``` The element `two three` was split into two arguments on `IFS`. Exactly the same thing happens with unquoted `${arr[*]}`. On top of splitting, each resulting word is a glob candidate, so an element that happens to be `*.txt` or `report[1].csv` can be replaced by matching filenames — or left as a literal that no longer matches anything. ShellCheck flags this as SC2068 ("double quote array expansions") for the same reason it flags unquoted scalars as SC2086. The practical rule is unconditional: **write `"${arr[@]}"` and never think about it again.** There is no realistic case where unquoted `${arr[@]}` is what you meant; if you genuinely want the elements re-split, you are building a string, and `"${arr[*]}"` says so explicitly. ## Mixing an array into a larger string One more trap sits between the two forms: ```bash echo "Args: ${arr[@]}" ``` A quoted `[@]` embedded in a longer string produces the first element glued to the prefix and the last glued to the suffix, with the middle elements as separate words — almost never what the author intended. If you want one readable string, use `[*]`: `echo "Args: ${arr[*]}"`. ## Slices and indices follow the same rule Every all-elements expansion inherits the distinction. `"${arr[@]:1:2}"` yields two words; `"${arr[*]:1:2}"` yields one joined word. `"${!arr[@]}"` — the list of subscripts — is a word list too, which is why iterating indices with it is safe. ## Summary `@` for arguments, `*` for a string, and always with the quotes. Everything else about arrays follows from getting this one right.

  • You want to log an array as a readable comma-separated list. How do you do it without permanently changing IFS?
    Set IFS only for the duration of a subshell or a single expansion context, for example `( IFS=,; printf '%s\n' "${arr[*]}" )`, or save and restore the old value. A prefix assignment like `IFS=, echo "${arr[*]}"` does not work, because the expansion is performed by the shell before the temporary assignment applies to the command's environment.
  • Why does `echo "Total: ${arr[@]}"` usually look wrong?
    A quoted [@] inside a bigger string still produces one word per element, so the prefix attaches to the first element only and the rest arrive as separate arguments. Use `"${arr[*]}"` when you want a single readable string, or print the elements with printf if you want one per line.
  • How does "$@" relate to "${arr[@]}"?
    The positional parameters behave like a built-in array, and "$@" follows exactly the same rule as "${arr[@]}": one word per parameter, contents preserved. That is why `mycmd "$@"` forwards arguments faithfully. The corresponding joined form is "$*", which concatenates on the first IFS character.

saying these in an interview costs you the question

  • Saying [@] and [*] are interchangeable
  • Believing quotes on an array expansion are cosmetic
  • Thinking [*] always joins with a space regardless of IFS
  • Claiming unquoted ${arr[@]} is safe because elements are separate
  • Using "${arr[@]}" inside a longer string for display

context