Bash from the ground up

Decisions with if

Branching on a test: if, else and elif, the test brackets, the different spellings for numbers and for text, and the safer double-bracket form.

Part 4 of 12

Beginner18 min readBack to Courses

A test, then a branch

An if asks a question and runs a block of lines when the answer is yes. The question is written between square brackets with spaces inside them, like [ "$score" -ge 70 ]: the brackets are a command, the spaces around them are part of its syntax, and forgetting them is the most common mistake in the whole language. The spaces are part of the command, not decoration.

More than two outcomes means an elif between them: another test, tried only when the first one was false. The final else runs when nothing above it matched, which is the branch worth writing deliberately rather than forgetting. The first true branch wins, so order matters.

The comparison inside uses different spellings for numbers and for text. Numbers use -ge, -gt, -le, -lt and -eq. Text uses = and !=, and that single equals sign compares text rather than assigning anything. Mixing the two families gives a test that is silently false.

What the tests can see

The same brackets can ask about files and folders, which is how a script checks the ground before it walks: -f is true for a file, -d for a folder, -x for something you can run, and -n is true when a piece of text is not empty. Those are the everyday ones, and they need no other tool to explain them. These tests ask the file system at the moment the line runs.

There is also a second form with double brackets. It does everything the single form does, and two things more: it does not split a value on its spaces, so quotes around the value are optional, and it understands pattern matching. That makes it the safer form when a value might contain spaces or when you want to ask "does this end in a certain suffix?". The double form is a shell feature, not a separate command.

Tests for numbers, text, files and patterns

# Numbers use two-letter tests: -ge is "at least".
if [ "$score" -ge 70 ]; then
  echo "passed"
else
  echo "try again"
fi
# Text uses a single equals sign.
if [ "$role" = "editor" ]; then
  echo "may edit"
fi

# Files: is it there, is it a folder?
if [ -d "/tmp" ]; then
  echo "/tmp is a folder"
fi

# The double form matches patterns and needs no quotes.
if [[ "$file" == *.sh ]]; then
  echo "a shell script"
fi

The test is a command

The word after if is a command, and its exit code is the question. Usually that command is [, which is also spelled test; the closing ] is an argument it requires. then starts the block that runs on zero, and fi closes the whole thing. Zero means the test passed; anything else means it did not.

Because any command works, if grep -q pear list.txt needs no brackets at all. That is also why if [ ... ] fails so loudly when the spaces are missing: the shell goes looking for a command whose name begins with a bracket and cannot find one. The brackets must be separate words with spaces around them.

Choosing the family of test

Numbers and text have separate families of tests for a reason. -eq, -ne, -lt, -le, -gt and -ge compare numbers, while = and != compare strings character by character. [ "10" -lt "9" ] is false, but [[ "10" < "9" ]] is true because the second compares text. The family has to match the data, or the answer is meaningless.

-z is true for an empty string and -n for a non-empty one, and -f, -d, -r, -w and -x ask about a path. Inside [[ ]] you can combine conditions with && and ||, match with == and a pattern, and skip most quoting worries; the single-bracket form needs a separate [ command for each condition. Inside double brackets a pattern is a glob, not a regular expression.

Reading a test that fails

A test that fails does not say why. [ "$score" -ge 70 ] is false when the score is 69, and also when the value is empty or contains a letter, because the numeric comparison cannot read it.

The brackets are a command, so a syntax error inside them is a failed command rather than a false answer. [ "$name" = "ada" ] with an empty name is safe because the quotes keep an empty argument; the unquoted form collapses to [ = "ada" ], which the command cannot parse.

Three decisions in one script

Run it and see which branch each test takes. Then change `score` so the other branch runs, change `file` to `notes.txt`, and add a test that asks whether `/tmp` is a folder.

Output
Click "Run" to execute your code

An empty value with a single-bracket test can be read as a broken command: [ "$name" = "ada" ] is safe because of the quotes, but a bare [ $name = "ada" ] becomes [ = "ada" ] when the name is empty, and that is a syntax error rather than a false answer.

Three common slips: no spaces inside the brackets, = where a number needs -eq, and an unquoted empty variable that collapses the test into a syntax error. The double-bracket form removes the last two problems, but it is still a command, so the spaces stay.

Try these next

  • Write an if that prints one message for an empty name with -z and another for a name with text.
  • Compare 9 and 10 with -lt and with <, and explain the two answers.
  • Ask whether a directory exists with -d, and print a different line when it does not.

Bands, arithmetic and an empty value

Run it, change the score so each branch takes its turn, then empty `name` and watch the `-z` branch take over.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Why do the test brackets need spaces inside them?