Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 8 – Arrays and lists

Indexed arrays: create, slice and loop

Store a list in a Bash indexed array, read and slice its elements, loop over values or indices, and work out the gaps that unset leaves behind.

  • Intermediate
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build an indexed array and read, append to and slice its elements
  • Use negative subscripts to read and change elements from the end
  • Debug the gaps that unset leaves and renumber a sparse array
  • Distinguish the expansions ${a[@]}, ${a[*]} and ${!a[@]}
  • Explain why arrays are a Bash feature that dash rejects

Before you start

What you’ll learn · Before you start

What you will learn

  • Build an indexed array and read, append to and slice its elements
  • Use negative subscripts to read and change elements from the end
  • Debug the gaps that unset leaves and renumber a sparse array
  • Distinguish the expansions ${a[@]}, ${a[*]} and ${!a[@]}
  • Explain why arrays are a Bash feature that dash rejects

Before you start

On this page

A plain variable holds one string, so four file names crammed into it, one of them two words, come apart into five once the shell splits the value. An indexed array: A Bash variable that holds many values at once, each under a whole-number subscript counting from 0; written a=(one two) and read with ${a[i]}. dash has no arrays. keeps them as four values instead, each whole under a number called its array subscript: The part in brackets that selects one element, as in a[i]; in an indexed array Bash evaluates it as an arithmetic expression, and from Bash 4.2 a negative value counts back from the last element., counting from 0, so a=(zero one "two words" three) has exactly four elements and two words stays one of them.

You will build arrays and read, append to and slice their elements, reach them from the end with negative subscripts, loop over the values or over the indices, work out the gaps that unset leaves, and see why the same array a shell script relies on makes dash stop with a syntax error.

Create an array and read an element by its index

Write the elements in parentheses, and Bash numbers them from 0. Read one back by putting its subscript in braces, ask how many there are with ${#a[@]}, and take a run of them with a slice. The session builds an array, changes it and reads it four ways:

Creating an array, then reading, appending and slicing it Python · create_session.py
from session import run

run("""
# Build an array: the words are kept in order, starting at index 0.
a=(zero one "two words" three)
echo "${a[2]}"
echo "count: ${#a[@]}"
# Assign one element by its index, leaving a gap at index 4.
a[5]=five
# Append to the end with += (Bash 3.1 and later).
a+=(six)
echo "all: ${a[*]}"
echo "indices: ${!a[@]}"
echo "a slice of two: ${a[@]:1:2}"
""")

Output

# Build an array: the words are kept in order, starting at index 0.
$ a=(zero one "two words" three)
$ echo "${a[2]}"
two words
$ echo "count: ${#a[@]}"
count: 4
# Assign one element by its index, leaving a gap at index 4.
$ a[5]=five
# Append to the end with += (Bash 3.1 and later).
$ a+=(six)
$ echo "all: ${a[*]}"
all: zero one two words three five six
$ echo "indices: ${!a[@]}"
indices: 0 1 2 3 5 6
$ echo "a slice of two: ${a[@]:1:2}"
a slice of two: one two words

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 create_session.py

${a[2]} is the third element, two words, kept as one value; ${#a[@]} counts the elements, 4 here (the word-splitting lesson introduced both "${a[@]}" and ${#a[@]}). Assigning a[5]=five put a value at index 5 and left nothing at index 4, and a+=(six) added one after the last element, so the set indices became 0 1 2 3 5 6. The slice ${a[@]:1:2} copied two elements from index 1. The braces matter: $a[2] is not an array read at all, but $a (the element at index 0) followed by the plain text [2]. A bare $a or ${a} always means ${a[0]}, the first element, which surprises a script that meant the whole list.

Counting with the wrong expansion

${#a} is the length of the first element, not the number of elements: for a=(delhi mumbai chennai) it is 5, the length of delhi, while ${#a[@]} is 3. Reach for ${#a[@]} (or ${#a[*]}) whenever you mean the count.

Checkpoint

Check: Creating, reading and appending

  1. Question 1 of 2 For a=(red green blue "two words"), what does ${a[1]} expand to?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: green

    In the session a=(zero one "two words" three) made ${a[2]} the third element; counting from 0, index 1 is the second.

  2. Question 2 of 2 After a=(x y); a[5]=z; a+=(w), what does ${#a[@]} print?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 4

    ${#a[@]} counts the elements that are set, not the highest index. x and y are at 0 and 1, z at 5, and a+=(w) appends after the last index, 6, so four elements exist although the indices are 0 1 5 6.

Slices copy a run of elements, so a queue can rotate

A slice, ${a[@]:start:count}, copies count elements starting at index start; drop the :count and it takes every element from start to the end. Because a slice returns a run of elements that you can drop back into a new array, slices are how a list behaves like a work queue. Read queue.sh first:

queue.sh: rotate, remove and insert with slices Shell · queue.sh
#!/usr/bin/env bash
# queue.sh: move jobs through a small work queue using slices.
queue=(build test lint deploy)

# Rotate: the first job goes to the back of the queue.
queue=("${queue[@]:1}" "${queue[0]}")
echo "after rotate: ${queue[*]}"

# Remove the job at index 1, then renumber so there is no gap.
unset 'queue[1]'
queue=("${queue[@]}")
echo "after remove: ${queue[*]}"

# Insert "scan" so it becomes the new index 1.
queue=("${queue[@]:0:1}" scan "${queue[@]:1}")
echo "after insert: ${queue[*]}"

declare -p queue

Each line rebuilds queue from slices of itself. Rotating moves the first job to the back; removing drops index 1 and rebuilds so there is no gap; inserting splices a new job between two slices. The session runs it, after ShellCheck confirms the quoting is sound:

ShellCheck, then queue.sh moving jobs through the queue Python · queue_session.py
from session import run, files

files("queue.sh")

run("""
# ShellCheck is happy with the quoted expansions in queue.sh.
shellcheck queue.sh && echo "shellcheck: clean"
bash queue.sh
""")

Output

# ShellCheck is happy with the quoted expansions in queue.sh.
$ shellcheck queue.sh && echo "shellcheck: clean"
shellcheck: clean
$ bash queue.sh
after rotate: test lint deploy build
after remove: test deploy build
after insert: test scan deploy build
declare -a queue=([0]="test" [1]="scan" [2]="deploy" [3]="build")

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 queue_session.py

The jobs rotate from build test lint deploy to test lint deploy build, lose lint, then gain scan at index 1. The last line is declare -p queue, which prints an array exactly as you would type it, every index and value, so it is the honest way to see what an array really holds.

Checkpoint

Check: Slicing a run of elements

  1. One question queue=(a b c d); queue=("${queue[@]:1}" "${queue[0]}"). What is ${queue[*]} now?

    Choose one answer.

    Show the answer: One question

    Answer: b c d a

    queue.sh rotated build test lint deploy to test lint deploy build in exactly this way.

Negative subscripts count back from the last element

You rarely know the last index of an array, so Bash lets a negative subscript count back from the end: ${a[-1]} is the last element and ${a[-2]} the one before it. Reading this way has worked since Bash 4.2, and assigning through a negative subscript since Bash 4.3, both recorded in the Bash NEWS file. The session reads, then assigns, then tries the same thing on the Bash that ships with macOS:

Negative subscripts in Bash 5.3, and the error from macOS Bash 3.2 Python · neg_session.py
from session import run

run("""
a=(mon tue wed thu fri)
# -1 is the last element, -2 the one before it (reading, Bash 4.2 and later).
echo "last: ${a[-1]}"
echo "second last: ${a[-2]}"
# Assigning through a negative subscript changes the last element (Bash 4.3 and later).
a[-1]=sat
echo "now: ${a[*]}"
# macOS ships Bash 3.2, which has no negative subscripts:
/bin/bash --version | head -1
/bin/bash -c 'a=(mon tue wed); echo "[${a[-1]}]"'
""")

Output

$ a=(mon tue wed thu fri)
# -1 is the last element, -2 the one before it (reading, Bash 4.2 and later).
$ echo "last: ${a[-1]}"
last: fri
$ echo "second last: ${a[-2]}"
second last: thu
# Assigning through a negative subscript changes the last element (Bash 4.3 and later).
$ a[-1]=sat
$ echo "now: ${a[*]}"
now: mon tue wed thu sat
# macOS ships Bash 3.2, which has no negative subscripts:
$ /bin/bash --version | head -1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
$ /bin/bash -c 'a=(mon tue wed); echo "[${a[-1]}]"'
/bin/bash: a: bad array subscript
[]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 neg_session.py

In Bash 5.3 ${a[-1]} gave fri and a[-1]=sat replaced the last element. The /bin/bash of macOS is 3.2.57, older than the feature, and there ${a[-1]} printed a: bad array subscript on standard error and expanded to nothing. Assigning to a far index, such as a[9]=late, is allowed too and leaves every index in between unset, another way an array becomes sparse.

Negative subscripts need Bash 4.2

Reading through a negative subscript needs Bash 4.2 and assigning through one 4.3; Bash 3.2.57 rejects both with “bad array subscript”. The shells and Bash versions lesson shows how to check which Bash a script is running under.

Checkpoint

Check: Negative subscripts

  1. One question a=(mon tue wed). What does echo "${a[-1]}" print in Bash 5.3?

    Choose one answer.

    Show the answer: One question

    Answer: wed

    The session read ${a[-1]} as fri on Bash 5.3 and showed the "bad array subscript" error only on /bin/bash 3.2.

Loop over values, or over indices when the array is sparse

To work through every element, loop over the values with for dir in "${a[@]}", the quoted form the for-loops lesson used so that each element arrives whole. When you need the position of each element, loop over the indices instead: ${!a[@]} expands to the subscripts that are set, and for i in "${!a[@]}" hands them over one at a time. The two loops matter because unset can leave an array sparse array: An indexed array whose set subscripts are not contiguous, usually because unset removed an element without renumbering, so its indices have a gap such as 0 2 3.: a hole where an element used to be. Step through one array as it gains, loses and renumbers elements:

Interactive · step by step Appending, unsetting and renumbering an array Step through a as it gains, loses and renumbers elements. Each box is one element; its sub-label is the index.
Appending, unsetting and renumbering an array, step 6 of 6. Showing north (index 0), south (index 2), west (index 3), north (index 0), south (index 1), west (index 2). In focus: north, south, west, north, south, west.northindex 0southindex 2westindex 3northindex 0southindex 1westindex 2

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Three elements. a=(north east south) holds three elements. Each sits at the next index, counting from 0, shown under its value.
  2. Append with +=. a+=(west) adds one element after the last, so west takes index 3. The array stays packed, 0 to 3.
  3. Unset one element. unset 'a[1]' removes east. Its slot at index 1 is gone, but the other elements keep the indices they had.
  4. The indices now have a gap. ${!a[@]} lists the indices that are set: 0 2 3, not 0 1 2. The array is sparse, with a hole where east was.
  5. A copy renumbers from zero. b=("${a[@]}") copies the three values into a fresh array, so they land at 0, 1, 2 with no gap.
  6. The rule. Appending and copying keep an array packed; unset leaves a gap that only a copy through "${a[@]}" closes.

unset 'a[1]' removed the element but not its slot, so the set indices became 0 2 3 rather than 0 1 2, and only a copy through "${a[@]}" closed the gap. sparse.sh shows both loops over such an array, and ends with the copy:

sparse.sh: looping over values and indices, then renumbering Shell · sparse.sh
#!/usr/bin/env bash
# sparse.sh: unsetting elements leaves gaps; a copy renumbers them.
a=(north east south west)

# Remove the element at index 1. Quote the subscript (see the lesson).
unset 'a[1]'

echo "values: ${a[*]}"
echo "indices: ${!a[*]}"

# Loop over the values: gaps do not matter, each value comes once.
for dir in "${a[@]}"; do
  echo "value $dir"
done

# Loop over the indices when you need the position, gaps and all.
for i in "${!a[@]}"; do
  echo "index $i holds ${a[i]}"
done

# A copy through "${a[@]}" renumbers the elements from 0.
b=("${a[@]}")
declare -p b
sparse.sh, and why unset needs a quoted subscript Python · sparse_session.py
from session import run, files

files("sparse.sh")

run("""
bash sparse.sh
# The glob trap: an unquoted subscript can be read as a filename pattern.
touch a1
a=(zero one two three)
# a[1] is a glob here: it matches the file a1, so unset deletes a variable named a1.
unset a[1]
echo "unquoted left: ${!a[@]}"
# Quote the subscript, and unset removes element 1 of the array.
unset 'a[1]'
echo "quoted left: ${!a[@]}"
""")

Output

$ bash sparse.sh
values: north south west
indices: 0 2 3
value north
value south
value west
index 0 holds north
index 2 holds south
index 3 holds west
declare -a b=([0]="north" [1]="south" [2]="west")
# The glob trap: an unquoted subscript can be read as a filename pattern.
$ touch a1
$ a=(zero one two three)
# a[1] is a glob here: it matches the file a1, so unset deletes a variable named a1.
$ unset a[1]
$ echo "unquoted left: ${!a[@]}"
unquoted left: 0 1 2 3
# Quote the subscript, and unset removes element 1 of the array.
$ unset 'a[1]'
$ echo "quoted left: ${!a[@]}"
quoted left: 0 2 3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sparse_session.py

Looping over the values printed north south west with no empty pass, while looping over the indices reported 0, 2 and 3, the real positions. The copy b=("${a[@]}") renumbered the three values to 0 1 2, which declare -p b confirmed.

Why unset 'a[i]' is quoted

Written bare, unset a[1] is a word with [1] in it, a glob pattern: A word containing *, ? or [ that Bash replaces with the sorted names of the files it matches; it stays as written when nothing matches, unless nullglob or failglob is set. More in “Globbing: wildcards, dotfiles, extglob, globstar”. With a file named a1 in the folder it matched that name, so the shell ran unset a1 and the array kept every element. The quotes in unset 'a[1]' stop the match, so unset sees the subscript and removes element 1.

With the subscript quoted, the two loops and the renumbering copy above are all you need to walk a sparse array and then pack it again.

Key idea

Loop over "${a[@]}" for the values and over "${!a[@]}" for the indices; after an unset the indices can have gaps, and only a copy through "${a[@]}" renumbers them from 0.

Checkpoint

Check: Loops and sparse arrays

  1. Question 1 of 2 a=(p q r s); unset 'a[1]'. What does echo "${!a[@]}" print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0 2 3

    sparse.sh removed index 1 from (north east south west) and ${!a[@]} reported 0 2 3.

  2. Question 2 of 2 A folder holds a file named a1. With a=(zero one two three), what does the bare command unset a[1] do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It matches the file a1, runs unset a1, and leaves the array unchanged

    In the session the bare unset a[1] left indices 0 1 2 3, while unset 'a[1]' left 0 2 3.

Arrays are a Bash feature, and quotes decide how they expand

Two subscripts expand every element, @ and *, and inside double quotes they differ. "${a[@]}" gives one word per element, each whole; "${a[*]}" joins them into a single word with the first character of IFS: The internal field separator: the characters Bash splits unquoted expansions on and read splits lines on; by default a space, a tab and a newline. More in “Word splitting and IFS: why "$var" needs quotes” between them, the comma below. Leaving the quotes off invites the word splitting: The step in which Bash cuts the unquoted results of parameter expansion, command substitution and arithmetic expansion into separate words at the characters of IFS. More in “Word splitting and IFS: why "$var" needs quotes” and globbing that ShellCheck warns about as SC2068. All of this is a Bash feature: the POSIX shell dash has no arrays at all. When an array has no elements, "${a[@]}" expands to nothing, so a command given it as arguments simply receives none.

Star against at, SC2068, dash with no arrays, and SC3030 in a sh script Python · dashsc_session.py
from session import run, write

write("report.sh", '#!/usr/bin/env bash\nfiles=(one "two words" three)\nfor f in ${files[@]}; do echo "$f"; done\n')
write("list.sh", '#!/bin/sh\nx=(foo bar)\necho "${x[0]}"\n')

run("""
a=(north east south west)
# "${a[*]}" joins the elements with the first character of IFS; "${a[@]}" keeps each whole.
IFS=,
echo "star: ${a[*]}"
printf 'at: %s\\n' "${a[@]}"
unset IFS
# ShellCheck flags the unquoted ${files[@]} loop in report.sh as SC2068.
!shellcheck report.sh
# dash has no arrays at all.
!dash -c 'a=(north east south)'
# In a #!/bin/sh script ShellCheck warns with SC3030.
!shellcheck list.sh
""")

Output

$ a=(north east south west)
# "${a[*]}" joins the elements with the first character of IFS; "${a[@]}" keeps each whole.
$ IFS=,
$ echo "star: ${a[*]}"
star: north,east,south,west
$ printf 'at: %s\n' "${a[@]}"
at: north
at: east
at: south
at: west
$ unset IFS
# ShellCheck flags the unquoted ${files[@]} loop in report.sh as SC2068.
$ shellcheck report.sh

In report.sh line 3:
for f in ${files[@]}; do echo "$f"; done
         ^---------^ SC2068 (error): Double quote array expansions to avoid re-splitting elements.

For more information:
  https://www.shellcheck.net/wiki/SC2068 -- Double quote array expansions to ...
[exit status 1]
# dash has no arrays at all.
$ dash -c 'a=(north east south)'
dash: 1: Syntax error: "(" unexpected
[exit status 2]
# In a #!/bin/sh script ShellCheck warns with SC3030.
$ shellcheck list.sh

In list.sh line 2:
x=(foo bar)
  ^-------^ SC3030 (warning): In POSIX sh, arrays are undefined.


In list.sh line 3:
echo "${x[0]}"
      ^-----^ SC3054 (warning): In POSIX sh, array references are undefined.

For more information:
  https://www.shellcheck.net/wiki/SC3030 -- In POSIX sh, arrays are undefined.
  https://www.shellcheck.net/wiki/SC3054 -- In POSIX sh, array references are...
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dashsc_session.py

With IFS=, the star form gave the single word north,east,south,west, while the at form gave four separate words. ShellCheck flagged the unquoted ${files[@]} as SC2068, an error, and in a #!/bin/sh script it warned about the array as SC3030 and about reading an element as SC3054. dash itself stopped at the opening parenthesis with Syntax error: "(" unexpected, which is why a script that uses arrays needs a Bash shebang, not #!/bin/sh.

Pick the expansion by what you want

Use "${a[@]}" to pass a list on to a command with every element intact, and "${a[*]}" only to build one string, setting IFS first to choose the separator. An unquoted ${a[@]} is almost never what you want.

Checkpoint

Check: How an array expands

  1. One question a=(a b c); IFS=,. What does echo "${a[*]}" print?

    Choose one answer.

    Show the answer: One question

    Answer: a,b,c

    The session set IFS=, and "${a[*]}" gave north,east,south,west, while "${a[@]}" stayed four separate words.

Practise: arrays that keep their shape

Write two functions that change an array in place through a nameref: A variable made with declare -n or local -n that stands for another variable, so every use of it reads or changes the variable it names. More in “Indirection, namerefs and ${var@Q} transforms” (local -n, from the namerefs lesson): rotate_left ARRAYNAME N, which moves the first N elements to the back, and remove_value ARRAYNAME VALUE, which drops every element equal to VALUE and renumbers the rest. Test them in your own shell and check the declare -p lines: tasks=(build test lint deploy ship) then rotate_left tasks 2 must give declare -a tasks=([0]="lint" [1]="deploy" [2]="ship" [3]="build" [4]="test"), and servers=(web1 db1 web2 db1 cache1) then remove_value servers db1 must give declare -a servers=([0]="web1" [1]="web2" [2]="cache1"). The self-check below puts these ideas onto arrays you have not met here:

Practice that adapts to you

Practise this lesson

12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 At which index does the first element of a Bash indexed array sit?

    Type a number.

    Show the answer: Question 1 of 12

    Answer: 0

    Indexed arrays are zero-based, so a=(zero one two) puts zero at index 0.

  2. Question 2 of 12 a=(zero one two). What does echo "$a[2]" print, with no braces?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: zero[2]

    The braces in ${a[subscript]} are what tell Bash to read an element; the lesson notes this under the first session.

  3. Question 3 of 12 a=(w x y z). What does echo "${a[@]:2}" print?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: y z

    ${a[@]:start} is ${a[@]:start:count} without the count, so it runs to the last element.

  4. Question 4 of 12 a=(one two three). Which assignment inserts mid so that a becomes one mid two three?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: a=("${a[@]:0:1}" mid "${a[@]:1}")

    queue.sh inserted scan the same way, with "${queue[@]:0:1}" scan "${queue[@]:1}".

  5. Question 5 of 12 From which Bash version can you read ${a[-1]}?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: Bash 4.2

    The Bash NEWS file records negative-subscript reads in 4.2 and assignment and unset in 4.3.

  6. Question 6 of 12 a=(apr may jun jul) in Bash 5.3. What does echo "${a[-2]}" print?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: jun

    Negative subscripts count back from the end, so -2 is the second-to-last element.

  7. Question 7 of 12 for dir in "${a[@]}"; do ...; done. What does dir hold on each pass?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: one element of the array, kept whole

    The for-loops lesson used exactly this quoted form so that each element arrives whole.

  8. Question 8 of 12 Why loop over "${!a[@]}" rather than counting 0 to ${#a[@]} - 1?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Because a sparse array has gaps, so its highest index can be larger than its count minus one

    For a packed array the two agree; after unset 'a[1]' the indices are 0 2 3, so only ${!a[@]} is right.

  9. Question 9 of 12 a=(p q r s); unset 'a[1]'; b=("${a[@]}"). What are the indices of b?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: 0 1 2

    In sparse.sh the copy b=("${a[@]}") gave declare -a b with indices 0 1 2.

  10. Question 10 of 12 What happens when dash runs a=(north east south)?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: It stops with Syntax error: "(" unexpected

    The session showed dash answering Syntax error: "(" unexpected, and ShellCheck flags arrays in sh as SC3030.

  11. Question 11 of 12 a=(one two three); IFS=:. What does echo "${a[*]}" print?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: one:two:three

    The lesson's session used IFS=, and "${a[*]}" joined with commas; a colon IFS joins with colons.

  12. Question 12 of 12 ShellCheck reads for f in ${files[@]}; do .... Which code does it report?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: SC2068, because the unquoted expansion re-splits and globs each element

    In the session ShellCheck marked for f in ${files[@]} as SC2068 (error).

Interview questions

Warm-up (fresher to 2–5 years): what does ${!a[@]} expand to, and when is it not simply 0 to n − 1?

${!a[@]} expands to the subscripts that currently have a value, in increasing order, not the values themselves. For an array built in one go, such as a=(zero one two three), those indices are exactly 0 1 2 3, the same as 0 to n − 1 for n elements, so looping over "${!a[@]}" and counting 0 to ${#a[@]} - 1 agree. They stop agreeing as soon as the array is sparse: after unset 'a[1]' the indices are 0 2 3, three elements but highest index 3, and b[9]=x on a new array b leaves a single element, at index 9. Whenever an array may have gaps, loop over "${!a[@]}" to get the real positions rather than assuming a contiguous 0 to n − 1.

The questions below go further: a job loop that stops halfway once it starts removing finished entries, and a last-element read that has to give the right answer on three Bash versions at once. Settle on an answer to each yourself first:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A cleanup loop for (( i=0; i < ${#jobs[@]}; i++ )); do handle "${jobs[i]}"; unset "jobs[i]"; done over jobs=(a b c d e f) handles only a, b and c and leaves d, e and f. Explain why, and give two correct ways.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A script must read the last element of an array that may be empty or sparse, on Bash 3.2, 4.1 and 5.3. Compare ${a[-1]}, ${a[${#a[@]}-1]} and ${a[@]: -1}, and choose a form that is correct on all three.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do you get one element of an indexed array, count its elements and append one?

    ${a[2]} reads the element at subscript 2, counting from 0; ${#a[@]} counts them; and a+=(x) appends one.

  2. What does ${a[@]:start:count} give?

    A copy of count elements beginning at subscript start, which is how a queue can be rotated or a run of items taken.

  3. What does ${a[-1]} give, and which Bash versions accept it?

    The last element. Reading a negative subscript works from Bash 4.2 and assigning one from 4.3; the Bash 3.2 of macOS answers bad array subscript.

  4. What does unset 'a[1]' leave behind?

    A sparse array: the other elements keep their subscripts, so the indices have a gap. "${!a[@]}" lists the indices that are set, and a copy b=("${a[@]}") renumbers from 0.

  5. When should a loop go over "${!a[@]}" instead of counting from 0?

    When the array may be sparse. "${!a[@]}" gives the subscripts that really have values, while counting 0 to ${#a[@]} - 1 misses or invents elements after an unset.

  6. Why should unset 'a[i]' be quoted?

    Unquoted, a[1] is a glob pattern. With a file named a1 in the folder it matched that name, so the shell ran unset a1 and the array kept every element; the quotes make unset see the subscript.

  7. How do "${a[@]}" and "${a[*]}" expand, and does sh have arrays?

    "${a[@]}" gives one word per element and "${a[*]}" joins them with the first character of IFS. Arrays are a Bash feature: dash rejects them and ShellCheck flags them as SC3030 in a #!/bin/sh script.

Key takeaways

  • An indexed array keeps many values under subscripts counting from 0; ${a[2]} reads one, ${#a[@]} counts them, a+=(x) appends, and ${a[@]:start:count} slices a run of elements.
  • Negative subscripts count back from the end, ${a[-1]} being the last element: reading works from Bash 4.2 and assigning from 4.3, but macOS Bash 3.2 answers bad array subscript.
  • unset 'a[i]' leaves a sparse array whose set indices have a gap; "${!a[@]}" lists those indices, and a copy b=("${a[@]}") renumbers the elements from 0.
  • Quote the subscript in unset 'a[i]' so [i] cannot act as a glob pattern that matches a file name.
  • "${a[@]}" expands to one word per element and "${a[*]}" joins them with the first character of IFS; arrays are a Bash feature, so dash rejects them and ShellCheck flags them as SC3030 in a #!/bin/sh script.

Terms in this lesson

array subscript
The part in brackets that selects one element, as in a[i]; in an indexed array Bash evaluates it as an arithmetic expression, and from Bash 4.2 a negative value counts back from the last element.
indexed array
A Bash variable that holds many values at once, each under a whole-number subscript counting from 0; written a=(one two) and read with ${a[i]}. dash has no arrays.
sparse array
An indexed array whose set subscripts are not contiguous, usually because unset removed an element without renumbering, so its indices have a gap such as 0 2 3.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.