The bash array is the data structure that holds multiple values under one variable name, and the associative array, introduced in bash 4, is the hash map that keys values by string. The two structures replace the space-separated variables and the naming conventions that the older scripts used, and the structured data is the difference between a script that works and a script that the next person can read. This article covers both types: the indexed array, the associative array, the iteration patterns and the practical use in the configuration processing.

The indexed array and the basic operations

The indexed array is declared with parentheses and accessed with the bracket syntax:

SERVERS=(web01 web02 db01)
echo ${SERVERS[0]}
echo ${#SERVERS[@]}

The declaration, the indexed access and the length. The iteration with for and the @ subscript:

for server in ${SERVERS[@]}; do
ssh admin@$server 'uptime'
done

The associative array and the key-value storage

The associative array is declared with declare -A and accessed with string keys:

declare -A CONFIG
CONFIG[host]=db.internal
CONFIG[port]=5432
echo ${CONFIG[host]}

The key-value pairs are the configuration data, and the iteration with ${!CONFIG[@]} gives the keys.

The practical patterns

The configuration processing that the scripts use is the associative array pattern: the config file is parsed into the array, the values are accessed by key, and the modifications are made in the array before writing the output. The array is the in-memory data structure that the script uses instead of re-reading the file for each value.

The comparison with other approaches

The indexed array replaces the space-separated variable, the associative array replaces the multiple variables with prefixed names, and both structures make the script readable. The structured data in the script is the maintainability feature, and the array syntax is the bash feature that every script in this series should use.

The bash array is the data structure that the production scripts use for the multi-value data, and the two array types, indexed and associative, cover the data structures that the script needs: the indexed array for the ordered lists and the associative array for the key-value mappings. The array syntax is the bash feature that makes the script readable, and the readable script is the one that the team can maintain.

The indexed array operations in detail

The indexed array supports the standard operations: the element access with the index, the length with the # symbol, the append with the += operator, the deletion with the unset command, and the slice with the : notation. The operations:

SERVERS=(web01 web02 web03)
SERVERS+=(web04)
echo ${SERVERS[-1]}
echo ${SERVERS[@]:1:2}
unset SERVERS[1]
echo ${SERVERS[@]}

The negative index accesses from the end, the slice notation accesses a range, the unset removes the element leaving a gap, and the operations are the array manipulation vocabulary that the script uses.

The expansion flags modify the output: ${SERVERS[@]#web} strips the prefix web from every element, ${SERVERS[@]%.conf} strips the suffix .conf, and ${SERVERS[@]^} capitalizes the first letter, the string operations applied to every element at once, the batch processing that the parameter expansion provides.

The associative array operations in detail

The associative array supports the standard hash map operations: the set, the get, the delete, the existence check and the iteration. The operations:

declare -A ENVS
ENVS[prod]=db01.internal
ENVS[staging]=db01.staging.internal
ENVS[dev]=db01.dev.internal

for env in ${!ENVS[@]}; do
echo $env -> ${ENVS[$env]}
done

The ! expansion gives the keys, the @ expansion gives the values, and the iteration covers the whole array. The existence check uses the -v test: [[ -v ENVS[prod] ]] returns true if the key exists, and the check that the script does before accessing the value prevents the unset variable error.

The arrays in the fleet management scripts

The fleet management scripts that the earlier articles described use the arrays for the server inventory: the indexed array for the ordered server list and the associative array for the per-server configuration. The server list that the script iterates:

SERVERS=(web01 web02 web03 db01 db02)
for server in ${SERVERS[@]}; do
ssh admin@$server 'systemctl is-active myapp'
done

and the associative array for the per-server config:

declare -A SERVER_IPS
SERVER_IPS[web01]=10.0.1.21
SERVER_IPS[web02]=10.0.1.22
SERVER_IPS[db01]=10.0.2.31

for server in ${!SERVER_IPS[@]}; do
ssh admin@${SERVER_IPS[$server]} 'uptime'
done

The indexed array for the iteration order and the associative array for the address lookup: the two arrays together provide the ordered iteration and the keyed access, the pattern that the fleet management scripts use.

The read and the array population

The read command with the -a flag populates an indexed array from a line of input, and the IFS variable controls the field splitting:

read -ra FIELDS <<< 'web01:8080 web02:8080 db01:5432'
echo ${FIELDS[0]}

which splits the input on whitespace and populates the array, the pattern that the script uses for the parsing. The mapfile command, also known as readarray, populates the array from a multi-line input:

mapfile -t SERVERS < server-list.txt

which reads the file line by line into the array, the -t flag removing the trailing newlines, and the mapfile pattern is the clean way to populate the array from a file.

The arrays and the export to the environment

The arrays cannot be exported to the environment directly, because the environment variables are strings rather than arrays, and the workaround is the delimiter-separated string that the script splits into an array:

export SERVER_LIST='web01 web02 db01'
SERVERS=($SERVER_LIST)

and the space-separated string is the environment-compatible representation that the script converts to the array. The alternative for the complex data is the JSON in the environment variable, the JSON that the script parses with jq into the array, the pattern that the configuration management uses for the structured data.

The performance and the bash array limitations

Bash arrays are hash tables and indexed arrays implemented in the shell interpreter, and the performance is adequate for the scripts that process hundreds or thousands of elements, but not for the scripts that process millions. The array operations are O(1) for the indexed access and O(1) for the associative lookup, and the performance ceiling is the interpreter's speed rather than the data structure's complexity.

The limitations that the arrays have: the maximum size is the memory that bash can allocate, the keys in the associative array must be strings, and the array cannot be passed to a function by reference without the nameref feature introduced in bash 4.3. The nameref, declare -n ref=array_name, creates a reference that the function can use to access the caller's array, and the nameref is the feature that enables the array-passing patterns that the functions need.

The checklist for the array usage in bash scripts

The checklist for the bash array usage in production scripts: declare with the local keyword inside functions, quote the expansions with ${array[@]} to preserve the element boundaries, use the -A declaration for the associative arrays, validate the keys before the access, use the mapfile for the file population, and document the array structure in the script's comments. The array that is documented, quoted and validated is the array that the script can rely on, and the reliability is the property that the production script needs.

The bash array is the data structure that the production scripts use for the multi-value data, and the two array types, indexed and associative, cover the data structures that the script needs: the indexed array for the ordered lists and the associative array for the key-value mappings. The array syntax is the bash feature that makes the script readable, and the readable script is the one that the team can maintain.

The indexed array operations in detail

The indexed array supports the standard operations: the element access with the index, the length with the # symbol, the append with the += operator, the deletion with the unset command, and the slice with the : notation. The operations:

SERVERS=(web01 web02 web03)
SERVERS+=(web04)
echo ${SERVERS[-1]}
echo ${SERVERS[@]:1:2}
unset SERVERS[1]
echo ${SERVERS[@]}

The negative index accesses from the end, the slice notation accesses a range, the unset removes the element leaving a gap, and the operations are the array manipulation vocabulary that the script uses.

The expansion flags modify the output: the prefix stripping with the # operator, the suffix stripping with the % operator, the case modification with the ^ and , operators. The batch processing that the parameter expansion provides applies the operation to every element at once, and the batch processing is the feature that eliminates the loop that the older scripts needed.

The associative array operations in detail

The associative array supports the standard hash map operations: the set, the get, the delete, the existence check and the iteration. The existence check uses the -v test, the iteration uses the ! and @ expansions, and the deletion uses the unset command.

The read and mapfile for array population

The read command with the -a flag populates an indexed array from a line of input, and the mapfile command populates the array from a multi-line input. The mapfile is the clean way to populate the array from a file or a command output:

mapfile -t SERVERS < server-list.txt

The -t flag removes the trailing newlines, and the array is ready for the iteration.

The arrays in the fleet management scripts

The fleet management scripts that this series described use the arrays for the server inventory: the indexed array for the ordered server list and the associative array for the per-server configuration. The two arrays together provide the ordered iteration and the keyed access, the pattern that the fleet management scripts use.

The arrays and the export to the environment

Arrays cannot be exported to the environment directly, because the environment variables are strings. The workaround is the delimiter-separated string that the script splits into an array:

export SERVER_LIST='web01 web02 db01'
SERVERS=($SERVER_LIST)

The alternative for the complex data is the JSON in the environment variable, the JSON that the script parses with jq into the array.

The performance and the bash array limitations

Bash arrays are adequate for the scripts that process hundreds or thousands of elements, but not for the scripts that process millions. The array operations are fast for the small arrays, and the performance ceiling is the interpreter's speed rather than the data structure's complexity.

The nameref feature introduced in bash 4.3 enables the array-passing patterns that the functions need: declare -n ref=array_name creates a reference that the function can use to access the caller's array. The nameref is the feature that enables the modular array processing, and the feature is available on every current distribution.

The checklist for the array usage

The checklist: declare with the local keyword inside functions, quote the expansions with ${array[@]} to preserve the element boundaries, use the -A declaration for the associative arrays, validate the keys before the access, use the mapfile for the file population, and document the array structure in the script's comments. The array that is documented and quoted is the array that the script can rely on.
The array is the data structure that elevates the bash script from the flat string manipulation to the structured data processing, and the scripts that use arrays are the scripts that the next maintainer reads without puzzlement.