Skip to content
shellhacks.dev
Bash

How to Use Arrays in Bash From the Basics to Array Slicing

Learn Bash arrays from declaration to slicing: indexed and associative arrays, counting items, quoting expansions, and when a plain string is better.

5 min read
Summarize in AI
Illustration for "How to Use Arrays in Bash From the Basics to Array Slicing": a terminal session, a padlock and key, a loop arrow.

An array is the right tool whenever a list contains values that may include spaces, such as file names, and a plain string would split them apart. The trap behind that is described in word splitting, and arrays are the standard way out of it. The examples assume Bash 5.2.

Create and read an array

Shell
servers=(web1 web2 "db server")
echo "${servers[0]}"
echo "${#servers[@]}"
servers+=(cache1)
for s in "${servers[@]}"; do echo "- $s"; done
Text
web1
3
- web1
- web2
- db server
- cache1

Indexes start at 0. +=(...) appends. The quoted "${servers[@]}" keeps db server as one word, which is the whole point. Without the quotes it would be split into two.

Slicing

Shell
nums=(10 20 30 40 50)
echo "${nums[@]:1:3}"
echo "${nums[@]: -2}"
echo "${nums[@]:2}"
Text
20 30 40
40 50
30 40 50

The form is start:count. A negative start needs a space after the colon, or parentheses, so Bash does not read it as the default-value operator :-. The slice returns elements, not a new array, so wrap it as part=("${nums[@]:1:3}") to keep it.

Associative arrays

Shell
declare -A ports=([http]=80 [https]=443 [ssh]=22)
echo "${ports[ssh]}"
for k in "${!ports[@]}"; do echo "$k=${ports[$k]}"; done
Text
22
http=80
https=443
ssh=22

declare -A must come first, or Bash treats the keys as arithmetic. Iteration order is unspecified, so sort the keys if the output matters. These need Bash 4.0; macOS ships Bash 3.2 as /bin/bash, which lacks them.

Passing and copying

Shell
copy=("${servers[@]}")
unset 'servers[1]'
echo "${#copy[@]} ${#servers[@]}"
Text
4 3

Copy with quotes around the expansion to preserve every element. A function cannot receive an array as one argument, and the workarounds are in passing an array to a function. For iteration patterns see looping over an array.

Indexed arrays are sparse maps

A Bash array is a set of index and value pairs, and the indexes need not be consecutive. Assigning arr[10]=x to an empty array creates one element with index 10, not eleven slots, and the length is 1. This model explains many surprises: the highest index is not the length minus one, unsetting an element leaves a gap, and a loop from 0 to length minus one skips or misses elements. Use "${!arr[@]}" to iterate over actual indexes, and the length expansion only for counting. Reassigning with arr=("${arr[@]}") closes the gaps by renumbering from zero.

The declaration can state intent: declare -a for an indexed array and declare -A for an associative one. Assigning a string with a subscript to an array that was not declared as associative treats the subscript as an arithmetic expression, so a key like name evaluates as a variable and becomes 0. Declare associative arrays before use.

Slicing and substring operations

The expansion ${arr[@]:start:count} takes a slice, for example the first three elements with ${arr[@]:0:3}, and negative starts work in recent versions with a space before the minus sign. Parameter expansion operators apply to every element when used with [@]: "${arr[@]/foo/bar}" replaces the first occurrence of foo in each element, and "${arr[@]#prefix}" strips a prefix from each. These are fast, since no subprocess is started, and they avoid loops. Remember to keep the quotes, which preserve element boundaries.

Sorting has no builtin. Print the elements one per line, sort them, and read them back with mapfile, using null delimiters if any element can contain a newline. For numbers, add -n to sort.

Associative arrays and their limits

Associative arrays need Bash 4 and map strings to strings. They are the right structure for counting, lookups and deduplication: (( count[$word]++ )) tallies words in a loop, and [[ -v map[key] ]] tests for a key, distinguishing a missing key from one with an empty value. Key order is not defined, so sort the keys before printing when output must be stable. They cannot hold nested structures or other arrays, and exporting them to child processes does not work, as arrays cannot be exported at all. When data crosses a process boundary, serialise it, for example as lines of key and value, or use files.

Because the strings are all the structure available, Bash arrays suit small collections of simple items. Past a few thousand elements or nested records, switch to a language with proper types.

Copying and passing

An assignment such as b=$a copies only the first element as a plain string, a frequent source of confusion. Copy with b=("${a[@]}"), which preserves elements with spaces. Functions receive arrays as separate arguments, so pass them with "${arr[@]}" and collect inside with local items=("$@"). Returning arrays needs a nameref or a global variable, covered in the neighbouring posts. In scripts, enable set -u with care on older Bash, where empty arrays trigger "unbound variable" errors. Write defensive tests: an empty array, an element with a space, an element with a glob character and an element that is an empty string. If all four behave in the loop and in the function boundary, the array code is sound.

Compare the Bash array with the alternatives before committing to it. A newline separated string in a variable is simpler but breaks on elements that contain newlines. A temporary file works for large data and for sharing between processes. A directory of small files, one per item, is a good fit when each item has its own lifecycle. The array is the best choice for a modest list of values inside one script, and the worst choice as an interface between scripts, where a documented text format is easier for everyone.

When the list comes from user input, validate each element as you add it. An array that can hold anything can also hold a string that later reaches a command line, so quote every expansion and avoid eval.

Where this does not apply

Arrays are not portable: a script with a #!/bin/sh line may run under dash and fail on (. Use set -- a b c and "$@" there. For key-value data with many entries, an array of strings is awkward; a small file read with while read or a tool such as jq may be clearer. The Bash manual on arrays is the complete reference.

When arrays are the wrong shape, say so early. A config file with fifty key-value pairs is better as a sourced file of plain assignments or a small .env file read line by line, because an associative array literal that long is hard to diff in review. Arrays shine for short, fixed lists inside one script: the hosts to ping, the flags to pass to a tool, the files to back up. Building the command as an array, cmd=(rsync -a "$src" "$dest"), and running "${cmd[@]}" is the safe way to assemble arguments conditionally without eval.

The ShellCheck SC2206 entry shows the unquoted assignment mistake that arrays exist to prevent.

Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.