Bash Arrays — Comprehensive Guide
A comprehensive guide to working with arrays in Bash — declaring, accessing, iterating, modifying, sorting, filtering, and passing arrays to functions
Arrays are one of the most useful data structures in Bash. They allow storing multiple values in a single variable, iterating over lists, and building more structured scripts. This post covers the most common array operations with practical examples — all verified against shellcheck.
Declaring Arrays
There are several ways to declare arrays in Bash.
Inline declaration:
array=("apple" "banana" "cherry")Declare empty first, then assign by index:
declare -a array
array[0]="first"
array[1]="second"
array[2]="third"From a string using IFS (Input Field Separator):
string="car, bus, bike"
IFS=', ' read -r -a array <<< "${string}"The IFS=', ' sets both comma and space as separators. This is a common pattern when splitting CSV-like strings.
From command output — use mapfile to avoid word splitting issues with filenames containing spaces:
mapfile -t array < <(printf '%s\n' /tmp/*)Associative array (key-value pairs, bash 4+):
declare -A assoc
assoc["name"]="Krzysztof"
assoc["role"]="DevOps"
assoc["location"]="Poland"Accessing Elements
fruits=("apple" "banana" "cherry" "date" "elderberry")
echo "${fruits[0]}" # first element: apple
echo "${fruits[-1]}" # last element: elderberry
echo "${fruits[-2]}" # second to last: date
echo "${fruits[*]}" # all elements
echo "${#fruits[@]}" # array length: 5
echo "${#fruits[0]}" # length of first element: 5Note the difference between [@] and [*]:
"${array[@]}"— expands each element as a separate word (use when iterating)"${array[*]}"— expands all elements as a single word joined by IFS (use in echo)
Slicing
echo "${fruits[*]:1}" # all elements from index 1
echo "${fruits[*]:1:2}" # 2 elements starting at index 1
echo "${fruits[*]: -2}" # last 2 elements (space before - is required)The space before -2 in the last example is mandatory — without it, Bash interprets :- as the default value operator.
Iterating
Over values:
for element in "${fruits[@]}"; do
echo "${element}"
doneOver indexes:
for index in "${!fruits[@]}"; do
echo "${index}: ${fruits[${index}]}"
doneOver an associative array:
for key in "${!assoc[@]}"; do
echo "${key}: ${assoc[${key}]}"
doneC-style loop:
for (( i=0; i<${#fruits[@]}; i++ )); do
echo "${i}: ${fruits[${i}]}"
doneModifying Arrays
colors=("red" "green" "blue")
# Append single element
colors+=("yellow")
# Append multiple elements
colors+=("purple" "orange")
# Modify element by index
colors[0]="crimson"
# Insert at specific index
colors=("${colors[@]:0:2}" "white" "${colors[@]:2}")Removing Elements
Remove by index — leaves a gap in indexes:
unset 'animals[2]'Re-index after unset — removes the gaps:
animals=("${animals[@]}")Remove by value — use mapfile and grep to avoid word splitting:
target="dog"
mapfile -t animals < <(printf '%s\n' "${animals[@]}" | grep -v "^${target}$") || trueRemove last element:
unset 'animals[-1]'Clear entire array:
unset animals
if [[ ${#animals[@]} -eq 0 ]]; then
echo "empty"
fiNote: checking ${var[@]:-default} doesn’t work reliably on arrays — always check the length with ${#array[@]}.
Searching and Filtering
Check if value exists:
search="docker"
found=false
for item in "${services[@]}"; do
if [[ "${item}" == "${search}" ]]; then
found=true
break
fi
doneFind index of element:
target="mongod"
for i in "${!services[@]}"; do
if [[ "${services[${i}]}" == "${target}" ]]; then
echo "Index: ${i}"
break
fi
doneFilter — keep only matching elements:
filtered=()
for item in "${services[@]}"; do
if [[ "${item}" == *"o"* ]]; then
filtered+=("${item}")
fi
doneSorting
Use mapfile with sort — avoids word splitting issues:
# Sort ascending
mapfile -t sorted < <(printf '%s\n' "${unsorted[@]}" | sort) || true
# Sort descending
mapfile -t sorted_desc < <(printf '%s\n' "${unsorted[@]}" | sort -r) || true
# Sort numerically
mapfile -t sorted_num < <(printf '%s\n' "${numbers[@]}" | sort -n) || trueThe || true is needed because mapfile reading from a process substitution can return a non-zero exit code when the pipeline finishes, which would cause the script to exit if set -e is enabled.
Array Operations
Merge two arrays:
merged=("${arr1[@]}" "${arr2[@]}")Copy an array (true copy, not a reference):
copy=("${original[@]}")Reverse:
reversed=()
for (( i=${#original[@]}-1; i>=0; i-- )); do
reversed+=("${original[${i}]}")
doneRemove duplicates:
mapfile -t unique < <(printf '%s\n' "${with_dupes[@]}" | sort -u) || trueString Manipulation on Elements
Uppercase all elements:
mapfile -t upper < <(printf '%s\n' "${envs[@]}" | tr '[:lower:]' '[:upper:]') || trueReplace substring in all elements:
replaced=("${envs[@]/_service/}")Add prefix to all elements:
prefixed=("${envs[@]/#/aws_}")Add suffix to all elements:
suffixed=("${envs[@]/%/_v2}")The /#/ and /%/ patterns are parameter expansion operators — # matches the beginning of each element, % matches the end.
Passing Arrays to Functions
Bash doesn’t support passing arrays directly to functions. The cleanest approach is to pass the array name as a string and use a nameref (local -n) inside the function to reference the original array:
print_array() {
local varname
varname="${1}"
local -n arr
arr="${varname}"
echo "Length: ${#arr[@]}"
for item in "${arr[@]}"; do
echo " - ${item}"
done
}
sum_array() {
local varname
varname="${1}"
local -n nums
nums="${varname}"
local total
total=0
for n in "${nums[@]}"; do
total=$(( total + n ))
done
echo "Sum: ${total}"
}
my_array=("terraform" "ansible" "docker")
my_numbers=(1 2 3 4 5)
print_array my_array
sum_array my_numbersNote that local and local -n declarations are separated from their assignments to avoid shellcheck warning SC2034 about masked return values.
Returning Arrays from Functions
The same nameref pattern works for returning arrays — the function writes into the caller’s variable via the nameref:
get_running_services() {
local varname
varname="${1}"
local -n result
result="${varname}"
result=("nginx" "docker" "sshd")
}
declare -a running
get_running_services running
echo "${running[*]}"ShellCheck Notes
A few patterns that keep shellcheck happy:
- Use
mapfile -tinstead ofarray=($(cmd))— avoids SC2207 word splitting warning - Always quote array expansions:
"${array[@]}"not${array[@]} - Use
${array[*]}inechoand${array[@]}when iterating - Separate
local varfromvar=value— avoids SC2155 masked return value warning - Use
unset 'array[index]'with quotes — avoids SC2184 glob expansion warning - Check array emptiness with
${#array[@]} -eq 0not${array[@]:-default}
Related Posts
Logging Into a Container Running on ECS Fargate
How to open an interactive shell inside a running Fargate container using ECS Exec and the AWS CLI
Shrinking a MongoDB Dump
How to reduce the size of a MongoDB dump using a temporary EC2 instance, mongorestore, and mongodump
FlairOps — Personal DevOps Site Built with Hugo
How I built my personal DevOps site using Hugo, Minimal Black theme, GitHub Actions, and GitHub Pages with a custom domain