Skip to main content
Bash intermediate Lesson 9 of 22

Bash Arrays

Work with indexed and associative arrays in Bash — creation, access, slicing, looping, and reading files with mapfile.

Indexed Arrays

Arrays solve the problem of managing a collection of values as a single unit. Without arrays, you would have to create separate variables like server1, server2, server3 — which makes iteration impossible and maintenance painful. Arrays let you loop over a list, pass it to a function, and manipulate it as a whole.

# Declare and initialize in one step
fruits=("apple" "banana" "cherry" "date")

# Declare first, then assign by index
declare -a servers
servers[0]="web01"
servers[1]="web02"
servers[2]="db01"

# Access by index
echo "${fruits[0]}"    # apple
echo "${fruits[2]}"    # cherry
echo "${fruits[-1]}"   # date  (negative index counts from the end)

# All elements — always use [@] in double quotes to handle spaces correctly
echo "${fruits[@]}"    # apple banana cherry date

# Number of elements
echo "${#fruits[@]}"   # 4

# All indices (useful when the array has gaps after unset)
echo "${!fruits[@]}"   # 0 1 2 3

Adding and Removing Elements

Arrays are dynamic — you can grow them at any time with += and shrink them with unset. One important detail: unset on an element leaves a gap in the indices; it does not shift subsequent elements down.

fruits=("apple" "banana" "cherry")

# Append a single element
fruits+=("date")
echo "${fruits[@]}"    # apple banana cherry date

# Append multiple elements at once
fruits+=("elderberry" "fig")
echo "${#fruits[@]}"   # 6

# Remove element by index — leaves a gap at index 1
unset fruits[1]
echo "${fruits[@]}"    # apple cherry date elderberry fig
echo "${!fruits[@]}"   # 0 2 3 4 5  (index 1 is gone)

# Re-index to close the gap
fruits=("${fruits[@]}")
echo "${!fruits[@]}"   # 0 1 2 3 4  (continuous again)

Slicing

Array slicing uses ${array[@]:start:length} — the same syntax as string substring extraction, applied to arrays. This is useful for processing parts of an argument list or splitting a large array into chunks.

letters=("a" "b" "c" "d" "e" "f")

# ${array[@]:start:length}
echo "${letters[@]:1:3}"    # b c d  (start at index 1, take 3 elements)
echo "${letters[@]:3}"      # d e f  (from index 3 to the end)
echo "${letters[@]: -2}"    # e f    (last 2 elements)

Looping Over Arrays

Looping over arrays is the most common array operation. Always use "${array[@]}" — the double quotes prevent word splitting on elements that contain spaces.

servers=("web01" "web02" "db01" "cache01")

# Standard loop — handles elements with spaces correctly
for server in "${servers[@]}"; do
  echo "Pinging $server..."
done

# Loop with index — useful when you need the position
for i in "${!servers[@]}"; do
  echo "$i: ${servers[$i]}"
done
# 0: web01
# 1: web02
# 2: db01
# 3: cache01

# C-style loop — useful when you need to manipulate the counter
for (( i = 0; i < ${#servers[@]}; i++ )); do
  echo "Server $i: ${servers[$i]}"
done

Associative Arrays

Associative arrays use arbitrary string keys instead of integer indices. They behave like a dictionary or hash map. They must be explicitly declared with declare -A — without the declaration, Bash silently converts the string keys to integers.

declare -A config=(
  [host]="localhost"
  [port]="5432"
  [database]="myapp"
  [user]="dbadmin"
)

# Access by key
echo "${config[host]}"      # localhost
echo "${config[port]}"      # 5432

# Add a new key or update an existing one
config[password]="s3cr3t"
config[port]="5433"

# All keys — order is not guaranteed in associative arrays
echo "${!config[@]}"        # host port database user password

# All values
echo "${config[@]}"         # localhost 5433 myapp dbadmin s3cr3t

# Number of entries
echo "${#config[@]}"        # 5

# Check if a key exists with -v
if [[ -v config[password] ]]; then
  echo "Password is set"
fi

Iterating over an associative array:

declare -A http_codes=(
  [200]="OK"
  [201]="Created"
  [400]="Bad Request"
  [401]="Unauthorized"
  [404]="Not Found"
  [500]="Internal Server Error"
)

for code in "${!http_codes[@]}"; do
  printf "HTTP %s: %s\n" "$code" "${http_codes[$code]}"
done

mapfile / readarray

mapfile (also called readarray) reads lines from stdin or a file directly into an indexed array. This is the correct way to populate an array from command output — it handles spaces in values and avoids the subshell problem of using a pipe.

# Read a file into an array (one element per line)
mapfile -t lines < /etc/hosts
echo "Lines: ${#lines[@]}"
echo "First: ${lines[0]}"

# Read command output into an array
mapfile -t containers < <(docker ps --format '{{.Names}}')
for c in "${containers[@]}"; do
  echo "Container: $c"
done

-t strips the trailing newline from each element. Always use it — without it, every element ends with \n and string comparisons will silently fail.

Array Operations

Filtering an array

Build a new array containing only elements that match a condition:

servers=("web01" "web02" "db01" "db02" "cache01")
db_servers=()

for s in "${servers[@]}"; do
  [[ "$s" == db* ]] && db_servers+=("$s")   # keep only db* servers
done

echo "${db_servers[@]}"   # db01 db02

Deduplicating an array

Bash has no built-in dedup, but an associative array works as a seen-set:

dedup() {
  declare -A seen
  local -a result=()
  for item in "$@"; do
    if [[ ! -v seen["$item"] ]]; then
      seen["$item"]=1
      result+=("$item")
    fi
  done
  echo "${result[@]}"
}

items=("a" "b" "a" "c" "b" "d")
unique=($(dedup "${items[@]}"))
echo "${unique[@]}"   # a b c d

Joining array elements

join_by() {
  local delimiter="$1"; shift
  local first="$1"; shift
  printf '%s' "$first" "${@/#/$delimiter}"
}

arr=("one" "two" "three")
echo "$(join_by ', ' "${arr[@]}")"   # one, two, three
echo "$(join_by '|' "${arr[@]}")"    # one|two|three

Sorting an array

Bash has no built-in sort for arrays. The standard approach is to pipe the elements through the sort command and read them back with mapfile:

fruits=("banana" "apple" "cherry" "date")

# Sort ascending
mapfile -t sorted < <(printf '%s\n' "${fruits[@]}" | sort)
echo "${sorted[@]}"   # apple banana cherry date

# Sort descending
mapfile -t sorted_desc < <(printf '%s\n' "${fruits[@]}" | sort -r)
echo "${sorted_desc[@]}"   # date cherry banana apple

# Sort numerically
nums=(10 2 30 4 20)
mapfile -t sorted_nums < <(printf '%s\n' "${nums[@]}" | sort -n)
echo "${sorted_nums[@]}"   # 2 4 10 20 30

Searching an array

contains() {
  local needle="$1"; shift
  for item in "$@"; do
    [[ "$item" == "$needle" ]] && return 0   # found
  done
  return 1   # not found
}

fruits=("apple" "banana" "cherry")

if contains "banana" "${fruits[@]}"; then
  echo "Found banana"
fi

if ! contains "mango" "${fruits[@]}"; then
  echo "No mango"
fi

Passing Arrays to Functions

Bash does not pass arrays by value. If you use "${array[@]}" as a function argument, the elements arrive as separate positional parameters — the array structure is lost. To pass an array and preserve it, use a nameref.

print_array() {
  local -n arr="$1"   # nameref — arr is an alias for the named variable
  for i in "${!arr[@]}"; do
    echo "  [$i] = ${arr[$i]}"
  done
}

fruits=("apple" "banana" "cherry")
print_array fruits   # pass the variable name, not its contents

What’s Next

The next tutorial covers file operations — testing file attributes, safely copying and moving files, and using find and stat.

Frequently Asked Questions

Why does my array lose its values inside a while loop?
Piping to a while loop runs it in a subshell, so variable changes don't propagate back. Use process substitution: while ... done < <(command) instead of command | while.
How do I copy an array in Bash?
Use new_array=("${old_array[@]}"). For associative arrays you need a loop, as there is no built-in copy syntax.
Can I have nested arrays in Bash?
No. Bash does not support nested arrays. Workarounds include encoding data as strings with delimiters, using nameref tricks, or switching to Python for complex data structures.