skip to content

In bash, why is `cmd1 && cmd2 || cmd3` not a safe replacement for if/then/else, and in what case does cmd3 run even though cmd1 succeeded?

level: middleimportance: should knowfreq 46%

answer

  1. they are list operators, not branch syntax
  2. equal precedence, left to right
  3. the else arm watches the whole left side
  4. a failing middle command triggers both arms
  5. fine for guards, wrong for two branches

basics

~20 s

In bash, && and || have equal precedence and associate left to right, so cmd1 && cmd2 || cmd3 means (cmd1 && cmd2) || cmd3. If cmd1 succeeds but cmd2 returns non-zero, cmd3 still runs — an if/then/else can never do that.

solid answer

~50 s

`&&` and `||` are short-circuit list operators, not branch syntax. Each one tests the status of everything to its left, and because they share the same precedence and associate left to right, `cmd1 && cmd2 || cmd3` parses as `(cmd1 && cmd2) || cmd3`. So `cmd3` runs whenever that whole left-hand list ends non-zero — which includes the case where `cmd1` succeeded and `cmd2` failed. The "else" arm therefore fires on a success path, which real if/then/else cannot do. It bites in practice whenever the middle command can legitimately return non-zero: a write to a full disk, a `grep` that finds nothing, a command whose non-zero status just means "no match". Use the chain only for the two-command forms — `cmd || exit 1`, `mkdir -p "$d" || die` — and write a real `if` the moment there is an else arm.

code

bash · 8 lines
bash
#!/usr/bin/env bash
if true && false; then
  echo "then branch"
else
  echo "else branch only"
fi

true && false || echo "chain: else arm ran on a success path"

go deeper

for a junior

Know that cmd1 && cmd2 runs the second only on success and cmd1 || cmd2 only on failure, and use the guard form cmd || exit 1 rather than trying to build an else arm.

for a middle

Explain the parse: equal precedence, left-associative, so a && b || c is (a && b) || c, and show the case where a successful first command still reaches the third.

for a senior

Show the review judgment — flag any three-part chain where the middle command can legitimately return non-zero, and rewrite it as an if before someone adds a second statement to an arm.

for a principal

Make it a style rule with a linting story: chains for single-arm guards, if/then/else the moment there are two outcomes. The cost of the terse form is a class of bugs that only appears on the rare failure path.

## What the operators actually mean `&&` and `||` join commands into an **AND-OR list**. The rule is purely about exit status: - `a && b` — run `a`; run `b` only if `a` returned `0`. - `a || b` — run `a`; run `b` only if `a` returned non-zero. Both short-circuit: the right side is skipped when the left side already decides the outcome. The list as a whole reports the status of the last command that actually ran. Crucially, **they have equal precedence and associate left to right**. There is no rule making `&&` bind tighter, the way `and` binds tighter than `or` in most languages. So: ```bash a && b || c # parses as (a && b) || c a || b && c # parses as (a || b) && c ``` The second one surprises people badly: if `a` succeeds, `b` is skipped, the list so far is successful, and `c` runs anyway. ## The failure mode The idiom `cond && then_part || else_part` works *only* when `then_part` can never return non-zero. The moment it can, you get a run where both arms execute: ```bash true && false || echo "this still prints" ``` `true` succeeds, `false` fails, the whole `&&` list is non-zero, and the `||` arm runs — even though the "condition" was true. Real-world versions of this are not contrived: ```bash backup_ok && echo "backup fine" >> "$LOG" || send_alert ``` If the log filesystem is full or read-only, the append fails and an alert fires for a backup that actually worked. Anything whose non-zero status means "nothing to report" rather than "broken" — a `grep` with no match, a `diff` finding differences — has the same effect when it sits in the middle position. The asymmetry to state in an interview: with `if/then/else`, taking the then branch guarantees the else branch cannot run. With `&& ||`, the else arm depends on the status of the then arm, so both can run in a single pass. ## Where the chain is genuinely good The two-command forms are idiomatic, readable, and used everywhere: ```bash command -v jq >/dev/null || { echo "jq required" >&2; exit 1; } mkdir -p "$dest" || exit 1 [ -f "$config" ] && source "$config" cd "$dir" && ./run.sh ``` Each of these is a guard or a sequencing constraint with no else arm at all, so the ambiguity never arises. `cmd || exit 1` and `cmd || die "message"` are the standard shell error-handling shape. ## Rewrites when you do need two arms Write the conditional: ```bash if backup_ok; then echo "backup fine" >> "$LOG" else send_alert fi ``` It is one line longer and unambiguous, and it survives someone adding a second statement to a branch later — which is exactly when the chained form quietly breaks. If you insist on a chain for a short line, make the middle arm incapable of failing, or group it so the intent is visible: ```bash cond && { do_thing; true; } || fallback ``` That `true` is a real fix — `true` and `:` are builtins that do nothing and return `0`, and `:` is the classic null command for exactly this purpose. But a reader now has to work out why it is there, which is an argument for the `if`. ## Grouping and precedence in practice Braces group commands in the current shell and are the normal way to attach several commands to one arm; note the required semicolon or newline before the closing brace, and that the braces need surrounding spaces because they are keywords rather than punctuation. Parentheses would also group, but they run the commands in a separate copy of the shell, which changes variable-assignment behaviour — a distinction that belongs with subshells. Negation composes here too: `! a && b` negates only the first pipeline, so it runs `b` when `a` failed. If you mean to negate the whole list, restructure it rather than guessing at precedence. ## What to say in an interview "They are short-circuit list operators with equal precedence, left-associative, so `a && b || c` is `(a && b) || c`. `c` runs whenever the left list ends non-zero, including when `a` succeeded and `b` failed. Fine for guards like `cmd || exit 1`; use a real `if` as soon as there is an else."

  • How does `a || b && c` group, and why does that surprise people?
    It groups as `(a || b) && c`, because the operators share one precedence level and associate left to right. If `a` succeeds, `b` is skipped, the list so far is successful, and `c` runs — so the command people intended as "fallback then continue" runs `c` on both paths. Anyone expecting `&&` to bind tighter, as in most languages, reads it backwards.
  • When is the chained form the right choice rather than an if?
    When there is no else arm. Guards like `command -v jq >/dev/null || die "jq required"`, `mkdir -p "$d" || exit 1`, and sequencing like `cd "$dir" && ./run.sh` are idiomatic and read well on one line. The rule of thumb: one arm, use the chain; two arms, write the `if`.
  • Why do people sometimes write `cond && { work; true; } || fallback`?
    The `true` forces the then arm's status to 0 so the `||` arm cannot fire on a success path. `true` and `:` are builtins that do nothing and return zero, and this is a legitimate use of them. It works, but it needs a comment to explain itself, so most reviewers would rather see a plain if/then/else.

saying these in an interview costs you the question

  • Calls && and || a ternary or an if/else equivalent
  • Assumes && binds tighter than || as in C
  • Thinks the || arm can only run when the first command failed
  • Believes short-circuiting means the rest of the line is skipped entirely
  • Adds more commands to a chained arm without noticing the status changes

context