Bash · lesson 9 of 22
Bash Arrays
Work with Bash indexed and associative arrays: slicing, quoting with [@] vs [*], mapfile, filtering, sorting, and passing arrays to functions.
By SkillByExample · Updated
The data-types lesson introduced arrays. This one covers the everyday operations: loops,
reading files, filtering, sorting, and passing arrays around. Every example is a complete
script, run with bash 5.2 on Ubuntu 24.04.
Growing, Slicing, Removing
fruits=("apple" "banana" "cherry")
fruits+=("date" "elderberry" "fig")
echo "count=${#fruits[@]} last=${fruits[-1]}"
echo "slice 1:3 -> ${fruits[*]:1:3}"
echo "last two -> ${fruits[*]: -2}"
unset 'fruits[1]'
echo "indices after unset: ${!fruits[*]}"
fruits=("${fruits[@]}")
echo "indices after re-pack: ${!fruits[*]}"
$ bash basics.sh
count=6 last=fig
slice 1:3 -> banana cherry date
last two -> elderberry fig
indices after unset: 0 2 3 4 5
indices after re-pack: 0 1 2 3 4
+=( ... ) appends any number of elements. unset leaves a hole in the indices, and
re-assigning the array from "${fruits[@]}" packs them together again. That is also how
you copy an indexed array.
[@] vs [*], Quoted and Not
How you expand an array decides how many words the command receives.
files=("report.txt" "my notes.txt" "todo.txt")
echo "unquoted \${files[@]}:"
for f in ${files[@]}; do echo " [$f]"; done
echo "quoted \"\${files[@]}\":"
for f in "${files[@]}"; do echo " [$f]"; done
echo "quoted \"\${files[*]}\":"
for f in "${files[*]}"; do echo " [$f]"; done
$ bash quoting.sh
unquoted ${files[@]}:
[report.txt]
[my]
[notes.txt]
[todo.txt]
quoted "${files[@]}":
[report.txt]
[my notes.txt]
[todo.txt]
quoted "${files[*]}":
[report.txt my notes.txt todo.txt]
Only "${files[@]}" gives one word per element. Unquoted, my notes.txt splits in two.
"${files[*]}" joins everything into one string, which is useful for display and nothing
else. When you need the positions as well as the values, loop over "${!arr[@]}":
servers=("web01" "web02" "db01")
for i in "${!servers[@]}"; do
echo "$i: ${servers[$i]}"
done
$ bash index-loop.sh
0: web01
1: web02
2: db01
Reading Lines with mapfile
mapfile (also called readarray) reads input into an array, one element per line.
printf 'alpha\nbeta gamma\n\ndelta\n' > hosts.txt
mapfile -t lines < hosts.txt
echo "with -t: ${#lines[@]} lines"
printf ' [%s]\n' "${lines[@]}"
mapfile no_t < hosts.txt
[[ ${no_t[0]} == "alpha" ]] && echo "without -t: equal" || echo "without -t: 'alpha' != '${no_t[0]%$'\n'}\\n'"
$ bash mapfile.sh
with -t: 4 lines
[alpha]
[beta gamma]
[]
[delta]
without -t: 'alpha' != 'alpha\n'
beta gamma stayed a single element and the blank line was kept. Without -t, every
element keeps its trailing newline, so an ordinary string comparison quietly fails. Always
pass -t. For command output, use mapfile -t arr < <(command).
Filter, Join, Sort
servers=("web01" "web02" "db01" "db02" "cache01")
db=()
for s in "${servers[@]}"; do
[[ $s == db* ]] && db+=("$s")
done
echo "filtered: ${db[*]}"
join_by() { local d="$1" first="$2"; shift 2; printf '%s' "$first" "${@/#/$d}"; echo; }
join_by ', ' "${servers[@]}"
nums=(10 2 30 4 20)
mapfile -t text_sorted < <(printf '%s\n' "${nums[@]}" | sort)
mapfile -t num_sorted < <(printf '%s\n' "${nums[@]}" | sort -n)
echo "sort: ${text_sorted[*]}"
echo "sort -n: ${num_sorted[*]}"
$ bash ops.sh
filtered: db01 db02
web01, web02, db01, db02, cache01
sort: 10 2 20 30 4
sort -n: 2 4 10 20 30
join_by uses ${@/#/$d} to prefix every remaining argument with the delimiter. Bash has
no array sort, so you print one element per line, pipe the lines through sort, and read
them back with mapfile. Plain sort compares text, which puts 10 before 2. Numbers
need sort -n.
Returning a List: Where arr=($(...)) Breaks
A function can return a list by printing one item per line. How you capture that output matters:
dedup() {
local -A seen=()
local item
for item in "$@"; do
[[ -v seen[$item] ]] || { seen[$item]=1; printf '%s\n' "$item"; }
done
}
items=("a" "new york" "a" "b" "new york")
broken=($(dedup "${items[@]}"))
echo "word-split capture: ${#broken[@]} elements"
printf ' [%s]\n' "${broken[@]}"
mapfile -t fixed < <(dedup "${items[@]}")
echo "mapfile capture: ${#fixed[@]} elements"
printf ' [%s]\n' "${fixed[@]}"
$ bash dedup.sh
word-split capture: 4 elements
[a]
[new]
[york]
[b]
mapfile capture: 3 elements
[a]
[new york]
[b]
The deduplication worked in both cases. The arr=($(...)) capture then split new york
in two, because unquoted command substitution splits on every space and also expands
globs. The associative array seen works as a set, and mapfile -t keeps each line as
exactly one element.
Passing Arrays to Functions
"${arr[@]}" passes the elements, not the array. To let a function read the array, or
change it, pass its name and use a nameref.
print_array() {
local -n arr="$1"
echo "$1 has ${#arr[@]} elements:"
for i in "${!arr[@]}"; do echo " [$i] = ${arr[$i]}"; done
}
add_item() {
local -n target="$1"
target+=("$2")
}
fruits=("apple" "banana")
add_item fruits "cherry"
print_array fruits
$ bash pass.sh
fruits has 3 elements:
[0] = apple
[1] = banana
[2] = cherry
add_item changed the caller’s array directly, with no subshell and nothing to capture.
Give the nameref a name the caller is unlikely to use. A nameref called arr that is
handed a variable also called arr is a circular reference.
Practice
-
Reverse
arr=(one two three four)into a new array.Solution
arr=(one two three four) rev=() for (( i = ${#arr[@]} - 1; i >= 0; i-- )); do rev+=("${arr[i]}") done echo "${rev[*]}"$ bash p1.sh four three two one -
Process
hosts=(h1 h2 h3 h4 h5)in batches of 2 using array slicing.Solution
hosts=(h1 h2 h3 h4 h5) size=2 for (( i = 0; i < ${#hosts[@]}; i += size )); do echo "batch: ${hosts[*]:i:size}" done$ bash p2.sh batch: h1 h2 batch: h3 h4 batch: h5A slice that runs past the end just returns whatever is left, so the last batch is shorter.
-
Remove every
bananafrom("apple" "banana" "cherry" "banana")and leave no gaps in the indices.Solution
fruits=("apple" "banana" "cherry" "banana") kept=() for f in "${fruits[@]}"; do [[ $f == banana ]] || kept+=("$f") done fruits=("${kept[@]}") echo "${#fruits[@]}: ${fruits[*]}"$ bash p3.sh 2: apple cherry -
Copy an associative array, then change the copy. Show that the original is untouched.
Solution
declare -A src=([host]=db [port]=5432) declare -A copy=() for k in "${!src[@]}"; do copy[$k]=${src[$k]}; done copy[port]=6543 echo "src port=${src[port]} copy port=${copy[port]} copy host=${copy[host]}"$ bash p4.sh src port=5432 copy port=6543 copy host=db