Bash from the ground up

Exit codes and joining commands

Every command answers with a number, $? holds the last one, and && and || let you decide what runs next without writing an if at all.

Part 3 of 12

Beginner15 min readBack to Courses

Every command answers with a number

When a command finishes it hands back a number, called its exit code. Zero means it worked; anything else means it did not, and the number itself says what went wrong. echo "done $?" prints that number, because $? always holds the exit code of whatever ran just before. The number is not the output: a command can print nothing and still succeed.

The name "exit code" sounds like something only a program has, but it belongs to every command, including the small ones. grep answering "was this text in the file?" does not print a yes or a no: it exits with 0 for yes and 1 for no, and it stays quiet either way. The question and the output are separate answers.

Two little words that join commands

&& runs the next command only if the one before it succeeded. || runs the next one only if it failed. Together they read like a sentence: do this, and if that worked, do the other; or if it did not, do this instead. The decision is made by the number, not by the words printed.

This is the everyday way a script makes a decision about a single command — check for a file and then act on it. Later you will meet the fuller if form, but most one-line checks in real scripts look exactly like the ones below. The joined form is for one check and one action.

Reading an answer, and acting on it

grep -q "pear" fruit.txt    # -q: quiet, just the answer
echo "grep said $?"        # 0 when it found the word

grep -q "plum" fruit.txt
echo "grep said $?"        # 1 when it did not

# The same two checks, as commands that join.
grep -q "pear" fruit.txt && echo "it is there"
grep -q "plum" fruit.txt || echo "it is not there"

# true and false are commands that always answer.
true && echo "this prints"
false || echo "so does this"

The number comes from somewhere

Every command ends by handing its parent a number between 0 and 255. The shell keeps the last one in $?, and a script can set it deliberately with exit 4. A function uses return 4 instead, which answers its caller without ending the script. Zero is the one success value; the rest are left for kinds of failure.

That number is the only thing an if ever looks at. A line such as if grep -q pear fruit.txt; then is not special syntax for grep: the if runs a command and takes zero as yes. Once you see that, every command becomes a question you can ask. The command after if can be any program, function or built-in.

A script that never calls exit finishes with the status of its last command, so the final line quietly becomes the script's answer. That is convenient in a one-liner and worth setting deliberately in anything longer. Set the final status deliberately when the script is part of a larger job.

Pipelines, negation and chains

In a pipeline the status belongs to the last command unless pipefail is switched on. grep missing file | wc -l succeeds because wc succeeded, even though grep found nothing — a fact that hides real failures in longer scripts. That is why a pipeline can look successful while an earlier command failed.

! reverses an answer, so if ! grep -q pear fruit.txt reads as "if the word is absent". && and || chain left to right, and each one reads the status of the whole list before it rather than only the command beside it. Separate if blocks are how you say precisely which grouping you meant. The grouping is decided by the operators, not by the line breaks.

A status is not an error message

A command can print an error and still exit with zero, or print nothing and exit with a failure. The status is the machine-readable answer, and the text is for the person reading the screen. A script that checks the text is guessing; a script that checks the status is asking the question the command was designed to answer.

The number is also small. It fits in a byte, so a program that wants to report more detail writes a message and picks a status from the small set the shell understands. Reading $? tells you success or failure; reading the message tells you what to do about it.

Asking commands how they went

Run it and follow the numbers. Then change the search word to something that is in the file, and add a line that prints a message only when the word is missing.

Output
Click "Run" to execute your code

Read $? straight after the command you are asking about. The next command overwrites it, so a line that prints something in between has already replaced the answer.

Three mistakes worth knowing before they bite: print $? immediately, because the next command replaces it; do not read the answer of a pipeline from its first command, since the last one decides; and use return inside a function, because exit there ends the entire script.

Try these next

  • Run true, print $?, then run false and print it again.
  • Feed a value into grep -q and turn the answer into words with && and ||.
  • Negate a test with ! and confirm the exit code flips between 0 and 1.

A check that answers with a code

Run it and follow the answers. Then change the search word to one that is present and watch every branch reverse.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does an exit code of 0 mean?