Chapter 21: Shell Scripting: Control Flow (if, else, case)
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the concept of exit codes and how they form the basis of conditional logic in shell scripts.
- Implement conditional branching using
if,elif, andelsestatements to control script execution. - Perform string, numeric, and file-based comparisons using the
test,[, and[[constructs. - Design and implement multi-way branching logic efficiently using the
casestatement for pattern matching. - Write robust shell scripts for the Raspberry Pi 5 that can make decisions based on system state, user input, or hardware status.
- Debug and troubleshoot common errors related to syntax and logic in conditional shell scripts.
Introduction
In our journey through embedded Linux, we have learned how to execute commands and string them together to perform simple, linear tasks. However, the true power of an embedded system lies in its ability to react to a changing environment. A weather station must respond differently to a sudden drop in temperature, a security camera must take action only when motion is detected, and a factory robot must halt if a sensor reports an anomaly. This ability to make decisions is the hallmark of intelligent systems, and in the world of shell scripting, it is made possible through conditional logic.
This chapter introduces the fundamental building blocks of decision-making in Bash: the if statement and the case statement. These constructs allow us to move beyond simple, sequential scripts and create programs that can analyze conditions and alter their execution path accordingly. We will explore how a script can check the status of hardware, validate user input, monitor system resources, and take specific actions based on what it finds. For an embedded developer working with the Raspberry Pi 5, mastering conditional logic is not merely an academic exercise; it is the essential skill that enables the creation of automated, responsive, and reliable applications that bridge the gap between software commands and the physical world. By the end of this chapter, you will be equipped to write scripts that do not just execute commands, but make intelligent choices.
Technical Background
At the heart of every decision a computer makes is a simple question: “Is a particular condition true or false?” In many programming languages, this is handled with Boolean true and false values. The shell, however, operates on a different but related principle: the exit code of a command. Every command or program you run in Linux finishes with an integer status code that it reports back to the shell. By convention, an exit code of 0 signifies success, while any non-zero value (from 1 to 255) indicates some form of failure or error. This simple mechanism is the foundation of all conditional logic in shell scripting.
The if statement doesn’t evaluate a Boolean expression directly; instead, it executes a command and examines its exit code. If the exit code is 0 (success), the condition is considered “true,” and the code block following the if is executed. If the exit code is non-zero (failure), the condition is “false,” and the block is skipped.
Consider the grep command, which searches for patterns in text. If grep finds the pattern, it exits with 0. If it doesn’t, it exits with 1. We can use this behavior directly in an if statement:
if grep -q "root" /etc/passwd
then
echo "The root user exists in the password file."
fi
Here, grep -q "root" /etc/passwd is the command being tested. The -q (quiet) option suppresses grep‘s normal output; we only care about its exit code. If root is found, the command succeeds (exit code 0), and the echo statement runs. If not, it fails (exit code 1), and the then...fi block is skipped.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
subgraph "Shell Conditional Logic"
direction TB
A["Start: Execute a Command<br>e.g., <i>grep root /etc/passwd</i>"]
D{{"Check Command's<br>Exit Code"}}
A --> D
D -- "Exit Code is 0 (Success)" --> T[Condition is TRUE<br>Execute 'then' block]
D -- "Exit Code is Non-Zero (Failure)" --> F[Condition is FALSE<br>Skip 'then' block]
T --> E[End]
F --> E[End]
end
%% Styling
style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
style D fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
style T fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style F fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff
style E fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
The test Command and […]
While using any command’s exit code is powerful, we often need to perform more explicit comparisons, such as checking if two numbers are equal or if a file exists. This is the role of the test command. The test command evaluates an expression and exits with a status of 0 if the expression is true and 1 if it is false.
For example, to check if a variable holds a specific value, you could write:
test “$USER” = “pi”
This command will exit with 0 if the $USER variable is equal to “pi”. While perfectly valid, this syntax is not as common as its more popular alias: the single bracket [. The command [ expression ] is a synonym for test expression.
Warning: The spaces around the brackets are mandatory. The
[is not just syntax; it is a command (a link totest, in fact), and like any command, it requires spaces to separate it from its arguments. Forgetting a space, as in[$VAR=5], is a very common syntax error.
Using this improved syntax, we can write more readable conditions.
Types of Comparisons
The test command and its bracketed forms support three main categories of comparisons, each with its own set of operators. It is crucial to use the correct operators for the type of data you are evaluating.
1. Numeric Comparisons
When comparing integers, you must use a specific set of operators. These operators do not work for string comparisons.
Attempting to use > or < for numeric comparison within single brackets will result in an error, as the shell will interpret them as redirection operators.
2. String Comparisons
For comparing textual data, a different set of operators is used.
Tip: Always enclose your variables in double quotes (e.g.,
"$STR") within test conditions. If a variable is empty or contains spaces, omitting the quotes can lead to thetestcommand receiving an unexpected number of arguments, causing a syntax error. For example, if$VARis empty,[ $VAR = "a" ]becomes[ = "a" ], which is invalid.[ "$VAR" = "a" ]becomes[ "" = "a" ], which is valid and evaluates correctly.
3. File Attribute Checks
A common task in embedded systems is to check the status of files, such as device nodes in /dev or configuration files in /etc. The test command provides a rich set of operators for this purpose.
These file operators are indispensable for writing scripts that safely interact with the filesystem, ensuring a file exists before trying to read it or that a directory is present before writing to it.
Branching with if-elif-else
An if statement on its own is useful, but its power grows when you add branching. The else and elif (short for “else if”) clauses allow you to build a decision tree, executing different blocks of code for different conditions.
The full structure looks like this:
if [ condition1 ]
then
# Code to run if condition1 is true (exit code 0)
elif [ condition2 ]
then
# Code to run if condition1 is false and condition2 is true
else
# Code to run if all preceding conditions are false
fi
This structure allows a script to navigate a series of questions. Imagine a script that checks the Raspberry Pi’s CPU temperature. It first asks, “Is the temperature in the critical range?” If so, it takes immediate action. If not, it proceeds to the elif to ask, “Is the temperature in the warning range?” If so, it logs a warning. If that’s also not true, the else block acts as a default, reporting that the temperature is normal. This cascading logic is fundamental to creating responsive and robust systems.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
subgraph "if-elif-else Decision Flow"
A[Start] --> C1{{"if (condition1 is true)?"}}
C1 -- "Yes" --> P1["Execute Block 1"] --> E[End]
C1 -- "No" --> C2{{"elif (condition2 is true)?"}}
C2 -- "Yes" --> P2["Execute Block 2"] --> E
C2 -- "No" --> P3["<b>else</b><br>Execute Block 3"] --> E
end
%% Styling
style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
style C1 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
style C2 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
style P1 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style P2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style P3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style E fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
The Modern Alternative: [[…]]
Bash introduced an enhanced version of the [ command: the double-bracket [[ ... ]]. While [ is a standard command bound by shell parsing rules, [[ is a keyword with its own special parsing rules, which resolves several quirks and adds new features.
Key advantages of [[ ... ]] include:
- No Word Splitting or Globbing: Inside
[[, variables don’t need to be quoted to prevent errors with spaces or empty values. The expression[[ $VAR = "some string" ]]is safe even if$VARis empty or contains spaces. - Regular Expression Matching: It introduces the
=~operator for matching against extended regular expressions, a powerful tool for string validation. - C-Style Logical Operators: You can use
&&(AND) and||(OR) directly inside the brackets, rather than using the-aand-ooperators oftest, which can be brittle.
Example using &&:
# Old style with [
if [ -f "$FILE" -a -r "$FILE" ]; then ...
# New, safer style with [[
if [[ -f "$FILE" && -r "$FILE" ]]; then ...
For new scripts written specifically for Bash, using [[ ... ]] is generally recommended for its improved safety and extended features. However, if you are writing scripts that must be portable to other shells (like sh or dash), you should stick with the POSIX-standard [.
Multi-Way Branching with the case Statement
When you have a long chain of if-elif-elif... statements all checking the same variable against different values, the code can become clumsy and hard to read. The case statement provides a much cleaner and more efficient solution for this scenario. It compares a single variable or value against a list of patterns and executes the code block associated with the first matching pattern.
The structure is as follows:
case "$VARIABLE" in
pattern1)
# Commands for pattern1
;;
pattern2|pattern3)
# Commands for when variable matches pattern2 OR pattern3
;;
*.txt)
# Commands for any value ending in .txt
;;
*)
# Default commands (matches anything)
;;
esac
The case statement is a perfect analogy for a switchboard. A single input value ($VARIABLE) is plugged in, and the case statement routes it to the correct destination based on a matching pattern. Each block of commands must be terminated with ;;, which prevents the execution from “falling through” to the next pattern. The *) pattern is a catch-all, similar to an else clause, that executes if no other patterns match.
One of the most powerful features of case is its use of shell wildcards in patterns. The asterisk (*) matches any sequence of characters, the question mark (?) matches any single character, and character sets ([abc]) can be used. This makes it exceptionally good at tasks like parsing command-line arguments, where you might want to handle options like --start, --stop, or --status.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
subgraph "case Statement Switchboard"
A[Start: Input Variable<br>e.g., $COMMAND] --> S{{"case $VARIABLE in"}}
S --> P1("pattern1)") --> B1["Execute<br>Commands 1"] --> T(";;")
S --> P2("pattern2 | pattern3)") --> B2["Execute<br>Commands 2"] --> T2(";;")
S --> P3("*.txt)") --> B3["Execute<br>Commands 3"] --> T3(";;")
S --> P4("*)<br>(Default)") --> B4["Execute<br>Default Commands"] --> T4(";;")
T --> E[esac<br>End]
T2 --> E
T3 --> E
T4 --> E
end
%% Styling
style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
style S fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
style P1 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
style P2 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
style P3 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
style P4 fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937
style B1 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style B2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style B3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style B4 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
style E fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
Practical Examples
Let’s apply these concepts to practical scenarios you might encounter while developing on a Raspberry Pi 5. These examples will read system information, control hardware, and manage services.
Example 1: System Health Monitor
A common task for an embedded device is to monitor its own health. This script uses an if-elif-else structure to check the CPU temperature and report its status. The Raspberry Pi 5 exposes its CPU temperature through a file in the /sys filesystem.
File Structure and Code
Create a file named health_check.sh.
#!/bin/bash
# health_check.sh
# A simple script to monitor the CPU temperature of a Raspberry Pi 5.
# The temperature is stored in this file in millidegrees Celsius.
# E.g., a value of 54321 means 54.321°C.
TEMP_FILE="/sys/class/thermal/thermal_zone0/temp"
# Define temperature thresholds in millidegrees Celsius.
# 80°C is a typical throttling threshold. 60°C is a warm/warning level.
CRITICAL_TEMP=80000
WARN_TEMP=60000
# First, check if the temperature file exists and is readable.
if [[ ! -r "$TEMP_FILE" ]]; then
echo "Error: Cannot read temperature file at $TEMP_FILE."
echo "Please ensure you are running on a Raspberry Pi and have permissions."
exit 1
fi
# Read the raw value from the file.
# The `cat` command outputs the content of the file.
# We use command substitution `$(...)` to capture that output into a variable.
current_temp_raw=$(cat "$TEMP_FILE")
# Check if the value read is a valid number.
# We use a regular expression to ensure it contains only digits.
if ! [[ "$current_temp_raw" =~ ^[0-9]+$ ]]; then
echo "Error: Invalid temperature value read: '$current_temp_raw'"
exit 1
fi
# Now, use our conditional logic to evaluate the temperature.
echo "--- System Health Report ---"
echo "Current raw temperature value: $current_temp_raw"
if [[ "$current_temp_raw" -ge "$CRITICAL_TEMP" ]]; then
# This block runs if the temperature is >= 80000.
echo "Status: CRITICAL! CPU temperature is above 80°C."
echo "Action: System may be throttling. Check cooling and system load."
elif [[ "$current_temp_raw" -ge "$WARN_TEMP" ]]; then
# This block runs if the first condition was false, but temp is >= 60000.
echo "Status: WARNING. CPU temperature is above 60°C."
echo "System is running warm. Monitor for further increases."
else
# This block runs if both of the above conditions were false.
echo "Status: OK. CPU temperature is within normal range."
fi
echo "--------------------------"
Build and Execution Steps
- Save the code above into a file named
health_check.sh. - Make the script executable using the
chmodcommand:chmod +x health_check.sh - Run the script from your terminal:
./health_check.sh
Expected Output
The output will vary based on your Pi’s current temperature.
If the Pi is idle and cool:
--- System Health Report ---
Current raw temperature value: 45123
Status: OK. CPU temperature is within normal range.
--------------------------
If the Pi is under load:
--- System Health Report ---
Current raw temperature value: 62580
Status: WARNING. CPU temperature is above 60°C.
System is running warm. Monitor for further increases.
--------------------------
This example demonstrates file checks (-r), numeric comparisons (-ge), and a clear if-elif-else decision path, providing a practical template for any monitoring task.
Example 2: GPIO LED Controller with case
This example shows how to use a case statement to parse command-line arguments to control an LED connected to a GPIO pin. It provides a clean interface for turning the LED on, off, or checking its status.
Hardware Integration
You will need:
- 1x LED (any color)
- 1x 330Ω resistor
- Jumper wires
Connect the components as follows:
- Connect the anode (longer leg) of the LED to GPIO 21 (Pin 40) on the Raspberry Pi 5.
- Connect the cathode (shorter leg) of the LED to one end of the 330Ω resistor.
- Connect the other end of the resistor to a Ground (GND) pin (e.g., Pin 39).
Warning: Always use a current-limiting resistor when connecting an LED to a GPIO pin to prevent damage to both the LED and the Raspberry Pi.
File Structure and Code
Create a file named led_control.sh. We will use the modern gpiod tools (gpioset, gpioget) which are the standard for GPIO control, replacing the older, deprecated /sys/class/gpio interface.
#!/bin/bash
# led_control.sh
# Controls an LED on GPIO 21 using a case statement for command parsing.
# Raspberry Pi 5 uses gpiochip4 for pins 0-27. GPIO 21 is on this chip.
GPIO_CHIP="gpiochip4"
GPIO_LINE=21 # Using GPIO pin 21
# The first argument to the script is our command ($1).
COMMAND=$1
# Check if a command was provided.
if [[ -z "$COMMAND" ]]; then
echo "Usage: $0 {on|off|status}"
echo " on: Turn the LED on."
echo " off: Turn the LED off."
echo " status: Check the current state of the LED."
exit 1
fi
# Use a case statement to handle the command.
case "$COMMAND" in
on)
echo "Turning LED on (GPIO $GPIO_LINE)..."
# gpioset <chip> <line>=<value>
# 1 means high/on.
gpioset "$GPIO_CHIP" "$GPIO_LINE"=1
echo "Done."
;; # End of this case
off)
echo "Turning LED off (GPIO $GPIO_LINE)..."
# 0 means low/off.
gpioset "$GPIO_CHIP" "$GPIO_LINE"=0
echo "Done."
;; # End of this case
status)
echo "Checking LED status (GPIO $GPIO_LINE)..."
# gpioget <chip> <line>
# It returns 0 or 1.
state=$(gpioget "$GPIO_CHIP" "$GPIO_LINE")
if [[ "$state" -eq 1 ]]; then
echo "LED is currently ON."
else
echo "LED is currently OFF."
fi
;; # End of this case
*) # Catch-all for any other argument
echo "Error: Invalid command '$COMMAND'."
echo "Usage: $0 {on|off|status}"
exit 1
;;
esac
exit 0
Build and Execution Steps
1. Ensure the gpiod tools are installed. They are standard on Raspberry Pi OS.
sudo apt-get update && sudo apt-get install gpiod2. Save the code as led_control.sh and make it executable:
chmod +x led_control.sh3. Run the script with different commands:
# Turn the LED on
./led_control.sh on
# Check its status
./led_control.sh status
# Turn the LED off
./led_control.sh off
# Test an invalid command
./led_control.sh blinkExpected Output
$ ./led_control.sh on
Turning LED on (GPIO 21)...
Done.
$ ./led_control.sh status
Checking LED status (GPIO 21)...
LED is currently ON.
$ ./led_control.sh off
Turning LED off (GPIO 21)...
Done.
$ ./led_control.sh blink
Error: Invalid command 'blink'.
Usage: ./led_control.sh {on|off|status}
This example elegantly demonstrates how a case statement can create a clean command-line interface, a common pattern in embedded scripts for testing and diagnostics.
Common Mistakes & Troubleshooting
When writing conditional logic in shell scripts, small syntax mistakes can lead to frustrating errors. Here are some of the most common pitfalls and how to avoid them.
Exercises
- Basic User Check: Write a script named
user_check.sh. It should check if the current user running the script isroot. If it is, it should print a warning message: “Warning: Running as root is not recommended.” Otherwise, it should print “Running as a standard user. Good.” (Hint: The current user is stored in the$USERvariable). - File or Directory Identifier: Create a script
type_check.shthat takes one argument: a path to a file or directory. The script should useif-elif-elseto check if the path exists. If it does, it should then determine if it’s a regular file or a directory and print an appropriate message (“X is a regular file.” or “X is a directory.”). If the path does not exist, it should print an error message. (Hint: Use-e,-f, and-d). - Raspberry Pi Model Identifier: Write a script
pi_model.shthat usesgrepand acasestatement to identify the Raspberry Pi model. Read the “Model” line from/proc/cpuinfo. The script should print a specific message for “Raspberry Pi 5 Model B” and have a default message like “Unknown or older Raspberry Pi model detected” for any other value. (Hint: Usegrep -i "Model" /proc/cpuinfoand command substitution). - Advanced LED Controller: Extend the
led_control.shexample. Add a new commandblinkto thecasestatement. This command should turn the LED on, wait for 0.5 seconds (sleep 0.5), turn it off, wait another 0.5 seconds, and repeat this cycle five times. (Hint: You will need aforloop inside theblinkcase). - Simple Service Manager: Write an interactive script
service_menu.sh. It should check if thesshservice is running usingsystemctl is-active --quiet ssh. Based on the exit code, it should present a menu using acasestatement. If the service is active, the menu should offer tostoporrestartit. If it’s inactive, the menu should offer tostartit. The script should then execute the chosen command usingsudo systemctl .... Include an option toexitthe script.
Summary
- Conditional Logic is Based on Exit Codes: In shell scripting, a command that exits with a status code of 0 is considered “true” (success), while any non-zero exit code is “false” (failure).
- The
ifStatement Tests Exit Codes: Theifstatement executes a command and, based on its exit code, conditionally runs a block of code. test,[, and[[are for Comparisons: These commands are used to perform numeric, string, and file-based comparisons, forming the core of most conditional expressions.- Quoting Variables is Crucial: Always enclose variables in double quotes (
"$VAR") inside conditional expressions to prevent errors from empty values or spaces. if-elif-elseCreates Decision Trees: This structure allows for complex, cascading logic where multiple conditions are tested in sequence.caseProvides Clean Multi-Way Branching: Thecasestatement is a more readable and efficient alternative to longif-elifchains, especially for parsing user input or matching patterns.[[...]]is the Modern Standard: For Bash scripts, the[[...]]construct offers safer parsing and more features (like regex matching) than the traditional[...].
Mastering these conditional constructs elevates your scripts from simple command sequences to dynamic programs capable of responding intelligently to the diverse conditions of an embedded environment.
Further Reading
- Bash Reference Manual (GNU): The official documentation for the Bash shell. The section on Conditional Constructs is the definitive source.
- Advanced Bash-Scripting Guide: An in-depth, classic resource covering a vast range of scripting topics, including detailed explanations of tests and comparisons.
- Raspberry Pi Documentation – GPIO: Official documentation on using GPIO on the Raspberry Pi, including information on modern
gpiodutilities. - Bash
casestatement (GeeksforGeeks): A well-structured tutorial focusing specifically on the syntax and usage of thecasestatement. - Greg’s Wiki – BashFAQ: A highly respected wiki that clarifies many common points of confusion in Bash scripting, including the differences between
[and[[.

