Every command reports success or failure
When any command finishes, it hands the shell an exit code: 0 means success, anything else (1-255) means some kind of failure. The special variable $? holds the exit code of the last command.
This is the shell's version of true/false, and it's what if actually checks. A useful pair to see it with: grep -q pattern file searches quietly (q = print nothing) and only reports through its exit code: 0 if found, 1 if not.
Reading exit codes from the special variable
true and false are tiny commands whose only job is to succeed and to fail. After each one, echo $? reveals the code it left behind. The same trick then shows how grep -q reports its findings without printing any text of its own.
true echo $? false echo $? echo "no dragons here" > report.txt grep -q dragons report.txt echo $? grep -q unicorns report.txt echo $?
Output
0 1 0 1
A 0 means success, and for grep -q it means specifically that the pattern was found. Note that dragons genuinely is in the file, since it appears inside the phrase no dragons here, so that search exits 0. The word unicorns is absent, so the final search exits 1.
if, then, else, fi
if [ -f notes.txt ]; then echo "notes.txt exists" else echo "notes.txt is missing" fi
Surprise: [ is itself a command (called test) that exits 0 or 1. That's why the spaces inside the brackets are mandatory. Its most useful checks:
| test | true when |
|---|---|
[ -f path ] | a file exists there |
[ -d path ] | a directory exists there |
[ "$a" = "$b" ] | strings are equal |
[ "$n" -ge 5 ] | number ≥ 5 (also -eq, -lt, -gt) |
Always quote variables inside [ ] ("$age", not $age), or empty values break the test.
Testing a file, then testing a number
The first if checks a file that was just created, and the second reads a number from stdin and compares it. The read -r age line takes one line of input, and this run supplies 21.
touch notes.txt if [ -f notes.txt ]; then echo "notes.txt exists" else echo "notes.txt is missing" fi read -r age if [ "$age" -ge 18 ]; then echo "access granted" else echo "too young" fi
Input
21Output
notes.txt exists access granted
Two different kinds of test appear here. -f asks whether a path exists as a regular file, while -ge means greater-than-or-equal and works on numbers only. The quotes around "$age" protect the test from breaking if the variable were ever empty.
if without if: && and ||
Because every command reports an exit code, bash can chain commands on success or failure without writing out a full if block:
| Chain | Meaning |
|---|---|
a && b | Run b only if a succeeded, exiting 0 |
a || b | Run b only if a failed, exiting non-zero |
So mkdir reports && cd reports enters the directory only if it was actually created, and grep -q error app.log || echo "clean" speaks up only when nothing was found. These two operators appear constantly in real scripts and one-liners, and they are the practical payoff of exit codes even for people who never type $?.
Do not confuse
||with the pipe|from unit 5. The pipe moves data between commands regardless of success, while&&and||decide whether the next command runs at all.
Three chains, three outcomes
The first chain succeeds and continues, the second fails and falls through to its || side, and the third combines both operators into a compact found-or-not report.
mkdir reports && echo "reports created" cd missing-dir 2> /dev/null || echo "could not enter missing-dir" grep -q pear fruit.txt 2> /dev/null && echo "found pear" || echo "no pear today"
Output
reports created could not enter missing-dir no pear today
Each line printed exactly one message, chosen by an exit code rather than by any explicit test. The 2> /dev/null parts use lesson 5-1's stderr redirection to hide the expected error messages, so only the script's own words appear. Creating a fruit.txt containing pear beforehand would flip the last line to found pear.
command1 && command2 runs command2 only if command1 succeeded. The shell checks the first command's exit code, and a 0 allows the second to run while anything else skips it.
|| is the mirror image, running its right-hand side only on failure. Neither should be confused with the pipe |, which sends output from one command into the next and ignores exit codes entirely. && and || pass along control, whereas | passes along data.
if [ -f notes.txt ] needs spaces after [ and before ] because [ is genuinely a command, and commands need whitespace separating them from their arguments.
[ is a real program, equivalent to test, whose final argument must be ]. Read that way, [ -f notes.txt ] is a command with three arguments, and as with every command since lesson 1-3, arguments are separated by spaces. Writing [-f instead makes the shell look for a command literally named [-f, which fails with command not found.
This script is a password gate. read -r word takes one line from stdin, a string comparison decides which branch runs, and each branch prints a different message. The test input is open sesame.
read -r word if [ "$word" = "open sesame" ]; then echo "the cave opens" else echo "nothing happens" fi
Input
open sesame
Output
the cave opens
What the syntax requires
- String comparison inside
[ ]uses a single=, unlike many programming languages that would use==. - Both sides are quoted, as in
[ "$word" = "open sesame" ], which keeps the two-word value as one argument and survives an empty variable. - The block structure is fixed:
if ...; then, the true branch,else, the false branch, andfito close it.