Writing simple shell scripts

Writing a script that runs: the shebang, exit status, arguments and variables, tests and conditionals, loops, and the two commands that turn a list into work.

Lesson 2 of 2 in objective 105. Shells and shell scripting, part of LPIC-1 Exam 102-500.

Numeric tests are worded and string tests are symbols — swapping them is the trap. Numbers — Equal: -eq; Not equal: -ne; Less / greater: -lt -le, -gt -ge; Emptiness: no operator. Strings — Equal: =; Not equal: !=; Less / greater: avoid: < and > redirect; Emptiness: -z empty, -n not Numbers Strings Equal -eq = Not equal -ne != Less / greater -lt -le, -gt -ge avoid: < and > redirect Emptiness no operator -z empty, -n not
Numeric tests are worded and string tests are symbols — swapping them is the trap.

Making a file a program

The first line is #!/bin/bash — the shebang — and it tells the kernel which interpreter to run the file with. Without it, the file is run by whatever happens to be interpreting, which is usually right until it is not. The file also needs execute permission (chmod +x), and it has to be found: a script in the current directory is run as ./script, because . is not in PATH.

Every command sets an exit status: 0 means success and anything else is failure, which is backwards from how truth is normally spelled and is the single thing to internalise here. $? holds the last status, exit N ends the script with a chosen one. && runs the next command only if the previous succeeded, || only if it failed.

That is what makes the one-line conditional in a cron entry work, and it is worth reading one against its neighbours. backup.sh || echo "backup failed" | mail -s "backup" root sends the mail only when backup.sh exited non-zero. With && instead, the mail goes out exactly when the backup SUCCEEDED, which is the inverted answer a question offers next to the right one. With a semicolon it goes out every night regardless. With a pipe it also goes out every night, because a pipe wires standard output to standard input and takes no interest in the exit status at all. Note also that the pipeline binds tighter than the list operators, so the echo and the mail together are the right-hand side of the ||.

Whether the next command runs, by the operator and the previous exit status. A grid of Operator against Exit status of the previous command. &&: 0 — it succeeded gives Next command runs; Anything else — it failed gives Next command skipped. ||: 0 — it succeeded gives Next command skipped; Anything else — it failed gives Next command runs. Exit status of the previous command → Operator ↓ 0 — it succeeded Anything else — it failed && Next command runs Next command skipped || Next command skipped Next command runs
Whether the next command runs, by the operator and the previous exit status.

Arguments, variables and tests

Positional parameters are $1 to $9 and beyond with ${10}; $0 is the script name, $# is how many arguments there were, $@ is all of them as separate words and $* as one. $$ is the script's own PID. Command substitution — $(command), or the older backticks — captures output into a variable. read NAME takes a line from standard input into a variable, which is how a script asks a question.

test EXPRESSION, spelled [ EXPRESSION ] with mandatory spaces inside the brackets, is how conditions are written. For files: -f is a regular file, -d a directory, -e exists at all, -r/-w/-x readable, writable, executable, -s non-empty. For strings: = and != compare, -z is empty and -n is non-empty. For numbers the operators are worded: -eq, -ne, -lt, -le, -gt, -ge, because < and > would be redirection. Quoting a variable in a test — [ "$x" = "y" ] — matters, since an unset variable would otherwise leave the bracket malformed.

How a script sees its own command line: $0 is the name and the arguments start at $1. ./report.sh /var/log weekly --mail — part 1, ./report.sh: $0 — the script name, and not counted in $#; part 2, /var/log: $1 — the first argument; part 3, weekly: $2, and past $9 the number needs braces: ${10}; part 4, --mail: $3, so $# is 3. $@ is three words, $* is one. 1 ./report.sh 2 /var/log 3 weekly 4 --mail 1 $0 — the script name, and not counted in $# 2 $1 — the first argument 3 $2, and past $9 the number needs braces: ${10} 4 $3, so $# is 3. $@ is three words, $* is one
How a script sees its own command line: $0 is the name and the arguments start at $1.

Branching and looping

if CONDITION; then … elif … else … fi. case VALUE in pattern) … ;; esac matches against glob patterns and is the readable choice when there are several options. Both close with their own name reversed, which is a habit rather than a rule to memorise.

for VAR in LIST; do … done iterates over words — often a glob, a command substitution, or seq output. while CONDITION; do … done repeats while a command succeeds, and until is the same with the sense reversed. break leaves a loop and continue starts the next iteration.

The pieces that connect scripts to the rest of the system: xargs turns lines into arguments, and a script called by cron gets a minimal environment — no PATH you set in ~/.bashrc, no aliases — so scripts intended for cron use full paths and set what they need.

exec is the other one, and it does something a loop or a conditional cannot. Given a command, it does NOT fork: the shell overlays itself with the new program, which keeps the same PID and the same open file descriptors and inherits whatever the script had set up. So a wrapper that ends exec /usr/bin/myapp "$@" leaves no idle shell hanging around as a parent, myapp is what a service manager tracks, and signals reach it directly — at the cost that anything written after that line never runs, because there is no shell left to return to. Without exec the shell would fork, wait for the child and carry on to the cleanup. Given no command at all, exec is a different instruction again: exec >> /var/log/run.log 2>&1 applies those redirections to the current shell and execution continues normally.

Worth carrying in

#!/bin/bash
The shebang: which interpreter runs this file.
$?
Exit status of the last command. 0 is success.
$1 $# $@ $0
First argument, argument count, all arguments, script name.
$(command)
Command substitution: capture output. Backticks are the older spelling.
[ -f file ]
Test a regular file exists. -d directory, -e any, -s non-empty.
-eq -lt -gt
Numeric comparison. = and != are for strings.
for x in list; do … done
Iterate over words. while/until loop on a condition.
case $x in a) … ;; esac
Match against glob patterns.
read NAME
Read one line of standard input into a variable.
cmd || fallback
Run the fallback only on failure. && is the other way round; ; and a pipe ignore the status.
exec command
Replace the shell with the command, same PID. Nothing after that line runs.

What the exam does with this

Objective
105. Shells and shell scripting
Share of the exam
15% (the whole objective)
Questions in this lesson
23
Signed for by a person
0

Partly checked. None of the 23 questions here has been read against the cited source by a person. 23 questions have been checked against their cited clause by an automated pass — which is not the same thing, and is not a signature.

Only questions a person has signed for are used in mock exams here. That is the whole difference between the two kinds of checking above.

How these questions are written — where each question comes from, what the verification ledger records, and what happens when one is found wrong.

Drill this lesson

A lesson is one sitting: the trainer draws a short run from these questions alone and spaces the ones you get wrong.

Practise Writing simple shell scripts

Questions in this lesson

Practise Writing simple shell scripts

The rest of objective 105