Bash Scripting 06 ๐ง Conditional Logic โ Using if statements and test operators
A script that cannot make decisions is a script that runs the same commands in the same order every time. Conditional logic is what allows a script to respond to its environment: to check whether a file exists before reading it, to verify that a required argument was provided, to branch based on the exit status of a command, or to fail gracefully when a precondition is not met. In Bash, this logic is expressed through the if statement and the test constructs that supply it with a condition.
The previous chapters established positional parameters and variable expansion. This chapter connects those inputs to control flow. The if statement itself does not evaluate conditions the way a programmer might expect from other languages; it evaluates the exit status of a command. The test commandsโ[, [[, and ((โare the commands that translate comparisons and file checks into exit statuses that if can interpret.
Key point: An if statement tests the exit status of a command, not a boolean value. Exit status 0 means success (true), and any non-zero status means failure (false) . The [ command (a synonym for test) returns 0 when its expression is true and 1 when it is false. Always quote variable expansions inside [ ] to prevent word splitting and empty-variable errors, and prefer [[ ]] in Bash for pattern matching and safer string comparisons.
Why conditional logic requires understanding exit status
The exit-status problem.
Bash does not have boolean literals. There is no true or false value that a variable can hold and that if can inspect. Instead, every command the shell executes returns a number between 0 and 255. By convention, 0 means success and any other value means failure . The if statement runs its condition command, checks the exit status, and branches accordingly. This is why if grep -q pattern file works: grep returns 0 when it finds a match and 1 when it does not. The if statement is not testing the string output; it is testing the command’s success.
The test-command problem.
The [ command is a builtin that evaluates comparison expressions and returns an exit status. test -f /etc/passwd and [ -f /etc/passwd ] are equivalent . The brackets are not syntax; they are the command name. This is why spaces are mandatory: [-f file] is a single word that the shell tries to execute as a command, and it fails. The opening bracket must be a separate token, the expression must be separate tokens, and the closing bracket must be a separate token.
The [[ ]] problem.
Bash introduced [[ ]] as a keyword, not a command, with extended features that [ ] does not have . Inside [[ ]], the == operator performs pattern matching, < and > perform lexicographic comparison without escaping, and unquoted variables are not subject to word splitting. These features make [[ ]] safer and more expressive for string handling. However, [[ ]] is a Bashism; scripts intended for POSIX sh must use [ ] instead.
a. The if statement syntax and exit status
The basic if statement runs a command and branches on its exit status. The then keyword begins the success branch, and fi closes the statement.
#!/usr/bin/env bash
# Basic if: test the exit status of a command
if grep -q "root" /etc/passwd; then
echo "The root user exists" # Runs if grep returns 0 (match found)
fi
# if/else: branch on success or failure
if ping -c 1 -W 1 192.168.1.1 > /dev/null 2>&1; then
echo "Gateway is reachable" # Runs if ping returns 0
else
echo "Gateway is unreachable" # Runs if ping returns non-zero
fi
# if/elif/else: multiple branches
if [ "$1" = "start" ]; then
echo "Starting service"
elif [ "$1" = "stop" ]; then
echo "Stopping service"
else
echo "Usage: $0 {start|stop}" >&2
exit 1
fi
The elif branch is evaluated only if the preceding condition failed. The else branch is the catch-all. The exit 1 in the final branch propagates a non-zero exit status to the caller, which is the correct behavior for a usage error.
The command after if can be any command, not just a test. if grep -q, if ping -c 1, and if systemctl is-active --quiet nginx are all valid because each command returns an exit status that if can interpret .
b. String comparisons with test operators
String comparisons use the = (or == in [[ ]]) and != operators. The -z operator tests for an empty string, and -n tests for a non-empty string .
#!/usr/bin/env bash
# String equality in [ ]: quote both sides
name="alice"
if [ "$name" = "alice" ]; then
echo "Match" # Runs because the strings are equal
fi
# String inequality
if [ "$name" != "bob" ]; then
echo "Not bob" # Runs because the strings differ
fi
# Empty string test
if [ -z "$1" ]; then
echo "No argument provided" >&2 # Runs when $1 is unset or empty
exit 1
fi
# Non-empty string test
if [ -n "$1" ]; then
echo "Argument: $1" # Runs when $1 has a value
fi
# Pattern matching in [[ ]]
if [[ "$name" == a* ]]; then
echo "Starts with a" # Runs because "alice" matches a*
fi
Inside [ ], the = operator performs literal string comparison. Inside [[ ]], the == operator performs pattern matching unless the pattern is quoted . This is a significant difference: [ "$name" = "a*" ] checks whether the string is literally a*, while [[ "$name" == a* ]] checks whether the string starts with a.
Quoting the variable inside [ ] is mandatory. If $name is unset and unquoted, [ $name = "alice" ] expands to [ = "alice" ], which is a syntax error. Quoting produces [ "" = "alice" ], which evaluates to false without an error.
c. Numeric comparisons and file tests
Numeric comparisons use the operators -eq, -ne, -lt, -le, -gt, and -ge. These are distinct from the string operators; = compares strings, -eq compares integers .
#!/usr/bin/env bash
# Numeric comparison
count=5
if [ "$count" -gt 0 ]; then
echo "Positive" # Runs because 5 > 0
fi
# Arithmetic evaluation with (( ))
if (( count > 3 )); then
echo "Greater than 3" # Runs because the arithmetic expression is non-zero
fi
# File tests
if [ -f "/etc/passwd" ]; then
echo "Regular file exists" # Runs because /etc/passwd is a regular file
fi
if [ -d "/tmp" ]; then
echo "Directory exists" # Runs because /tmp is a directory
fi
if [ -r "/etc/shadow" ]; then
echo "Readable" # Runs only if the script has read permission
fi
if [ -x "/usr/bin/env" ]; then
echo "Executable" # Runs because env is executable
fi
# Negation with !
if [ ! -f "/nonexistent" ]; then
echo "File does not exist" # Runs because the file is absent
fi
The file test operators cover the common checks: -e for existence, -f for a regular file, -d for a directory, -r for readability, -w for writability, -x for executability, and -s for a non-empty file . The ! operator negates any test. The (( )) construct evaluates arithmetic expressions and returns 0 when the result is non-zero, which is the opposite of the command convention but matches the intuition of a mathematical comparison .
Complete Example Session
#!/usr/bin/env bash
# ============================================
# PART 1: THE USAGE CHECK
# ============================================
# Require exactly one argument; exit with an error if missing
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <filename>" >&2
exit 1
fi
filename="$1"
# ============================================
# PART 2: THE FILE EXISTENCE CHECK
# ============================================
# Verify the file exists before attempting to read it
if [ ! -e "$filename" ]; then
echo "Error: $filename does not exist" >&2
exit 1
fi
# ============================================
# PART 3: THE FILE TYPE CHECK
# ============================================
# Distinguish between a regular file and a directory
if [ -d "$filename" ]; then
echo "$filename is a directory" >&2
exit 1
elif [ ! -f "$filename" ]; then
echo "$filename is not a regular file" >&2
exit 1
fi
# ============================================
# PART 4: THE READABILITY CHECK
# ============================================
# Confirm the file is readable before attempting to read
if [ ! -r "$filename" ]; then
echo "Error: $filename is not readable" >&2
exit 1
fi
# ============================================
# PART 5: THE EMPTY FILE CHECK
# ============================================
# Warn if the file is empty, but do not exit
if [ ! -s "$filename" ]; then
echo "Warning: $filename is empty" >&2
fi
# ============================================
# PART 6: THE CONTENT CHECK
# ============================================
# Count lines and branch on the count
line_count=$(wc -l < "$filename")
if [ "$line_count" -eq 0 ]; then
echo "File has no lines"
elif [ "$line_count" -lt 10 ]; then
echo "Small file: $line_count lines"
else
echo "Large file: $line_count lines"
fi
# ============================================
# PART 7: STRING COMPARISON ON FILENAME
# ============================================
# Check the file extension with a pattern match
if [[ "$filename" == *.conf ]]; then
echo "Configuration file detected"
fi
# ============================================
# PART 8: COMMAND EXIT STATUS CHECK
# ============================================
# Use the exit status of a command directly as a condition
if grep -q "^#" "$filename"; then
echo "File contains comments"
else
echo "File has no comments"
fi
# ============================================
# PART 9: NEGATION AND COMBINED CONDITIONS
# ============================================
# Combine conditions with && inside [[ ]]
if [[ -f "$filename" && -r "$filename" ]]; then
echo "File is regular and readable"
fi
# ============================================
# PART 10: THE FINAL BRANCH
# ============================================
# All checks passed; perform the operation
echo "Processing $filename..."
The ten parts demonstrate the conditional patterns that scripts use most often: argument count validation, file existence and type checks, readability checks, numeric comparisons on line counts, string pattern matching on filenames, and direct command exit status checks. The order matters: the usage check runs before the file check, and the file check runs before the read attempt. A missing prerequisite causes the script to fail with a clear message rather than a cryptic error from a later command.
Quick Reference
Test Operators
| Operator | Type | True When |
|---|---|---|
= / == | String | Strings are equal (== also pattern-matches in [[ ]]) |
!= | String | Strings differ |
-z | String | Length is zero |
-n | String | Length is non-zero |
-eq | Numeric | Integers equal |
-ne | Numeric | Integers differ |
-lt / -le | Numeric | Less than / less or equal |
-gt / -ge | Numeric | Greater than / greater or equal |
File Tests
| Operator | True When |
|---|---|
-e | File exists (any type) |
-f | Regular file |
-d | Directory |
-r / -w / -x | Readable / writable / executable |
-s | File size greater than zero |
-L | Symbolic link |
Construct Comparison
| Construct | Type | Best For |
|---|---|---|
[ ] | Builtin command | POSIX compatibility |
[[ ]] | Bash keyword | Pattern matching, safer strings |
(( )) | Arithmetic | Numeric comparisons |
Best Practices
โ Do This:
# Quote variables inside [ ]
if [ "$name" = "alice" ]; then ... fi # โ
# Use [[ ]] for pattern matching
if [[ "$filename" == *.conf ]]; then ... fi # โ
# Test the exit status of a command directly
if grep -q pattern file; then ... fi # โ
# Use -eq for numbers, = for strings
if [ "$count" -eq 0 ]; then ... fi # โ
# Check prerequisites before using them
[ -f "$file" ] && [ -r "$file" ] || exit 1 # โ
# Use (( )) for arithmetic
if (( count > 10 )); then ... fi # โ
โ Don’t Do This:
# Omit spaces around brackets
if [-f file]; then ... fi # Tries to run "[-f" # โ
# Leave variables unquoted in [ ]
if [ $name = "alice" ]; then ... fi # Fails if $name unset # โ
# Use = for numbers
if [ "$count" = 0 ]; then ... fi # String comparison # โ
# Use -eq for strings
if [ "$name" -eq "alice" ]; then ... fi # Error # โ
# Test file before checking it exists
cat "$file" # Fails before the if can help # โ
# Assume [[ ]] is POSIX
#!/bin/sh # [[ ]] is not available in POSIX sh # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
[ error: missing ] | No space before ] | [ -f file ], not [ -f file] |
| Unary operator expected | Unquoted empty variable | Quote: [ "$var" = "x" ] |
| Integer expression expected | Used = instead of -eq | Use -eq for numeric comparison |
Pattern does not match in [ ] | = does not pattern-match | Use [[ ]] with == |
| Script continues after failed check | Missing exit after error | Add exit 1 in the error branch |
Real-World Examples
1. Argument count check
if [ "$#" -lt 2 ]; then echo "Usage: $0 SRC DST" >&2; exit 1; fi
2. File existence check
if [ ! -f "$config" ]; then echo "Missing: $config" >&2; exit 1; fi
3. Directory check
if [ -d "$target" ]; then echo "Target is a directory"; fi
4. String equality
if [ "$1" = "verbose" ]; then set -x; fi
5. Empty string check
if [ -z "${1:-}" ]; then echo "No argument"; exit 1; fi
6. Numeric comparison
if [ "$lines" -gt 100 ]; then echo "Large file"; fi
7. Arithmetic evaluation
if (( count % 2 == 0 )); then echo "Even"; fi
8. Command exit status
if ping -c 1 -W 1 "$host" > /dev/null 2>&1; then echo "Up"; fi
9. Pattern match in [[ ]]
if [[ "$file" == *.log ]]; then echo "Log file"; fi
10. Combined condition
if [[ -f "$f" && -r "$f" ]]; then cat "$f"; fi
Visual
Exit Status and If
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ IF TESTS EXIT STATUS, NOT BOOLEAN VALUES โ
โ โ
โ if grep -q "root" /etc/passwd; then โ
โ โ โ
โ โผ โ
โ grep runs โ
โ โ โ
โ โโโ match found โ exit 0 โ then branch runs โ
โ โ โ
โ โโโ no match โ exit 1 โ else branch runs โ
โ โ
โ The if statement does not inspect grep's output. โ
โ It inspects the number grep returns to the shell. โ
โ โ
โ Convention: 0 = success (true), non-zero = failure (false) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[ ] vs [[ ]]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE TWO TEST CONSTRUCTS HAVE DIFFERENT RULES โ
โ โ
โ [ ] (builtin command): โ
โ โโโ POSIX-compatible โ
โ โโโ Requires quoting: [ "$x" = "y" ] โ
โ โโโ = is literal string comparison โ
โ โโโ < and > must be escaped: [ "$a" \< "$b" ] โ
โ โ
โ [[ ]] (Bash keyword): โ
โ โโโ Bash-specific โ
โ โโโ No word splitting: [[ $x == y ]] is safe โ
โ โโโ == performs pattern matching: [[ $x == a* ]] โ
โ โโโ < and > work without escaping: [[ $a < $b ]] โ
โ โ
โ Use [[ ]] in Bash scripts. Use [ ] for POSIX sh. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Prerequisite Chain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ORDER OF CHECKS MATTERS โ
โ โ
โ 1. Argument count โ
โ if [ "$#" -ne 1 ]; then exit 1; fi โ
โ โ โ
โ โผ โ
โ 2. File exists โ
โ if [ ! -e "$file" ]; then exit 1; fi โ
โ โ โ
โ โผ โ
โ 3. File is readable โ
โ if [ ! -r "$file" ]; then exit 1; fi โ
โ โ โ
โ โผ โ
โ 4. Use the file โ
โ cat "$file" โ
โ โ
โ Each check depends on the previous one having passed. โ
โ Reversing the order produces confusing errors. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
if condition | Exit status of a command (0 = true) |
[ ] | Builtin test command; requires quoting and spaces |
[[ ]] | Bash keyword; pattern matching and safer strings |
(( )) | Arithmetic evaluation; returns 0 when non-zero |
| String equality | = (literal in [ ]), == (pattern in [[ ]]) |
| Numeric equality | -eq, -ne, -lt, -gt, -le, -ge |
| Empty string | -z (zero length), -n (non-zero length) |
| File existence | -e (any), -f (regular), -d (directory) |
| Negation | ! before the expression |
| Combination | && and || inside [[ ]] |
Key takeaways:
iftests exit status, not values. The command afterifruns, and its return code determines the branch.0is success (true), and non-zero is failure (false) .- The brackets are a command.
[is a synonym fortest, and it requires spaces around it and around every operator.[-f file]is a single word that fails to execute . - Quote variables inside
[ ]. An unquoted empty variable expands to nothing, producing[ = "x" ], which is a syntax error. Quoting produces[ "" = "x" ], which is valid and false. - Use
[[ ]]in Bash for strings. It performs pattern matching with==, does not word-split unquoted variables, and handles<and>without escaping . - Use
-eqfor numbers,=for strings. The operators are not interchangeable.[ "$a" -eq "$b" ]compares integers;[ "$a" = "$b" ]compares strings. - Check prerequisites before using them. Test that a file exists before reading it, that an argument was provided before using it, and that a directory exists before changing into it. The order of checks determines whether errors are clear or cryptic.
Remember: Conditional logic in Bash is built on exit status. The if statement runs a command and branches on the number it returns. The test constructsโ[ ], [[ ]], and (( ))โare the commands that translate comparisons into exit statuses. Quote variables in [ ], use [[ ]] for pattern matching, use -eq for numbers and = for strings, and check prerequisites in order before using them. A script that tests its environment before acting on it fails predictably instead of mysteriously.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!