Conditional statements are the brain of your scripts, enabling them to react intelligently to different situations. They are fundamental for error handling, input validation, and creating scripts that can adapt to varying conditions.
The if statement is the cornerstone of conditional logic in Bash. It allows your script to execute different blocks of code based on whether a condition is true or false. You can extend if with elif (else if) to check multiple conditions and else to provide a default action.
Syntax Deep Dive:
if [ condition ]; then # '[' is actually a command! Spaces are required around brackets.
# Code to execute if the first condition is TRUE
elif [ another condition ]; then # Optional 'else if' - check another condition if the first one was false
# Code to execute if the 'elif' condition is TRUE
else # Optional 'else' - default action if no conditions were true
# Code to execute if ALL conditions above were FALSE
fi # 'fi' marks the end of the 'if' block - essential!Important: Spaces around brackets [ condition ]: In Bash, [ is not just syntax; it's actually a command (an alias for test). Therefore, you must have spaces between the brackets and the condition. For example, [ -f "$file" ] is correct, while [-f "$file"] will cause a syntax error.
Real-World Example: Checking for a configuration file and handling different scenarios:
#!/bin/bash
set -e # Exit on error
config_file="/etc/myapp/config.conf" # Path to the main configuration file
backup_dir="/etc/myapp/backups" # Path to the backup directory
if [ -f "$config_file" ]; then # '-f' file test operator: Checks if the file exists AND is a regular file
echo "Configuration file found at '$config_file'. Starting service..."
./start_service.sh # Assume 'start_service.sh' is in the same directory or PATH
elif [ -d "$backup_dir" ]; then # '-d' file test operator: Checks if the directory exists
echo "Main configuration missing! Checking for backups in '$backup_dir'..."
backup_config="$backup_dir/config.conf.backup" # Assume backup config filename
if [ -f "$backup_config" ]; then
echo "Backup configuration found. Restoring from backup..."
cp "$backup_config" "$config_file" # Copy backup to main config location
./start_service.sh # Try starting service again with restored config
else
echo "No backup configuration found either!" >&2 # Output error to standard error (stderr)
echo "Critical error: Cannot start service without configuration." >&2
exit 1 # Exit script with an error code (non-zero)
fi
else # 'else' block: Executed if neither the config file nor backup directory exists
echo "Critical error: No configuration file or backups directory found!" >&2 # Output critical error to stderr
exit 1 # Exit script with an error code
fi
echo "Service startup process completed (script continuing)." # Script continues only if it reaches this point without exiting
exit 0 # Exit script with success code (zero)Key Condition Operators for [ ... ] and [[ ... ]]:
-
Numeric Comparisons:
-eq: Equal to (e.g.,[ "$count" -eq "10" ])-ne: Not equal to (e.g.,[ "$status" -ne "0" ])-gt: Greater than (e.g.,[ "$age" -gt "18" ])-lt: Less than (e.g.,[ "$score" -lt "60" ])-ge: Greater than or equal to (e.g.,[ "$version" -ge "2.0" ])-le: Less than or equal to (e.g.,[ "$limit" -le "100" ])
-
File Test Operators:
-f: File exists and is a regular file (e.g.,[ -f "$file_path" ])-d: Directory exists (e.g.,[ -d "$dir_path" ])-e: File or directory exists (e.g.,[ -e "$path" ])-s: File exists and is not empty (e.g.,[ -s "$log_file" ])-r: File is readable (e.g.,[ -r "$file" ])-w: File is writable (e.g.,[ -w "$file" ])-x: File is executable (e.g.,[ -x "$script" ])
-
String Comparisons:
=or==: String equality (e.g.,[ "$string1" = "$string2" ]or[[ "$string1" == "$string2" ]])!=: String inequality (e.g.,[ "$string1" != "$string3" ]or[[ "$string1" != "$string3" ]])-z: String is empty (zero length) (e.g.,[ -z "$empty_var" ])-n: String is not empty (non-zero length) (e.g.,[ -n "$non_empty_var" ])<: Less than (lexicographical order) - Use with[[ ... ]]for strings (e.g.,[[ "$str1" < "$str2" ]])>: Greater than (lexicographical order) - Use with[[ ... ]]for strings (e.g.,[[ "$str1" > "$str2" ]])
Loops are essential for automating tasks that need to be repeated multiple times. Bash provides several loop constructs to handle different types of iteration.
The for loop is designed to iterate over a predefined list of items. This list can be explicitly defined, generated using brace expansion, or obtained from command output.
Use Cases: Processing multiple files, iterating through a list of servers, performing actions for each item in a collection.
Enhanced Example: Renaming all .txt files in a directory to .bak and providing more informative output:
#!/bin/bash
set -e # Exit on error
# Check if there are any .txt files in the current directory
if ! shopt -s nullglob; files=(*.txt); shopt -u nullglob; then # Safely handle cases with no .txt files using globbing and shell options
echo "No .txt files found in the current directory."
exit 0 # Exit gracefully if no files to process
fi
echo "Renaming .txt files to .bak in the current directory:"
for file in *.txt; do # Iterate over all files matching the pattern '*.txt' (globbing)
if [ -f "$file" ]; then # Double-check if it's a regular file (safety measure)
new_name="${file%.txt}.bak" # Parameter expansion: Remove '.txt' suffix and add '.bak'
mv "$file" "$new_name" # Rename the file
echo " Renamed: '$file' --> '$new_name'" # Provide clear output of the renaming action
else
echo "Warning: '$file' is not a regular file, skipping." # Handle cases where globbing might match directories or other non-files (unlikely with '*.txt' but good practice)
fi
done
echo "File renaming process completed."
exit 0 # Exit with successBrace Expansion for Number Sequences: A handy shortcut for generating sequences of numbers or characters directly within a for loop.
#!/bin/bash
set -e # Exit on error
echo "Processing items from 1 to 5:"
for i in {1..5}; do # Brace expansion: Generates the sequence 1 2 3 4 5
echo " Processing item number: $i"
done
echo "Processing items from 10 to 20, stepping by 2:"
for j in {10..20..2}; do # Brace expansion with increment: 10 12 14 16 18 20
echo " Processing even item: $j"
done
echo "Processing letters from a to e:"
for letter in {a..e}; do # Brace expansion for characters: a b c d e
echo " Processing letter: $letter"
done
echo "Loop examples completed."
exit 0 # Exit with successThe while loop continues to execute a block of code as long as a specified condition remains true. It's ideal for situations where you don't know in advance how many iterations are needed.
Use Cases: Reading data from a file line by line, continuously monitoring system resources, waiting for a specific event to occur.
Real-World Example: Monitoring memory usage and triggering an alert when it exceeds a threshold:
#!/bin/bash
set -e # Exit on error
threshold=90 # Memory usage threshold in percentage
echo "Monitoring memory usage. Alert threshold: $threshold%..."
while true; do # 'true' is always true, creating an infinite loop (until explicitly broken)
usage=$(free | awk '/Mem/{printf "%.0f", $3/$2*100}') # Calculate memory usage percentage using 'free' and 'awk'
echo "Current memory usage: $usage%" # Display current memory usage
if [ "$usage" -ge "$threshold" ]; then # Check if usage is greater than or equal to the threshold
echo "ALERT! Memory usage is over $threshold% ($usage%)!" >&2 # Output alert message to stderr
# Add actions to take when memory is high, e.g., send email, restart service, etc.
exit 1 # Exit script with an error code, indicating a problem
fi
sleep 5 # Wait for 5 seconds before checking again - prevent excessive CPU usage
done # Loop continues indefinitely until the condition inside the 'if' is met and the script exitsImportant: Loop Termination: When using while true, ensure there is a mechanism within the loop to eventually break out of it (e.g., using break statement based on a condition, or exit to terminate the script). Otherwise, you'll create an infinite loop that runs forever, potentially consuming system resources. In the example above, the exit 1 statement inside the if condition serves as the termination mechanism when memory usage gets too high.
The until loop is the inverse of the while loop. It executes a block of code until a specified condition becomes true. It's useful for waiting for a service to start, retrying operations until they succeed, or polling for a specific state.
Real-World Example: Waiting for a web server to become responsive before proceeding:
#!/bin/bash
set -e # Exit on error
server_url="http://localhost:8080/healthcheck" # URL to check for server health
echo "Waiting for server at '$server_url' to become available..."
until curl -s "$server_url" > /dev/null 2>&1; do # 'until' loop: Continues until the command succeeds (exit code 0)
# 'curl -s "$server_url"' attempts to access the URL silently (-s)
# '> /dev/null 2>&1' redirects both standard output and standard error to /dev/null (discarding output)
echo "Server not yet ready. Waiting..."
sleep 10 # Wait for 10 seconds before retrying
done # Loop continues until 'curl' command succeeds (server is up and responding)
echo "Server is up and responding at '$server_url'!" # Executed once the 'until' loop terminates (server is ready)
# Proceed with actions that require the server to be running
echo "Continuing with script execution..."
exit 0 # Exit with successExplanation of curl -s http://localhost:8080/healthcheck > /dev/null 2>&1:
-
curl -s "$server_url": Uses thecurlcommand to make an HTTP request to the specified$server_url. The-soption makescurlsilent, suppressing progress bars and error messages to standard error. -
> /dev/null 2>&1: This part redirects the output of thecurlcommand.> /dev/null: Redirects the standard output (wherecurlnormally prints the webpage content) to/dev/null./dev/nullis a special "black hole" file that discards anything written to it.2>&1: Redirects standard error (file descriptor 2) to the same location as standard output (file descriptor 1), which is/dev/null. This ensures that any error messages fromcurlare also discarded.
The key here is that the
untilloop cares about the exit code of thecurlcommand, not its output. Ifcurlsuccessfully connects to the server and gets a response (even an error response like 404, as long as the connection is established), it will typically exit with a zero exit code (success). Ifcurlfails to connect (e.g., server is down, URL is wrong), it will exit with a non-zero exit code (failure). Theuntilloop continues to iterate as long ascurlfails (non-zero exit code), and stops whencurlsucceeds (zero exit code).
The case statement provides a structured way to handle multiple conditional branches based on matching a value against a series of patterns. It's particularly useful for creating menus, processing command-line arguments, or handling different input options.
Ideal For: Creating command-line interfaces, parsing user input, handling different file types, implementing menu-driven scripts.
Advanced Example: Creating a CLI tool to manage a service (start, stop, restart, status):
#!/bin/bash
set -e # Exit on error
service_name="myservice" # Name of the service to manage
# Prompt user for action
read -p "Enter action for service '$service_name' (start/stop/restart/status): " user_input
case "$user_input" in # 'case' statement: Match the user input against patterns
start|s) # Pattern 'start' OR 's' (using '|') - case-sensitive matching
echo "Starting service '$service_name'..."
systemctl start "$service_name" # Replace with actual service management command
echo "Service '$service_name' started."
;; # Double semicolon ';;' is essential to terminate each case block and prevent fall-through
stop|halt) # Pattern 'stop' OR 'halt'
echo "Stopping service '$service_name'..."
systemctl stop "$service_name" # Replace with actual service management command
echo "Service '$service_name' stopped."
;;
restart|reload) # Pattern 'restart' OR 'reload'
echo "Restarting service '$service_name'..."
systemctl restart "$service_name" # Replace with actual service management command
echo "Service '$service_name' restarted."
;;
status) # Pattern 'status'
echo "Checking status of service '$service_name'..."
systemctl status "$service_name" # Replace with actual service management command
;;
*) # Default case - matches anything that didn't match the above patterns ('* ' is a wildcard)
echo "Invalid action: '$user_input'" >&2 # Output error to stderr
echo "Usage: $0 {start|stop|restart|status}" >&2 # Provide usage instructions
exit 1 # Exit with error code
;; # Terminate the default case as well
esac # 'esac' marks the end of the 'case' statement - like 'fi' for 'if'
exit 0 # Exit with success if a valid action was processedPattern Matching Features in case:
|(OR operator): Allows matching multiple patterns for a single case (e.g.,start|s)matches both "start" and "s").*(Wildcard): Matches any string. Used for the default case (often named*)to handle unexpected input.?(Wildcard): Matches any single character.[](Character ranges): Matches any character within the specified range (e.g.,[0-9]matches any digit,[a-z]matches any lowercase letter).\(Escape character): Used to escape special characters if you want to match them literally (e.g.,\*matches a literal asterisk).
Task: Write a script named number_analyzer.sh that:
- Prints numbers from 1 to 20 using a
forloop. - Bonus 1: Even/Odd Check: Inside the loop, use a conditional statement (
iforcase) to check if each number is even or odd. Print each number along with its type (e.g., "Number 2 is Even", "Number 3 is Odd"). Hint: Use the modulo operator%to check for even/odd. - Bonus 2: Summation and Average: Calculate the sum of all numbers from 1 to 20 and compute the average. Print the total sum and the average after the loop completes.
- Expert Challenge: Prime Number Check: For each number, add a check to determine if it's a prime number. If it is, print "(Prime)" after the number and its even/odd type (e.g., "Number 7 is Odd (Prime)"). Prime number check logic will require nested loops or a function.
Example Solution (including Even/Odd and Summation Bonuses):
#!/bin/bash
set -e # Exit on error
total_sum=0 # Initialize variable to store the sum of numbers
count=0 # Initialize variable to count the numbers
echo "Analyzing numbers from 1 to 20:"
for num in {1..20}; do # Loop through numbers 1 to 20
count=$((count + 1)) # Increment the count of numbers processed
# Check if the number is even or odd using the modulo operator (%)
if (( num % 2 == 0 )); then # '(( ... ))' for arithmetic conditions, check if remainder when divided by 2 is 0
type="Even"
else
type="Odd"
fi
echo "Number $num is $type" # Print the number and its type
total_sum=$((total_sum + num)) # Add the current number to the total sum
done
average=$((total_sum / count)) # Calculate the average (integer division) - for more precise average, use awk or bc for floating-point arithmetic
echo "-------------------------"
echo "Total sum of numbers: $total_sum" # Print the total sum
echo "Average of numbers: $average" # Print the average (integer average)
exit 0 # Exit with successExample Solution (Expert Challenge - Prime Number Check):
#!/bin/bash
set -e # Exit on error
total_sum=0
count=0
is_prime() { # Function to check if a number is prime
local n="$1" # Function argument: number to check
if (( n < 2 )); then # Numbers less than 2 are not prime
return 1 # Return 'false' (non-zero exit code) - not prime
fi
for (( i=2; i*i<=n; i++ )); do # Loop from 2 up to the square root of n
if (( n % i == 0 )); then # If n is divisible by i, it's not prime
return 1 # Return 'false' - not prime
fi
done
return 0 # If loop completes without finding a divisor, it's prime - return 'true' (zero exit code)
}
echo "Analyzing numbers from 1 to 20 (including prime check):"
for num in {1..20}; do
count=$((count + 1))
if (( num % 2 == 0 )); then
type="Even"
else
type="Odd"
fi
if is_prime "$num"; then # Call the is_prime function to check if the number is prime
prime_status="(Prime)"
else
prime_status=""
fi
echo "Number $num is $type $prime_status" # Print number, type, and prime status
total_sum=$((total_sum + num))
done
average=$((total_sum / count))
echo "-------------------------"
echo "Total sum of numbers: $total_sum"
echo "Average of numbers: $average"
exit 0- Avoid Infinite Loops (Especially with
while true): Always ensure thatwhile trueloops have a clear exit condition within their body, typically usingbreakorexitstatements based on some condition. Unintentional infinite loops can freeze your script and consume system resources. - Quote Your Variables in Conditions: Just like with commands, always quote your variables when using them in conditional expressions (e.g.,
[ "$var" -eq "10" ],[[ "$string_var" == "value" ]]). This prevents word splitting and pathname expansion issues, making your conditions reliable, especially when dealing with strings that might contain spaces or special characters. - Use
(( ... ))for Arithmetic Conditions: For numerical comparisons and arithmetic operations within conditions, use the$(( ... ))arithmetic expansion or(( ... ))for arithmetic evaluation withinifor loop conditions.(( ... ))is often preferred within conditionals for better readability and cleaner syntax (e.g.,if (( count > 10 )); then ...). - Understand Exit Codes: Bash conditions and control structures heavily rely on command exit codes. A command that executes successfully typically returns an exit code of 0 (true in conditional contexts), while a command that fails returns a non-zero exit code (false in conditional contexts). Utilize exit codes effectively for error handling and control flow.
Common Gotchas to Watch Out For:
- Forgetting
;;incaseStatements: Incasestatements, remember to terminate each case block with;;(double semicolon). Forgetting;;will cause "fall-through," where the script will continue executing the code blocks of subsequent cases, even if they don't match, leading to unexpected and incorrect behavior. - Missing Spaces in
[ $a -eq $b ]: As mentioned earlier, remember that[is a command, and it requires spaces around its arguments and operators.[ $a -eq $b ]is correct, but[$a -eq $b]or[ $a-eq $b ]will result in syntax errors because Bash will not recognize them as valid commands with properly separated arguments. - Confusing String and Numeric Operators: Be mindful of using the correct operators for string and numeric comparisons. Using numeric operators (like
-eq,-gt) for strings or string operators (like==,<) for numbers can lead to incorrect comparisons and logic errors. Always use-eq,-ne,-lt,-gt,-le,-gefor numerical comparisons within[ ... ]and[[ ... ]]. Use==,!=,<,>for string comparisons within[[ ... ]].