Lesson 09 / 24
Conditions and Tests
A condition being an exit code, file/string/number tests, the parsing difference between single and double brackets, pattern matching, and multi-way selection.
Contents
The script assumes that the log given to it exists and is readable. Testing this assumption requires a condition. In the Programming Fundamentals course, a condition was a logical expression, and it reduced to a true or false value.
There is no such type in the shell. A condition is a command that gets
executed; its result is the exit code. The if construct does not
evaluate an expression, it runs a command and executes the body if its
code is 0. This single difference explains all the syntax oddities of
shell conditions.
if Takes a Command
if command; then ... elif other_command; then ... else ... fi
Any command can be a condition:
if grep -q '404' access.log; then echo "the log has a 404" fi
The [ ... ] form used for conditions is not a syntax element, it is a
second name for the command called test. This is why a space between [
and the first argument is mandatory, and the closing ] is this
command’s last argument. Writing [-f file] looks for a command named
[-f and does not find it.
File Tests
test defines a family of operators related to the file system.
| Operator | True when |
|---|---|
-e |
the path exists (regardless of type) |
-f |
regular file |
-d |
directory |
-r, -w, -x |
read / write / execute permission exists |
-s |
the file exists and its size is greater than zero |
-L |
symbolic link |
a -nt b |
a was modified more recently than b |
[ -f access.log ]; echo "regular file -> $?" [ -d access.log ]; echo "directory -> $?" [ -x access.log ]; echo "executable -> $?"
regular file -> 0 directory -> 1 executable -> 1
Permission tests do not check the file’s permission bits, they check whether the calling process can perform that operation on that file; the result can come out different for the superuser. The permission model established in the Introduction to Linux course turns into behavior here.
The distinction between -e and -f must not be neglected: a directory
also passes the -e test, but it cannot be read with cut.
String and Number Tests
String tests: -z is it empty, -n is it non-empty, = are they equal,
!= are they different.
Number tests use separate operators: -eq, -ne, -lt, -le, -gt,
-ge.
The two families need to be separate, because in the shell every value is a character string, and “equality” carries two different meanings:
[ "007" = "7" ]; echo "string equality -> $?" [ "007" -eq "7" ]; echo "number equality -> $?"
string equality -> 1 number equality -> 0
007 and 7 are different strings but the same number. It is the one
writing the code who decides which meaning is wanted; the choice of
operator is the declaration of that decision.
Number operators work only with integers. If a decimal value is given,
test reports an error. If a decimal comparison is needed, a calculation
tool is used instead; this is taken up in the Arithmetic and Command
Substitution lesson.
The Single Bracket’s Parsing Problem
Because [ is an ordinary command, its arguments are first expanded by
the shell and split into words. The result of splitting can change the
argument count test expects.
empty="" [ $empty = "x" ]
bash: [: =: unary operator expected
After expansion, only the arguments =, x, ] reached the [
command; test took this as an expression with a single operand. The
same problem shows up in the opposite direction with a value containing
a space:
d="two words" [ $d = "two words" ]
bash: [: too many arguments
Both errors are fixed with quoting:
[ "$empty" = "x" ]; echo "code=$?"
code=1
In older scripts, a pattern of the form [ "x$a" = "xb" ] is seen. Its
purpose is to prevent the variable’s value from being confused with an
operator; since quoting is enough, this pattern is unnecessary now, but
it should be recognized for what it does when encountered.
Double Brackets
Bash and ksh define a separate syntax element called [[ ... ]]. This is
not a command; the shell recognizes it at the parsing stage and does
not apply word splitting or pathname expansion to the expansions
inside it.
empty=""; d="two words" [[ $empty = "x" ]]; echo "empty -> $?" [[ $d = "two words" ]]; echo "spaced -> $?"
empty -> 1 spaced -> 0
The unquoted forms did not error. This means quotes are not required
inside [[ ]] — but writing them does no harm either, and can be
preferred so as not to break the habit.
Double brackets have three additional capabilities.
Pattern matching. If the right side of the = or == operator is
unquoted, it is interpreted as a pathname pattern:
path="/api/data" [[ $path == /api/* ]] && echo "under api" [[ $path == "/api/*" ]] || echo "quoted pattern is literal"
under api quoted pattern is literal
Quoting the right side turns the pattern into an ordinary string. This is the control needed in situations where pattern matching is not wanted.
Regular expression matching. The =~ operator treats the right side
as an extended regular expression:
code="404" [[ $code =~ ^[45][0-9][0-9]$ ]] && echo "error-class code"
error-class code
Regular expression syntax will be taken up in detail in the Text Processing topic. What matters here is that the pattern is written unquoted: a quoted right side looks for a literal match.
Logical operators. && and || combine conditions inside the
brackets:
[[ -f access.log && -r access.log ]] && echo "readable file"
readable file
Inside [ ], the -a and -o operators exist for the same job, but
they are not recommended because they produce parsing ambiguity. In
portable code, two separate tests are chained with &&:
[ -f access.log ] && [ -r access.log ] && echo "readable file"
[[ ]] is a bash extension; it does not exist in the POSIX shell
language. It cannot be used in a script declaring #!/bin/sh.
Decision rule: if bash is declared, [[ ]] is preferred — it is immune
to quoting mistakes; if POSIX is required, [ ] and careful quoting are
used.
Multi-Way Selection
Instead of if–elif chains that test the same value against many
possibilities, case is written. The patterns use pathname pattern
syntax.
for code in 200 301 404 500 xyz; do case "$code" in 2??) class="successful" ;; 3??) class="redirect" ;; 4??) class="client error" ;; 5??) class="server error" ;; *) class="unknown" ;; esac printf '%-4s -> %s\n' "$code" "$class" done
200 -> successful 301 -> redirect 404 -> client error 500 -> server error xyz -> unknown
case runs the first matching branch and stops; there is no fall-through
between branches. Multiple options are added to a pattern with |:
200|201|204). The *) written as the last branch catches values that
match no pattern, and it should be present in every case construct —
if it is missing, an unrecognized value is silently ignored.
case is POSIX and provides pattern matching without requiring [[ ]];
it is the correct tool for pattern testing in portable scripts.
Applying It to the Script
report.sh is extended to validate its inputs. Giving separate codes to
error classes continues the contract from the previous lesson.
log_file="${1:-access.log}" if ! [[ "$count" =~ ^[0-9]+$ ]] || [ "$count" -lt 1 ]; then echo "error: -n expects a positive integer: $count" >&2 exit 2 fi if [ ! -e "$log_file" ]; then echo "error: log does not exist: $log_file" >&2 exit 3 elif [ ! -f "$log_file" ]; then echo "error: not a regular file: $log_file" >&2 exit 3 elif [ ! -r "$log_file" ]; then echo "error: no read permission: $log_file" >&2 exit 3 elif [ ! -s "$log_file" ]; then echo "warning: log is empty: $log_file" >&2 fi
The reason the regular-expression test runs before the
[ "$count" -lt 1 ] test in the number check is order: if count were
not a number, -lt would error out. Because || short-circuits, the
second test is reached only when the first one passes.
Four calls follow four paths:
./report.sh missing.log; echo "code=$?" ./report.sh -n abc; echo "code=$?" ./report.sh /etc; echo "code=$?"
error: log does not exist: missing.log code=3 error: -n expects a positive integer: abc code=2 error: not a regular file: /etc code=3
Summary
- In the shell, a condition is a command;
iflooks at the exit code.[ ]is a second name for thetestcommand, and the spaces around it are mandatory. - String equality is tested with
=, number equality with-eq;007and7are different as strings, the same as numbers. - Because expansions inside
[ ]undergo word splitting, quoting is mandatory; an empty or space-containing value otherwise produces a parsing error. [[ ]]is recognized at the parsing level, applies no splitting, and offers pattern and regular expression matching along with logical operators; it is a bash extension.caseruns the first matching branch, there is no fall-through, and the*)branch should always be written.
Next Step
The report currently produces a single list. When a separate count is wanted for each status-code class, the same pipeline has to be duplicated by hand. The next lesson takes up iteration: loops that run over a list, counted loops, and the correct form of reading a file line by line — which is anything but obvious in the shell.
To keep your progress and take notes, Log in
My notes
Log in to take notes.