
Semicolons, `&&`, and `||`: The Command Chains We Type Ten Thousand Times a Day but Rarely Understand
Every morning, I receive pleas for help like "my shell script crashed." Eighty percent of the time, the issue isn't logic—it's this single line:
📋 实验室验证报告
Semicolons, `&&`, and `||`: The Command Chains We Type Ten Thousand Times a Day but Rarely Understand
Every morning, I receive pleas for help like "my shell script crashed." Eighty percent of the time, the issue isn't logic—it's this single line:
git pull && deploy.sh || echo "Deployment failed"
It seems perfectly logical: if pulling the code succeeds, deploy; if it fails, shout out an error. But if you actually test it, you’ll find two pitfalls—the "Deployment failed" message might appear even when deployment succeeds, and if the deployment script itself crashes, it might not trigger that branch at all. This is one of the most commonly used yet misunderstood mechanisms in the CLI, worth five minutes to clarify.
Core Rule: Check the Previous Command’s Exit Code First
The difference between these three connectors can be summarized in one sentence:
- **`;` (Semicolon)**: Runs the next command after the previous one finishes, **regardless of success or failure**. It is an "unconditional relay."
- **`&&` (AND)**: Executes the next command only if the previous one **succeeds** (exit code 0); otherwise, the entire chain stops.
- **`||` (OR)**: Executes the next command only if the previous one **fails**; if it succeeds, the next command is skipped.
Exit codes are the foundation of everything: **0 means success, non-zero means failure**. You can check the exit code of the last command anytime using `echo $?`, or use `#` in comments to note "why it was non-zero."
git pull
echo "Git exit code: $?" # 0 = success, 128+ usually indicates a fatal error
Scenario: Chaining Three Operations
Suppose you need to "backup database → run migrations → send notification," and **migration failures must terminate the process**, otherwise bad code will be deployed:
pg_dump mydb > backup.sql && migrate-v2.0 && notify.sh "Migration complete"
The benefit of `&&`: If `pg_dump` fails (disk full, permission errors), subsequent commands won’t run, containing the fault within the smallest possible radius. While `set -e` can abort the entire script on any failure, it is global and prone to false positives (e.g., `grep` returning no matches also yields a non-zero exit code). For fine-grained control, explicit `&&` is more comfortable.
When to Use What
- **Need "continue only on success": Use `&&`**. Builds, migrations, and deployments—failure at any step pollutes the next.
- **Need "don't stop on failure, just log it": Use `;`**.
```bash
npm run build; echo "Build finished (printed regardless of success)"
```
Or, more elegantly, handle the failure branch with `||`:
```bash
npm run build || { echo "Build failed" >&2; exit 1; }
```
- **Need "either/or": Use `||` as a fallback**.
- `command1 || command2`: If `command1` succeeds, skip; if it fails, run `command2`.
- However, use the `A && B || C` pattern seen at the beginning with caution. Intuitively, "success goes to B, failure goes to C" seems correct, but there is a third scenario: **if A succeeds but B fails, C will also run**. If B is `deploy` and C is a "deployment failure alert," this is fine. But if C has other side effects, you may encounter weird bugs where C executes even though deployment succeeded. The safe approach:
```bash
if git pull; then
deploy.sh
else
echo "Pull failed, aborting" >&2
exit 1
fi
```
The semantics are clear and unambiguous.
A Real Pitfall: `echo` Masks Failure
Many people write:
$TOOL_MIGRATE || echo "Secondary data migration error"
It looks like `||` only outputs the alert when `$TOOL_MIGRATE` fails. If you add `set -x` or debug logs, you’ll see that `$TOOL_MIGRATE` indeed failed and the alert printed—correct so far. The problem is that many teams’ alert channels don’t work on that specific machine (network isolation, log service restarts). The `echo` merely "looks like it caught the error," but in reality, it’s a **silent failure**. `||` does not guarantee your fallback succeeds; it only guarantees "don’t run on success, run on failure." Therefore, alerts should have dual channels, use `echo ... >&2`, and rely on cron/atop/logwatch for final fallback monitoring.
Combining Pipes with `;`
Pipes `|` pass **stdout**, and the exit code defaults to that of the **last** command:
cat data.json | jq '.items' > out.json && echo "OK"
If `jq` errors out (field doesn’t exist), `echo "OK"` will still print, even if `out.json` is empty or contains an error. To ensure "any failure in the chain causes the whole chain to fail," you need `pipefail`:
set -o pipefail
cat data.json | jq '.items' > out.json && echo "OK"
Now, if `jq` fails, the pipeline returns a non-zero exit code, and the command after `&&` stops. This is the most commonly overlooked line when handling data pipelines (log collection → transformation → storage).
When Not to Use Command Chains
- **Don’t write chains longer than 3–5 steps on a single line**. The longer the line, the worse the readability, and with multiple exit codes, it becomes hard to trace. For more than 3 steps, write a `.sh` file, define a function for each step, start functions with `set -euo pipefail`, and log a `[step]` message at the end of each step.
- **State, error handling, and retries for foreground chains**: Chained commands lack retry logic, timeouts, or segmented state tracking. If this is a production script, at least add `timeout` and manual structures like `for i in 1 2 3; do ... && break; done`.
- **Debugging chained commands in interactive shells**: `Ctrl-Z` suspends the entire chain, and resuming can cause disorder. Break them into single steps for debugging.
Common Misjudgments
1. **"Skip on failure" is `||`, not `;`**. `;` always executes the next command; it has no concept of "skipping."
2. **`set -e` is not a silver bullet**. It globally exempts conditional statements involving `&&`, `||`, and `if` (these are normal control flow, not errors), which often causes pitfalls in `make` recipes or Ansible inline tasks. Explicit `&&`/`||` always takes precedence.
3. **Pipeline exit codes default to the last command only**. The `&& jq ... && echo OK` example above won’t stop if `jq` fails unless you add `pipefail`.
4. **`$?` / `$status` retrieves the exit code of the *previous* command**. Note it is the *immediately preceding* command. After writing `echo`, `$?` becomes the exit code of `echo` (which is almost always 0). Capture exit codes immediately; don’t insert any commands in between.
Checklist: Review Before Writing Chains
- [ ] If this step fails, should the next step continue? Yes → `;`; No → `&&`
- [ ] Can the fallback branch of `||` itself fail? Yes → Encapsulate in a function with internal fallbacks
- [ ] Are there pipes? Did you add `pipefail`?
- [ ] Do critical error codes need separate handling (don’t rely solely on the binary non-zero/0 distinction)?
- [ ] More than 3 steps? Consider writing a script instead of adding more `&&`
- [ ] Running in an environment with `set -e` / `set -x`? Could it be silently ignored (due to conditional statement exemptions)?
Summary
Command chains are lightweight, but they have many silent paths. Treat `&&` as a "guard gate," `;` as a "mindless relay," and `||` as a "fallback channel." Use explicit `if/else` for binary choices and `pipefail` for pipelines, and you can elevate your scripts from "just running" to "immediately identifying where it broke when it crashes."
One final counter-intuitive but crucial point: **If a branch can be written as `if/else`, avoid the implicit semantics of `||`.** The next person maintaining your script—including yourself six months from now—will thank you.
⚙️ 安装与赋能
clawhub install skill-20260908-command-chains安装后在你的 Agent 配置中启用此技能,重启 Agent 即可生效。