funwithlinux guide

Making the Most of Bash Arrays: A Complete Tutorial

Bash, the Bourne-Again Shell, is a staple of Unix-like systems, powering everything from simple command-line tasks to complex automation scripts. While Bash is often criticized for lacking the sophistication of modern programming languages, it includes a powerful feature that’s frequently underutilized: **arrays**. Bash arrays allow you to store and manipulate multiple values under a single variable name, making your scripts more efficient, readable, and maintainable. Whether you’re handling lists of files, processing command-line arguments, or counting word frequencies, arrays simplify tasks that would otherwise require messy loops or multiple variables. In this tutorial, we’ll dive deep into Bash arrays, covering everything from basic declaration to advanced operations and practical use cases. By the end, you’ll be equipped to leverage arrays to write cleaner, more powerful Bash scripts.

Table of Contents

  1. What Are Bash Arrays?
  2. Types of Bash Arrays
  3. Declaring and Initializing Arrays
  4. Accessing Array Elements
  5. Modifying Arrays: Adding, Updating, and Deleting Elements
  6. Array Length and Element Size
  7. Looping Through Arrays
  8. Advanced Array Operations
  9. Associative Arrays in Depth
  10. Practical Examples
  11. Common Pitfalls and Best Practices
  12. Conclusion
  13. References

What Are Bash Arrays?

A Bash array is a variable that can hold multiple values, each identified by an index (for indexed arrays) or a key (for associative arrays). Unlike regular Bash variables, which store a single string, arrays let you group related data, such as lists of filenames, user inputs, or configuration options.

Arrays were introduced in Bash 3.0 (2004) and have been expanded in later versions (e.g., associative arrays in Bash 4.0). They are supported by all modern Linux distributions and macOS (though macOS uses Bash 3.2 by default; see Pitfalls for details).

Types of Bash Arrays

Bash supports two primary types of arrays:

1. Indexed Arrays

The default array type, where elements are accessed via numeric indices (starting at 0 by default). Indices are integers, and elements are stored in contiguous or non-contiguous positions (sparse arrays).

2. Associative Arrays

Introduced in Bash 4.0, associative arrays use string keys instead of numeric indices. They are ideal for storing key-value pairs (e.g., dictionaries or hash maps).

3. Sparse Arrays

A subset of indexed arrays where indices are non-consecutive (e.g., arr[0]=a; arr[2]=b skips index 1). Sparse arrays are useful for scenarios where you need to map arbitrary integers to values, but they can complicate iteration.

Declaring and Initializing Arrays

Indexed Arrays

There are several ways to declare and initialize indexed arrays:

Method 1: Implicit Declaration

You can initialize an array directly without prior declaration:

# Space-separated values in parentheses; elements with spaces must be quoted
fruits=("apple" "banana" "cherry" "date")

Method 2: Explicit Declaration with declare -a

Use declare -a to explicitly define an indexed array (optional but improves readability):

declare -a cars  # Declare array
cars=("Toyota" "Honda" "Ford")  # Initialize later

Method 3: Individual Element Assignment

Assign elements to specific indices after declaration:

colors=()  # Empty array
colors[0]="red"
colors[1]="green"
colors[3]="blue"  # Creates a sparse array (index 2 is undefined)

Associative Arrays

Associative arrays require explicit declaration with declare -A (Bash 4.0+):

Method 1: Key-Value Initialization

declare -A capitals  # Declare associative array
capitals=(
  ["France"]="Paris"
  ["Germany"]="Berlin"
  ["Japan"]="Tokyo"
)

Method 2: Dynamic Key Assignment

declare -A user  # Empty associative array
user["name"]="Alice"
user["age"]="30"
user["city"]="New York"

Accessing Array Elements

Accessing Indexed Array Elements

Use ${array[index]} to retrieve a single element:

fruits=("apple" "banana" "cherry")
echo ${fruits[0]}  # Output: apple (first element)
echo ${fruits[2]}  # Output: cherry (third element)

Accessing All Elements

To access all elements of an array, use ${array[@]} or ${array[*]}:

  • ${array[@]}: Expands to individual elements (preserves spaces in elements when quoted).
  • ${array[*]}: Expands to a single string with elements joined by the first character of IFS (default: space).

Example:

echo "Using @: ${fruits[@]}"  # Output: apple banana cherry
echo "Using *: ${fruits[*]}"  # Output: apple banana cherry (same here, but see quoting below)

# Quoted @ preserves spaces in elements; quoted * does not
words=("hello world" "foo bar")
echo "Quoted @: ${words[@]}"  # Output: hello world foo bar (two elements)
echo "Quoted *: ${words[*]}"  # Output: hello world foo bar (single string, joined by space)

Accessing Ranges of Elements

Use ${array[@]:start:count} to slice an array (start at start index, return count elements):

numbers=(0 1 2 3 4 5)
echo ${numbers[@]:1:3}  # Output: 1 2 3 (start at index 1, return 3 elements)
echo ${numbers[@]: -2}  # Output: 4 5 (negative start = offset from end)

Accessing Associative Array Elements

Use ${array[key]} to retrieve values by key:

echo ${capitals["France"]}  # Output: Paris
echo ${user["name"]}        # Output: Alice

Modifying Arrays

Adding Elements

Indexed Arrays

  • Append to end: Use += to add elements to the end of the array:
    fruits+=("elderberry" "fig")  # Now fruits has 6 elements
  • Insert at index: Assign to a specific index (overwrites if the index exists):
    fruits[2]="coconut"  # Replaces "cherry" with "coconut"

Associative Arrays

Add new key-value pairs by assignment:

capitals["Italy"]="Rome"  # New key "Italy" with value "Rome"

Updating Elements

Overwrite existing elements by reassigning their index/key:

# Indexed array
fruits[0]="apricot"  # Update first element to "apricot"

# Associative array
capitals["Germany"]="Berlin (Capital)"  # Update value for "Germany"

Deleting Elements

Use unset to remove elements or entire arrays:

# Delete indexed array element
unset fruits[1]  # Removes "banana" (index 1)

# Delete associative array element
unset capitals["Japan"]  # Removes key "Japan"

# Clear entire array
unset fruits  # Deletes the array itself

Array Length and Element Size

Total Number of Elements

Use ${#array[@]} to get the length of an array (number of elements):

fruits=("apple" "banana" "cherry")
echo ${#fruits[@]}  # Output: 3 (3 elements)

Length of a Specific Element

Use ${#array[index]} to get the character count of an element:

echo ${#fruits[0]}  # Output: 5 ("apple" has 5 characters)

Associative Array Size

Same syntax applies to associative arrays:

echo ${#capitals[@]}  # Output: 2 (after deleting "Japan" earlier)

Looping Through Arrays

Looping is critical for processing array elements. Use these patterns:

Looping Through Indexed Arrays

Method 1: For Loop with ${array[@]}

Iterate over elements directly (preserves spaces in elements when quoted):

fruits=("apple" "banana split" "cherry")
for fruit in "${fruits[@]}"; do
  echo "Current fruit: $fruit"
done

Output:

Current fruit: apple
Current fruit: banana split
Current fruit: cherry

Method 2: C-Style For Loop (Indices)

Loop over indices using a numeric range (avoids issues with sparse arrays):

for ((i=0; i<${#fruits[@]}; i++)); do
  echo "Index $i: ${fruits[i]}"
done

Looping Through Associative Arrays

Loop Over Values

for city in "${capitals[@]}"; do
  echo "Capital: $city"
done

Loop Over Keys

Use ${!array[@]} to get keys, then access values:

for country in "${!capitals[@]}"; do
  echo "$country: ${capitals[$country]}"
done

Output:

France: Paris
Germany: Berlin (Capital)
Italy: Rome

Advanced Array Operations

Slicing

Extract a subset of elements with ${array[@]:start:count} (see Accessing Ranges).

Concatenation

Combine two arrays by expanding them into a new array:

arr1=("a" "b")
arr2=("c" "d")
combined=("${arr1[@]}" "${arr2[@]}")  # combined=("a" "b" "c" "d")

Sorting

Use the sort command to sort array elements (works for indexed arrays):

numbers=(5 2 8 1 3)
sorted_numbers=($(printf "%s\n" "${numbers[@]}" | sort -n))  # -n for numeric sort
echo "${sorted_numbers[@]}"  # Output: 1 2 3 5 8

Filtering

Use grep to filter elements matching a pattern:

words=("apple" "apricot" "banana" "avocado")
apple_related=($(printf "%s\n" "${words[@]}" | grep "^ap"))  # Elements starting with "ap"
echo "${apple_related[@]}"  # Output: apple apricot

Associative Arrays in Depth

Associative arrays shine for key-value storage. Here are advanced tips:

Check if a Key Exists

Use parameter expansion to test for keys:

if [[ -v capitals["France"] ]]; then
  echo "France's capital is ${capitals["France"]}"
else
  echo "France not found"
fi

(-v checks if the key is set; Bash 4.3+.)

Iterate Over Keys in Order

Associative array keys are unordered by default. To sort keys:

for country in $(printf "%s\n" "${!capitals[@]}" | sort); do
  echo "$country: ${capitals[$country]}"
done

Practical Examples

Example 1: Command-Line Argument Processor

Use $@ (array of arguments) to handle inputs:

#!/bin/bash
args=("$@")  # Store arguments in array
echo "You provided ${#args[@]} arguments:"
for i in "${!args[@]}"; do
  echo "Arg $((i+1)): ${args[i]}"
done

Usage:

./script.sh hello world "bash arrays"

Output:

You provided 3 arguments:
Arg 1: hello
Arg 2: world
Arg 3: bash arrays

Example 2: Word Frequency Counter

Use an associative array to count word occurrences in a file:

#!/bin/bash
declare -A word_counts

# Read file line by line
while read -r line; do
  # Split line into words (handle punctuation with tr)
  words=($(echo "$line" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]'))
  for word in "${words[@]}"; do
    ((word_counts["$word"]++))  # Increment count for each word
  done
done < "sample.txt"

# Print results sorted by frequency
echo "Word frequencies:"
for word in $(printf "%s\n" "${!word_counts[@]}" | sort); do
  echo "$word: ${word_counts[$word]}"
done

Example 3: To-Do List Manager

Use an indexed array to manage tasks:

#!/bin/bash
declare -a todos=()

add_task() {
  todos+=("$1")
  echo "Added task: '$1' (Total: ${#todos[@]})"
}

list_tasks() {
  echo -e "\nTo-Do List:"
  for i in "${!todos[@]}"; do
    echo "$((i+1)). ${todos[i]}"
  done
}

# Usage: ./todo.sh add "Buy milk"
case "$1" in
  add) add_task "$2" ;;
  list) list_tasks ;;
  *) echo "Usage: $0 {add 'task'|list}" ;;
esac

Common Pitfalls and Best Practices

1. Quoting ${array[@]}

Always quote ${array[@]} to preserve spaces in elements:

# Bad: Splits "hello world" into two elements
for item in ${words[@]}; do ... done

# Good: Preserves "hello world" as one element
for item in "${words[@]}"; do ... done

2. Use @ Instead of * in Loops

${array[*]} joins elements into a single string, which breaks iteration over elements with spaces. Use ${array[@]} instead.

3. Associative Arrays Require Bash 4+

macOS ships with Bash 3.2 (due to licensing). To use associative arrays, install Bash 4+ via Homebrew:

brew install bash  # macOS

Then run scripts with bash script.sh (not sh).

4. Avoid Sparse Arrays Unless Necessary

Sparse arrays (e.g., arr[0]=a; arr[2]=b) can cause unexpected behavior in loops using for ((i=0; ...)). Use for i in "${!arr[@]}" to iterate over defined indices.

5. Unset vs. Clearing Arrays

unset arr deletes the array entirely, while arr=() clears elements but keeps the array defined.

Conclusion

Bash arrays are a versatile tool for organizing and processing data in shell scripts. From simple lists (indexed arrays) to key-value stores (associative arrays), they replace messy collections of individual variables and simplify iteration, filtering, and modification.

By mastering arrays, you’ll write cleaner, more efficient scripts that handle complex data with ease. Start small—replace space-separated strings with indexed arrays, then graduate to associative arrays for advanced use cases like counting or configuration management.

References

Let me know if you have any questions or need further clarification! Happy scripting! 🚀