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
| Form | Purpose |
|---|---|
if cmd; then ...; fi | Run if cmd succeeds |
if cmd; then ...; else ...; fi | Run either branch |
if cmd1; then ...; elif cmd2; then ...; fi | Chain multiple tests |
if cmd1 && cmd2; then ...; fi | Both must succeed |
if cmd1 || cmd2; then ...; fi | Either must succeed |
The File Test Operators
| Operator | True When |
|---|---|
-e file | File exists |
-f file | Regular file |
-d file | Directory |
-r file | Readable |
-w file | Writable |
-x file | Executable |
-s file | File size > 0 |
The String Test Operators
| Operator | True When |
|---|---|
-z string | String is empty |
-n string | String is non-empty |
s1 = s2 | Strings equal |
s1 != s2 | Strings differ |
[[ s1 == pattern ]] | String matches pattern |
The Numeric Test Operators
| Operator | True When |
|---|---|
n1 -eq n2 | Equal |
n1 -ne n2 | Not equal |
n1 -gt n2 | Greater than |
n1 -ge n2 | Greater or equal |
n1 -lt n2 | Less than |
n1 -le n2 | Less or equal |
The case Terminators
| Terminator | Behavior |
|---|---|
;; | 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “Too many arguments” | Unquoted variable expands to multiple words | Quote: [ -f "$1" ] |
Syntax error near ] | Missing space before ] | Use [ ... ] with spaces |
| Numeric comparison fails | Using > instead of -gt | Use -eq, -gt, etc. |
String < treated as redirection | Unescaped in [ ] | Escape or use [[ ]] |
case never matches | Pattern doesn’t include wildcard | Add *) catch-all |
if always true | test returns 0 for empty string | Use -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
| Item | Value |
|---|---|
| Basic if | if cmd; then ...; fi |
| if-else | if cmd; then ...; else ...; fi |
| elif | if 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 AND | cmd1 && cmd2 or [ a ] && [ b ] |
| Combine OR | cmd1 || cmd2 or [ a ] || [ b ] |
| case | case "$1" in pattern) ...;; esac |
| catch-all | *) pattern in case |
Key takeaways:
ifchecks exit status, not truth values. Theifstatement runs a command and checks whether it returned0. Thetestcommand โ invoked as[โ is the tool that turns comparisons into exit statuses.- The square bracket is a command, not syntax.
[ -f "$1" ]is thetestcommand with-fand 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$1contains 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,-ltfor numbers.=,!=,<,>for strings. Using>with numbers gives a redirection error in[ ]; using-gtwith 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
casefor multi-branch decisions. When comparing one value against many patterns,caseis cleaner than a chain ofelif. The*)pattern is the catch-all. - The
:command is a no-op. Use it to create an empty branch when theifstructure 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!