test, brackets and conditional branching
◈ 11 cardsWrite 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.
source POSIX.1-2024 (IEEE Std 1003.1-2024) XCU, test — the -a and -o primaries are marked obsolescent