Skip to content
academia.sh

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 ifelif 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; if looks at the exit code. [ ] is a second name for the test command, and the spaces around it are mandatory.
  • String equality is tested with =, number equality with -eq; 007 and 7 are 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.
  • case runs 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.

Start typing to search.

↑↓ Esc navigate · open · close