Linux & Bash · Part I · Module 11 · Exit Status and Command Chaining
Running Commands in Sequence
Putting two commands on one line is the first thing everybody learns after single commands, and the separator they learn is the wrong one for most of what they use it for. It runs the second command whatever happened to the first — which is fine until the first one was the part that mattered.
Foundation16 min readLinuxPart IFundamentalsExit StatusBash
Part I · Module 11 · Chapter 11.2 · Running Commands in Sequence
Chapter 11.1 established that every command returns a number saying whether it worked. This chapter is about the separator that ignores it completely.
1. The Semicolon
$ echo one; echo two; echo three
one
two
threeA semicolon separates commands. The shell runs them strictly in order, waiting for each to finish before starting the next.
And a newline does exactly the same job:
$ false
$ echo "after a newline -> $?"
after a newline -> 1A script is therefore just a list of commands separated by newlines, and ; is what you use when you want several on one line. There is no other difference — this is why you can take any three lines of a script, join them with semicolons, and get identical behaviour.
A trailing semicolon is legal and redundant:
$ echo one;
oneThe one place it is required is before a closing brace, which is Chapter 11.4.
2. The Status of a Sequence
$ false; true; echo "false; true -> $?"
false; true -> 0
$ true; false; echo "true; false -> $?"
true; false -> 1The status of a sequence is the status of the last command in it. Everything earlier is discarded.
That is reasonable on its own terms — the shell has one $? and something has to go in it. But combined with the next section it is the source of a great deal of trouble.
3. The Problem
$ cd /nonexistent 2>/dev/null
$ echo " (cd failed, but this still ran, in $(pwd))"
(cd failed, but this still ran, in /tmp/m11v)The cd failed. The next command ran anyway, in the directory you were already in.
Now the version that costs people real time:
$ cd build; makeRead it the way the shell does: change directory, then run make, regardless. If build does not exist — a typo, a clean checkout, a different machine — the cd fails, prints an error that scrolls past, and make runs in whatever directory you happened to be standing in.
Measured, with a stand-in for make:
$ cd /tmp/demo
$ cd build; ./fake_make.sh
MAKE RAN in /tmp/demo/build
$ cd /tmp/demo
$ cd buidl; ./fake_make.sh
bash: cd: buidl: No such file or directory
MAKE RAN in /tmp/demoSame two commands, one typo, and the build ran somewhere it should never have run. Depending on what that build does, the results range from "nothing happened" to object files scattered through your source tree to a clean target deleting the wrong directory.
4. When a Semicolon Is Right
It is not a defective operator. It is the right one whenever the commands are genuinely independent — where the second makes just as much sense whether or not the first worked.
date; uptime; df -h /scratch # three unrelated readings
echo "=== logs ==="; ls -lt *.log # a label and then a listing
cd /tmp; pwd # a cd that cannot meaningfully failIt is also the natural separator for several commands sent somewhere as one line:
ssh buildhost 'cd /work && make' # && inside, because they are dependent
ssh buildhost 'date; uptime' # ; because they are notAnd in a for loop written on one line, where the semicolons are part of the syntax rather than a choice:
for f in *.log; do echo "$f"; done(Chapter 17.1 covers loops; the semicolons before do and done are required there.)
5. The Other Separators
; is one of four things that can sit between two commands, and it is worth seeing them together because they are easy to confuse by eye.
| Separator | Runs the next one | Waits? |
|---|---|---|
; | Always | Yes |
| newline | Always | Yes |
&& | Only if the previous succeeded | Yes |
|| | Only if the previous failed | Yes |
& | Always | No |
The last one you already know from Chapter 7.5: & both separates commands and backgrounds the one before it.
$ sleep 5 & echo "this printed immediately"
[1] 4821
this printed immediatelyThat is a genuine difference in kind, and it causes a specific confusion: & and && look alike and behave nothing alike.
make & test.sh # start make in the BACKGROUND, then immediately run test.sh
make && test.sh # run make, wait, and run test.sh only if it succeededA single missing & turns "build then test" into "build and test simultaneously, testing whatever binary was there before". Worth a second look whenever you type either.
6. Sequences in Scripts
In a script the separator is usually a newline, so the question becomes invisible — and the problem does not go away:
#!/bin/bash
cd /work/project/build
make clean
make all
cp sim.out /results/Four lines, no semicolons, and exactly the hazard from §3. If the cd fails, the other three run in whatever directory the script was started from — make clean in particular is capable of deleting things in a directory that is not the build directory.
Three ways to fix it, in increasing order of care:
cd /work/project/build || exit 1 # stop if the cd failedset -e # stop on ANY failure
cd /work/project/buildcd /work/project/build && make clean && make all && cp sim.out /results/The first is explicit and local. The second is a blanket policy with caveats that Chapter 20.2 covers. The third is Chapter 11.3 and is the subject of the next chapter.
7. Reading Somebody Else's One-Liner
Long chained lines are common in build instructions and continuous-integration files, and reading them is a matter of finding the separators first:
mkdir -p build; cd build; cmake ..; make -j8; ctestFive commands, four semicolons, and not one of them checks anything. If cmake fails, make runs on a configuration that does not exist, ctest runs on binaries that were not built, and the line's overall status — the status of ctest — is the only one anybody sees.
The same line, written to stop at the first problem:
mkdir -p build && cd build && cmake .. && make -j8 && ctestNow the status of the whole line is meaningful: zero means every stage succeeded.
That is the real argument for &&, and it is not about typos. A chain of semicolons reports the status of its last command, so it tells you whether the final step worked and nothing more. A chain of && reports whether all of it worked, which is the thing a build system actually needs to know.
Practice
mkdir -p /tmp/seq/build && cd /tmp/seq
printf '#!/bin/bash\necho " MAKE RAN in $(pwd)"\n' > build/fake_make.sh
chmod +x build/fake_make.sh
cp build/fake_make.sh ./fake_make.sh1 — Confirm a sequence takes its last status.
false; true; echo "false; true -> $?"
true; false; echo "true; false -> $?"2 — Confirm a newline is the same separator.
false
echo "-> $?"3 — Reproduce the hazard.
cd /tmp/seq; cd build; ./fake_make.sh
cd /tmp/seq; cd buidl; ./fake_make.shThe second runs in the wrong directory. Read the error that scrolled past.
4 — See the fix work, before reading 11.3.
cd /tmp/seq; cd buidl && ./fake_make.sh; echo " nothing ran, status $?"5 — Tell & from &&.
sleep 2 & echo " with & : printed immediately"
sleep 2 && echo " with && : printed after two seconds"6 — Count the unchecked steps. Take this line and say what happens if the third command fails:
mkdir -p out; cd out; false; echo "still here in $(pwd)"7 — Guard a cd.
( cd /nonexistent || { echo " guard caught it"; exit 1; }; echo " NOT REACHED" )8 — Apply the "regardless" test to five lines from your own shell history. How many should have been &&?
9 — Clean up. cd /tmp && rm -rf /tmp/seq
Check Your Understanding
1. What is the exit status of false; false; true?
2. cd reports; ./summarise.sh and the script runs but produces nothing useful. What is the likely cause?
3. What is the difference between a semicolon and a newline between two commands?
4. When is a semicolon the right separator?
5. make & test.sh and make && test.sh — what does each do?
6. A five-command line joined by semicolons ends with status 0. What does that tell you about the first four?
8. Remember
- A semicolon separates commands and runs them in order, waiting for each.
- A newline is the same separator. A script is a list of commands separated by newlines.
- A sequence's exit status is its last command's. Everything earlier is discarded.
- A semicolon ignores whether the previous command worked, which is the whole problem.
cd build; makeruns make in the wrong directory the momentcdfails. Measured, not hypothetical.- A semicolon means "then", not "and then, if it worked".
- Say the line with "regardless" in it. If that sounds careless, the commands are dependent and you want
&&. - It is the right separator for independent commands —
date; uptime— and required by loop syntax. &is also a separator and does not wait.make & test.shandmake && test.share one character and a world apart.- In a script the separator is a newline and the hazard is invisible. Guard every
cdwith|| exit. - A chain of semicolons reports only its final step. A chain of
&&reports whether all of it worked — which is what a build system needs.
The fix has now been named three times without being taught. The next chapter is the two operators that read the number from Chapter 11.1 and act on it — along with the one way of combining them that looks like an if-then-else and is not.
Where this fits
Part of the Linux & Bash curriculum.
