Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 16 · Conditionals and Pattern Branches

if, elif, else, and What Bash Is Actually Testing

Bash conditionals look strange to anyone arriving from another language — the brackets, the mandatory spaces, the semicolon before then. Every one of those oddities follows from a single fact, and this chapter is that fact. if does not evaluate a condition. It runs a command.

Intermediate20 min readLinuxPart IBashConditionalsExit StatusScripting

Part I · Module 16 · Chapter 16.1 · if, elif, else, and What Bash Is Actually Testing

Module 11 taught you that every command returns a number, and that zero means success. Module 15 gave you a script with a name, a shebang, arguments and a return status. This module spends that investment: it is where a script stops running the same commands every time and starts deciding.

And it opens with the fact that makes the rest of the module trivial instead of arbitrary.

1. There Is No Condition

Here is a conditional, as small as one gets:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ if true; then echo "taken"; fi
taken

$ if false; then echo "taken"; fi
$

Nothing surprising yet. But look at what true and false actually are:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ type true false
true is a shell builtin
false is a shell builtin

$ true; echo $?
0
$ false; echo $?
1

They are not keywords. They are not boolean literals. They are commands — commands whose entire job is to return 0 and 1 respectively and do nothing else. Bash ships them precisely because the thing after if has to be a command, and sometimes you want a command that just succeeds.

That is the whole mechanism:

What a conditional actually does

What a conditional actually does. It runs the command you gave it, waits for that command to finish, looks at the single number the command returned, and takes the first branch when that number is zero. There is no boolean expression anywhere in the sequence, which is why any command at all can be a condition.01IT RUNS THE COMMANDan ordinary command, with output and all02IT WAITS FOR THE COMMAND TO FINISHand reads the one number it returned03ZERO TAKES THE FIRST BRANCHanything else takes the other one04NO BOOLEAN IS EVER EVALUATEDwhich is why any command can be a condition
There is no boolean expression anywhere in this sequence. Bash runs the command, waits for it, reads the one number it returned, and takes the first branch when that number is zero. Every oddity in Bash conditional syntax follows from this, and so does the fact that literally any command can be a condition.

Read it as a sentence and the syntax decodes itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if  <run this command>  ; then
      <run these if it returned 0>
else
      <run these if it returned anything else>
fi

The semicolon is there because then is a separate word from the command, and Bash needs to know the command's argument list has ended. A newline does the same job, which is why the multi-line form has no semicolon:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if make
then
  echo "built"
fi

Both forms are identical to Bash. The one-line form with the semicolon is what you type at a prompt; the multi-line form is what goes in a script.

2. The Demonstration That Settles It

Talk is cheap. Here is the test that convinces people:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ if [ 1 ]; then echo "true"; else echo "false"; fi
true

$ if [ 0 ]; then echo "true"; else echo "false"; fi
true

[ 0 ] is true.

Everyone who believes if evaluates a boolean predicts false for the second one, and gets it wrong. Nothing about zero is being evaluated as a value at all. [ is a command (Chapter 16.2 is entirely about that), and given a single argument, it answers one question: is this string non-empty? The string 0 is one character long, so the answer is yes, so [ returns 0, so the branch is taken.

Which predicts this, correctly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ if [ "" ]; then echo "true"; else echo "false"; fi
false

$ if [ ]; then echo "true"; else echo "false"; fi
false

Empty string: not non-empty, so [ returns 1, so the else branch runs. Nothing at all between the brackets: same answer.

3. Any Command, Which Means Any Command

If the thing after if is just a command, then there is no special list of things allowed there. Watch:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ if ls /etc/hosts > /dev/null; then echo "it exists"; fi
it exists

$ if grep -q root /etc/passwd; then echo "found"; fi
found

$ if cd /tmp; then echo "now in $PWD"; fi
now in /tmp

$ if mkdir /root/nope 2>/dev/null; then echo "made it"; else echo "could not"; fi
could not

ls, grep, cd, mkdir — all ordinary commands, all usable as conditions, because all of them return a status. This is not a trick or an advanced technique; it is the normal, idiomatic way to write Bash, and Chapter 16.7 is dedicated to doing it well.

There is one consequence that catches people, and it follows directly from "it runs the command":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ if echo "hello"; then echo "branch taken"; fi
hello
branch taken

The condition's output still goes where output goes. Bash did not quietly capture it. echo printed hello to your terminal exactly as it would anywhere else, and then its status was read. This is why real conditions are full of -q and > /dev/null:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if grep -q "ERROR" run.log; then        # -q: find it, print nothing
if command -v vcs > /dev/null 2>&1; then # discard both streams

Both of those are Module 12's redirections doing exactly what they always do. Nothing new is happening — the condition is a command, so it behaves like a command.

4. else, and Why It Is Not Always Wanted

else is one branch for "the command did not return zero" — which lumps every possible failure together:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if make > build.log 2>&1; then
  echo "build succeeded"
else
  echo "build failed, see build.log"
fi

That is perfectly good when all you need to know is pass or fail. But remember from Chapter 11.1 that the number carries information:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ grep -q "pattern" /etc/hosts; echo $?
1
$ grep -q "pattern" /nonexistent-file 2>/dev/null; echo $?
2

grep returns 1 for "no match" and 2 for "something went wrong". An else branch cannot tell those apart, and a script that reports "no errors found in the log" when the log did not exist is lying. Reading the specific number needs Chapter 16.3's numeric comparisons on $?, or case, which is Chapter 16.6.

5. elif, and That It Is Ordered

elif chains conditions. The first one that returns zero wins, and the rest are never run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
# dispatch.sh — pick a simulator from whatever is installed

if command -v vcs > /dev/null 2>&1; then
  sim="vcs"
elif command -v xrun > /dev/null 2>&1; then
  sim="xrun"
elif command -v questa > /dev/null 2>&1; then
  sim="questa"
else
  echo "dispatch.sh: no supported simulator on PATH" >&2
  exit 2
fi

echo "using $sim"

Two properties of that chain matter more than the syntax:

It is ordered, and the order is a decision. This script prefers VCS. If both VCS and Xcelium are installed, xrun is never even looked for. That is a policy statement written in the sequence of the branches, and it should be a deliberate one.

Exactly one branch runs. Not "at most one" — with the else present, exactly one. That is the whole value of a chain over a sequence of separate if statements, which would each be tested independently:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# NOT the same thing — all three are tested, all three can run
if command -v vcs  > /dev/null 2>&1; then sim="vcs";  fi
if command -v xrun > /dev/null 2>&1; then sim="xrun"; fi   # overwrites!

Here the last matching branch wins instead of the first, which is almost certainly not what was intended. The chain is not cosmetic.

Here is the ordering made visible, with a function standing in for a tool that may or may not be present:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ check() { [ "$1" = "yes" ]; }
$ pick() {
>   if check "$1"; then echo "first"
>   elif check "$2"; then echo "second"
>   else echo "neither"; fi
> }
$ pick yes yes
first
$ pick no yes
second
$ pick no no
neither

pick yes yes prints first and not both — the second condition was never evaluated.

6. The Status of an if Is the Status of Its Body

One subtlety worth knowing before Module 20, and it surprises people:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ fail3() { return 3; }
$ if fail3; then echo "ok"; else echo "no"; fi
no
$ echo $?
0

$? is 0, not 3. The condition returned 3, but the thing that ran last was echo "no", which succeeded. $? after fi is the status of whichever branch body ran, not the status of the condition.

And a conditional whose branch does not run at all reports zero:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ if false; then echo "never"; fi
$ echo $?
0

This is specified behaviour: an if with no else and a false condition returns 0. It is the reason set -e does not kill a script at a failing if condition — a fact Module 20 depends on, and one that would look like a bug if you had not seen this section.

To keep the condition's status, capture it inside the branch:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if ! make > build.log 2>&1; then
  rc=$?                      # the status of make, read immediately
  echo "make failed with status $rc" >&2
  exit "$rc"
fi

rc=$? has to be the branch's first line, for exactly the reason Chapter 11.1 gave: the next command overwrites $?.

7. A Complete Script

Everything so far, in something that would survive a code review. It checks a simulation log the way a real wrapper does — and notice that only one line in it is new since Module 15:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
# check-run.sh — report on a simulation log
# usage: check-run.sh <logfile>

log=$1

if [ -z "$log" ]; then
  echo "usage: ${0##*/} <logfile>" >&2
  exit 2
fi

if [ ! -f "$log" ]; then
  echo "${0##*/}: no such log: $log" >&2
  exit 2
fi

if grep -q "UVM_FATAL" "$log"; then
  echo "FATAL   $log"
  exit 1
elif grep -q "UVM_ERROR" "$log"; then
  echo "ERRORS  $log"
  exit 1
elif grep -q "TEST PASSED" "$log"; then
  echo "PASS    $log"
  exit 0
else
  echo "UNKNOWN $log — ran but reported nothing"
  exit 3
fi
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf 'starting\nUVM_ERROR : 2 mismatches\n' > run.log
$ ./check-run.sh run.log; echo "status=$?"
ERRORS  run.log
status=1

$ printf 'starting\nTEST PASSED\n' > run.log
$ ./check-run.sh run.log; echo "status=$?"
PASS    run.log
status=0

$ printf 'starting\nkilled\n' > run.log
$ ./check-run.sh run.log; echo "status=$?"
UNKNOWN run.log — ran but reported nothing
status=3

$ ./check-run.sh missing.log; echo "status=$?"
check-run.sh: no such log: missing.log
status=2

Four behaviours, four distinct statuses, and the UNKNOWN branch is the one that earns its place: a run that neither failed nor passed is the single most common real outcome of a simulation that got killed, and a script with only pass/fail branches would have silently called it a pass or a failure.

The ordering is a decision again. UVM_FATAL is checked before UVM_ERROR because a fatal log usually contains both, and the more severe answer is the more useful one.

Practice

  1. Predict, then run: if [ "0" ]; then echo A; else echo B; fi. Then try it with [ "" ], [ "false" ] and [ " " ] — one space between the quotes. Explain the last one.
  2. Run if echo marker; then echo done; fi. Where did marker go, and what single change stops it appearing while keeping the conditional working?
  3. Write a conditional whose condition is cd /tmp and whose body prints $PWD. Then do the same with cd /nonexistent. Why does the second one still print a message to your terminal even though the branch did not run?
  4. Define f() { return 7; }. Run if f; then echo y; else echo n; fi; echo $?. Why is the answer 0? Now get 7 to print instead.
  5. Take the three-way elif chain from Section 5 and reverse the order of the first two branches. Construct a situation where the two versions give different answers, then say which order you would actually ship and why.
  6. Extend check-run.sh with a branch that distinguishes "the log exists but is completely empty" from the UNKNOWN case. You do not have the operator for it yet — guess it, check whether you guessed right, and read Chapter 16.4 to confirm.

Check Your Understanding

  1. Bash evaluates the expression between if and then and takes the branch if it is true. What is wrong with that sentence?
  2. Why is [ 0 ] true?
  3. You write if grep "ERROR" run.log; then echo "found"; fi and the matching lines appear on your screen. Is this a bug in grep, in if, or in your command?
  4. What is the difference between an elif chain and three separate if statements?
  5. After if false; then echo hi; fi, what is $? and why?
  6. Name one thing you can write after if that is not a [ test.

8. Remember

  • if runs a command and reads its exit status. There is no boolean expression and nothing is evaluated for truthiness.
  • Zero takes the first branch — because zero means the command succeeded, not because zero is true.
  • true and false are commands, not keywords. They exist because the slot after if needs a command.
  • [ 0 ] is true and [ ] is false, which is the proof: [ is testing string emptiness, not reading a number.
  • The condition's output still reaches your terminal, which is why real conditions carry -q or > /dev/null.
  • An elif chain is ordered and exclusive. The first zero wins; later conditions never run.
  • $? after fi is the branch body's status, not the condition's. Capture $? on the branch's first line if you need the real one.
  • The semicolon before then is a separator, nothing more. A newline does the same job.

Chapter 16.2 takes the thing this chapter kept using without explaining — those square brackets — and shows that [ is a program on your disk with a name made of one punctuation character, which is the entire reason its spaces are not optional.

Where this fits

Part of the Linux & Bash curriculum.