Every command that finishes leaves behind a number: 0 if it succeeded, non-zero if it didn't. That number is the only thing the shell knows about whether the last thing you did worked, and it is the foundation every conditional in this lesson is built on.
Zero meaning success is backwards from most languages, and there is a reason: there is exactly one way to succeed, and many ways to fail. Non-zero values let a program say which failure.
This one is a recording — the commands are real, but nothing is running here. Type them into your own shell to follow along.
$? is overwritten by every command — including the echo that prints it. Capture it in a variable the moment you need to keep it:
some_command
STATUS=$?
Your own script sets its exit status with exit:
exit 0 # success
exit 1 # generic failure
A script with no exit returns the status of its last command, which is very often not what you meant.
A script ends with echo "done" after a cp that failed. What exit status does the script return?
test, better known as [test evaluates a condition and exits 0 if it's true. It is more commonly written as [, which is a genuine command — a link to test on most systems, and a builtin in every modern shell.
That [ is a command, not punctuation, explains its single most notorious rule: it needs spaces around it.
[ "$NAME" = "Ada" ] # correct
["$NAME" = "Ada"] # sh: [Ada: command not found
Without the space the shell reads ["$NAME" as one word — a command whose name happens to start with a bracket. And the closing ] is not decoration either: test requires it as its final argument when invoked as [, purely so the thing reads like a bracket.
Why must there be a space after [?
Files:
| Test | True when |
|---|---|
-e FILE | it exists at all |
-f FILE | it exists and is a regular file |
-d FILE | it exists and is a directory |
-r / -w / -x FILE | you can read / write / execute it |
-s FILE | it exists and is not empty |
Strings:
| Test | True when |
|---|---|
"$A" = "$B" | equal |
"$A" != "$B" | not equal |
-z "$A" | empty (zero length) |
-n "$A" | not empty |
Numbers — different operators entirely, because = on strings and = on numbers disagree about whether 10 equals 10.0, and about whether 9 is less than 10:
| Test | Meaning |
|---|---|
-eq / -ne | equal / not equal |
-lt / -le | less than / less or equal |
-gt / -ge | greater than / greater or equal |
Use = for strings and -eq for integers, and never mix them. [ "10" = "10.0" ] is false; [ 10 -eq 10 ] is true. And [ "9" \> "10" ] is true as a string comparison, because 9 sorts after 1.
::deep-dive{title="Why [ -z \"$VAR\" ] needs those quotes"}
Suppose VAR is empty and you write it unquoted:
[ -z $VAR ]
The shell substitutes the value — nothing — and word splitting removes the word entirely. test is called as [ -z ], which is a one-argument test asking "is the string -z non-empty?". That is true. So an empty variable makes the emptiness check report false.
With quotes, the shell passes an empty argument rather than no argument, and the test does what it says.
The same failure hits every comparison. If NAME is unset, [ $NAME = "Ada" ] becomes [ = "Ada" ] — a syntax error, from a line that looks fine. [ "$NAME" = "Ada" ] becomes [ "" = "Ada" ], which is simply false.
An older trick you will still meet in the wild prefixes both sides with a character: [ "x$NAME" = "xAda" ]. It solves the same problem for shells so old they mishandled a leading - in the value. Quoting is the modern answer.
::
ifif [ -f "$CONFIG" ]
then
echo "Found config"
elif [ -f "$CONFIG.example" ]
then
echo "Only the example is here"
else
echo "No config at all"
exit 1
fi
Written on one line, the then needs a separator — that is what the semicolon is doing in the form you see everywhere:
if [ -d /var/log ]; then echo "yes"; fi
And fi is if backwards. So is esac for case, in the next lesson but one. It is a Bourne-shell joke that stuck.
The condition is not special syntax. if runs a command and branches on its exit status. [ is simply the command people run most often there:
if grep -q "ERROR" app.log; then
echo "Errors present"
fi
No brackets, no $?. grep -q exits 0 when it finds a match, which is precisely the question being asked.
Write the condition that tests whether the directory /var/backups exists. Include the brackets.
&& and ||Two commands can be chained on the exit status of the first:
mkdir /tmp/build && cd /tmp/build # cd only if mkdir worked
cd /tmp/build || exit 1 # bail out if cd failed
&& runs the right side only if the left succeeded; || only if it failed. Both short-circuit — the right side isn't run at all otherwise.
The || exit 1 idiom is worth adopting immediately. Without it, a script whose cd fails carries on running the rest of its commands in whatever directory it happened to be in, which is how a cleanup script ends up deleting the wrong tree.
What does cd /opt/app || exit 1 protect against?
Next up: loops — doing the same thing to every file, every line, or every server in a list.
