Arrays let a Bash script keep lists, such as servers, databases, files and options, without fragile space-separated strings. Bash has indexed arrays (numbered from 0) and, since Bash 4, associative arrays (key → value maps).

Indexed arrays

servers=(web1 web2 db1)
servers[3]=cache1        # set a specific index
servers+=(worker1)       # append

Access:

echo "${servers[0]}"         # web1
echo "${servers[-1]}"        # last element (Bash 4.3+)
echo "${servers[@]}"         # all elements
echo "${#servers[@]}"        # number of elements
echo "${!servers[@]}"        # all indexes: 0 1 2 3 4

The braces are required. $servers[0] prints the first element followed by a literal [0].

Looping

for s in "${servers[@]}"; do
  ssh -n "$s" uptime
done

Always quote "${arr[@]}". Each element then stays one argument, even if it contains spaces. "${arr[*]}" joins all elements into one string, separated by the first character of IFS:

files=("my report.pdf" "notes.txt")
printf '[%s]\n' "${files[@]}"   # [my report.pdf] [notes.txt]
printf '[%s]\n' "${files[*]}"   # [my report.pdf notes.txt]

Loop with indexes:

for i in "${!servers[@]}"; do
  echo "$i: ${servers[$i]}"
done

Slicing, removing and replacing

echo "${servers[@]:1:2}"       # 2 elements starting at index 1
unset 'servers[1]'             # delete element 1 (leaves a gap in the indexes)
servers=("${servers[@]}")      # re-index to remove gaps
echo "${servers[@]/web/app}"   # replace text in every element (output only)

Remove an element by value:

for i in "${!servers[@]}"; do
  [[ "${servers[$i]}" == "db1" ]] && unset 'servers[i]'
done

Check whether an array contains a value

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

contains "web2" "${servers[@]}" && echo "found"

Fill arrays from commands and files

mapfile -t lines < hosts.txt                      # one element per line
mapfile -t dbs < <(mysql -N -e 'SHOW DATABASES')  # from command output
readarray -t users < <(cut -d: -f1 /etc/passwd)   # readarray = mapfile

files=(/var/backups/*.sql.gz)                     # from a glob

Avoid arr=($(command)). It splits on spaces and expands globs. mapfile -t is the safe way.

For NUL-separated output (filenames):

mapfile -d '' -t files < <(find /srv -name '*.log' -print0)

Arrays for command arguments

This is one of the most useful patterns. Building command options in a string breaks on spaces, while an array doesn’t:

opts=(-avz --delete --exclude='cache/')
[[ "$DRY_RUN" == 1 ]] && opts+=(--dry-run)
rsync "${opts[@]}" /var/www/site/ backup:/srv/site/

Associative arrays (maps)

Declare them first:

declare -A port
port[http]=80
port[https]=443
port[ssh]=22

# or all at once
declare -A owner=([shop]=alice [blog]=bob)

Use them:

echo "${port[https]}"           # 443
echo "${!port[@]}"              # keys (in no particular order)
echo "${#port[@]}"              # count
[[ -v port[ftp] ]] || echo "no ftp entry"     # key exists? (Bash 4.2+)
unset 'port[ssh]'

for k in "${!port[@]}"; do
  printf '%-6s %s\n' "$k" "${port[$k]}"
done

Keys come out in arbitrary order. Pipe through sort if order matters.

Counting things, for example requests per IP:

declare -A hits
while read -r ip _; do
  (( hits[$ip]++ ))
done < /var/log/nginx/access.log
for ip in "${!hits[@]}"; do echo "${hits[$ip]} $ip"; done | sort -rn | head

sort | uniq -c is simpler and faster for big logs. See sort and uniq.

Passing arrays to functions

Pass the elements as arguments:

deploy_all() { for h in "$@"; do echo "deploying to $h"; done; }
deploy_all "${servers[@]}"

Or by name, using a nameref (Bash 4.3+):

print_map() {
  local -n m="$1"
  for k in "${!m[@]}"; do echo "$k=${m[$k]}"; done
}
print_map port

Limits

  • No nested arrays (arrays of arrays). For structured data, use jq with JSON, or another language.
  • Associative arrays need Bash 4+. Older macOS ships Bash 3.2, while servers have Bash 5.
  • sh and dash have no arrays at all. Use #!/usr/bin/env bash.

Quick reference

Task Syntax
Create a=(x y z)
Append a+=(w)
Element "${a[1]}"
All, safely "${a[@]}"
Count "${#a[@]}"
Indexes / keys "${!a[@]}"
Slice "${a[@]:1:2}"
Delete element unset 'a[1]'
From lines mapfile -t a < file
Map declare -A m; m[key]=val

Related: Bash variables, for loops and functions.

  • Bash
  • Scripting
  • Linux