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
grep PATTERN FILEsearches FILE for lines containing PATTERN and prints them.-q(quiet) prints nothing - it only sets the exit status: 0 if found, 1 if not./etc/passwdis the file listing every user account on the box.command -v jqasks "is there a program called jq?" and succeeds only if there is (more in lesson 1.9).>/dev/nullthrows its output away - see below.
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
ls /var/log | wc -l- list the directory, count the lines: how many entries.grep -c bash--cprints the count of matching lines instead of the lines.cut -d: -f1- split each line at:(the delimiter) and keep field 1: the usernames.sortputs lines in order;head -3keeps the first 3.
(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
- Check whether a command worked with
$?, and decode 127, 130, 137. - Chain commands so the second runs only if the first worked.
- Send normal output and errors to different places, or to the same file in the right order, and connect commands with pipes.