| |

Bash Scripting 06 ๐Ÿง Conditional Logic โ€” Using if statements and test operators

โฑ๏ธ Estimated read: 12 min | |

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

OperatorTypeTrue When
= / ==StringStrings are equal (== also pattern-matches in [[ ]])
!=StringStrings differ
-zStringLength is zero
-nStringLength is non-zero
-eqNumericIntegers equal
-neNumericIntegers differ
-lt / -leNumericLess than / less or equal
-gt / -geNumericGreater than / greater or equal

File Tests

OperatorTrue When
-eFile exists (any type)
-fRegular file
-dDirectory
-r / -w / -xReadable / writable / executable
-sFile size greater than zero
-LSymbolic link

Construct Comparison

ConstructTypeBest For
[ ]Builtin commandPOSIX compatibility
[[ ]]Bash keywordPattern matching, safer strings
(( ))ArithmeticNumeric 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

PitfallWhy It HappensFix
[ error: missing ]No space before ][ -f file ], not [ -f file]
Unary operator expectedUnquoted empty variableQuote: [ "$var" = "x" ]
Integer expression expectedUsed = instead of -eqUse -eq for numeric comparison
Pattern does not match in [ ]= does not pattern-matchUse [[ ]] with ==
Script continues after failed checkMissing exit after errorAdd 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

ItemValue
if conditionExit 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:

  • if tests exit status, not values. The command after if runs, and its return code determines the branch. 0 is success (true), and non-zero is failure (false) .
  • The brackets are a command. [ is a synonym for test, 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 -eq for 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.


Was this helpful?


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!