Chapter 22: Shell Scripting: Loops (for, while, until)

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamental principles of iteration in shell scripting and its importance in automating embedded tasks.
  • Implement for loops to iterate over static lists, file system objects, and command output.
  • Construct while loops to execute commands repeatedly based on the success of a condition.
  • Utilize until loops to perform actions as long as a condition remains false.
  • Debug common loop-related issues such as infinite loops, off-by-one errors, and quoting problems.
  • Apply looping constructs to solve practical problems on a Raspberry Pi 5, such as polling hardware and processing data.

Introduction

In the world of embedded systems, automation is not a luxury; it is a necessity. Embedded devices often operate autonomously, performing repetitive tasks without human intervention. Whether it’s a weather station periodically logging sensor data, a security camera monitoring for motion, or an industrial controller maintaining a set temperature, the ability to repeat actions is fundamental. This is where the power of shell scripting loops becomes indispensable. Loops provide the control structure needed to execute a block of code multiple times, forming the backbone of automation, monitoring, and data processing scripts.

This chapter delves into the core iterative constructs of the shell: the for, while, and until loops. While they all facilitate repetition, each has a distinct purpose and is suited to different scenarios. The for loop is your tool of choice when you have a known, finite set of items to process, such as a list of files or a range of numbers. The while and until loops, on the other hand, are designed for situations where the number of iterations is unknown beforehand. They continue executing as long as a specific condition is met (or not met), making them perfect for tasks like waiting for a sensor to reach a certain value or polling a network service until it becomes available.

By mastering these looping mechanisms on your Raspberry Pi 5, you will unlock a new level of control over your embedded environment. You will move from executing simple, one-off commands to writing sophisticated scripts that can manage system resources, interact with hardware, and perform complex, stateful operations, bringing your embedded projects to life.

Technical Background

At its heart, a loop is a control flow statement that allows a sequence of instructions to be executed repeatedly. The shell, being a command-line interpreter, provides powerful and flexible looping constructs that are essential for automating tasks. Understanding the nuances of each loop type is crucial for writing efficient, readable, and robust shell scripts.

The for Loop: Iterating Over a Finite World

The for loop is the most intuitive looping construct for many developers. Its primary purpose is to iterate over a predefined, finite list of items. For each item in the list, the loop executes a block of commands, with the current item’s value assigned to a variable. This makes it exceptionally useful for batch processing files, working through a list of servers, or performing an operation a set number of times.

The classic for loop syntax is straightforward:

Bash
for variable_name in item1 item2 item3 ...
do
    # Commands to execute for each item
    # The current item is available as $variable_name
done

Here, variable_name is a placeholder that takes on the value of each item in the list sequentially. The do and done keywords enclose the body of the loop.

A common use case in embedded systems is processing a set of data files. Imagine you have a series of log files from a sensor, named sensor_log_20240701.txt, sensor_log_20240702.txt, and so on. A for loop can process each one in turn:

Bash
for logfile in sensor_log_*.txt
do
    echo "Processing $logfile..."
    # Further commands to analyze or archive the file
done

The shell’s globbing feature (*) expands sensor_log_*.txt into a list of all matching filenames in the current directory. The for loop then iterates over this dynamically generated list. This is far more efficient and scalable than writing a separate command for each file.

It’s critical to understand how the shell performs word splitting. The in part of the loop expects a space-separated list of “words.” If an item in your list contains spaces (like a filename My Sensor Data.txt), the shell will treat “My,” “Sensor,” and “Data.txt” as three separate items. To handle this correctly, you should always quote the variable when you use it:

Bash
echo "Processing '$logfile'..."

Tip: Always double-quote variables ("$variable_name") inside a loop’s body to prevent unexpected behavior from word splitting and filename expansion, especially when dealing with filenames that might contain spaces or special characters.

Beyond file globbing, for loops can iterate over the output of a command. By using command substitution ($(command)), you can turn the output of any command into a list for the loop. For instance, you could iterate over all subdirectories in the current path:

Bash
for dir in $(find . -maxdepth 1 -type d)
do
    echo "Found directory: $dir"
done

Finally, for developers coming from a C/C++ or Java background, the shell also supports a C-style for loop, which is useful for a fixed number of iterations:

Bash
for (( i=0; i<5; i++ ))
do
    echo "Iteration number $i"
done

This syntax is often cleaner and less error-prone than generating a sequence of numbers with an external command like seq. It is ideal for tasks like blinking an LED a specific number of times or taking a fixed number of sensor readings.

The while Loop: Repeating While a Condition is True

While the for loop is perfect for known sets of data, many tasks in embedded systems require a loop to run until a certain condition changes. For this, we turn to the while loop. A while loop repeatedly executes a block of code as long as a specified condition evaluates to true (a successful exit code of 0).

The general syntax is:

Bash
while [ condition ]
do
    # Commands to execute
done

The condition is typically a test command, often enclosed in square brackets [ ], which is an alias for the test command. The loop continues as long as the test command returns an exit code of 0 (success).

A classic example is a counter-controlled loop, similar to the C-style for loop but constructed differently:

Bash
counter=0
while [ $counter -lt 5 ]
do
    echo "Counter is at $counter"
    counter=$((counter + 1))
done

In this example, the condition [ $counter -lt 5 ] checks if the value of counter is less than 5. The arithmetic expansion $((...)) is used to increment the counter in each iteration. The loop will run exactly five times.

However, the true power of while loops in embedded contexts comes from their ability to use any command as the condition. The loop doesn’t care about “true” or “false” in a boolean sense; it only cares about the exit code of the command. A command that successfully completes returns 0, and any other value indicates failure.

This allows for powerful constructs. For example, you can have a loop that waits for a specific device file to appear, which is common during system boot-up when device drivers are loading asynchronously:

Bash
while [ ! -e /dev/ttyUSB0 ]
do
    echo "Waiting for USB serial device to appear..."
    sleep 1
done
echo "Device /dev/ttyUSB0 is now available."

Here, [ ! -e /dev/ttyUSB0 ] tests for the non-existence (! -e) of the file /dev/ttyUSB0. The loop will pause for one second (sleep 1) and re-check, continuing until the file is created, at which point the condition fails (returns a non-zero exit code) and the loop terminates.

Another common pattern is reading a file line by line:

Bash
while read -r line
do
    echo "Read line: $line"
done < input.txt

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    A[Start] --> B(Redirect file to loop<br><i>done < input.txt</i>);
    B --> C{while read -r line};
    C -- "Success: Line was read" --> D["Execute Commands<br><i>(using $line)</i>"];
    D --> C;
    C -- "Failure: End of File reached" --> E[Loop Terminates];
    E --> F[Continue Script];

    subgraph "File: input.txt"
        direction LR
        L1[Line 1] --> L2[Line 2] --> L3[...] --> L_EOF(EOF);
    end

    subgraph "Loop Body"
        D
    end

    linkStyle 0 stroke-width:1px;
    style L1 fill:#fff,stroke:#333,stroke-width:1px;
    style L2 fill:#fff,stroke:#333,stroke-width:1px;
    style L3 fill:#fff,stroke:#333,stroke-width:1px;
    style L_EOF fill:#ef4444,stroke:#ef4444,color:#fff;


    %% Styling
    classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;

    class A,B primary;
    class C decision;
    class D process;
    class E,F success;

In this elegant construct, the read command is the loop’s condition. read returns a successful exit code (0) as long as it can read a line from its standard input. When it reaches the end of the file, it fails, returning a non-zero exit code and terminating the loop. The -r option prevents backslash interpretation, which is a best practice. The redirection < input.txt feeds the file input.txt into the standard input of the while loop.

The until Loop: Repeating While a Condition is False

The until loop is the logical inverse of the while loop. It executes a block of commands as long as its condition evaluates to false (a non-zero exit code). Once the condition becomes true (exit code 0), the loop terminates.

The syntax is nearly identical to while:

Bash
until [ condition ]
do
    # Commands to execute
done

You can think of until [ condition ] as being equivalent to while ! [ condition ]. The choice between while and until is often a matter of readability. Sometimes, expressing a condition in the positive (until) is more natural than expressing it in the negative (while !).

For example, let’s revisit the device-waiting script. We could rewrite it with until to wait until the device exists:

Bash
until [ -e /dev/ttyUSB0 ]
do
    echo "Waiting for USB serial device to appear..."
    sleep 1
done
echo "Device /dev/ttyUSB0 is now available."

For many, this reads more naturally: “Do this stuff until the file exists.” This can make scripts easier to understand and maintain.

Another practical use case is waiting for a network service to start. An embedded device might need to connect to a server on startup, but the network connection or the server itself might not be ready immediately. An until loop can poll for connectivity:

Bash
until ping -c 1 -W 1 8.8.8.8 &> /dev/null
do
    echo "Network is not up yet. Retrying in 5 seconds..."
    sleep 5
done
echo "Network connection established."

In this example, ping -c 1 -W 1 8.8.8.8 sends a single packet to Google’s DNS server with a 1-second timeout. The &> /dev/null part redirects both standard output and standard error to /dev/null, so we don’t see the ping command’s output on the console. The ping command will return a successful exit code (0) only if it receives a reply. The until loop will continue executing its body as long as the ping command fails, making it a robust way to pause a script until network connectivity is confirmed.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    subgraph For Loop
        direction LR
        FOR_START(Start Iteration) --> FOR_PROCESS{For each item in list};
        FOR_PROCESS --> FOR_EXEC["Execute Commands<br><i>(using current item)</i>"];
        FOR_EXEC --> FOR_CHECK{More items?};
        FOR_CHECK -- Yes --> FOR_PROCESS;
    end

    subgraph While Loop
        direction TB
        WHILE_START(Start Loop) --> WHILE_COND{Is Condition<br><b>TRUE</b>?};
        WHILE_COND -- Yes --> WHILE_EXEC[Execute Commands];
        WHILE_EXEC --> WHILE_COND;
    end

    subgraph Until Loop
        direction TB
        UNTIL_START(Start Loop) --> UNTIL_COND{Is Condition<br><b>FALSE</b>?};
        UNTIL_COND -- Yes --> UNTIL_EXEC[Execute Commands];
        UNTIL_EXEC --> UNTIL_COND;
    end

    START_NODE[Start Script] --> CHOOSE_LOOP{Choose Loop Type};

    CHOOSE_LOOP -- "Finite List" --> FOR_START;
    CHOOSE_LOOP -- "Condition is True?" --> WHILE_START;
    CHOOSE_LOOP -- "Condition is False?" --> UNTIL_START;

    FOR_CHECK -- No --> END_NODE[Continue Script];
    WHILE_COND -- No --> END_NODE;
    UNTIL_COND -- No --> END_NODE;

    %% Styling
    classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;

    class START_NODE,FOR_START,WHILE_START,UNTIL_START primary;
    class END_NODE success;
    class CHOOSE_LOOP,FOR_CHECK,WHILE_COND,UNTIL_COND decision;
    class FOR_PROCESS,FOR_EXEC,WHILE_EXEC,UNTIL_EXEC process;
Shell Loop Comparison: `for` vs. `while` vs. `until`
Feature for Loop while Loop until Loop
Primary Use Case Iterating over a known, finite list of items (files, numbers, strings). Repeating as long as a condition is true. Ideal when the number of iterations is unknown. Repeating as long as a condition is false. Best for “wait until” scenarios.
Condition Logic Iterates through each item supplied in a list. The loop itself doesn’t have a conditional test in the same way as `while` or `until`. while [ condition ]
Loop continues if the `condition` command has an exit code of 0 (success).
until [ condition ]
Loop continues if the `condition` command has a non-zero exit code (failure).
Syntax Example for f in *.log; do echo "Found $f" done i=0 while [ $i -lt 5 ]; do echo $i i=$((i+1)) done until ping -c 1 host &>/dev/null; do echo "Waiting..." sleep 5 done
Embedded Example Batch renaming or archiving a set of sensor data files. Continuously reading a GPIO pin’s state or monitoring CPU temperature. Waiting for a USB device to be connected or a network service to start.

Practical Examples

Theory provides the foundation, but true understanding comes from hands-on application. In this section, we will apply our knowledge of loops to solve practical problems on the Raspberry Pi 5. These examples will involve interacting with the file system, reading hardware status, and managing system processes.

Example 1: Archiving Log Files with a for Loop

Scenario: Your embedded application generates daily log files in /var/log/sensors/. You need a script that runs periodically to compress any uncompressed log files (.log) into .tar.gz archives and then delete the original.

File Structure:

Assume the log directory looks like this:

Plaintext
/var/log/sensors/
├── 2024-07-05.log
├── 2024-07-06.log
├── 2024-07-07.log
└── 2024-07-04.tar.gz

The Script: archive_logs.sh

Bash
#!/bin/bash

# A script to archive sensor log files.

LOG_DIR="/var/log/sensors"
ARCHIVE_DIR="/var/log/sensors/archive"

# Ensure the archive directory exists
mkdir -p "$ARCHIVE_DIR"

# Check if we are in the correct directory
if ! cd "$LOG_DIR"; then
    echo "Error: Could not change to log directory $LOG_DIR" >&2
    exit 1
fi

echo "Starting log archival process..."

# Use a for loop to find all .log files
for logfile in *.log; do
    # The glob *.log will literally return "*.log" if no files match.
    # We must check if the file actually exists before processing.
    if [ -f "$logfile" ]; then
        echo "Processing '$logfile'..."
        
        # Define the name of the archive file
        archive_name="${logfile%.log}.tar.gz"
        
        # Compress the file
        tar -czf "$ARCHIVE_DIR/$archive_name" "$logfile"
        
        # Check if tar command was successful (exit code 0)
        if [ $? -eq 0 ]; then
            echo "Successfully created archive '$archive_name'."
            # Remove the original log file
            rm "$logfile"
            echo "Removed original log file '$logfile'."
        else
            echo "Error: Failed to archive '$logfile'." >&2
        fi
        echo "---"
    fi
done

echo "Log archival process finished."

Explanation:

  1. Shebang and Setup: #!/bin/bash ensures the script is run with Bash. We define variables for the log and archive directories for easy modification.
  2. Directory Management: mkdir -p creates the archive directory if it doesn’t already exist. The script then changes into the log directory. This is important because it allows us to use *.log without worrying about the full path.
  3. The for Loop: for logfile in *.log; do starts the loop. The shell expands *.log to a list of all files in the current directory ending with .log.
  4. Existence Check: if [ -f "$logfile" ]; then is a crucial safety check. If no .log files exist, the glob *.log will not expand and the loop will run once with logfile literally being the string “*.log”. This check ensures we only process actual files.
  5. Archiving: tar -czf ... creates a gzipped tarball. The ${logfile%.log} is a parameter expansion that removes the .log suffix from the filename.
  6. Error Checking: if [ $? -eq 0 ]; then checks the exit status of the most recent command (tar). If tar was successful (returned 0), we proceed to delete the original file. Otherwise, we print an error. This prevents data loss if the archiving step fails.
  7. Quoting: Note that "$logfile" is quoted everywhere to handle filenames with spaces or other special characters gracefully.

How to Run:

  1. Save the code as archive_logs.sh.
  2. Make it executable: chmod +x archive_logs.sh.
  3. Create some dummy log files: touch /var/log/sensors/2024-07-05.log /var/log/sensors/2024-07-06.log.
  4. Run the script: ./archive_logs.sh.

Expected Output:

Plaintext
Starting log archival process...
Processing '2024-07-05.log'...
Successfully created archive '2024-07-05.tar.gz'.
Removed original log file '2024-07-05.log'.
---
Processing '2024-07-06.log'...
Successfully created archive '2024-07-06.tar.gz'.
Removed original log file '2024-07-06.log'.
---
Log archival process finished.

Example 2: Monitoring CPU Temperature with a while Loop

Scenario: You want to monitor the Raspberry Pi 5’s CPU temperature and log a warning if it exceeds a certain threshold (e.g., 75°C). The script should run continuously until manually stopped.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    A[Start Script] --> B(Initialize<br>THRESHOLD=75°C);
    B --> C{while true};
    C -- "Loop forever" --> D[Read Temp from<br><i>/sys/class/thermal/thermal_zone0/temp</i>];
    D --> E[Convert to Celsius];
    E --> F{Temp > THRESHOLD?};
    F -- "Yes" --> G[Log Warning to File<br><i>temp_warnings.log</i>];
    G --> H[Wait 5 seconds<br><i>sleep 5</i>];
    F -- "No" --> H;
    H --> C;

    %% Styling
    classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef system fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff;
    classDef warning fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937;

    class A,B primary;
    class C,F decision;
    class D,E,H process;
    class G warning;

Hardware Integration: No external hardware is needed. We will read the temperature from a virtual file in the /sys filesystem.

The Script: monitor_temp.sh

Bash
#!/bin/bash

# A script to continuously monitor CPU temperature.

# The file where the CPU temperature is stored (in millidegrees Celsius)
TEMP_FILE="/sys/class/thermal/thermal_zone0/temp"

# The temperature threshold in degrees Celsius.
THRESHOLD=75

# The log file for warnings.
LOG_FILE="/home/pi/temp_warnings.log"

echo "Starting CPU temperature monitor. Press [CTRL+C] to stop."
echo "Warnings will be logged to $LOG_FILE"

# Infinite while loop
while true; do
    # Read the raw temperature value
    raw_temp=$(cat "$TEMP_FILE")
    
    # Convert from millidegrees to degrees Celsius
    # Bash only handles integers, so we use printf for floating point, or simple integer division.
    # For simplicity, we'll use integer arithmetic.
    current_temp=$((raw_temp / 1000))
    
    echo "Current CPU Temperature: ${current_temp}°C"
    
    # Check if the temperature exceeds the threshold
    if [ "$current_temp" -gt "$THRESHOLD" ]; then
        # Get the current timestamp
        timestamp=$(date +"%Y-%m-%d %H:%M:%S")
        warning_msg="WARNING: CPU temperature ($current_temp°C) exceeded threshold (${THRESHOLD}°C)."
        
        echo "$timestamp - $warning_msg"
        # Append the warning to the log file
        echo "$timestamp - $warning_msg" >> "$LOG_FILE"
    fi
    
    # Wait for 5 seconds before the next check
    sleep 5
done

Explanation:

  1. Constants: We define variables for the temperature file path, the threshold, and the log file location. This makes the script easy to configure.
  2. Infinite Loop: while true; do creates a loop that will run forever, because the true command always returns a successful exit code (0). This is a standard pattern for continuous monitoring tasks.
  3. Reading Temperature: raw_temp=$(cat "$TEMP_FILE") reads the temperature. The value in this file is in millidegrees Celsius (e.g., 54321 means 54.321°C).
  4. Conversion: current_temp=$((raw_temp / 1000)) performs integer division to get the temperature in whole degrees Celsius.
  5. Conditional Check: if [ "$current_temp" -gt "$THRESHOLD" ]; then compares the current temperature to our threshold.
  6. Logging: If the threshold is exceeded, a timestamped warning is printed to the console and appended (>>) to the log file.
  7. Delay: sleep 5 pauses the loop for 5 seconds. This is crucial to prevent the script from consuming 100% of a CPU core by constantly reading the file.

How to Run:

  1. Save the code as monitor_temp.sh.
  2. Make it executable: chmod +x monitor_temp.sh.
  3. Run the script: ./monitor_temp.sh.
  4. To test the warning, you can temporarily lower the THRESHOLD to a value below the current temperature.
  5. Press Ctrl+C to stop the script.

Expected Output (console):

Plaintext
Starting CPU temperature monitor. Press [CTRL+C] to stop.
Warnings will be logged to /home/pi/temp_warnings.log
Current CPU Temperature: 52°C
Current CPU Temperature: 53°C
Current CPU Temperature: 52°C
...

Example 3: Waiting for a Service with an until Loop

Scenario: You have a custom application that depends on a web server running on localhost:8000. Your startup script needs to wait until this web server is active and responding before proceeding.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    A[Start Script] --> B(Initialize<br>HOST, PORT, MAX_RETRIES);
    B --> C{until service responds};
    C -- "Loop while service is down" --> D{Retry count < MAX_RETRIES?};
    D -- "Yes" --> E[Send HTTP Head Request<br><i>curl --head ...</i>];
    E --> F{"Response contains 200 OK?"};
    F -- "No" --> G[Increment Retry Count];
    G --> H[Wait 5 seconds<br><i>sleep 5</i>];
    H --> C;
    F -- "Yes" --> I[Service is Available!<br>Continue Script];
    D -- "No" --> J[Exit with Error<br>Timeout Reached];

    %% Styling
    classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef network fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef error fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff;

    class A,B primary;
    class I success;
    class C,D,F decision;
    class E network;
    class G,H process;
    class J error;

The Script: wait_for_service.sh

Bash
#!/bin/bash

# A script to wait for a local web service to become available.

HOST="localhost"
PORT="8000"
MAX_RETRIES=12
RETRY_COUNT=0

echo "Waiting for service at $HOST:$PORT..."

# Loop until the service is reachable or we run out of retries.
# We use curl to check for a response.
until curl -s --head "http://${HOST}:${PORT}" | grep "200 OK" > /dev/null; do
    RETRY_COUNT=$((RETRY_COUNT + 1))
    if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
        echo "Error: Service at $HOST:$PORT did not become available after $MAX_RETRIES retries." >&2
        exit 1
    fi
    
    echo "Service not ready yet. Retrying in 5 seconds... (Attempt ${RETRY_COUNT}/${MAX_RETRIES})"
    sleep 5
done

echo "Service at $HOST:$PORT is now available. Proceeding with script."
# Add subsequent commands here...

Explanation:

  1. Configuration: We set the HOST, PORT, and a MAX_RETRIES limit to prevent the script from waiting forever.
  2. The until Loop: The loop’s condition is curl -s --head "http://${HOST}:${PORT}" | grep "200 OK" > /dev/null. Let’s break this down:
    • curl -s --head ...: curl attempts to fetch the headers from the specified URL. -s makes it silent (no progress meter), and --head tells it to only fetch headers, not the full page content, which is faster.
    • | grep "200 OK": The output of curl is piped to grep. grep searches for the string “200 OK”, which is part of a standard successful HTTP response header.
    • > /dev/null: We discard the output of grep, as we only care about its exit code.
    • Logic: grep will return a successful exit code (0) only if it finds “200 OK”. The until loop continues as long as this command fails (i.e., as long as the service is not responding with “200 OK”).
  3. Retry Limit: The if statement inside the loop checks if we’ve exceeded our maximum number of retries. This is a critical safety measure to prevent an infinite loop if the service never starts. If the limit is reached, it prints an error and exits with a non-zero status.
  4. Success: Once grep finds “200 OK”, its exit code is 0, the until condition becomes true, and the loop terminates.

How to Run:

  1. Save the code as wait_for_service.sh and make it executable.
  2. In one terminal, run the script: ./wait_for_service.sh. It will start waiting.
  3. In a second terminal, start a simple Python web server to simulate the service: python3 -m http.server 8000.
  4. Observe the first terminal. As soon as the server starts, the script will detect it and exit.

Expected Output:

Plaintext
Waiting for service at localhost:8000...
Service not ready yet. Retrying in 5 seconds... (Attempt 1/12)
Service not ready yet. Retrying in 5 seconds... (Attempt 2/12)
Service at localhost:8000 is now available. Proceeding with script.

Common Mistakes & Troubleshooting

Loops are powerful, but they are also a common source of bugs. Understanding these pitfalls can save you hours of debugging.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Infinite Loop Script hangs and never finishes. CPU usage might be high. Repetitive output without end.
Solution:
Ensure the loop’s condition will eventually terminate.
  • while/until: Check that a variable inside the loop is modified (e.g., i=$((i+1))).
  • External state: Add a timeout or max retry counter to escape if the condition (e.g., service availability) never changes.
Example:
while [ $i -lt 5 ]; do echo $i; done (forgot to increment i)
while [ $i -lt 5 ]; do echo $i; i=$((i+1)); done
Word Splitting on for Loop A filename with spaces like "My File.txt" is processed as two separate items: "My" and "File.txt".
Solution:
Always enclose the loop variable in double quotes when it is used. This treats the entire item as a single string.
Example:
for f in *; do cat $f; done
for f in *; do cat "$f"; done
Off-by-One Error Loop runs one time too many or one time too few.
Solution:
Carefully check your boundary conditions. Use -lt (less than) vs -le (less than or equal to) correctly. For C-style loops, check < vs <=.
Example (run 5 times):
for ((i=0; i<5; i++))
while [ "$i" -lt 5 ]
Empty Glob Expansion A loop like for f in *.log runs once with f being the literal string “*.log” if no .log files exist.
Solution:
Add a check inside the loop to ensure the file actually exists before processing it.
Example:
for f in *.log; do
  if [ -f "$f" ]; then
    echo "Processing $f"
  fi
done
Incorrect Test Logic Loop behaves unexpectedly because the condition is flawed.
Solution:
Use the correct comparison operators:
  • Integers: -eq, -ne, -gt, -lt, -ge, -le
  • Strings: = (or ==), !=
  • File checks: -e (exists), -f (is file), -d (is directory)
Test commands outside the loop with echo $? to verify their exit codes.

Exercises

These exercises are designed to be completed on your Raspberry Pi 5. They will reinforce the concepts covered in this chapter.

  1. LED Blinker with a for Loop:
    • Objective: Write a shell script that blinks an LED connected to a GPIO pin 10 times.
    • Guidance:
      1. You will need to use the gpiodetect, gpioset, and gpioget commands. Find the GPIO chip for the pin you want to use (e.g., gpiochip0).
      2. Use a C-style for loop that iterates from 1 to 10.
      3. Inside the loop, use gpioset to turn the LED on, sleep for a short duration (e.g., 0.5 seconds), use gpioset again to turn it off, and sleep again.
    • Verification: The LED should blink exactly 10 times.
  2. User Countdown with a while Loop:
    • Objective: Write a script that prompts the user for a number and then counts down to zero from that number.
    • Guidance:
      1. Use the read -p command to ask the user for a starting number.
      2. Use a while loop with an integer comparison ([ "$number" -ge 0 ]).
      3. Inside the loop, echo the current number and then decrement it using number=$((number - 1)).
      4. Include a sleep 1 in the loop to make the countdown visible.
    • Verification: If the user enters 5, the script should print 5, 4, 3, 2, 1, 0 at one-second intervals.
  3. System Uptime Waiter with an until Loop:
    • Objective: Write a script that waits until the system has been running for at least 5 minutes before executing a command.
    • Guidance:
      1. The system uptime in seconds can be found in /proc/uptime. The first number on the line is the uptime. You can extract it with cut -d' ' -f1 /proc/uptime.
      2. Use an until loop. The condition should check if the uptime is greater than 300 seconds.
      3. Inside the loop, print a status message like “System not ready, uptime is X seconds…” and sleep for 10 seconds.
      4. After the loop terminates, print a message like “System has been up for over 5 minutes. Starting main task.”
    • Verification: The script should repeatedly print status messages until the 5-minute mark is passed.
  4. File Content Processor:
    • Objective: Create a script that reads a configuration file line by line and prints only the lines that are not comments (i.e., do not start with #) and are not empty.
    • Guidance:
      1. Create a sample configuration file config.txt with a mix of settings, comments, and blank lines.
      2. Use a while read -r line loop to process the file.
      3. Inside the loop, use an if statement with two conditions. You can check if a line starts with # using a pattern match: [[ "$line" =~ ^# ]]. You can check for a non-empty line with [ -n "$line" ].
      4. Combine the conditions to print the line only if it’s not a comment AND it’s not empty.
    • Verification: The script’s output should be only the valid configuration lines from your sample file.
  5. Batch Rename Utility:
    • Objective: Write a script that renames all .jpeg files in a directory to .jpg.
    • Guidance:
      1. Create a directory and populate it with some dummy .jpeg files: touch image1.jpeg image2.jpeg.
      2. Use a for loop to iterate over all *.jpeg files.
      3. Inside the loop, use parameter expansion to construct the new filename: new_name="${file%.jpeg}.jpg".
      4. Use the mv command to perform the rename: mv "$file" "$new_name".
      5. Add echo statements to report what is being renamed.
    • Verification: After running the script, ls should show that all the files now have a .jpg extension.

Summary

  • Loops are fundamental for automation in embedded Linux, enabling repetitive tasks like monitoring, polling, and batch processing.
  • The for loop is best for iterating over a known, finite set of items, such as files on the filesystem (for f in *.txt), a static list of strings (for s in one two three), or a sequence of numbers (for ((i=0; i<10; i++))).
  • The while loop executes a block of code as long as a condition is true (returns a zero exit code). It is ideal for tasks where the number of iterations is unknown, such as reading a file line-by-line (while read) or running until a state changes (while [ -d /tmp/processing ]).
  • The until loop is the inverse of while, executing as long as a condition is false (returns a non-zero exit code). It often improves readability for “wait-for” logic (until ping -c 1 server).
  • Quoting variables ("$var") is critical to prevent errors from word splitting and globbing, especially when dealing with filenames.
  • Always consider exit conditions to avoid infinite loops, implementing timeouts or retry counters as a safety measure.
Practical Guide: Choosing the Right Loop
Scenario / Task Recommended Loop & Rationale
Process all .log files in a directory. for loop (for f in *.log)
Ideal for a finite set of items generated by file globbing.
Blink an LED exactly 20 times. C-style for loop (for ((i=0; i<20; i++)))
The cleanest and most direct way to perform a fixed number of iterations.
Continuously monitor CPU temperature. while true loop
Perfect for creating an infinite loop for a daemon-like task that runs until manually stopped.
Read a configuration file line by line. while read loop
The standard, safest way to process text files. The loop elegantly terminates at the end of the file.
Wait for a network connection to be established. until loop (until ping ...)
The logic “loop until this command succeeds” is more readable and intuitive than the `while !` equivalent.
Wait for a specific USB device file to appear. until loop (until [ -e /dev/ttyUSB0 ])
Similar to waiting for a network, this “wait until something exists” logic is a perfect fit for `until`.

Further Reading

  1. Bash Manual (Loops): The official GNU Bash documentation is the definitive source.
  2. The Linux Command Line by William Shotts: A highly regarded, comprehensive book on the shell. Chapter 27 is dedicated to loops.
  3. Raspberry Pi Documentation – The gpiozero library (for Python context): While not shell scripting, understanding how hardware is controlled in a high-level language provides good context for what you might automate with shell scripts.
  4. Advanced Bash-Scripting Guide: An in-depth, classic resource covering many advanced topics, including subtleties of loops.
  5. Greg’s Wiki – BashFAQ: An excellent resource that answers frequently asked questions with best-practice solutions, including many related to loops.
  6. man test: The manual page for the test command (and its [ alias) is essential reading for writing correct conditions for while and until loops. Access it directly on your Raspberry Pi.
  7. man bash: The full manual for the Bash shell, available on your device. It contains the most authoritative information.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top