Skip to content
Lesson 6 of 14

Step 1 of 5 · Reading · ~3 min

Learn

Control Flow

if and case

By the end of this lesson you can branch on numbers, strings and file properties, pick the right comparison operators for each, and route text through a case statement. The idea that makes bash's odd syntax make sense: there is no boolean type, only exit statuses. if does not test a condition. It runs a command and branches on whether that command succeeded.

if runs commands

if grep -q "timeout" server.log; then
    echo "found a timeout"
fi

Every command ends with an exit status: 0 means success and anything else means failure (the exit-status lesson covers this fully). grep exits 0 when it finds a match, so if takes the branch. Any command can be the condition. The square-bracket forms are just commands built for comparisons:

if [[ "$retries" -gt 3 ]]; then
    echo "giving up"
elif [[ "$retries" -eq 0 ]]; then
    echo "first attempt"
else
    echo "retrying"
fi

[[ ... ]] succeeds or fails depending on the comparison inside. Spaces inside the brackets are mandatory because [[ and ]] are words like any other. The block ends with fi, if spelled backwards; bash closes blocks with reversed keywords (fi, esac, and done for loops).

Two comparison alphabets

Bash has different operators for numbers and for text:

[[ "$a" -eq "$b" ]]    # numbers: -eq -ne -lt -le -gt -ge
[[ "$s" == "yes" ]]    # text:    ==  !=  <  >
[[ -z "$s" ]]          # text is empty        (-n: not empty)
[[ -f "$p" ]]          # a regular file exists (-d: a directory)

Mixing them "works" just often enough to hurt. [[ 10 < 9 ]] is true, because < compares text and 1 sorts before 9. If the values are numbers, use the dash-letters, or the arithmetic form below.

Combine tests with && (and), || (or) and ! (not): [[ -f "$p" && -r "$p" ]].

The arithmetic condition

(( )) is a condition in which C-style operators work on numbers directly:

if (( temp >= 30 )); then
    echo "hot"
fi

Use (( )) for number tests and [[ ]] for text and files. In a chain of elif tests, order matters: the first test that succeeds wins, so put the most specific test first. A chain that checks temp >= 20 before temp >= 30 never reaches the second branch.

case: pattern matching for text

When one value is routed to many branches, case is cleaner than an elif ladder. It matches glob patterns, not just literal text:

case "$cmd" in
    start|restart)  echo "launching" ;;
    stop)           echo "halting" ;;
    log-*)          echo "reading a log" ;;
    *)              echo "unknown command" ;;
esac

Each branch is pattern) commands ;;. | separates alternatives, * matches any text, ? one character, [abc] one of a set, and the last branch *) is the default. Branches are tried top to bottom and the first match wins, exactly like elif. The word is quoted, the patterns are not. case is regular-expression free: *.txt) matches names ending in .txt, while .*\.txt) does not.

Common mistakes

  • < and > on numbers. Inside [[ ]] they compare as text, so 99 is "greater" than 100. Use -lt/-gt or (( )).
  • No else. A chain with no final else prints nothing for the input nobody planned for.
  • General before specific. *.sh) placed above .*) swallows hidden scripts such as .setup.sh before the hidden-file rule sees them.
  • Missing spaces. [[$x -eq 1]] is the word [[$x followed by garbage. Keep a space inside both brackets.
  • Forgetting ;;. Every case branch ends with ;;, or bash reports a syntax error.

Your exercise

Classify a file by two properties: its name picks a kind (a case job, with rules that overlap, so their order matters) and its size picks a size class (a number job). The graded edge cases are the boundaries between size classes and names that match more than one rule.

Up nextLoopsControl Flow

Discussion

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

Sign in to post a comment or reply.

Loading…