Bash arrays come in two flavors: indexed (integer keys, the default) and associative (string keys, Bash 4+ only). Both are first-class but the syntax has just enough rough edges to bite you: the difference between ${arr[@]} and ${arr[*]}, the must-quote-or-it-breaks rule, the way unset arr[2] leaves a sparse array rather than shifting indexes. Below is the full reference: declaration, indexing, iteration with for in "${arr[@]}", appending, slicing, deleting, splitting strings on a delimiter, reading a file into an array with mapfile, and the macOS Bash 3.2 limitations that catch cross-platform scripts.
How do I create a Bash array?
A Bash indexed array holds an ordered list of values under integer keys starting at 0. Declare with arr=(value1 value2 value3) or assign by index: arr[0]="a", arr[1]="b". Read a single element with "${arr[0]}", all elements as separate quoted words with "${arr[@]}", the array length with ${#arr[@]}, and the list of indexes with "${!arr[@]}". To iterate, use for x in "${arr[@]}"; do ...; done — the double quotes and [@] are both required; the unquoted or [*] forms collapse elements joined by $IFS and break on values containing spaces. Associative arrays (string keys) require Bash 4+ and are declared with declare -A arr; arr[key]="value". Append with arr+=(new_element), slice with ${arr[@]:1:3}, delete an element with unset arr[2], and delete the whole array with unset arr. For loop syntax that consumes the array, see Bash For Loops and Bash While Loops.
Jump to:
- Indexed arrays: the basics
- The
[@]vs[*]and quoting gotcha - Iterating values and indexes
- Length, appending, and slicing
- Deleting elements and arrays
- Associative arrays (Bash 4+)
- Reading lines into an array with
mapfile - Splitting a string into an array
- Common pitfalls
- macOS Bash 3.2 vs Linux Bash 4+
- What to do next
- FAQ
Indexed arrays: the basics
# Declare and initialize in one go
fruits=("apple" "banana" "cherry")
# Or assign by index
colors[0]="red"
colors[1]="green"
colors[2]="blue"
# Read a single element
echo "${fruits[0]}" # apple
echo "${fruits[2]}" # cherry
# Read all elements as separate words
echo "${fruits[@]}" # apple banana cherry
# Length of the array
echo "${#fruits[@]}" # 3
# Length of a single element
echo "${#fruits[0]}" # 5 (length of "apple")A few things that catch newcomers:
- No spaces around
=.fruits = ("apple")is a command invocation, not an assignment. Writefruits=("apple"). - Indexes start at 0. Not 1.
- Sparse arrays are legal.
arr[0]="a"; arr[5]="f"is valid; indexes 1-4 are simply unset. - The braces matter.
$fruits[0](no braces) expands as$fruitsfollowed by the literal string[0]. Always use${fruits[0]}.
The [@] vs [*] and quoting gotcha
The single biggest source of array bugs. Four expansion forms, four different behaviors:
| Expansion | Behavior |
|---|---|
"${arr[@]}" | Each element as one quoted word. What you usually want. |
${arr[@]} | Each element split on $IFS. Breaks on elements containing spaces. |
"${arr[*]}" | All elements joined into ONE string with the first character of $IFS between them. |
${arr[*]} | Like [*] quoted, then split on $IFS. Almost never what you want. |
Concrete example:
items=("hello world" "foo bar" "baz")
# ✅ Correct — 3 iterations
for x in "${items[@]}"; do
echo "Item: $x"
done
# ❌ Wrong — 5 iterations (every word treated separately)
for x in ${items[@]}; do
echo "Item: $x"
doneThe rule: "${arr[@]}" (with the double quotes) is the only correct form for iterating an array. Memorise it. The unquoted form is the #1 source of "but it worked on my machine" Bash bugs.
The [*] form has one legitimate use: joining an array into a single string with a custom separator.
parts=("usr" "local" "bin")
IFS='/' echo "${parts[*]}" # usr/local/binSetting IFS=/ and using [*] joins with /. Note the IFS assignment is scoped to the single echo command — it doesn't persist.
Iterating values and indexes
fruits=("apple" "banana" "cherry")
# Iterate values
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# Iterate indexes
for i in "${!fruits[@]}"; do
echo "$i: ${fruits[$i]}"
done
# Iterate with C-style counter (works for dense arrays only)
for ((i = 0; i < ${#fruits[@]}; i++)); do
echo "$i: ${fruits[$i]}"
doneThe "${!arr[@]}" form (note the !) gives you the list of valid indexes. Prefer this over the C-style counter — it works correctly on sparse arrays too.
For the loop forms themselves, see Bash For Loops and Bash While Loops.
Length, appending, and slicing
arr=("a" "b" "c")
# Length
echo "${#arr[@]}" # 3
# Append a single element
arr+=("d") # arr is now (a b c d)
# Append multiple elements
arr+=("e" "f") # arr is now (a b c d e f)
# Concatenate two arrays
more=("g" "h")
all=("${arr[@]}" "${more[@]}") # all is (a b c d e f g h)
# Slice: start at index 2, take 3 elements
echo "${arr[@]:2:3}" # c d e
# Slice: from index 2 to the end
echo "${arr[@]:2}" # c d e f
# Last element
echo "${arr[-1]}" # f (Bash 4.3+ negative indexing)+=() is the canonical append operator. Don't write arr[${#arr[@]}]=value — that works for dense arrays but breaks on sparse ones.
Slicing returns a list, so it can be passed to for or assigned to another array:
arr=("a" "b" "c" "d" "e" "f")
middle=("${arr[@]:1:3}") # (b c d)
for x in "${middle[@]}"; do echo "$x"; doneDeleting elements and arrays
arr=("a" "b" "c" "d")
# Delete one element by index
unset 'arr[2]' # arr is now (a b _ d) — index 2 is unset, NOT shifted
# Verify the sparse state
echo "${arr[@]}" # a b d
echo "${#arr[@]}" # 3 (count of set elements)
echo "${!arr[@]}" # 0 1 3 (indexes 0, 1, 3 — 2 is gone)
# Delete the entire array
unset arr
echo "${arr[@]}" # emptyQuote the index argument to unset. Without quotes, arr[2] is subject to globbing and pathname expansion if any file matches the pattern. unset 'arr[2]' is safe.
If you want to delete an element AND re-index, rebuild the array:
arr=("a" "b" "c" "d")
unset 'arr[1]'
arr=("${arr[@]}") # rebuild — indexes are now contiguous 0, 1, 2
echo "${arr[@]}" # a c d
echo "${!arr[@]}" # 0 1 2Associative arrays (Bash 4+)
Associative arrays use string keys instead of integer indexes. They require Bash 4 or later — meaning NOT macOS's default Bash 3.2.
# Declaration is mandatory (no implicit init like indexed arrays)
declare -A versions
versions[node]="20"
versions[python]="3.12"
versions[go]="1.22"
# Or initialize in one go
declare -A tools=([editor]="vim" [shell]="bash" [pager]="less")
# Read by key
echo "${versions[node]}" # 20
# Iterate keys
for tool in "${!versions[@]}"; do
echo "$tool: ${versions[$tool]}"
done
# Iterate values
for ver in "${versions[@]}"; do
echo "Version: $ver"
done
# Check if a key exists
if [[ -v versions[node] ]]; then
echo "node version is set"
fiThe declare -A step is mandatory. Without it, versions[node]=20 treats node as an arithmetic expression evaluating to 0 and assigns to index 0 of an indexed array. That's a silent data-loss bug — the first associative-array gotcha.
For an example of associative arrays driving a case statement dispatch, pair this with the conditional patterns from the if/else reference.
Reading lines into an array with mapfile
mapfile (also spelled readarray) reads input into an array, one line per element. Bash 4+ only.
# Read every line of a file into the array
mapfile -t lines < file.txt
echo "${lines[0]}" # first line
echo "${#lines[@]}" # number of lines
# Read from a command's output
mapfile -t users < <(cut -d: -f1 /etc/passwd)
# Strip blank lines and comments
mapfile -t config < <(grep -v '^#\|^$' app.conf)The -t flag strips the trailing newline from each line (you almost always want this).
For files with NUL-delimited records (from find -print0, for example):
mapfile -d '' -t files < <(find . -name "*.log" -print0)This is the safe way to load filenames that may contain spaces or newlines into an array. The same -print0 / -d '' pair shows up in the Bash For Loops safe-file-iteration section.
Without mapfile (Bash 3.2 fallback):
# Bash 3.2 portable: read into array one line at a time
lines=()
while IFS= read -r line; do
lines+=("$line")
done < file.txtSlower for very large files but works everywhere.
Splitting a string into an array
CSV strings, colon-separated paths, comma-delimited inputs — Bash splits with read -ra:
csv="apple,banana,cherry"
IFS=',' read -ra fruits <<< "$csv"
echo "${fruits[0]}" # apple
echo "${fruits[1]}" # banana
echo "${fruits[2]}" # cherryThree things going on:
<<<is a here-string. It feeds the string as stdin toread.IFS=','sets the field separator just for thisreadcommand (doesn't persist).-racombines-r(raw, no backslash interpretation) and-a(read into array).
For multi-character delimiters, read doesn't help directly — use parameter expansion or awk:
# Split on "::"
input="apple::banana::cherry"
IFS=':' read -ra parts <<< "${input//::/:}" # collapse :: to : firstFor splitting $PATH into entries:
IFS=':' read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
echo "PATH entry: $entry"
doneCommon pitfalls
1. Unquoted ${arr[@]}. Breaks on elements containing spaces. Always "${arr[@]}".
2. Using [*] when you mean [@]. "${arr[*]}" collapses to one string. Use [*] only when you specifically want a single joined string.
3. Missing declare -A on associative arrays. Without it, arr[key]=value silently writes to index 0 of an indexed array because Bash evaluates key as arithmetic.
4. unset arr[2] leaves a hole. It doesn't shift later indexes down. Rebuild with arr=("${arr[@]}") if you need contiguous indexes.
5. Unquoted unset arr[2]. Without quotes, the bracket pattern can be glob-expanded against the working directory. Use unset 'arr[2]'.
6. Space around = in declaration. arr = (a b c) is a command invocation; arr=(a b c) is an assignment. Bash assignment forbids whitespace around =.
7. $arr without index returns only arr[0]. echo "$arr" prints the first element, not the whole array. Use "${arr[@]}".
8. mapfile on macOS Bash 3.2. mapfile/readarray don't exist. Use the while IFS= read -r line; do arr+=("$line"); done fallback.
9. Pipe subshell scoping with arrays. cat file | while read x; do arr+=("$x"); done — arr is lost after the loop because the pipe creates a subshell. Use process substitution: while read x; do arr+=("$x"); done < <(cat file). Same bug as covered in Bash While Loops.
macOS Bash 3.2 vs Linux Bash 4+
macOS ships Bash 3.2 (frozen since 2007 due to GPLv3 licensing). Linux distros ship Bash 4 or 5. Several array features only work on Bash 4+:
| Feature | Bash 3.2 (macOS default) | Bash 4+ (Linux, brew) |
|---|---|---|
Associative arrays (declare -A) | error | works |
mapfile / readarray | not available | works |
Negative indexing (${arr[-1]}) | not available | works (4.3+) |
${!arr[@]} for indexes | works | works |
declare -p arr to dump | works | works |
If your script needs Bash 4+ features and might run on macOS, either install modern Bash via Homebrew (brew install bash, gives you 5.x at /opt/homebrew/bin/bash or /usr/local/bin/bash) or write portable fallbacks. The fallbacks: while read; arr+=() instead of mapfile, ${arr[$((${#arr[@]}-1))]} instead of ${arr[-1]}, and an indexed-array workaround for associative arrays (parallel arrays with matching index, or a single string with delimited key=value pairs).
If you can require Bash 4+, the shebang should reflect that:
#!/usr/bin/env bash
if (( BASH_VERSINFO[0] < 4 )); then
echo "This script requires Bash 4.0 or later. Current: $BASH_VERSION" >&2
exit 1
fiDebugging Bash arrays
# Dump the array's full declaration
declare -p arr
# Trace expansions
set -x
echo "${arr[@]}"
set +x
# Static analysis catches every array pitfall above
shellcheck script.shdeclare -p arr prints the exact declaration syntax that would recreate the array — including sparse indexes for indexed arrays and key-value pairs for associative arrays. It's the single best debugging tool for "why is my array doing that".
For automating array-driven workflows in production, the How to Export All MySQL Databases script uses arrays to hold the list of databases pulled from mysql -e 'SHOW DATABASES', and the How to Export and Import PuTTY Settings workflow uses associative-style key/value pairs (via parallel arrays on macOS) to map session names to host details.
What to do next
- Bash For Loops — the canonical way to consume an array with
for x in "${arr[@]}". - Bash While Loops —
while readinto an array, including the subshell-scoping fix. - Bash if, else, and elif — branch on array element values with
caseand[[ ]]. - Bash Functions — pass arrays to functions by name (Bash 4.3+
declare -nnameref) for shared array operations. - How to Export All MySQL Databases — loops over an array of database names with retry and notify.
- How to Increase Google Cloud VM Disk Size Without Rebooting — uses array indexing to track expected vs observed device states during a resize.
- External: the Bash Reference Manual: Arrays is the authoritative spec.

![Bash Arrays: Indexed, Associative, and Iteration Patterns (2026) Bash arrays reference: declaration, indexing, [@] vs [*] quoting, iteration, appending, slicing, mapfile/readarray for lines, IFS-based string splitting, plus macOS Bash 3.2 limits.](https://techearl.com/cdn-cgi/image/width=1536,quality=85,format=auto/https://images.techearl.com/bash-arrays/bash-arrays.jpg?v=2026-02-12T14%3A18%3A00Z)

![Bash while loop reference: read files with while IFS= read -r, retry-with-backoff, wait-for-service polling, the subshell-scoping bug fix, the until and select siblings, plus [[ ]] vs [ ] vs (( )) test contexts.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=80/https://images.techearl.com/bash-while-loop/bash-while-loop.jpg?v=2026-04-25T13%3A18%3A00Z)

