Skip to content
Go back

Bash Arrays: The Feature That Makes Scripts Readable

By SumGuy 5 min read
Bash Arrays: The Feature That Makes Scripts Readable

You’re looping over command output and the loop variable sometimes has spaces in it. You’re declaring a list of commands but it’s a mess of escaped quotes. You’re trying to parse structured data in bash and you’re using awk.

Arrays are the answer. They make bash scripts actually readable. Most people don’t use them. I’ve seen scripts where people fake arrays with grep and cut when they could just use bash’s built-in array syntax.

Here’s the thing: once you start using arrays, you can’t write bash scripts without them.

Declaring Arrays

Indexed array (default):

indexed_array.sh
#!/bin/bash
# Method 1: explicit declaration
declare -a fruits=("apple" "banana" "cherry")
# Method 2: implicit
fruits=("apple" "banana" "cherry")
# Method 3: add elements individually
servers[0]="web1.example.com"
servers[1]="web2.example.com"
servers[2]="db1.example.com"

Associative array (like a dict):

assoc_array.sh
#!/bin/bash
declare -A config
config[database]="postgres"
config[port]="5432"
config[username]="admin"
echo "${config[database]}" # postgres
echo "${config[port]}" # 5432

Accessing Array Elements

access_array.sh
#!/bin/bash
arr=("first" "second" "third")
# Single element
echo "${arr[0]}" # first
echo "${arr[2]}" # third
echo "${arr[-1]}" # third (last element, bash 4.3+)
# All elements
echo "${arr[@]}" # first second third
echo "${arr[*]}" # first second third (usually same)
# Length
echo "${#arr[@]}" # 3

The @ vs * difference matters when you quote:

at_vs_star.sh
#!/bin/bash
arr=("file1.txt" "file2.txt" "file with spaces.txt")
# WRONG: ${arr[*]} treats array as single word
for file in ${arr[*]}; do
echo "$file"
done
# Output:
# file1.txt
# file2.txt
# file
# with
# spaces.txt
# RIGHT: "${arr[@]}" treats each element as separate word
for file in "${arr[@]}"; do
echo "$file"
done
# Output:
# file1.txt
# file2.txt
# file with spaces.txt

Always use "${arr[@]}" when iterating.

Iterating Over Arrays

loop_array.sh
#!/bin/bash
commands=("ls -la" "echo hello" "pwd")
for cmd in "${commands[@]}"; do
echo "Running: $cmd"
eval "$cmd"
done

With index:

loop_with_index.sh
#!/bin/bash
servers=("web1" "web2" "db1")
for i in "${!servers[@]}"; do
echo "Server $i: ${servers[$i]}"
done
Terminal window
$ bash loop_with_index.sh
Server 0: web1
Server 1: web2
Server 2: db1

The "${!servers[@]}" gives you the array indices.

Appending and Deleting

Append:

append_array.sh
#!/bin/bash
arr=("one" "two")
arr+=("three") # Append single element
arr+=("four" "five") # Append multiple
echo "${arr[@]}" # one two three four five

Delete element:

delete_array.sh
#!/bin/bash
arr=("one" "two" "three" "four")
unset arr[1] # Delete index 1
echo "${arr[@]}" # one three four
echo "${#arr[@]}" # 3 (deleted element doesn't count)

Be careful: indices don’t renumber after deletion.

Clear entire array:

clear_array.sh
#!/bin/bash
arr=("one" "two" "three")
unset arr
echo "${arr[@]}" # Empty

Slicing Arrays

Extract a portion of an array:

slice_array.sh
#!/bin/bash
arr=("a" "b" "c" "d" "e")
# Get 3 elements starting at index 1
slice=("${arr[@]:1:3}")
echo "${slice[@]}" # b c d

Real-World Example 1: Safe Command Execution

safe_commands.sh
#!/bin/bash
# Array of commands to run, in order
steps=(
"echo 'Building project...'"
"npm run build"
"npm run test"
"npm run lint"
)
for i in "${!steps[@]}"; do
echo "Step $((i + 1))/${#steps[@]}: ${steps[$i]}"
eval "${steps[$i]}" || {
echo "Step $((i + 1)) failed"
exit 1
}
done
echo "All steps completed"

Much cleaner than a big if-then ladder.

Real-World Example 2: Associative Array for Config

config_array.sh
#!/bin/bash
declare -A nginx_config
nginx_config[server_name]="example.com"
nginx_config[listen]="80"
nginx_config[upstream]="localhost:3000"
nginx_config[client_max_body_size]="10m"
cat <<-EOF
server {
server_name ${nginx_config[server_name]};
listen ${nginx_config[listen]};
client_max_body_size ${nginx_config[client_max_body_size]};
location / {
proxy_pass http://${nginx_config[upstream]};
}
}
EOF

Cleaner than a bunch of variables.

Real-World Example 3: Reading Command Output Safely

Avoid word-splitting disasters:

safe_read.sh
#!/bin/bash
# WRONG: word splitting breaks filenames
for file in $(find . -name "*.txt"); do
echo "File: $file"
done
# RIGHT: read into array
while IFS= read -r -d '' file; do
arr+=("$file")
done < <(find . -name "*.txt" -print0)
for file in "${arr[@]}"; do
echo "File: $file"
done

The while read loop populates an array safely, even with spaces and special characters.

Real-World Example 4: Parallel Execution

parallel_tasks.sh
#!/bin/bash
servers=("web1.example.com" "web2.example.com" "db1.example.com")
pids=()
# Start tasks in parallel
for server in "${servers[@]}"; do
ssh "$server" "long-running-task" &
pids+=($!) # Save the PID
done
# Wait for all to complete
for pid in "${pids[@]}"; do
wait "$pid"
if [ $? -ne 0 ]; then
echo "Task failed: $pid"
fi
done
echo "All tasks done"

Arrays of PIDs make parallel execution manageable.

Arrays vs String Splitting

BAD (fragile string splitting):

bad_split.sh
#!/bin/bash
servers="web1,web2,web3"
IFS=',' read -ra arr <<< "$servers"
for server in "${arr[@]}"; do
echo "Server: $server"
done

This works for simple cases but breaks with edge cases (spaces, special chars).

GOOD (explicit array):

good_explicit.sh
#!/bin/bash
declare -a servers=("web1" "web2" "web3")
for server in "${servers[@]}"; do
echo "Server: $server"
done

Clear intent, no hidden splitting rules.

Debugging Arrays

Print array contents nicely:

debug_array.sh
#!/bin/bash
arr=("one" "two" "three")
# Method 1: simple
declare -p arr
# Method 2: custom format
for i in "${!arr[@]}"; do
echo " [$i] = ${arr[$i]}"
done
Terminal window
$ bash debug_array.sh
declare -a arr=([0]="one" [1]="two" [2]="three")
[0] = one
[1] = two
[2] = three

The Pattern: Arrays Make Scripts Readable

template.sh
#!/bin/bash
set -euo pipefail
# Indexed array
declare -a files=()
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find . -name "*.txt" -print0)
# Process
for file in "${files[@]}"; do
echo "Processing: $file"
done
# Associative array for config
declare -A config
config[input_dir]="./input"
config[output_dir]="./output"
config[format]="json"
# Use config
mkdir -p "${config[output_dir]}"
echo "Config:"
for key in "${!config[@]}"; do
echo " $key = ${config[$key]}"
done

That’s readable. That’s maintainable. That’s bash done right.

The One Thing

Your next bash script shouldn’t have this:

Terminal window
# NO
items="one,two,three"
# ... hand-parsing with grep/cut

It should have this:

Terminal window
# YES
declare -a items=("one" "two" "three")
for item in "${items[@]}"; do
# ...
done

Arrays aren’t fancy. They’re basic. Use them.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Cloudflare Tunnels: The Zero-Port-Forward Guide to Exposing Your Services
Next Post
Bash Process Substitution: What <() and >() Actually Do

Related Posts