Skip to content
if and case
step 1/5

Reading — step 1 of 5

Learn

~3 min readControl Flow

if and case

Bash conditionals hide a beautiful secret: there is no boolean type — there are exit statuses. if doesn't test a condition; it runs a command and branches on whether that command succeeded. Once you see it, half of bash's odd syntax snaps into focus.

if runs commands

if grep -q "error" logfile.txt; then
    echo "found errors"
fi

grep exits 0 when it finds a match (success), non-zero otherwise — and if branches on exactly that. Any command can be the condition. So what's the square-bracket thing everyone writes?

if [[ "$n" -gt 10 ]]; then
    echo "big"
fi

[[ … ]] is itself a test construct that succeeds or fails — purpose-built for comparisons. (Its ancestor [ is literally a program — /usr/bin/[ — which explains the mandatory spaces around it. Use modern [[ ]] in bash scripts: safer parsing, more features; recognize [ ] in old scripts.)

The two comparison alphabets

Bash's original sin: numbers and strings compare with different operators.

[[ "$a" -eq "$b" ]]    # numeric: -eq -ne -lt -le -gt -ge
[[ "$s" == "yes" ]]    # string:  ==  !=  <  >
[[ -z "$s" ]]          # string is empty
[[ -n "$s" ]]          # string is non-empty

Mixing them "works" just often enough to be dangerous: [[ "10" < "9" ]] is TRUE (string comparison is alphabetical, and "1" sorts before "9"). Numbers get dash-letters, strings get symbols — drill it now. Files get their own test flags, everywhere in real scripts: -f file exists-and-is-a-file, -d is-a-directory, -r readable, -x executable.

Combine with && / || inside [[ ]], and ! negates. if/elif/else/fi closes with fi — bash blocks end in reversed keywords (fi, esac, done), a 1970s Algol homage you'll stop noticing within a week.

case: pattern matching for text

When one variable routes to many branches, case beats an elif ladder — and it matches globs, not just literals:

case "$file" in
    *.txt)      echo "text" ;;
    *.jpg|*.png) echo "image" ;;
    backup_*)   echo "backup" ;;
    *)          echo "unknown" ;;
esac

Each pattern ends with ), each branch ends with ;;, *) is the default, esac closes. case is the tool for argument parsing and file-type routing — you'll meet it again in the getopts lesson.

Your exercise: FizzBuzz for one number

Read n; multiples of 3 → Fizz, 5 → Buzz, both → FizzBuzz, else the number. Bash edition:

read n
if (( n % 15 == 0 )); then
    echo "FizzBuzz"
elif (( n % 3 == 0 )); then
    echo "Fizz"
elif (( n % 5 == 0 )); then
    echo "Buzz"
else
    echo "$n"
fi

Note the third bracket flavor: (( )) — arithmetic conditions, where C-style operators (%, ==, <) work on numbers directly, no dash-letters needed. Rule of thumb: (( )) for math tests, [[ ]] for strings and files. And the eternal FizzBuzz law, fourth language running: most specific case first — 15 before 3 before 5. Some lessons are the same in every language; that's why they're fundamentals.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…