Why this matters
A script that cannot decide anything is just a list of commands. Real scripts check before they act: does the config file exist, did the user pass an argument, is the disk more than 90% full, did the download succeed. bash's if is unusual compared with other languages: it does not test a true/false value, it runs a command and looks at its exit status. Once that clicks, the rest of this module is details.
if runs a command
The shape:
if COMMANDS; then
COMMANDS
elif COMMANDS; then
COMMANDS
else
COMMANDS
fi
bash runs the commands after if. If the last one exits with status 0 (success), the then block runs. Otherwise bash tries each elif in turn, and if none succeeds, runs else. elif and else are optional; fi (if backwards) closes the statement.
Any command will do, which is the important part:
if grep -q '^learner:' /etc/passwd; then
echo "user learner exists"
fi
if mkdir /work/cache 2>/dev/null; then
echo "created cache"
else
echo "cache already there (or not allowed)"
fi
$ bash check.sh
user learner exists
created cache
$ bash check.sh
user learner exists
cache already there (or not allowed)
This is the same exit-status logic as && and || from module 1, written in a form that does not have the a && b || c trap: when the then block fails, else does not run.
The then may go on its own line instead of after ;. Both forms are common:
if grep -q root /etc/passwd
then
echo found
fi
The test command, also known as [
Most conditions are not "did a program succeed" but "is this string empty", "is this number bigger", "does this file exist". For that there is a command whose only job is to evaluate an expression and exit 0 (true) or 1 (false): test.
$ test 5 -gt 3; echo $?
0
$ test 5 -gt 8; echo $?
1
test has a second name, [. When called as [, it insists that its last argument is ]. These two lines are identical:
if test -f config.txt; then echo yes; fi
if [ -f config.txt ]; then echo yes; fi
[ is not shell syntax like parentheses in other languages. It is a command name. bash has it as a builtin for speed, and there is even a program file for it:
$ type -a [
[ is a shell builtin
[ is /usr/bin/[
So, after expansion, [ "$n" -gt 3 ] is a command named [ with four arguments: the value of n, -gt, 3, and ]. Every spacing rule follows from that: [ needs a space after it (it is a separate word), every operator and operand is its own word, and ] is a separate final word.
What test can check
Strings:
| Test | True when |
|---|---|
[ -z "$s" ] | s is empty (zero length) |
[ -n "$s" ] | s is not empty |
[ "$a" = "$b" ] | the strings are equal (== also works in bash, but = is portable) |
[ "$a" != "$b" ] | the strings differ |
Integers (only whole numbers; bash has no floating point):
| Test | Meaning |
|---|---|
-eq, -ne | equal, not equal |
-lt, -le | less than, less or equal |
-gt, -ge | greater than, greater or equal |
Files:
| Test | True when the path |
|---|---|
-e | exists (any kind) |
-f | is a regular file |
-d | is a directory |
-L | is a symbolic link |
-s | exists and is not empty |
-r, -w, -x | is readable, writable, executable by you |
a -nt b | a is newer than b (modification time) |
Combining and negating:
| Form | Meaning |
|---|---|
[ ! -f x ] | not a file |
[ -f a ] && [ -f b ] | both (preferred) |
[ -f a ] || [ -f b ] | either (preferred) |
test also has -a and -o for and/or inside one bracket, but they are ambiguous in some cases and POSIX marks them obsolete; use && and || between separate tests instead.
Why are numbers compared with -lt and not <? Because < and > are redirection operators. [ 5 > 3 ] does not compare anything: bash creates a file named 3, redirects ['s output into it, and runs [ 5 ], which is true because "5" is a non-empty string. Try it in the sandbox and then look at ls.
A worked example
Open the shell for this lesson and create age.sh with nano:
#!/bin/bash
# age.sh: classify an age given as the first argument
if [ $# -ne 1 ]; then
echo "usage: age.sh AGE" >&2
exit 2
fi
age=$1
if [ "$age" -lt 13 ]; then
echo "child"
elif [ "$age" -lt 20 ]; then
echo "teenager"
else
echo "adult"
fi
$ bash age.sh 8
child
$ bash age.sh 19
teenager
$ bash age.sh 45
adult
$ bash age.sh
usage: age.sh AGE
$ echo $?
2
$ bash age.sh old
age.sh: line 9: [: old: integer expression expected
age.sh: line 11: [: old: integer expression expected
adult
The last run shows a weakness: [ reports an error for a non-number, returns a failure status (2), and if treats any non-zero status as false, so execution falls through to else. Lesson 3 shows how to validate input before doing arithmetic on it.
Why you quote inside [ ]
Because [ is a normal command, its arguments go through word splitting and globbing like any other command (module 7). An unquoted empty variable simply disappears:
$ name=
$ [ $name = admin ] && echo yes
bash: [: =: unary operator expected
$ [ "$name" = admin ] && echo yes
$
Unquoted, [ received = admin ]: three arguments, which it reads as "unary operator =" and fails. Quoted, it received "" = admin ], a valid comparison that is simply false. A value with a space does the opposite damage: name="a b" gives [: too many arguments. Rule: always double-quote variables inside [ ].
Common mistakes
- Missing spaces.
[$x = 1]givesbash: [1: command not found(or similar).["$x" = 1 ]is just as bad.[ "$x" = 1]givesbash: [: missing ']'. =with spaces in an assignment vs. in a test.x = 1on its own tries to run a command namedx. Inside[ ],=needs spaces around it:[ "$x"=1 ]is a single non-empty string, so it is always true.- Using
>or<for numbers. They redirect. Use-gt,-lt, or(( ))(lesson 3). - Using
-eqfor strings.[ abc -eq abc ]gives[: abc: integer expression expected. - Forgetting
thenorfi.syntax error near unexpected token 'else'orsyntax error: unexpected end of fileusually means a missingthenorfi. - Testing
$?needlessly.cmd; if [ $? -eq 0 ]works butif cmdis shorter and cannot be broken by something running in between.
Exercises
The quizzes check that you think of [ as a command and that you choose string or numeric operators deliberately. The two code exercises are run with several arguments, including edge cases and an argument with a space in it, and the exit status is checked too.
Recap
ifruns a command and branches on its exit status: 0 means thethenbranch.test EXPRand[ EXPR ]are the same command;]must be its last argument.- Strings use
=,!=,-z,-n; integers use-eq -ne -lt -le -gt -ge; files use-e -f -d -s -r -w -x. - Every word inside
[ ]needs spaces around it, and variables need double quotes. <and>are redirections, not comparisons, inside[ ].
Going further: help test lists every operator. Then try [ -t 0 ] && echo terminal interactively and inside a pipeline (echo | bash -c '[ -t 0 ] && echo terminal || echo pipe'): -t tests whether a file descriptor is connected to a terminal.