Arrays
📚 Topic 8 — Arrays
Bash supports two kinds of arrays: indexed arrays (numbered from zero, like lists) and associative arrays (key-value pairs, like dictionaries). Both let you store multiple values in a single variable and iterate or look them up efficiently. This chapter covers creating, reading, modifying, and deleting array elements; slicing and copying arrays; sorting; splitting strings into arrays; and the important patterns for passing arrays in and out of functions.
1 — Indexed Arrays
An indexed array is a zero-based numbered list of values. Elements can be added, read, updated, or removed individually.
#!/bin/bash
# Method 1: assign all elements at once with ( )
fruits=( apple banana cherry mango )
# Method 2: declare first, then assign individually
declare -a colours
colours[0]="red"
colours[1]="green"
colours[2]="blue"
# Method 3: build from command output
files=( $(ls /etc/*.conf) ) # caution: word-splits on spaces in names
files=()
while IFS= read -r -d '' f; do # safer: null-delimited via find
files+=( "$f" )
done < <(find /etc -name "*.conf" -print0)
Reading Array Elements
fruits=( apple banana cherry mango )
# Single element — MUST use curly braces
echo "${fruits[0]}" → apple
echo "${fruits[2]}" → cherry
# Last element
echo "${fruits[-1]}" → mango (bash 4.2+)
echo "${fruits[${#fruits[@]}-1]}" → mango (portable)
# All elements as separate words — use in loops and commands
echo "${fruits[@]}" → apple banana cherry mango
# All elements as a single string (joined by first char of IFS)
echo "${fruits[*]}" → apple banana cherry mango
# Number of elements
echo "${#fruits[@]}" → 4
# Length of a specific element
echo "${#fruits[1]}" → 6 (length of "banana")
# All indices (useful when array may have gaps)
echo "${!fruits[@]}" → 0 1 2 3
"${arr[@]}" (not ${arr[*]}) in loops and command arguments — it keeps elements with spaces intact as separate items.$fruits (no brackets) is equivalent to ${fruits[0]} — it gives only the first element and silently discards the rest. Always use ${fruits[@]} to mean "all elements".
Slicing an Array
letters=( a b c d e f g )
# ${arr[@]:offset:length}
echo "${letters[@]:2:3}" → c d e (3 elements from index 2)
echo "${letters[@]:4}" → e f g (from index 4 to end)
echo "${letters[@]: -2}" → f g (last 2 elements)
# Copy a slice into a new array
middle=( "${letters[@]:2:3}" )
echo "${middle[@]}" → c d e
2 — Modifying Arrays
arr=( one two three )
# Append one element
arr+=( four )
echo "${arr[@]}" → one two three four
# Append multiple elements
arr+=( five six )
echo "${arr[@]}" → one two three four five six
# Update a specific element
arr[1]="TWO"
echo "${arr[@]}" → one TWO three four five six
# Remove an element by index — leaves a gap (sparse array)
unset 'arr[2]'
echo "${arr[@]}" → one TWO four five six
echo "${!arr[@]}" → 0 1 3 4 5 (index 2 is missing!)
# Re-index after deletion to close gaps
arr=( "${arr[@]}" )
echo "${!arr[@]}" → 0 1 2 3 4 (gap closed)
# Remove the entire array
unset arr
Concatenating Arrays
a=( one two three )
b=( four five six )
# Merge by expanding both into a new array
combined=( "${a[@]}" "${b[@]}" )
echo "${combined[@]}"
one two three four five six
# Prepend elements
a=( zero "${a[@]}" )
echo "${a[@]}"
zero one two three
3 — Iterating Over Arrays
planets=( Mercury Venus Earth Mars Jupiter Saturn )
# Pattern 1: iterate over values (most common)
for planet in "${planets[@]}"; do
echo "Planet: $planet"
done
# Pattern 2: iterate over indices (needed when index matters)
for i in "${!planets[@]}"; do
printf "%d: %s\n" "$i" "${planets[$i]}"
done
0: Mercury
1: Venus
...
# Pattern 3: C-style — fine when array has no gaps
for (( i=0; i<${#planets[@]}; i++ )); do
echo "${planets[$i]}"
done
# Pattern 4: iterate over a sparse array safely (use indices)
sparse=()
sparse[0]="first"
sparse[5]="sixth"
sparse[10]="eleventh"
for i in "${!sparse[@]}"; do
echo "[$i] = ${sparse[$i]}"
done
[0] = first
[5] = sixth
[10] = eleventh
4 — Useful Array Operations
Sorting
Bash has no built-in array sort — use sort and capture the output.
names=( Charlie Alice Dave Bob Eve )
# Sort alphabetically
sorted=( $(printf "%s\n" "${names[@]}" | sort) )
echo "${sorted[@]}"
Alice Bob Charlie Dave Eve
# Sort in reverse
rsorted=( $(printf "%s\n" "${names[@]}" | sort -r) )
# Sort numerically
nums=( 20 3 100 15 7 )
nsorted=( $(printf "%s\n" "${nums[@]}" | sort -n) )
echo "${nsorted[@]}"
3 7 15 20 100
$( printf ... | sort ) approach uses word splitting to rebuild the array, so it breaks on element values containing spaces. For elements with spaces, pipe through sort using null delimiters or use a different approach.Removing Duplicates
tags=( bash linux bash python linux shell python )
# Sort and deduplicate with sort -u
unique=( $(printf "%s\n" "${tags[@]}" | sort -u) )
echo "${unique[@]}"
bash linux python shell
# Deduplicate while preserving original order (using associative array as a set)
declare -A _seen
ordered_unique=()
for tag in "${tags[@]}"; do
if [[ -z "${_seen[$tag]+x}" ]]; then
_seen["$tag"]=1
ordered_unique+=( "$tag" )
fi
done
echo "${ordered_unique[@]}"
bash linux python shell # first-seen order preserved
Searching an Array
allowed=( read write execute admin )
# Search function — returns 0 if found, 1 if not
in_array() {
local needle="$1"; shift
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
if in_array "write" "${allowed[@]}"; then
echo "'write' is allowed"
fi
'write' is allowed
if ! in_array "delete" "${allowed[@]}"; then
echo "'delete' is NOT in the list"
fi
5 — Associative Arrays
Associative arrays (bash 4.0+) use arbitrary string keys instead of integers. They behave like dictionaries or hash maps in other languages.
#!/bin/bash
# Must use declare -A — this is not optional
declare -A capitals
# Assign key-value pairs
capitals["France"]="Paris"
capitals["Japan"]="Tokyo"
capitals["Hungary"]="Budapest"
capitals["UK"]="London"
# Or all at once:
declare -A capitals=(
["France"]="Paris"
["Japan"]="Tokyo"
["Hungary"]="Budapest"
["UK"]="London"
)
# Read a value by key
echo "Capital of France : ${capitals[France]}"
Capital of France : Paris
# Number of entries
echo "Count: ${#capitals[@]}"
Count: 4
# All values
echo "${capitals[@]}"
Paris Tokyo Budapest London (order is not guaranteed)
# All keys
echo "${!capitals[@]}"
France Japan Hungary UK
Iterating Over Associative Arrays
declare -A scores=( [Alice]=92 [Bob]=78 [Carol]=85 [Dave]=91 )
# Iterate over keys, access values
for name in "${!scores[@]}"; do
printf "%-10s %d\n" "$name" "${scores[$name]}"
done
# Sorted by key
for name in $(printf "%s\n" "${!scores[@]}" | sort); do
printf "%-10s %d\n" "$name" "${scores[$name]}"
done
Alice 92
Bob 78
Carol 85
Dave 91
Checking Whether a Key Exists
declare -A config=( [host]="localhost" [port]="8080" )
# ${var+x} expands to "x" if the key exists, empty if not
if [[ -n "${config[host]+x}" ]]; then
echo "host is set: ${config[host]}"
fi
host is set: localhost
if [[ -z "${config[timeout]+x}" ]]; then
echo "timeout key does not exist"
fi
timeout key does not exist
# Delete a key
unset 'config[port]'
echo "${!config[@]}"
host
6 — Splitting Strings into Arrays
Two common techniques turn a delimited string into an array — read -a with a here-string, and IFS-based word splitting.
# read -a with a here-string — split on IFS
csv="apple,banana,cherry"
IFS=',' read -r -a items <<< "$csv"
echo "${items[@]}" → apple banana cherry
echo "${items[1]}" → banana
echo "${#items[@]}" → 3
# Split a colon-delimited string (like PATH)
IFS=':' read -r -a path_dirs <<< "$PATH"
for dir in "${path_dirs[@]}"; do
echo "$dir"
done
# Split on whitespace using ( $(...) ) — quick but breaks on spaces in values
words=( $(echo "one two three") )
echo "${#words[@]}" → 3
# Convert a multi-line string to an array (one line per element)
multiline="first line
second line
third line"
IFS=$'\n' read -r -d '' -a lines <<< "$multiline"
echo "${#lines[@]}" → 3
echo "${lines[1]}" → second line
7 — Arrays and Functions
Bash does not pass arrays to functions directly — when you write func "${my_array[@]}", the function receives a flat list of arguments, not an array object. There are three clean patterns for working around this.
sum_array() {
local total=0
for val in "$@"; do
(( total += val ))
done
echo "$total"
}
numbers=( 5 10 15 20 )
total=$(sum_array "${numbers[@]}")
echo "Total: $total"
Total: 50
# Works well when the function only needs to read the values.
# Limitation: you cannot pass two arrays this way without a separator.
print_array() {
local -n _arr="$1" # nameref — _arr IS the caller's array
local i
for i in "${!_arr[@]}"; do
printf " [%s] %s\n" "$i" "${_arr[$i]}"
done
}
fruits=( apple banana cherry )
print_array fruits # pass the NAME, not ${fruits[@]}
[0] apple
[1] banana
[2] cherry
# Works for both indexed and associative arrays.
declare -A config=( [host]="localhost" [port]="8080" )
print_array config
get_even_numbers() {
local -n _result="$1" # output array — write via nameref
local -i max="$2"
_result=() # clear it first
for (( i=2; i<=max; i+=2 )); do
_result+=( "$i" )
done
}
get_even_numbers evens 20
echo "${evens[@]}"
2 4 6 8 10 12 14 16 18 20
8 — Practical Examples
KEY=VALUE config file into an associative array.#!/bin/bash
declare -A cfg
# Read config.ini: host=localhost port=8080 debug=true
while IFS='=' read -r key val; do
[[ "$key" == \#* || -z "$key" ]] && continue # skip comments/blanks
cfg["${key// /}"]="${val// /}" # trim spaces from key/val
done < config.ini
echo "Host : ${cfg[host]}"
echo "Port : ${cfg[port]}"
stack=()
push() { stack+=( "$1" ); }
pop() {
local -n _out="$1"
[[ "${#stack[@]}" -eq 0 ]] && { echo "Stack empty" >&2; return 1; }
_out="${stack[-1]}"
unset 'stack[-1]'
}
push "first"
push "second"
push "third"
pop item; echo "Popped: $item"
Popped: third
pop item; echo "Popped: $item"
Popped: second
9 — Quick Reference
Indexed Arrays
| Syntax | What it does |
|---|---|
arr=(a b c) | Create indexed array |
declare -a arr | Declare (empty) indexed array |
${arr[n]} | Element at index n |
${arr[-1]} | Last element (bash 4.2+) |
${arr[@]} | All elements (each properly quoted) |
${arr[*]} | All elements joined as one string |
${#arr[@]} | Number of elements |
${!arr[@]} | All indices |
${arr[@]:i:n} | Slice: n elements starting at index i |
arr+=(x y) | Append element(s) |
arr[n]=val | Set/update element at index n |
unset 'arr[n]' | Remove element (leaves sparse gap) |
arr=("${arr[@]}") | Re-index to close sparse gaps |
unset arr | Delete the entire array |
Associative Arrays
| Syntax | What it does |
|---|---|
declare -A map | Declare associative array (required) |
map[key]=val | Set a key-value pair |
${map[key]} | Read a value by key |
${map[@]} | All values |
${!map[@]} | All keys |
${#map[@]} | Number of entries |
${map[key]+x} | Non-empty if key exists |
unset 'map[key]' | Remove a key |
✏️ Exercises
Apply what you have learned in this chapter. Try each exercise yourself before looking at the sample solution.
word_count.sh that reads a sentence from the user, splits it into an array of words, prints the total word count, lists each word with its index, and then prints the unique words in alphabetical order.read -r -a words to split on spaces. Use ${!words[@]} for indices. Pipe "${words[@]}" through printf "%s\n" | sort -u for unique sorted words.#!/bin/bash
# word_count.sh
read -r -p "Enter a sentence: " sentence
read -r -a words <<< "$sentence"
echo "Word count: ${#words[@]}"
echo "── All words ──"
for i in "${!words[@]}"; do
printf " [%d] %s\n" "$i" "${words[$i]}"
done
echo "── Unique words (sorted) ──"
printf "%s\n" "${words[@]}" | sort -u | while read -r w; do
printf " %s\n" "$w"
done
phone_book.sh that uses an associative array to store names and phone numbers. The script should support three operations via command-line arguments: add NAME NUMBER, lookup NAME, and list (prints all entries sorted by name). Store the data in a plain text file (phonebook.dat) between runs by writing and reading the array to/from it.declare -p phonebook > phonebook.dat and restore with source phonebook.dat. Use a case statement on $1 for the three operations. Check if the file exists before sourcing.#!/bin/bash
# phone_book.sh
DATA_FILE="phonebook.dat"
declare -A phonebook
# Load existing data if available
[[ -f "$DATA_FILE" ]] && source "$DATA_FILE"
save() { declare -p phonebook > "$DATA_FILE"; }
case "$1" in
add)
[[ -z "$2" || -z "$3" ]] && { echo "Usage: $0 add NAME NUMBER"; exit 1; }
phonebook["$2"]="$3"
save
echo "Added: $2 → $3"
;;
lookup)
[[ -z "$2" ]] && { echo "Usage: $0 lookup NAME"; exit 1; }
if [[ -n "${phonebook[$2]+x}" ]]; then
echo "$2: ${phonebook[$2]}"
else
echo "Not found: $2"
fi
;;
list)
if [[ "${#phonebook[@]}" -eq 0 ]]; then
echo "Phone book is empty."
else
printf "%-20s %s\n" "Name" "Number"
printf '%.0s─' {1..35}; echo
for name in $(printf "%s\n" "${!phonebook[@]}" | sort); do
printf "%-20s %s\n" "$name" "${phonebook[$name]}"
done
fi
;;
*)
echo "Usage: $0 {add NAME NUMBER | lookup NAME | list}"
exit 1
;;
esac
stats.sh that accepts a list of numbers as command-line arguments, stores them in an array, and calculates and prints: the count, the sum, the minimum, the maximum, and the mean (to 2 decimal places). Test with: ./stats.sh 15 3 42 8 27 19 6"$@" to build the array and accumulate the sum. Track min and max by comparing each element. Use bc for the mean division.#!/bin/bash
# stats.sh — usage: ./stats.sh 15 3 42 8 27 19 6
[[ $# -eq 0 ]] && { echo "Usage: $0 number [number ...]"; exit 1; }
nums=( "$@" )
sum=0
min="${nums[0]}"
max="${nums[0]}"
for n in "${nums[@]}"; do
(( sum += n ))
(( n < min )) && min=$n
(( n > max )) && max=$n
done
count="${#nums[@]}"
mean=$(echo "scale=2; $sum / $count" | bc)
printf "Count : %d\n" "$count"
printf "Sum : %d\n" "$sum"
printf "Min : %d\n" "$min"
printf "Max : %d\n" "$max"
printf "Mean : %s\n" "$mean"
inventory.sh that uses two parallel associative arrays — one mapping item names to quantities, another mapping item names to unit prices — to manage a simple inventory. Implement add, sell (reduce quantity), and report commands. The report should print a formatted table showing each item, its quantity, unit price, and total value, plus a grand total at the bottom.declare -A qty and declare -A price. Persist both with declare -p. For the report, iterate over sorted keys of qty and multiply ${qty[$item]} by ${price[$item]} using bc.#!/bin/bash
# inventory.sh — usage: ./inventory.sh {add NAME QTY PRICE | sell NAME QTY | report}
DATA="inventory.dat"
declare -A qty
declare -A price
[[ -f "$DATA" ]] && source "$DATA"
save() {
{ declare -p qty; declare -p price; } > "$DATA"
}
case "$1" in
add)
qty["$2"]=$(( ${qty[$2]:-0} + $3 ))
price["$2"]="$4"
save
echo "Added $3 × $2 @ £$4 each."
;;
sell)
if (( ${qty[$2]:-0} < $3 )); then
echo "Insufficient stock (have ${qty[$2]:-0})." >&2; exit 1
fi
qty["$2"]=$(( qty[$2] - $3 ))
save
echo "Sold $3 × $2. Remaining: ${qty[$2]}."
;;
report)
grand="0"
printf "%-20s %6s %8s %10s\n" "Item" "Qty" "Price" "Value"
printf '%.0s─' {1..48}; echo
for item in $(printf "%s\n" "${!qty[@]}" | sort); do
val=$(echo "scale=2; ${qty[$item]} * ${price[$item]}" | bc)
grand=$(echo "$grand + $val" | bc)
printf "%-20s %6d %8.2f %10.2f\n" \
"$item" "${qty[$item]}" "${price[$item]}" "$val"
done
printf '%.0s─' {1..48}; echo
printf "%-36s %10.2f\n" "TOTAL VALUE" "$grand"
;;
*)
echo "Usage: $0 {add NAME QTY PRICE | sell NAME QTY | report}"
;;
esac