As a Linux system administrator, being able to efficiently process data sets is an invaluable skill for automating tasks. The bash shell offers simple, yet powerful array functionality that unlocks this capability. In this expert guide, we will thoroughly cover bash for loops for iterating through array data, with practical examples of usage along the way.

A Quick Intro to Bash Arrays and Loops

First, what exactly are bash arrays and loops?

Arrays allow you to store multiple data elements under a single variable name. Instead of creating a separate variable for each item, you can use a single array with indexed positions:

fruits=(apple orange banana)  

We can now access each element by its index. For example, to print the second fruit:

echo ${fruits[1]} # orange

Loops let you repeat a section of code multiple times. The bash for loop iterates over a list of values, running once per value:

for i in 1 2 3; do
    echo "Loop iteration $i" 
done

Output:

Loop iteration 1
Loop iteration 2 
Loop iteration 3

When we combine arrays and loops, we get the ability to process every element of an array automatically. Let‘s see it in action.

Displaying Array Contents with a Loop

Let‘s store a list of names in an array, then loop through and print each one:

#!/bin/bash

names=("John" "James" "Sarah" "Mary")

for name in "${names[@]}"; do
    echo $name
done 

When run, this outputs:

John  
James  
Sarah
Mary

The loop iterates through each array index, with $name set to the element value for easy access. No matter how many items are added to the array, this code scales.

Some key points:

  • "${names[@]}" expands to all array elements
  • The quotes prevent issues with spaces in values
  • ${names[index]} also works if you need direct access

Now we can operate on each name inside the loop!

Transforming Array Elements Within the Loop

Building on the previous example, let‘s append a greeting to names:

#!/bin/bash  

names=("Amy" "Brian" "Cathy")  

for name in "${names[@]}"; do
    greeting="Hello $name!"
    echo $greeting  
done

Prints:

Hello Amy!  
Hello Brian!
Hello Cathy!

You can perform any tasks like formatting transformations, writing to files, updating databases, sending API calls, and anything else needed to process array data.

The loop body runs once per element, automating these tasks at scale.

Conditional Logic

Beyond basic operations, we can introduce control flow like if statements for more advanced array processing:

names=("Jim" "Pam" "Dwight")  

for name in "${names[@]}"; do
    if [[ "$name" == "Dwight" ]]; then
        echo "Beets, Bears, Battlestar Galactica" 
    else
        echo "Hello $name"
    fi  
done

Prints:

Hello Jim
Hello Pam  
Beets, Bears, Battlestar Galactica

Now code execution can change dynamically based on the current element value.

Writing Array Elements to a File

Let‘s log our array data to a CSV file for further analysis:

numbers=(12 55 32 71 24)

for n in "${numbers[@]}"; do
    echo $n >> numbers.csv  
done   

This populates numbers.csv with:

12
55  
32
71
24

The redirect >> appends rather than overwrites the file. We have quickly exported array data without needing temporary holders or a database.

Looping Through Associative Array Keys/Values

Associative arrays contain key-value pairs. We can loop through just keys or just values if needed.

Given this array:

declare -A sounds  

sounds[dog]="bark"
sounds[cow]="moo"   
sounds[bird]="chirp"

To print all keys:

for sound in "${!sounds[@]}"; do
   echo $sound
done

# Prints:
dog
cow
bird

The ! provides the array keys rather than the values.

To print all values:

for sound in "${sounds[@]}"; do
   echo $sound
done 

# Prints: 
bark
moo
chirp 

This provides more flexibility when needing to operate on just one or the other.

Multidimensional Arrays

Bash allows arrays containing other arrays. Consider this 2D matrix:

matrix=(
    (1 2 3)
    (4 5 6)
    (7 8 9) 
)

We can access elements like matrix[1][2] = 5.

To loop through all values:

for row in "${matrix[@]}"; do
    for col in "${row[@]}"; do
       echo $col 
    done
done

Prints out each number:

1
2
3
4
5  
6
7
8
9

The nested loop iterates through the outer rows, then inner columns.

This extends well to processing tabular data for reports and analytics.

Associative Arrays

Associative arrays contain key-value pairs for efficient lookups, like dictionaries in other languages.

Define with:

declare -A sounds

sounds[dog]="bark" 
sounds[cow]="moo"
sounds[bird]="chirp"  

We access values via:

echo ${sounds[bird]} # chirp

Checking keys:

animal="frog"

if [[ -z ${sounds[$animal]+x} ]]; then
   echo "$animal sound not found" 
fi

Prints frog sound not found since that key does not exist.

These data structures open many new scripting possibilities!

Practical Examples

While we have covered basic array iteration, you may still wonder how these concepts apply in real shell scripts. Let‘s go through some practical examples.

Read Command Line Arguments into Array

Bash scripts often accept runtime arguments:

#!/bin/bash

files=()

for arg in "$@"; do
    files+=("$arg")  
done

# Operate on files array 

Now the provided filenames get stored for future processing!

Recursive Directory Reading

To recursively traverse a directory structure:

#!/bin/bash

shopt -s globstar nullglob

all_files=() 

for file in **/*; do   
    all_files+=("$file") 
done

# all_files contains full paths recursively

This provides all files under the current directory in an array for easy iteration.

Splitting Strings into Arrays

We can split strings into arrays on a delimiter using IFS and read:

string="apple,orange,banana"

IFS=‘,‘ read -ra items <<< "$string"

echo "${items[1]}" # orange

This splits the string on commas into an array. Great for parsing!

Executing SSH Commands with Arrays

To run SSH commands stored in arrays:

hosts=(
  "server1.example.com"
  "server2.example.com"
)

commands=(
  "uname -a"
  "uptime"
)


for host in "${hosts[@]}"; do
  for cmd in "${commands[@]}"; do 
    ssh $host "$cmd"
  done  
done

This executes each command on each host automatically using native bash process substitution.

Parsing Log Files

To parse a log file into arrays per line:

lines=() 

while read -r line; do
  lines+=("$line")    
done < "app.log"

# Process log data line-by-line  

The while loop populates the array efficiently, avoiding memory issues for large logs.

Randomizing Array Order

To randomize array contents:

nums=(1 2 3 4 5 6 7 8) 

for ((i=0; i<${#nums[@]}; i++)); do
    j=$((RANDOM % ${#nums[@]})) 
    tmp=${nums[i]} 
    nums[i]=${nums[j]}
    nums[j]=$tmp
done

echo ${nums[@]} # Random order!

This fisher yates shuffle algorithm uses swapping to randomize order.

Array Memory Performance

Bash arrays have fast insertion and low memory overhead compared to strings:

Strings:

names=""  
for i in John James Sarah; do
  names="$names $i"  
done

echo $names
# John James Sarah 

Arrays: 

names=()
for i in John James Sarah; do
  names+=("$i")  
done

echo ${names[@]}
# John James Sarah

The string append requires creating new string allocations constantly, becoming inefficient at scale. Arrays only allocate once.

Thus for large data applications, arrays have superior performance.

In Closing

Bash arrays, loops, and their combinations serve as integral tools for nearly any automation task. Whether it is processing command line arguments, traversing directories, transforming data, or exporting to files, arrays simplify the workflow.

I encourage you to explore arrays as fundamental data structures when writing your next shell scripts. Feel free to leave any questions below on creative ways to leverage arrays and loops together in your environment!

Similar Posts