Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 19 · Arrays and Lists of Values

"${array[@]}" vs "${array[*]}"

This is the chapter in the module to get right. An array holds your items correctly; whether they arrive at a command correctly depends on four characters, and three of the four combinations lose or invent items. All four are measured here by argument count, because the obvious way to look at them does not show the difference.

Intermediate19 min readLinuxPart IBashArraysQuotingIFSScripting

Part I · Module 19 · Chapter 19.3 · "${array[@]}" vs "${array[*]}"

Chapter 19.1 ended on a warning: an array holds the right thing, and the wrong expansion throws it away again. This chapter is that warning, measured.

1. Measure It With Argument Count, Not Output

First, the instrument. The temptation is to look at the four forms with printf:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(smoke "two words" burst)
$ printf '[%s]\n' "${a[@]}"
[smoke]
[two words]
[burst]

That works, but printf reuses its format once per argument (Chapter 15.4), so it is easy to misread — and with zero arguments it still prints one line, which actively misleads. A function that reports $# cannot:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count() { echo "  $# args:"; printf '    [%s]\n' "$@"; }

Argument count is the thing that matters, because "how many arguments did the command receive" is the entire question.

2. The Four Forms

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(smoke "two words" burst)

$ count "${a[@]}"
  3 args:
    [smoke]
    [two words]
    [burst]

$ count "${a[*]}"
  1 args:
    [smoke two words burst]

$ count ${a[@]}
  4 args:
    [smoke]
    [two]
    [words]
    [burst]

$ count ${a[*]}
  4 args:
    [smoke]
    [two]
    [words]
    [burst]

Three items, and the answers are 3, 1, 4 and 4.

Four forms, four argument counts

The four ways to expand a three-item array where one item contains a space, and how many arguments each produces. Quoted with the at sign gives three, one per item. Quoted with the star gives one, everything joined. Either of them unquoted gives four, because the result is split again on whitespace. Only the first is ever what you want for a list.01QUOTED, WITH THE AT SIGNthree items in, three arguments out02QUOTED, WITH THE STARone argument, everything joined together03EITHER ONE UNQUOTEDfour arguments, because it is split again04SO THERE IS ONE CORRECT FORMand the other three lose or invent items
The same three-item array, where one item contains a space, expanded four ways. Quoted with the at sign gives one argument per item. Quoted with the star gives a single argument with everything joined. Either form unquoted gives four, because the result is split again on whitespace. Only the first preserves what the array was holding.

Read the four as two independent choices:

[@] or [*] decides whether you get many words or one. [@] produces one word per element; [*] concatenates every element into a single word.

The quotes decide whether that result is then re-split. Unquoted, whatever came out goes through Chapter 9.2's word-splitting, which collapses the distinction — which is why the two unquoted forms give the same answer and why that answer is 4 either way.

So only "${a[@]}" both produces separate words and protects them. Write "${a[@]}". Always. It is the form, and the other three are for the specific jobs below.

3. $@ and $* Are the Same Rules

Chapter 15.4 taught "$@" versus "$*" for script arguments and Chapter 18.2 repeated it for functions. Those are not an analogy — the positional parameters are an array, and @ and * mean exactly what they mean here:

Array formPositional equivalentResult
"${a[@]}""$@"one word per item
"${a[*]}""$*"one word, items joined by the first IFS character
${a[@]}$@re-split on whitespace
${a[*]}$*re-split on whitespace

That is one rule to remember rather than two. And it is why for x; do — Chapter 17.1's shorthand for for x in "$@"; do — is safe, and why set -- "${a[@]}" is the standard way to turn an array into the positional parameters.

4. What [*] Is Actually For

[*] is not a mistake looking for a victim. It has one job: producing a human-readable string from a list.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tests=(smoke "two words" burst)
$ echo "running: ${tests[*]}"
running: smoke two words burst

That is a message, not a list, and joining is exactly right. Chapter 18.2's note() { printf '%s: %s\n' "${0##*/}" "$*" >&2; } is the same choice for the same reason.

And [*] joins with the first character of IFS, which is occasionally very useful:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(x y z)
$ echo "default: [${a[*]}]"
default: [x y z]
$ IFS=,; echo "IFS=,:   [${a[*]}]"
IFS=,:   [x,y,z]
$ IFS=;  echo "IFS=:    [${a[*]}]"
IFS=:    [xyz]

A comma-separated list in one expansion, with no loop. The idiomatic way to do that without disturbing the rest of the script is a subshell, so IFS is restored for free:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(x y z)
$ csv=$(IFS=,; echo "${a[*]}")
$ echo "$csv"
x,y,z

[@] is unaffected by IFS when quoted:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(x y z); IFS=,
$ printf '[%s]' "${a[@]}"; echo
[x][y][z]

Which is another way of saying that "${a[@]}" is the form that does not depend on the environment. "${a[*]}" produces different text depending on a global variable someone else may have set — fine for a message you are about to print, not fine for data.

5. Transforming Every Element at Once

Chapter 8.3's pattern-removal and substitution forms work on an array, and when you use [@] they apply to every element, returning a list of the same length:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(run_1.log run_2.log "my run.log")
$ printf '  [%s]\n' "${a[@]%.log}"
  [run_1]
  [run_2]
  [my run]

One expansion, three results, and my run.log kept its space. The same works for every form from Chapter 8.3:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf '  [%s]\n' "${a[@]#run_}"
  [1.log]
  [2.log]
  [my run.log]
$ b=(a-b a-c); printf '  [%s]\n' "${b[@]/-/_}"
  [a_b]
  [a_c]
$ c=(aXbXc);   printf '  [%s]\n' "${c[@]//X/-}"
  [a-b-c]

Note the second result: my run.log is unchanged by #run_ because it does not start with that prefix. The transformation is applied per element and an element that does not match is passed through — there is no error and no gap.

This is the idiomatic way to derive one list from another, and the count survives if you quote:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(run_1.log "my run.log")
$ t=("${a[@]%.log}")
$ echo "count=${#t[@]}"
count=2
$ printf '  [%s]\n' "${t[@]}"
  [run_1]
  [my run]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ t=(${a[@]%.log})
$ echo "count=${#t[@]}"
count=3

Section 2's rule, one more time: the transformation was correct and the unquoted expansion undid it on the way into the new array.

set -- is the other way to move a list, when what you want is the positional parameters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(p "q r" s)
$ set -- "${a[@]}"
$ echo "$#"
3
$ printf '  [%s]\n' "$@"
  [p]
  [q r]
  [s]
$ set -- ${a[@]}; echo "unquoted: $#"
unquoted: 4

Useful when you want to hand an array to something that reads $1, $2, or to re-use shift — and it obeys the identical rule, because §3 established that the positional parameters are an array.

6. Lengths and Slices

${#a[@]} and ${#a[*]} are both the element count — the @/* distinction does not apply to #:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(hello world)
$ echo "${#a[@]}  ${#a[*]}  ${#a[0]}"
2  2  5

Only ${#a[0]} is different, and it is a string length. Chapter 19.2 §2 covered that.

Slices follow the same quoting rule as the whole array:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ a=(p "q r" s t)
$ count "${a[@]:1:2}"
  2 args:
    [q r]
    [s]
$ count ${a[@]:1:2}
  3 args:
    [q]
    [r]
    [s]

Quoted slice, two elements with the space intact. Unquoted, three. Quote the slice too — the rule is about the expansion, not about whether you took all of it.

7. A Complete Script

A wrapper that has to do all three jobs — pass a list as arguments, join it for a message, and count it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
# run-tests.sh — run a list of tests, reporting with the right expansion
# usage: run-tests.sh <test>...
#
# exit: 0 all passed · 1 at least one failed · 2 bad usage

set -o nounset

note() { printf '%s: %s\n' "${0##*/}" "$*" >&2; }

# stand-in for the simulator: receives each test as a separate argument
simulate() {
  printf '  simulate received %d argument(s):\n' "$#"
  local t
  for t in "$@"; do
    printf '    [%s]\n' "$t"
  done
  # a test whose name contains "bad" fails
  for t in "$@"; do
    case $t in *bad*) return 1 ;; esac
  done
  return 0
}

[ $# -ge 1 ] || { note "usage: ${0##*/} <test>..."; exit 2; }

# "$@" into an array — boundaries preserved all the way in
declare -a tests=("$@")

# [*] for the human-readable announcement
note "about to run ${#tests[@]} test(s): ${tests[*]}"

# [*] with a different separator, for a one-line record
summary=$(IFS=,; echo "${tests[*]}")
note "list=$summary"

# [@] quoted for the actual call — the only correct form
if simulate "${tests[@]}"; then
  printf 'PASS  %d test(s)\n' "${#tests[@]}"
  exit 0
fi

printf 'FAIL  one of %d test(s)\n' "${#tests[@]}"
exit 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./run-tests.sh smoke "cache coherency" burst
run-tests.sh: about to run 3 test(s): smoke cache coherency burst
run-tests.sh: list=smoke,cache coherency,burst
  simulate received 3 argument(s):
    [smoke]
    [cache coherency]
    [burst]
PASS  3 test(s)
$ echo $?
0

$ ./run-tests.sh smoke bad_reset
run-tests.sh: about to run 2 test(s): smoke bad_reset
run-tests.sh: list=smoke,bad_reset
  simulate received 2 argument(s):
    [smoke]
    [bad_reset]
FAIL  one of 2 test(s)
$ echo $?
1

$ ./run-tests.sh
run-tests.sh: usage: run-tests.sh <test>...
$ echo $?
2

Now break exactly one character — simulate ${tests[@]} without the quotes — and run the identical command:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./broken.sh smoke "cache coherency" burst
broken.sh: about to run 3 test(s): smoke cache coherency burst
broken.sh: list=smoke,cache coherency,burst
  simulate received 4 argument(s):
    [smoke]
    [cache]
    [coherency]
    [burst]
PASS  3 test(s)
$ echo $?
0

simulate received four tests and the summary still says three. It would have run a test called cache and a test called coherency, neither of which exists, and reported a pass. Both announcement lines are still correct, because they use [*] and ${#tests[@]} — which both work fine. The only thing that changed is what the tool was actually asked to do.

Three decisions:

declare -a tests=("$@") copies the positional parameters into an array with boundaries intact. Drop those quotes and the damage happens a line earlier, which is worse in an interesting way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./b2.sh smoke "cache coherency" burst
b2.sh: about to run 4 test(s): smoke cache coherency burst
b2.sh: list=smoke,cache,coherency,burst
  simulate received 4 argument(s):
    [smoke]
    [cache]
    [coherency]
    [burst]
PASS  4 test(s)

Here the array itself is wrong, so every message is consistently wrong — the count says 4, the comma-separated record says 4, and the tool got 4. Nothing disagrees with anything, which removes the one clue the previous version left behind. A list damaged at the point of construction is harder to spot than one damaged at the point of use.

${tests[*]} for the two messages, "${tests[@]}" for the call. Different jobs, different forms, and the script is a two-line demonstration of which is which.

summary=$(IFS=,; echo "${tests[*]}") sets IFS inside a subshell, so the script's own IFS is untouched afterwards — the §4 idiom.

Practice

  1. Write the count helper and run all four forms on a=(smoke "two words" burst). Record the four argument counts. Then do the same on an array with no spaces in any item and explain why the bug hides.
  2. State in one sentence what [@] versus [*] decides, and in another what the quotes decide.
  3. Build a comma-separated string from a=(x y z) two ways: by setting IFS directly, and inside $( ). Print IFS afterwards in both cases and say which you would ship.
  4. Set IFS=, and then print "${a[@]}" and "${a[*]}". Which one changed, and what does that tell you about which form depends on the environment?
  5. Print ${#a[@]}, ${#a[*]} and ${#a[0]} for a=(hello world). Which is the odd one out?
  6. Take a quoted slice "${a[@]:1:2}" of a=(p "q r" s t) through count, then the unquoted version. Explain the difference without using the word "array".
  7. Fill in the §3 table from your own measurements, using set -- smoke "two words" burst and a script that reports $#.
  8. Take run-tests.sh, remove the quotes from simulate "${tests[@]}", and run it with a two-word test name. Report what simulate received, what the summary said, and the exit status. Why is this the worst possible failure mode?
  9. In run-tests.sh, change declare -a tests=("$@") to tests=($@) and re-run with a two-word test name. Where does it break now, and which messages are still correct?
  10. Take a=(run_1.log run_2.log "my run.log") and strip .log from every element in one expansion. Then assign the result to a new array twice — quoted and unquoted — and compare the counts.
  11. Run a=(ab cd); echo "${a[@]^^}" on your machine. If the output is lowercase, say what that means for a script that normalises a tool name, and write the portable replacement.
  12. Use set -- "${a[@]}" to turn an array into the positional parameters, then shift once and print "$@". Why does this work at all?

Check Your Understanding

  1. Three items, one containing a space. What argument counts do the four forms give?
  2. Why do the two unquoted forms give the same answer?
  3. Which single form should you write for a list?
  4. What is [*] legitimately for?
  5. What character does [*] join with?
  6. Is "${a[@]}" affected by IFS?
  7. ${#a[@]} and ${#a[0]} — which is the count?

Chapter 27.2 takes these forms to a real tool invocation and measures what each one makes the simulator receive, including the expansion that passes one empty argument instead of none.

8. Remember

  • "${a[@]}" is the form. One word per element, protected from re-splitting. Write it every time you mean a list.
  • Three items can arrive as 3, 1 or 4 depending on two characters, measured — and all four forms run without error.
  • [@] or [*] decides many words or one; the quotes decide whether that is re-split. Unquoted collapses the distinction, which is why both bare forms give 4.
  • The bug hides on test data. Every form agrees until an item contains a space.
  • $@ and $* follow identical rules, because the positional parameters are an array.
  • [*] is for a message, not for data — an announcement, a log line, a summary.
  • [*] joins with the first character of IFS. $(IFS=,; echo "${a[*]}") builds a comma-separated string without disturbing the script.
  • Quoted [@] ignores IFS. That is why it is the one form that does not depend on the environment.
  • ${#a[@]} is the count; ${#a[0]} is a string length.
  • Quote slices too. "${a[@]:1:2}" — the rule is about the expansion, not about taking all of it.
  • Pattern forms apply to every element: "${a[@]%.log}" strips the suffix from all of them and preserves the count. An element that does not match passes through unchanged.
  • set -- "${a[@]}" turns an array into the positional parameters, same rule.
  • ${a[@]^^} is Bash 4 and a silent no-op on 3.2. Use tr to change case portably.

Chapter 19.4 fills an array from somewhere other than a literal. The obvious one-liner — assigning a command's output directly — is the Chapter 19.1 bug in a new costume, and the measured result is five elements for three files.

Where this fits

Part of the Linux & Bash curriculum.