Linux & Bash · Part I · Module 11 · Exit Status and Command Chaining
Conditional Chaining with && and ||
Two operators turn the number from the last two chapters into control flow: run this only if that worked, or only if it did not. They are the shortest correct way to express a dependency — and there is exactly one way of combining them that nearly everybody gets wrong.
Foundation18 min readLinuxPart IFundamentalsExit StatusBash
Part I · Module 11 · Chapter 11.3 · Conditional Chaining with && and ||
Chapter 11.2 named the fix three times and did not teach it. Here it is.
1. Two Operators
$ true && echo " ran"
ran
$ false && echo " ran"&& runs the next command only if the previous one succeeded. The second line printed nothing.
$ false || echo " ran"
ran
$ true || echo " ran"|| runs the next command only if the previous one failed.
That is the whole mechanism. Both read the exit status from Chapter 11.1 — zero or not zero — and decide whether to continue.
cd build && make # make, but only if we got there
make || echo "build failed" >&2 # complain, but only if it broke2. Short-Circuiting
The second command is not run at all when the condition is not met. That matters more than it sounds.
$ false && rm -rf "$dir"rm never started. No process, no arguments evaluated, nothing. Compare with Chapter 11.2's semicolon, which would have run it.
This is what makes the cd guard actually safe:
$ cd /tmp/demo && cd buidl && ./fake_make.sh; echo " stopped, status $?"
bash: cd: buidl: No such file or directory
stopped, status 1Three commands, the second failed, the third never ran, and the chain's status is 1 — the failure, preserved.
3. The Four Outcomes
$ false && true; echo "false && true -> $?"
false && true -> 1
$ true && false; echo "true && false -> $?"
true && false -> 1
$ false || true; echo "false || true -> $?"
false || true -> 0
$ false || false; echo "false || false -> $?"
false || false -> 1| Chain | Second runs? | Status |
|---|---|---|
false && true | no | 1 — the first command's |
true && false | yes | 1 — the second command's |
false || true | yes | 0 — the second command's |
false || false | yes | 1 — the second command's |
The rule: the chain's status is that of the last command that actually ran.
Which gives && the property that makes it worth using in a build:
mkdir -p build && cd build && cmake .. && make -j8 && ctestThis line's exit status is zero only if every stage succeeded. With semicolons it would report ctest's status alone, telling you nothing about the four steps before it — Chapter 11.2 §7's point, now with the mechanism behind it.
4. The Trap
Here is the construct almost everyone writes, believing it is an if-then-else:
command && echo "ok" || echo "failed"It usually behaves correctly, which is what makes it dangerous.
$ true && echo " B ran" || echo " C ran"
B ranSo far so good. Now make the middle command fail while the first still succeeds:
$ true && false || echo " C ran even though A SUCCEEDED"
C ran even though A SUCCEEDEDAnd the realistic version, using Chapter 11.1's fact that grep returns 1 when it simply finds nothing:
$ true && grep zzz f1.txt || echo " C ran because grep found nothing"
C ran because grep found nothing5. Precedence: There Isn't Any
&& and || have equal precedence and associate left to right. There is no "and binds tighter than or" as there is in most languages.
$ false || echo A && echo B
A
BRead strictly left to right: false || echo A — the false failed, so echo A ran and succeeded. The chain so far has status 0, so && echo B runs too.
$ true || echo A && echo B
Btrue || echo A — succeeded, so echo A is skipped. The chain's status is still 0, so echo B runs.
echo A is skipped and echo B runs in both cases where the chain succeeded. If you expected A && B to be a unit, neither result matches.
6. The Idioms Worth Knowing
Four patterns that cover most real use.
Guard a step
cd "$build_dir" && make
command -v vcs >/dev/null && echo "simulator found"
[ -f "$config" ] && source "$config"Fail loudly
cd "$build_dir" || { echo "cannot enter $build_dir" >&2; exit 1; }
command -v vcs >/dev/null || { echo "vcs not on PATH" >&2; exit 3; }|| { …; exit N; } is the standard guard in a script. The braces are needed because two things happen — a message and an exit — and the semicolon before the closing brace is required (Chapter 11.4).
Say you do not care
grep ERROR run.log || true
rm -f /tmp/scratch.$$ || trueChapter 11.1 showed set -e stopping a script because grep found nothing. || true is the explicit "a non-zero here is expected and fine", and it is the right way to say it — far better than disabling set -e for the whole script.
Measured:
$ cat se.sh
#!/bin/bash
set -e
grep zzz f1.txt || true
echo "survived the grep that found nothing"
grep zzz f1.txt
echo "NOT REACHED"
$ ./se.sh; echo " script exited $?"
survived the grep that found nothing
script exited 1The guarded grep was survivable; the unguarded one ended the script.
Provide a fallback
command -v nvim >/dev/null && EDITOR=nvim || EDITOR=vi
mkdir -p "$d" || { echo "cannot create $d" >&2; exit 1; }The first is the a && b || c shape — and it is safe here, because a plain assignment cannot fail.
7. Chaining in Real Work
#!/bin/bash
set -uo pipefail
run_dir="${1:?usage: $0 <run-dir>}"
cd "$run_dir" || { echo "no such run directory: $run_dir" >&2; exit 2; }
command -v vcs >/dev/null || { echo "vcs not on PATH — source the tool setup" >&2; exit 3; }
# Build, elaborate and run, stopping at the first failure.
vcs -full64 -sverilog -f filelist.f -o simv \
&& ./simv +UVM_TESTNAME=smoke_test \
&& echo "simulation completed"
status=$?
# A clean run must say PASSED and must not say UVM_FATAL.
if [ "$status" -eq 0 ]; then
grep -q 'TEST PASSED' sim.log \
&& ! grep -q 'UVM_FATAL' sim.log \
&& echo "RESULT: pass" \
|| { echo "RESULT: fail — log checks did not hold" >&2; status=1; }
fi
exit "$status"Four things in there worth pointing at:
|| { …; exit N; }on both preconditions, each with its own documented code from Chapter 11.1.- The three-stage
&&chain meanssimvruns only on a successful compile, and the status afterwards reflects the whole thing. status=$?on the very next line, because Chapter 11.1 §2.- The log check uses
!to negate —! grep -q UVM_FATALsucceeds when the pattern is absent, which is the idiomatic way to assert that something is not in a file.
And note that the final chain is an a && b && c || d — which is the trap from §4. It is acceptable here only because the preceding commands are grep -q and echo, whose failure modes are understood and are exactly what the || is meant to catch. In anything less carefully reasoned it should be an if.
Practice
mkdir -p /tmp/cc && cd /tmp/cc
printf 'alpha\nbeta\n' > f1.txt1 — Watch both operators short-circuit.
true && echo " && after success"
false && echo " && after failure (should not print)"
false || echo " || after failure"
true || echo " || after success (should not print)"2 — Confirm the four statuses.
false && true; echo "false && true -> $?"
true && false; echo "true && false -> $?"
false || true; echo "false || true -> $?"
false || false; echo "false || false -> $?"3 — Spring the trap.
true && echo " B ran" || echo " C ran"
true && false || echo " C ran even though A succeeded"
true && grep zzz f1.txt || echo " C ran because grep found nothing"4 — Confirm equal precedence, left to right.
false || echo A && echo B
true || echo A && echo BPredict each before running it.
5 — Build the standard guard.
cd /nonexistent || { echo " guard fired"; }
( cd /nonexistent || { echo " guard fired and exited"; exit 1; }; echo " NOT REACHED" )6 — Make set -e survive a clean grep.
printf '#!/bin/bash\nset -e\ngrep zzz /tmp/cc/f1.txt || true\necho "reached"\ngrep zzz /tmp/cc/f1.txt\necho "NOT REACHED"\n' > se.sh
chmod +x se.sh; ./se.sh; echo " exited $?"7 — Negate a check.
grep -q alpha f1.txt && echo " alpha is present"
! grep -q zzz f1.txt && echo " zzz is absent"8 — Write the chain that reports everything.
mkdir -p out && cd out && touch a && ls a && echo " all four ok -> $?"
cd /tmp/cc
mkdir -p out && cd nowhere && touch a && ls a; echo " stopped -> $?"9 — Clean up. cd /tmp && rm -rf /tmp/cc
Check Your Understanding
1. What is the difference between cd build; make and cd build && make?
2. What is the exit status of true && false, and which command does it come from?
3. deploy.sh && echo "deployed" || echo "deploy failed" printed both messages. How?
4. false || echo A && echo B prints A and B. Walk through why.
5. Why is || true after a grep sometimes necessary?
6. When should a chain become an if?
8. Remember
&&runs the next command only if the previous one succeeded;||only if it failed.- Read them as "and then, if that worked" and "or else, if that failed" — they are control flow, not boolean operators.
- The second command does not run at all when the condition is not met.
- A chain's status is that of the last command that actually ran.
- An
&&chain reports zero only if every stage succeeded — which is what makes it right for a build, and what a semicolon chain cannot do. a && b || cis not an if-then-else. It is three commands, each looking only at the one before it.- It is safe only when
bcannot fail.cmd && echo ok || echo failedis fine; almost anything else is a latent bug. &&and||have equal precedence and run left to right. There is no "and binds tighter".- More than one operator means you should be grouping.
|| { echo "..." >&2; exit N; }is the standard guard in a script.|| truesays a non-zero status is expected — the right fix forgrepunderset -e.! cmdnegates a status, which is how you assert that a pattern is absent.- Keep chains to two or three commands. Beyond that you want
ifor a function.
The last two sections both reached for braces and parentheses without explaining them. The final chapter of the module is those: what each one groups, which of them starts a separate shell, and why that difference decides whether your directory change survives.
Where this fits
Part of the Linux & Bash curriculum.
