OnCallReady

Lesson 1.7 · First Login · 27 min read

Driving the shell: exit codes, chaining, redirection

In plain words

Imagine every errand you send someone on comes back with two things: the shopping, and a thumbs up or thumbs down. The thumbs is the exit status: 0 means "done", anything else means "something went wrong", and the number says what. If a thief grabbed them on the way, the number is 128 plus the thief's number.

Commands also have two mouths: one for normal answers (stdout) and one for complaints (stderr). Both talk to your screen, so they sound like one voice. Redirection is choosing which mouth talks into which bucket: > file, 2> err, 2>&1. A pipe | carries only the normal mouth to the next person. && means "only if thumbs up", || means "only if thumbs down".

A nightly job "ran fine" - its log file has no errors in it - yet nothing got done. Two things fooled everyone: the errors were never written to the log, and the next step ran even though the first one failed. This lesson is how the shell reports success and failure, and where output actually goes.

What you need to know already: 1.5 Making things (>, >>, ;, wc -l).

Every command answers twice

A command prints output, and it also returns an exit status (also called exit code): one number, 0 to 255. 0 means success. Anything else means failure, and the number often says which kind. The shell keeps the last one in a special variable, $?, and echo $? prints it.

$ ls /etc/hostname
/etc/hostname
$ echo $?
0
$ ls /nope
ls: cannot access '/nope': No such file or directory
$ echo $?
2

Read $? immediately. It is overwritten by the very next command - including the echo you used to look at it:

$ ls /nope; echo "first: $?"; echo "second: $?"
ls: cannot access '/nope': No such file or directory
first: 2
second: 0

The numbers you will meet constantly:

0     success
1     generic failure ("it did not work")
2     misuse: bad option, missing argument. ls uses 2 for "cannot access"
126   found the file, cannot execute it (no x permission)
127   command not found
130   killed by Ctrl+C          (128 + 2, the signal SIGINT)
137   killed by SIGKILL         (128 + 9)
143   killed by SIGTERM         (128 + 15)

The last three are about signals: short numbered messages the kernel delivers to a running program - "stop now", "please shut down". Each has a number and a name: SIGINT (2) is what Ctrl+C sends, SIGTERM (15) is a polite "please exit", SIGKILL (9) is "die immediately, no argument". Chapter 3 is all about them. For now, one rule: an exit status above 128 means "died from signal N", where N = status - 128. 137 is the one you will see most: something force-killed the program - often the kernel itself, when memory runs out (chapter 5).

Chaining: ; && ||

cmd1 ; cmd2       run cmd2 no matter what
cmd1 && cmd2      run cmd2 only if cmd1 SUCCEEDED (exit 0)
cmd1 || cmd2      run cmd2 only if cmd1 FAILED (not 0)
$ mkdir -p ~/tmp/x && cd ~/tmp/x && pwd
/home/learner/tmp/x
$ cd /nope && rm -rf *
bash: cd: /nope: No such file or directory

That second line is the reason && exists. With ; instead, the cd fails, you stay where you are, and rm -rf * deletes everything in your current directory. The famous deleted-home-directory scripts are exactly this.

|| is the "otherwise" branch:

$ grep -q learner /etc/passwd && echo "user exists" || echo "no such user"
user exists
$ command -v jq >/dev/null || echo "jq is not installed"
jq is not installed

Two output streams, not one

Every running program starts with three numbered channels open, called file descriptors (fd):

0  stdin    where input comes from   (your keyboard, or a pipe)
1  stdout   normal output            (your screen)
2  stderr   errors and diagnostics   (also your screen)

stdout ("standard output") and stderr ("standard error") both land on the terminal, so they look like one stream. They are not, and redirection is where that becomes visible:

$ ls /etc/hostname /nope > out.txt
ls: cannot access '/nope': No such file or directory
$ cat out.txt
/etc/hostname

> captured stdout only. The error still came to the screen because it went to fd 2.

cmd > file           stdout to file (empty the file first)
cmd >> file          stdout appended to file
cmd 2> err.txt       stderr (fd 2) to file
cmd 2>/dev/null      throw stderr away
cmd > all.txt 2>&1   BOTH into all.txt
cmd &> all.txt       the same, bash shorthand
cmd < file           stdin read from file

/dev/null is a special file that swallows everything written to it. 2>/dev/null is how you silence "Permission denied" noise from commands that search the disk.

The order of 2>&1 matters

2>&1 means "make fd 2 point wherever fd 1 points right now". The shell handles redirections left to right, so:

$ ls /etc/hostname /nope > all.txt 2>&1      # stdout -> file, then stderr -> (file)
$ cat all.txt
ls: cannot access '/nope': No such file or directory
/etc/hostname

$ ls /etc/hostname /nope 2>&1 > all.txt      # stderr -> (screen), then stdout -> file
ls: cannot access '/nope': No such file or directory

The second form points fd 2 at the terminal before fd 1 is moved to the file, so errors still hit the screen. This exact bug shows up in scheduled jobs whose logs "capture everything" and quietly lose every error.

Pipes connect stdout to stdin

A pipe, |, feeds the stdout of the command on its left into the stdin of the command on its right. Small tools, chained:

$ ls /var/log | wc -l
7
$ grep -c bash /etc/passwd
2
$ cut -d: -f1 /etc/passwd | sort | head -3
appuser
backup
bin

(On the real VM _apt sorts first - the underscore comes before letters.)

| only carries stdout. Errors go around the pipe, straight to the screen:

$ ls /nope | wc -l
ls: cannot access '/nope': No such file or directory
0

To send errors through the pipe too: ls /nope 2>&1 | wc -l (prints 1).

And the exit status of a pipeline is the status of the last command. wc succeeded, so the whole thing "succeeded" even though ls failed. Chapter 6 (bash scripting) shows the setting that fixes this, set -o pipefail.

Line editing that saves hours

Tab          complete a command or path. Twice: show every candidate.
Up / Down    walk through history (the commands you typed before)
Ctrl+A / E   jump to start / end of line
Ctrl+U / K   delete to start / to end of line
Ctrl+W       delete the previous word
Ctrl+L       clear the screen (the line you are typing survives)
Ctrl+C       abandon this line, or interrupt the running command
Ctrl+D       end of input - on an empty prompt, logs you out
!!           the whole previous command     (sudo !!, lesson 1.11)
!$           the last argument of the previous command
$ mkdir -p ~/oncall-lab/labs/1a-linux/scratch
$ cd !$
cd ~/oncall-lab/labs/1a-linux/scratch

bash prints the expanded line before running it, so you always see what !! or !$ turned into. On the real VM, Ctrl+R also searches history backwards as you type - the single biggest speed-up of all (the simulator does not do Ctrl+R).

history lists what you typed, numbered; !42 re-runs entry 42.

What you can now do

Why it helps

This lesson is the vocabulary of every failure you will investigate. A program ended with 137: you know instantly it was force-killed with SIGKILL, not a bug that happened to return 137. 143 means it was asked politely to stop. 127 means a command was not found - a missing tool, not a failing program.

Redirection order explains scheduled jobs whose logs contain no errors, and "2>&1 in the wrong place" is a classic review catch. && versus ; is the difference between a script that stops at a failed cd and one that deletes the wrong directory. And a pipeline's exit status being the last command's is why something | tee log can "succeed" while something failed.

Commands in this lesson

ls echo mkdir cd grep command cat cut

FAQ

Why did echo $? print 0 when the command before it failed?

Because $? holds the status of the most recent command, and if anything ran in between (another echo, a test) it was overwritten. Read it immediately, or save it in a variable: cmd; rc=$?; echo "rc=$rc". For a pipeline, $? is the status of the last command in it, not the first.

What is the difference between 2>&1 and &>?

In bash, cmd &> file is shorthand for cmd > file 2>&1: both streams into the same file. &>> appends both. They are bash extras, though; the plainer shell sh reads &> differently, as "run in the background, then empty the file". If a script might be run by sh, write the long form > file 2>&1, which works everywhere.

Why do errors still show on screen when I pipe output to grep?

A pipe connects only stdout (fd 1) of the left command to the stdin of the right one. stderr (fd 2) still points at your terminal, so error messages go around grep and appear directly. To filter errors too, merge them first: cmd 2>&1 | grep x. To drop them, cmd 2>/dev/null | grep x.

Is a || b the same as "if a fails, run b"?

Yes. But a && b || c is not a true "if a then b else c". If a succeeds but b fails, c runs too, because || looks at the status of b, the command just before it. For one-liners where b is an echo that cannot fail, the pattern is fine. When b does real work, use a proper if, which chapter 6 covers.

Why is exit code 137 linked to running out of memory?

137 does not strictly mean memory; it means the process was killed by signal 9, SIGKILL, because the shell reports death-by-signal as 128 + the signal number. The kernel's out-of-memory killer, which stops a program when the machine runs out of memory, is the most common sender of SIGKILL, hence the link (chapter 5). But kill -9 by a person produces 137 too.

In an interview Junior

What do exit codes 0, 1, 126, 127, 130 and 137 mean?

Every command returns an exit status from 0 to 255; echo $? right after it shows it.

The rule behind the last two: above 128 means "died from signal N", N = status - 128 (143 = SIGTERM, 15). And read $? immediately - the next command, even the echo, overwrites it.

Also asked: What is the difference between cd /some/dir && rm -rf * and cd /some/dir; rm -rf *? · What is the difference between cmd > file 2>&1 and cmd 2>&1 > file? · What are stdin, stdout and stderr?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.