Why write scripts at all
Every morning you type the same six commands to check the box. One day you want them to run at 3am from a systemd timer (Ch 2), with nobody watching. So you put them in a file and let bash run the file. That file is a script.
The problem this chapter solves: nobody is watching a script. If one command in it fails, bash by default just runs the next one - with a missing file, an empty value, the wrong directory. A good script stops at the first problem and says so.
What you need to know already: exit status and $?, && and ||, stdout vs stderr, > and pipes | (1.7 "Driving the shell"); chmod +x and the x bit (4.5); signals and exit 137 (3.6, 5.11).
The words you need first
- Script - a text file of commands, run top to bottom, as if you typed them.
- Interpreter - the program that reads the script and runs it. For us: bash.
- Variable - a name holding a value. Set it with
name=value(no spaces around=), read it with$name. Replacing$nameby its value is called expansion. - Unset - a variable nobody gave a value to. By default
$nobodyjust expands to nothing, silently.
greeting="hello"
echo "$greeting world" # prints: hello world
echo "[$typo]" # prints: [] - an unset variable is empty
Four lines, then the script
Every script in this chapter starts like this:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
One line at a time.
#!/usr/bin/env bash - the shebang (from "hash" # + "bang" !). It must be the very first line. When you run ./script.sh, the kernel reads it to learn which interpreter to start. /usr/bin/env bash means "find bash on the PATH" (the list of folders the shell searches for commands) instead of hard-coding /bin/bash.
Why bash and not sh? On Ubuntu /bin/sh is dash, a smaller shell that lacks many things this chapter uses (arrays, [[ ]], $'...'). A script that says #!/bin/sh but uses bash features works on one machine and breaks on the next.
set changes how bash itself behaves. Each letter is one option:
-e(errexit) - exit the script as soon as a command fails (non-zero exit status), instead of carrying on with bad state. It has four blind spots - the next lesson (6.3) is about them.-u(nounset) - using an unset variable is an error instead of an empty string. This catches typos:
rm -rf "$BUILD_DIR/" # the variable was set as BULID_DIR (typo).
# Without -u this expands to rm -rf / - everything.
-o pipefail- a pipeline is several commands joined with|. Normally its exit status is the last command's only, sofalse | true"succeeds". With pipefail, if any stage fails, the whole pipeline fails.
(false and true are tiny commands that do nothing except exit 1 and exit 0. They are handy for experiments.)
IFS=$'\n\t' - IFS is the internal field separator: the characters bash uses to cut an unquoted value into separate words. Normally space, tab and newline. Removing the space means a filename like old report.txt is not cut in two by accident. $'\n\t' is bash's way to write "newline, tab" (a string in $'...' understands backslash codes). It is a safety net - proper quoting (6.6) matters more.
Exit codes, as a script sees them
You met these in 1.7:
0 success
1-125 the program's own errors
126 found but not executable
127 command not found
128+N killed by signal N -> 130 = Ctrl+C (2), 137 = SIGKILL (9),
143 = SIGTERM (15)
A script ends with an exit code too: the one you give to exit N, or else the status of the last command it ran. $? holds the most recent status - read it immediately, because the next command overwrites it:
cp a.txt b.txt
status=$? # save it first
echo "cp said $status"
Why it matters: whatever runs your script only sees that number. A script that always exits 0 is invisible - a systemd service will not restart it, a timer will not log a failure, a person running it will think it worked. Exit non-zero when something is wrong.
What you can now do
- Start every script with a shebang and
set -euo pipefail, and say why each part is there. - Read an exit code and tell success, "not found" and "killed by a signal" apart.