Skip to content

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 runs

No 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_dir

If 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 put

The 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 exists

Common 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/else the 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 lesson PIPESTATUS taught 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"

ls
Failed to create project
new_project

The 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

ls
Directory created, but failed to populate it
new_project

The 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/ch15

2. 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
ls

4. Clean up.

cd ~
rm -rf ~/shell-course/ch15
rm -rf /tmp/shell-course-demo

You’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.

Last updated on