Arrays in Bash
A Bash array lets a single variable hold multiple values instead of just one, so you can store a list of filenames, server names, or config values and work with them as a collection. Bash supports two kinds of arrays: indexed arrays, where each element has a numeric position, and associative arrays, where each element is looked up by a string key. Arrays are everywhere in real scripts: collecting files found by a loop, building a list of arguments to pass to a command, or mapping service names to ports.
Overview / How arrays work in Bash
Under the hood, a Bash indexed array is not a fixed-size block of memory the way an array is in C. It is a sparse map from integer indices to string values. That means indices do not have to be contiguous — you can have elements at positions 0, 1, and 5 with nothing in between, and the array still “has” only 3 elements. This is different from most other languages and it explains a common bug: if you delete an element from the middle, the array does not shrink and renumber itself; it just has a gap.
An associative array works the same way internally, except the “index” is an arbitrary string instead of an integer. Bash only supports associative arrays because it explicitly tracks the type of the variable as -A; unlike some languages, indexed and associative arrays are genuinely different variable types in Bash, not the same structure used two ways. You must declare an associative array with declare -A before assigning to it with string keys, or Bash will silently create an ordinary indexed array and evaluate your “key” as an arithmetic expression instead of a string.
Every array element is stored as a string (or unset), just like every other Bash variable — there is no dedicated numeric or list type. When you write arr=(one two three), Bash performs word splitting on the right-hand side (unless parts are quoted) and assigns each resulting word to the next integer index starting at 0. This is why quoting inside an array literal matters: arr=("report draft.txt" notes.txt) creates a 2-element array, while arr=(report draft.txt notes.txt) creates a 3-element array where a file name got split apart.
Syntax
The general forms for declaring and using arrays are shown below. This is a syntax outline, not a runnable script — treat the words in it as placeholders for your own variable, index, and value names.
arrayname=(element1 element2 element3)
declare -a arrayname # explicitly declare an indexed array
declare -A arrayname # explicitly declare an associative array (must come first)
arrayname[index]=value # set a single element
${arrayname[index]} # read a single element
${arrayname[@]} # all elements, each as a separate word
${arrayname[*]} # all elements joined into one word using $IFS
${#arrayname[@]} # number of elements (the array's length)
${!arrayname[@]} # all indices (or keys) currently in use
arrayname+=(more elements) # append elements to the end
unset arrayname[index] # remove one element (leaves a gap)
unset arrayname # remove the whole array
${arrayname[@]:start:count} # slice: count elements starting at start
| Form | Meaning |
|---|---|
declare -a |
Declares (or re-declares) a variable as an indexed array |
declare -A |
Declares a variable as an associative array; required before string-key assignment |
${#arr[@]} |
Element count — works for both indexed and associative arrays |
${!arr[@]} |
Lists indices/keys; the safe way to iterate a sparse array |
arr+=(x) |
Appends x as a new element instead of overwriting the array |
${arr[@]:1:2} |
Slice starting at index 1, taking 2 elements |
Examples
Example 1: A basic indexed array
#!/usr/bin/env bash
fruits=("apple" "banana" "cherry")
echo "First fruit: ${fruits[0]}"
echo "All fruits: ${fruits[@]}"
echo "Total fruits: ${#fruits[@]}"
for fruit in "${fruits[@]}"; do
echo "- $fruit"
done
Output:
First fruit: apple
All fruits: apple banana cherry
Total fruits: 3
- apple
- banana
- cherry
The array literal ("apple" "banana" "cherry") creates three elements at indices 0, 1, and 2. ${fruits[0]} reads a single element, ${#fruits[@]} counts them, and "${fruits[@]}" expands to three separate quoted words — which is exactly why the for loop processes each fruit correctly even if a fruit name contained a space.
Example 2: Building an array from a glob
#!/usr/bin/env bash
logs=()
for file in /var/log/app/*.log; do
logs+=("$file")
done
echo "Found ${#logs[@]} log files"
printf '%s\n' "${logs[@]}"
Output:
Found 3 log files
/var/log/app/access.log
/var/log/app/debug.log
/var/log/app/error.log
This is the pattern you will use constantly: start with an empty array (logs=()), then use +=() inside a loop to append one filename per iteration. Because the glob itself and the "$file" expansion are both quoted, filenames with spaces are handled correctly. printf '%s\n' with an array expansion is a reliable way to print one element per line, since printf recycles its format string for every argument it receives.
Example 3: An associative array of service ports
#!/usr/bin/env bash
declare -A service_ports
service_ports[nginx]=80
service_ports[postgres]=5432
service_ports[redis]=6379
for service in "${!service_ports[@]}"; do
echo "$service listens on port ${service_ports[$service]}"
done
Output:
postgres listens on port 5432
redis listens on port 6379
nginx listens on port 80
declare -A service_ports tells Bash this variable stores string keys, not integer indices. "${!service_ports[@]}" expands to the keys (nginx, postgres, redis), and ${service_ports[$service]} looks up the value for each key inside the loop. Note the output order is not guaranteed to match insertion order — associative arrays in Bash are unordered, so never write a script that depends on a particular iteration order.
How it works step by step
Walking through Example 2: when the shell hits for file in /var/log/app/*.log; do, it first expands the glob into a list of matching pathnames (this happens before the loop even starts, in the parent shell, not one file at a time). Each matched path is then assigned to file in turn. Inside the loop body, logs+=("$file") does two things: ${#logs[@]} increases by one, and Bash appends the current value of $file as a new highest-indexed element — it does not overwrite the existing elements, because += is the append operator for arrays, distinct from plain = which would replace the entire array with a single new element at index 0. After the loop, ${#logs[@]} reflects the final count, and "${logs[@]}" re-expands every stored element as an individually quoted word for printf to consume.
Common Mistakes
Mistake 1: Expanding an array without quotes
#!/usr/bin/env bash
files=("report draft.txt" "notes.txt")
for f in ${files[@]}; do
echo "Processing: $f"
done
Without quotes, ${files[@]} undergoes word splitting on whitespace, so "report draft.txt" is torn into two separate loop iterations (report and draft.txt) instead of one. Always quote array expansions:
#!/usr/bin/env bash
files=("report draft.txt" "notes.txt")
for f in "${files[@]}"; do
echo "Processing: $f"
done
Mistake 2: Using string keys without declare -A
#!/usr/bin/env bash
ports[nginx]=80
ports[postgres]=5432
echo "${ports[nginx]}"
Without declare -A first, ports defaults to an indexed array, and Bash evaluates nginx and postgres as arithmetic expressions. Since both are undefined variables, they evaluate to 0, so both assignments silently collide at index 0 and the second overwrites the first. Fix it by declaring the associative array explicitly before assigning:
#!/usr/bin/env bash
declare -A ports
ports[nginx]=80
ports[postgres]=5432
echo "${ports[nginx]}"
Mistake 3: Assuming indices stay contiguous after deleting
#!/usr/bin/env bash
colors=("red" "green" "blue")
unset 'colors[1]'
for ((i = 0; i < ${#colors[@]}; i++)); do
echo "$i: ${colors[i]}"
done
After unset 'colors[1]', the array has 2 elements but they live at indices 0 and 2 — index 1 is simply gone, it is not shifted down. ${#colors[@]} now reports 2, so the C-style loop only checks i=0 and i=1, printing a blank for the missing index and never reaching blue at index 2. Iterate over the actual indices instead of a numeric range:
#!/usr/bin/env bash
colors=("red" "green" "blue")
unset 'colors[1]'
for i in "${!colors[@]}"; do
echo "$i: ${colors[i]}"
done
Best Practices
- Always quote array expansions:
"${arr[@]}", not${arr[@]}, unless you specifically want word splitting. - Use
${arr[@]}(not${arr[*]}) when you want each element treated as a separate word, e.g. as arguments to another command; use${arr[*]}only when you deliberately want one joined string. - Always run
declare -A namebefore assigning any string-keyed value to that array. - Iterate with
for key in "${!arr[@]}"rather than a numericfor ((i=0; i<n; i++))loop whenever the array might be sparse. - Use
arr+=(value)to append; never assume plainarr=(value)adds to an existing array — it replaces it. - Prefer
mapfile -t arr < file(orreadarray -t arr) to read lines from a file into an array, instead ofarr=($(cat file)), which word-splits on whitespace and expands globs unexpectedly. - Do not rely on the iteration order of associative arrays — it is unspecified.
Practice Exercises
- Write a script that collects every
.txtfile in a directory into an indexed array using aforloop over a glob, then prints the total count followed by each filename and its line count fromwc -l. - Create an associative array mapping HTTP status codes (
200,404,500) to their text descriptions (“OK”, “Not Found”, “Internal Server Error”), then write a function that takes a status code as$1and prints its description, or “Unknown status” if the key is not present. - Given a variable
usernames="alice bob carol", split it into an array (hint: unquoted expansion of a plain variable performs word splitting) and print each username on its own line, prefixed with its 1-based position number.
Summary
- Bash indexed arrays map integer indices to string values and can be sparse; associative arrays (
declare -A) map string keys to values. ${arr[@]}expands to separate words,${arr[*]}joins into one string with$IFS;${#arr[@]}counts elements and${!arr[@]}lists indices or keys.- Always quote array expansions to avoid word splitting on elements containing spaces.
- Declare associative arrays with
declare -Abefore assigning string keys, or Bash silently misinterprets the keys as arithmetic. - Use
arr+=(value)to append and iterate with"${!arr[@]}"to safely handle gaps left byunset.
