| | |

LFCA 58 ๐Ÿง Shell Scripting โ€” Conditionals

A script that runs the same commands in the same order every time is useful. A script that makes decisions โ€” that checks whether a file exists before deleting it, whether a service is running before restarting it, whether a user provided the right number of arguments โ€” is powerful. Conditionals are what turn a sequence of commands into a program.

The LFCA exam places shell scripting under Linux Fundamentals, which carries 20% of the total weight. The competencies include “command line” and “basic programming”. Conditionals sit at the intersection of both: they are the control flow structures that let a script respond to its environment instead of blindly executing a fixed list. Every system administration script โ€” backup, log rotation, health check, deployment โ€” relies on them.

This chapter covers the if statement, the test command and its [ shorthand, case for multi-branch decisions, and the logical operators that combine conditions.

Key point: In Bash, if does not evaluate “conditions” in the abstract. It executes a command and checks its exit status. A command that returns 0 (success) makes the if branch run. A command that returns non-zero (failure) makes the else branch run. The test command โ€” invoked as test or as [ โ€” is the tool that turns comparisons into exit statuses. Understanding this relationship is the difference between writing conditionals that work and conditionals that mysteriously don’t.


Why conditionals exist

A script without conditionals is a list. A script with conditionals is a decision tree.

The safety problem. A backup script that runs rm -rf /backup/old without checking whether the directory exists will fail, or worse, delete something unexpected. A conditional guard โ€” if [ -d /backup/old ]; then โ€” prevents the script from proceeding when the precondition isn’t met.

The input problem. A script that expects two arguments should check $# before using $1 and $2. Without the check, missing arguments cause confusing errors deep in the script’s execution. With the check, the script can print a usage message and exit cleanly.

The environment problem. A deployment script needs to know whether it’s running on production or staging. A conditional reading an environment variable โ€” if [ "$ENV" = "prod" ]; then โ€” lets the same script behave differently in different contexts.

The command problem. Not every command succeeds. A curl request may fail. A systemctl start may return non-zero. A conditional checking $? lets the script respond to failure instead of continuing blindly.

The trade-off. Conditionals add branches, and branches add complexity. A script with twenty nested if statements is harder to debug than one with five. The case statement exists specifically to flatten multi-branch decisions into something readable. Use the simplest structure that handles your logic.


a. The if Statement: Testing Exit Status

The if statement executes commands based on the exit status of a command. The basic form is if command; then ...; fi. If command returns 0, the then branch runs. Otherwise, execution continues after fi.

#!/bin/bash
if grep -q "^root:" /etc/passwd; then
    echo "Root account exists"
fi

Here, grep -q searches for a pattern and returns 0 if found, non-zero if not. The if checks that exit status. No test command is involved โ€” grep itself is the condition. This is the most direct use of if: any command can serve as the condition.

The else clause runs when the command fails. The elif clause chains additional tests:

#!/bin/bash
if [ $# -eq 0 ]; then
    echo "Usage: $0 <filename>"
    exit 1
elif [ ! -f "$1" ]; then
    echo "Error: $1 is not a file"
    exit 2
else
    echo "Processing $1"
fi

The elif is equivalent to else if in other languages. Each elif executes its own command and checks the result. The first branch that succeeds runs; the rest are skipped. The else runs only if no branch matched.

A subtle but important point: the commands between then and elif/else/fi are a command list. They can contain multiple commands, and the exit status of the last command in the list becomes the exit status of the if statement itself. This matters when the if is nested inside another conditional.

The no-op command : is useful for empty branches. If you want a branch to do nothing but still need a command, use ::

if [ -z "$DEBUG" ]; then
    :
else
    echo "Debug mode enabled"
fi

b. The test Command: Comparisons as Exit Codes

The test command evaluates a condition and returns 0 if true, 1 if false. It is invoked either as test condition or as [ condition ]. The square bracket form is a synonym โ€” [ is literally a command, and the closing ] is its last argument.

if test -f "$1"; then
    echo "File exists"
fi

if [ -f "$1" ]; then
    echo "File exists"
fi

Both forms are functionally identical. The square bracket form is more common and more readable. The spaces inside the brackets are required โ€” [ -f "$1" ] is correct, [-f "$1"] is a syntax error.

The test command supports three categories of comparisons:

File tests check the filesystem. -f checks for a regular file, -d for a directory, -e for existence, -r for readability, -w for writability, -x for executability.

String tests compare text. -z checks for empty string, -n for non-empty, = for equality, != for inequality. The < and > operators for string comparison must be escaped in [ ] โ€” [ "$a" \< "$b" ] โ€” or they will be interpreted as redirection.

Numeric tests compare integers. -eq, -ne, -gt, -ge, -lt, -le. These are distinct from string operators. [ 10 -gt 2 ] is true. [ "10" \> "2" ] is false because string comparison is lexicographic โ€” “10” comes before “2” alphabetically.

The [[ ]] construct is an extended test command adopted from ksh88. It does not perform word splitting or filename expansion, and it supports pattern matching with == and !=. Inside [[ ]], the < and > operators work without escaping, and && and || can be used directly.

if [[ "$filename" == *.txt ]]; then
    echo "Text file"
fi

if [[ "$a" > "$b" ]]; then
    echo "$a is alphabetically after $b"
fi

The [[ ]] form is generally safer because unquoted variables cannot cause word splitting errors. But it is Bash-specific โ€” scripts requiring POSIX compatibility should use [ ].


c. Combining Conditions and the case Statement

Complex decisions often need multiple conditions. Bash provides && (AND) and || (OR) for combining commands, and the test command supports -a (AND) and -o (OR) inside [ ].

if [ -f "$1" ] && [ -r "$1" ]; then
    echo "File exists and is readable"
fi

if [ "$OS" = "Linux" ] || [ "$OS" = "Darwin" ]; then
    echo "Unix-like system"
fi

The && and || operators work on command exit statuses. cmd1 && cmd2 runs cmd2 only if cmd1 succeeds. cmd1 || cmd2 runs cmd2 only if cmd1 fails. Inside [[ ]], the && and || operators work as logical operators directly.

The case statement is a cleaner alternative to long if-elif-else chains when comparing a single value against multiple patterns. It is the shell’s version of a switch statement.

case "$1" in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart)
        echo "Restarting service..."
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
        ;;
esac

The ;; terminates each branch. The * pattern is the catch-all, equivalent to else. The case statement performs pattern matching, not just equality โ€” patterns can include wildcards like *.txt or character classes.

Bash also supports ;& and ;;& terminators. ;& continues execution into the next clause without testing, and ;;& tests the next clause’s pattern and executes if it matches. These are rarely used but useful for fall-through logic.


Complete Example Session

This session builds conditionals for a service management script, from a simple file check to a full case-based command handler.

#!/bin/bash
# svc.sh - A simple service management script

# ============================================
# PART 1: THE BASIC IF
# ============================================

if [ -f /etc/passwd ]; then
    echo "/etc/passwd exists"
fi

# ============================================
# PART 2: THE IF-ELSE
# ============================================

if [ -d /var/log ]; then
    echo "Log directory found"
else
    echo "No log directory"
fi

# ============================================
# PART 3: THE ELIF CHAIN
# ============================================

if [ $# -eq 0 ]; then
    echo "Usage: $0 {start|stop|status}"
    exit 1
elif [ "$1" = "start" ]; then
    echo "Starting..."
elif [ "$1" = "stop" ]; then
    echo "Stopping..."
else
    echo "Unknown command: $1"
fi

# ============================================
# PART 4: THE STRING TEST
# ============================================

ENV="${ENVIRONMENT:-dev}"
if [ "$ENV" = "prod" ]; then
    echo "Production mode"
elif [ "$ENV" = "staging" ]; then
    echo "Staging mode"
else
    echo "Development mode"
fi

# ============================================
# PART 5: THE NUMERIC TEST
# ============================================

LOAD=$(cat /proc/loadavg | cut -d' ' -f1)
if [ "$LOAD" -gt 4 ]; then
    echo "High load: $LOAD"
elif [ "$LOAD" -gt 2 ]; then
    echo "Moderate load: $LOAD"
else
    echo "Normal load: $LOAD"
fi

# ============================================
# PART 6: THE FILE TEST
# ============================================

LOGFILE="/var/log/app.log"
if [ ! -f "$LOGFILE" ]; then
    echo "Log file missing, creating"
    touch "$LOGFILE"
elif [ ! -w "$LOGFILE" ]; then
    echo "Log file not writable"
    exit 2
fi

# ============================================
# PART 7: THE COMBINED CONDITION
# ============================================

if [ -n "$DB_HOST" ] && [ -n "$DB_PORT" ]; then
    echo "Database configured at $DB_HOST:$DB_PORT"
else
    echo "Database environment incomplete"
fi

# ============================================
# PART 8: THE EXTENDED TEST WITH PATTERN
# ============================================

FILENAME="$1"
if [[ "$FILENAME" == *.tar.gz ]]; then
    echo "Compressed archive"
elif [[ "$FILENAME" == *.log ]]; then
    echo "Log file"
fi

# ============================================
# PART 9: THE CASE STATEMENT
# ============================================

case "${1:-status}" in
    start)
        echo "Starting service..."
        # systemctl start myapp
        ;;
    stop)
        echo "Stopping service..."
        # systemctl stop myapp
        ;;
    restart)
        echo "Restarting service..."
        # systemctl restart myapp
        ;;
    status)
        echo "Service status: running"
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

# ============================================
# PART 10: THE EXIT CODE CHECK
# ============================================

if systemctl is-active --quiet nginx; then
    echo "nginx is running"
else
    echo "nginx is not running"
    # systemctl start nginx
fi

The ten parts cover the basic if, the if-else, the elif chain, string tests, numeric tests, file tests, combined conditions, extended tests with patterns, the case statement, and checking an external command’s exit status.


Quick Reference

The if Forms

FormPurpose
if cmd; then ...; fiRun if cmd succeeds
if cmd; then ...; else ...; fiRun either branch
if cmd1; then ...; elif cmd2; then ...; fiChain multiple tests
if cmd1 && cmd2; then ...; fiBoth must succeed
if cmd1 || cmd2; then ...; fiEither must succeed

The File Test Operators

OperatorTrue When
-e fileFile exists
-f fileRegular file
-d fileDirectory
-r fileReadable
-w fileWritable
-x fileExecutable
-s fileFile size > 0

The String Test Operators

OperatorTrue When
-z stringString is empty
-n stringString is non-empty
s1 = s2Strings equal
s1 != s2Strings differ
[[ s1 == pattern ]]String matches pattern

The Numeric Test Operators

OperatorTrue When
n1 -eq n2Equal
n1 -ne n2Not equal
n1 -gt n2Greater than
n1 -ge n2Greater or equal
n1 -lt n2Less than
n1 -le n2Less or equal

The case Terminators

TerminatorBehavior
;;End this branch, skip to esac
;&Fall through to next branch unconditionally
;;&Test next pattern, run if matches

Best Practices

โœ… Do This:

if [ -f "$1" ]; then                                  # โœ… quote variables in tests
if [ $# -eq 0 ]; then                                 # โœ… check arguments first
    echo "Usage: $0 <file>"
    exit 1
fi
case "$1" in                                          # โœ… use case for multi-branch
    start) ... ;;
    stop) ... ;;
    *) exit 1 ;;
esac
if [[ "$file" == *.log ]]; then                       # โœ… use [[ ]] for patterns
if cmd1 && cmd2; then                                 # โœ… combine with && / ||

โŒ Don’t Do This:

if [ -f $1 ]; then                                    # โŒ unquoted variable in test
if [ -f "$1" ]; then                                  # โŒ no argument check
if [ "$a" > "$b" ]; then                              # โŒ > needs escaping in [ ]
if [ 10 > 2 ]; then                                   # โŒ numeric uses -gt, not >
if [ $# -eq 0 ]; then                                 # โŒ empty then branch
fi

Common Pitfalls

PitfallWhy It HappensFix
“Too many arguments”Unquoted variable expands to multiple wordsQuote: [ -f "$1" ]
Syntax error near ]Missing space before ]Use [ ... ] with spaces
Numeric comparison failsUsing > instead of -gtUse -eq, -gt, etc.
String < treated as redirectionUnescaped in [ ]Escape or use [[ ]]
case never matchesPattern doesn’t include wildcardAdd *) catch-all
if always truetest returns 0 for empty stringUse -n with quotes

Real-World Examples

1. Argument Check

if [ $# -eq 0 ]; then
    echo "Usage: $0 <filename>"
    exit 1
fi

2. File Exists Guard

if [ ! -f "$LOGFILE" ]; then
    echo "Log missing"
    exit 2
fi

3. String Equality

if [ "$ENV" = "prod" ]; then
    echo "Production"
fi

4. Numeric Threshold

if [ "$LOAD" -gt 4 ]; then
    echo "High load"
fi

5. Combined Condition

if [ -n "$HOST" ] && [ -n "$PORT" ]; then
    echo "Configured"
fi

6. Pattern Match

if [[ "$file" == *.tar.gz ]]; then
    tar -xzf "$file"
fi

7. Case Multi-Branch

case "$1" in
    start) systemctl start app ;;
    stop) systemctl stop app ;;
    *) echo "Usage: $0 {start|stop}" ;;
esac

8. Exit Code Check

if systemctl is-active --quiet nginx; then
    echo "Running"
else
    systemctl start nginx
fi

9. Elif Chain

if [ "$1" = "a" ]; then
    echo "A"
elif [ "$1" = "b" ]; then
    echo "B"
else
    echo "Other"
fi

10. Empty Branch with :

if [ -z "$DEBUG" ]; then
    :
else
    echo "Debug on"
fi

Visual: if Statement Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  IF STATEMENT FLOW                           โ”‚
โ”‚                                              โ”‚
โ”‚  if [ condition ]; then                      โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  condition returns 0?                        โ”‚
โ”‚    โ”‚              โ”‚                          โ”‚
โ”‚   YES            NO                          โ”‚
โ”‚    โ”‚              โ”‚                          โ”‚
โ”‚    โ–ผ              โ–ผ                          โ”‚
โ”‚  then branch    else branch (if present)     โ”‚
โ”‚    โ”‚              โ”‚                          โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                          โ”‚
โ”‚           โ–ผ                                  โ”‚
โ”‚          fi                                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: test Command and Exit Status

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  TEST COMMAND AND EXIT STATUS                โ”‚
โ”‚                                              โ”‚
โ”‚  test -f "$1"    โ†’ 0 if file exists          โ”‚
โ”‚  [ -f "$1" ]     โ†’ same thing                โ”‚
โ”‚                                              โ”‚
โ”‚  [ -z "$VAR" ]   โ†’ 0 if empty                โ”‚
โ”‚  [ "$a" = "$b" ] โ†’ 0 if equal                โ”‚
โ”‚  [ 5 -gt 2 ]     โ†’ 0 if true                 โ”‚
โ”‚                                              โ”‚
โ”‚  if [ -f "$1" ]; then                        โ”‚
โ”‚    echo "exists"                             โ”‚
โ”‚  fi                                          โ”‚
โ”‚                                              โ”‚
โ”‚  The [ command returns 0 or 1.               โ”‚
โ”‚  The if checks that return value.            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: case Statement Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CASE STATEMENT FLOW                         โ”‚
โ”‚                                              โ”‚
โ”‚  case "$1" in                                โ”‚
โ”‚    start)                                    โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚      โ–ผ                                       โ”‚
โ”‚    matches "start"?                          โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚     YES โ†’ run start commands โ†’ ;;            โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚      โ–ผ                                       โ”‚
โ”‚    matches "stop"?                           โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚     YES โ†’ run stop commands โ†’ ;;             โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚      โ–ผ                                       โ”‚
โ”‚    matches *)?   โ† catch-all                 โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚     YES โ†’ run default โ†’ ;;                   โ”‚
โ”‚      โ”‚                                       โ”‚
โ”‚      โ–ผ                                       โ”‚
โ”‚    esac                                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Comparison Operators

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  COMPARISON OPERATORS                        โ”‚
โ”‚                                              โ”‚
โ”‚  STRINGS:              NUMBERS:              โ”‚
โ”‚  [ "$a" = "$b" ]       [ "$a" -eq "$b" ]     โ”‚
โ”‚  [ "$a" != "$b" ]      [ "$a" -ne "$b" ]     โ”‚
โ”‚  [[ "$a" < "$b" ]]     [ "$a" -lt "$b" ]     โ”‚
โ”‚  [[ "$a" > "$b" ]]     [ "$a" -gt "$b" ]     โ”‚
โ”‚                                              โ”‚
โ”‚  FILES:                LOGIC:                โ”‚
โ”‚  [ -f "$f" ]           [ cond1 ] && [ cond2 ]โ”‚
โ”‚  [ -d "$d" ]           [ cond1 ] || [ cond2 ]โ”‚
โ”‚  [ -e "$e" ]                                 โ”‚
โ”‚  [ -r "$r" ]                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Basic ifif cmd; then ...; fi
if-elseif cmd; then ...; else ...; fi
elifif cmd1; then ...; elif cmd2; then ...; fi
test shorthand[ condition ]
Extended test[[ condition ]]
String equality[ "$a" = "$b" ]
Numeric comparison[ "$a" -gt "$b" ]
File test[ -f "$file" ]
Combine ANDcmd1 && cmd2 or [ a ] && [ b ]
Combine ORcmd1 || cmd2 or [ a ] || [ b ]
casecase "$1" in pattern) ...;; esac
catch-all*) pattern in case

Key takeaways:

  • if checks exit status, not truth values. The if statement runs a command and checks whether it returned 0. The test command โ€” invoked as [ โ€” is the tool that turns comparisons into exit statuses.
  • The square bracket is a command, not syntax. [ -f "$1" ] is the test command with -f and the filename as arguments. The spaces inside the brackets are required, and the closing ] is the command’s last argument.
  • Quote variables inside tests. [ -f $1 ] fails when $1 contains spaces. [ -f "$1" ] always works. This is the most common source of “too many arguments” errors.
  • Numeric and string comparisons use different operators. -eq, -gt, -lt for numbers. =, !=, <, > for strings. Using > with numbers gives a redirection error in [ ]; using -gt with strings gives a syntax error.
  • [[ ]] is safer than [ ]. It suppresses word splitting, supports pattern matching with ==, and allows <, >, &&, and || without escaping. It is Bash-specific.
  • Use case for multi-branch decisions. When comparing one value against many patterns, case is cleaner than a chain of elif. The *) pattern is the catch-all.
  • The : command is a no-op. Use it to create an empty branch when the if structure requires a command but you want nothing to happen.

Remember: Conditionals are how scripts stop being lists and start being programs. The if statement tests exit statuses. The test command produces those statuses from comparisons. The case statement flattens multi-branch logic into something readable. Master the [ command’s operators, quote your variables, and know the difference between -gt and > โ€” that is the foundation every LFCA scripting question builds on.


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!