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.

🐧 Creating and populating indexed arrays
#!/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

🐧 Accessing elements by index
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
Always use "${arr[@]}" (not ${arr[*]}) in loops and command arguments — it keeps elements with spaces intact as separate items.
⚠️ $fruits without brackets is not an array. Writing $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

🐧 Extracting a sub-range of elements
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

🐧 Append, update, and remove elements
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

🐧 Merging arrays together
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

🐧 Loop patterns for 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.

🐧 Sorting an array
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
The $( 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

🐧 Deduplicating an array
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

🐧 Checking if a value exists in 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.

🐧 Creating and using associative arrays
#!/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
Associative arrays do not have a defined order — the iteration order of keys can change between bash versions and runs. If order matters, maintain a separate indexed array of keys.

Iterating Over Associative Arrays

🐧 Looping over key-value pairs
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

🐧 Key existence check
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.

🐧 String-to-array conversion
# 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.

🐧 Pattern 1: pass elements as arguments
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.
🐧 Pattern 2: pass the array name, use nameref (bash 4.3+)
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
🐧 Pattern 3: return an array via nameref
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

Associative array as a config file parser
Read a simple 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 using an indexed array
Arrays naturally implement push/pop stack behaviour.
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

SyntaxWhat it does
arr=(a b c)Create indexed array
declare -a arrDeclare (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]=valSet/update element at index n
unset 'arr[n]'Remove element (leaves sparse gap)
arr=("${arr[@]}")Re-index to close sparse gaps
unset arrDelete the entire array

Associative Arrays

SyntaxWhat it does
declare -A mapDeclare associative array (required)
map[key]=valSet 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.

Exercise 1
Write a script called 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.
Hint: use read -r -a words to split on spaces. Use ${!words[@]} for indices. Pipe "${words[@]}" through printf "%s\n" | sort -u for unique sorted words.
Sample Solution
#!/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
Exercise 2
Write a script called 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.
Hint: save to file with 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.
Sample Solution
#!/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
Exercise 3
Write a script called 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
Hint: loop over "$@" to build the array and accumulate the sum. Track min and max by comparing each element. Use bc for the mean division.
Sample Solution
#!/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"
Exercise 4
Write a script called 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.
Hint: use 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.
Sample Solution
#!/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