Memra

test, brackets and conditional branching

◈ 11 cards

Write a test expression over files, strings and integers; branch on it with if, elif and case; keep every expansion quoted; and tell the string operators from the integer ones.

if runs a command; test is that command

if does not evaluate a boolean expression. It runs a command and looks at the exit status that comes back: zero takes the then branch, anything else does not. Once you have that, everything else in this lesson follows.

test is an ordinary command whose only job is to evaluate a condition and exit 0 or 1. [ is the same program under a second name, with one extra requirement: its final argument must be ]. These two lines are identical in every respect, and the exam paper uses both:

if test -f report.txt
if [ -f report.txt ]

Everything painful about brackets follows from that. A command receives words, and words are separated by whitespace, so every operator and every operand needs whitespace on both sides. Leave a space out and the shell does not tell you about a missing space — it tells you about a missing program:

$ x=John
$ ["$x" = y ]
[John: command not found
$ x=4
$ [$x -gt 3 ]
[4: command not found

Both messages name a program the shell genuinely went looking for, because the bracket and the value were glued into one word.

Growing a file inspector

Version 1 — independent one-way tests. Every if runs, and every one that succeeds prints:

if [ -e "$f" ]
then
    echo "$f exists"
fi
if [ -d "$f" ]
then
    echo "$f is a directory"
fi

Correct, but it asks questions it already knows the answer to, and it can produce output that reads like a contradiction.

Version 2 — two-way. When the alternatives are mutually exclusive, say so with else, and switch from test to the bracket form, which is easier to scan:

if [ -d "$f" ]
then
    echo "$f is a directory"
else
    echo "$f is not a directory"
fi

Version 3 — multiway with elif. The tests are tried top to bottom, the first success wins, and else catches everything left over:

if [ ! -e "$f" ]
then
    echo "$f: no such file"
elif [ -d "$f" ]
then
    echo "$f: directory"
elif [ -s "$f" ]
then
    echo "$f: non-empty ordinary file"
else
    echo "$f: exists but is empty, or is something special"
fi

The order is not arbitrary. The ! -e test has to come first, because every operator after it quietly assumes the name exists.

Notice that the variable is quoted in every single test. That is not house style, it is the difference between a script that works and one that fails on the first awkward filename. With the quotes off and f empty, the [ command receives one argument too few and reports [: =: unary operator expected — a message that says nothing at all about the real problem. The runnable exercise below models exactly what the shell hands to [ in each case.

The operator table

File tests take one pathname:

  • -e — exists, of any kind
  • -f — exists and is a regular file
  • -d — exists and is a directory
  • -r — you can read it
  • -w — you can write it
  • -x — you can execute it (for a directory, search it)
  • -s — exists and has a size greater than zero

-r, -w and -x ask about your access rather than about the mode bits in the abstract: they take ownership, group membership and the effective user ID into account, which is why the superuser passes nearly all of them regardless of what ls -l shows.

String tests take strings: -z s is true when the string is empty, -n s when it is not, and s1 = s2 and s1 != s2 compare two of them.

Integer comparisons take numbers: -eq, -ne, -lt, -le, -gt, -ge.

The trap the paper sets is = against -eq. The equals sign compares two strings, character by character. -eq converts both sides to integers and compares numbers. So [ 010 = 10 ] is false — three characters against two — while [ 010 -eq 10 ] is true, both being the number ten. The mistake is worse in the other direction: [ "$n" -eq 5 ] when n holds abc is not false, it is a run-time error, [: abc: integer expected, with exit status 2. A string operator on numbers gives a wrong answer; an integer operator on text gives an error.

Combining. ! negates a test. The book's -a (and), -o (or) and escaped parentheses for grouping all still work, and you must be able to read them, but they are marked obsolescent because a test expression with four or more arguments cannot always be parsed unambiguously. The modern spelling is clearer anyway: put each condition in its own [ ] command and join them with the shell's own && and ||.

if [ -f "$f" ] && [ -r "$f" ]

Multiway on a value: case

Where an elif chain asks a series of yes/no questions, case matches one string against a list of patterns:

case "$ans" in
    l|L)  ls -l ;;
    q|Q)  exit 0 ;;
    *)    echo "unrecognised: $ans" ;;
esac

The patterns are shell patterns — the same *, ? and bracket classes filename globbing uses — not regular expressions. A vertical bar separates alternatives within one branch, which is how l|L accepts either case of the letter. The *) branch is the catch-all, and leaving it out means an invalid answer produces silence, which users read as "it worked". Every branch ends with ;;, and that is not optional — Lesson 6 shows what the shell does when one is missing.

ClassOperatorsOperandsWhere it goeswrongfile-e -f -d -r -w -x-sone pathname-e accepts adirectory; -f doesnotstring-z -n = !=one or two stringstext compare: 010is not equal to 10integer-eq -ne -lt -le -gt-getwo integersnon-numeric operandis an ERROR, notfalsecombine! -a -o and escapedparenswhole expressionsprefer two [ ]commands joined by&& or ||Every operator and operand must be its own word.
The middle two rows are the examinable confusion: the equals sign is a string operator and -eq is an integer operator, and swapping them fails in two different ways.
falsefalsefalseif [ ! -e f ]true: report "no such file", doneelif [ -d f ]true: report "directory", doneelif [ -s f ]true: report "non-empty file", doneelsenothing matched: empty or specialOnly one branch ever runs.
An elif chain is read strictly top to bottom and the first success wins, which is why the existence test has to be the first one.

source POSIX.1-2024 (IEEE Std 1003.1-2024) XCU, test — the -a and -o primaries are marked obsolescent

NORMAL ~/memra/learn/comp-325/test-and-conditional-branching utf-8 LF