Command Channing and Exit Status
You’ve been checking exit status and branching on it way before than this. This chapter covers the operators that chain commands together based on that same exit status — ;, &&, || — and a genuinely common mistake that comes from treating an &&/|| chain as if it were a full if/then/else, when it isn’t quite one.
Recap: Exit Status
Every command produces an exit status: 0 for success, nonzero for failure, checkable via $?. Everything in this chapter is built directly on that single fact.
; — Sequential Execution
; just separates commands, running each one regardless of whether the previous one succeeded or failed — functionally identical to putting each command on its own line:
false; echo "This still runs"This still runsNo conditional logic at all here — ; is purely a separator, not a test of anything.
&& — Run Only If The Previous Command Succeeded
mkdir -p new_dir && cd new_dir
echo "Now in: $(pwd)"Now in: /home/you/new_dirIf mkdir had failed, cd new_dir would never have run at all — && short-circuits, skipping the right-hand command entirely the moment the left-hand one reports failure.
|| — Run Only If The Previous Command Failed
cd /nonexistent-directory || echo "cd failed, staying put"cd failed, staying putThe mirror image of &&: the right-hand command runs only when the left-hand one fails.
Combining && And ||
Chained together, command1 && command2 || command3 often reads like “if command1 succeeds, do command2, otherwise do command3” — and for the simple two-command case, that reading happens to be correct:
[[ -f "/etc/hosts" ]] && echo "File exists" || echo "File missing"File existsCommon Mistake: Assuming &&/|| Behaves Like if/else
The reading above quietly stops being accurate the moment command2 can also fail on its own:
true && false || echo "This runs — but why?"This runs — but why?true succeeded. The else-shaped intuition suggests the trailing echo shouldn’t run at all — and yet it does, because || doesn’t check whether the first command failed. It checks whether the command immediately before it failed, and that command is false, not true. Once a chain has more than one action after &&, the trailing || branch becomes a catch-all for any failure in the chain, not specifically a failure of the first condition — and that’s a meaningfully different thing than if/else actually provides.
Best Practices
- Use
&&/||for genuinely simple, single-action “run only if” cases.mkdir dir && cd dir,command || echo "failed"— these read clearly and behave exactly as expected. - Switch to a real
if/then/elsethe moment more than one action needs to happen on the success side of a chain, or the moment it matters which step failed rather than just that something did. - Don’t rely on
$?after a;-separated sequence to represent anything but the very last command — earlier failures in the sequence are invisible unless you check each one individually, the same lessonPIPESTATUStaught for pipelines before.
Shell-Safety Considerations
Here’s the &&/|| misconception from above, at real-world scale — a setup script where reporting the wrong outcome could genuinely mislead whoever runs it.
Naive (broken) version:
mkdir -p /tmp/shell-course-demo
cd /tmp/shell-course-demo
mkdir new_project && cp /this/path/does/not/exist.txt new_project/ && echo "Project created successfully" || echo "Failed to create project"
lsFailed to create project
new_projectThe message claims total failure — but ls proves new_project really was created. mkdir succeeded; it was the follow-up cp that failed, and because || only reacts to whatever failed most recently in the chain, the resulting message inaccurately implies nothing happened at all. Anyone reading just that message, without independently checking ls, would have no idea a partially-completed directory was left behind.
Corrected version — nested if/else, so each step’s outcome is reported accurately:
rm -rf new_project
if mkdir new_project; then
if cp /this/path/does/not/exist.txt new_project/; then
echo "Project created and populated successfully"
else
echo "Directory created, but failed to populate it"
fi
else
echo "Failed to create the project directory"
fi
lsDirectory created, but failed to populate it
new_projectThe message now matches reality exactly — the directory exists, and the script says so, rather than reporting a generic failure that obscures the partial state it actually left behind.
Hands-On: Chaining Commands And Fixing A Misleading Failure Message
1. Set up a working directory.
mkdir -p ~/shell-course/ch15
cd ~/shell-course/ch152. Try ;, &&, and || on their own.
false; echo "Ran after ; regardless"
mkdir -p new_dir && echo "mkdir succeeded, ran cd next" && cd new_dir
cd ~/shell-course/ch15
cd /nonexistent-directory || echo "cd failed, handled gracefully"3. Reproduce the misleading &&/|| chain, then fix it.
mkdir -p /tmp/shell-course-demo
cd /tmp/shell-course-demo
echo "--- misleading chain ---"
mkdir new_project && cp /this/path/does/not/exist.txt new_project/ && echo "Project created successfully" || echo "Failed to create project"
ls
rm -rf new_project
echo "--- accurate nested if ---"
if mkdir new_project; then
if cp /this/path/does/not/exist.txt new_project/; then
echo "Project created and populated successfully"
else
echo "Directory created, but failed to populate it"
fi
else
echo "Failed to create the project directory"
fi
ls4. Clean up.
cd ~
rm -rf ~/shell-course/ch15
rm -rf /tmp/shell-course-demoYou’ve now chained commands with ;, &&, and ||, and seen directly why a multi-step && chain with a trailing || can report a misleading, overly generic failure message — then fixed it with nested if/else that reports exactly what actually happened. The next chapter covers background jobs: running commands with &, tracking them with $!, and collecting their real exit status with wait.