Command Chaining

Quote

“This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.”

Doug McIlroy, Bell System Technical Journal (1978)

Command chaining is exit-status routing. Bash and PowerShell both let the previous command decide what happens next, but they expose that decision differently: Bash uses numeric exit codes everywhere, while PowerShell mixes $?, $LASTEXITCODE, and object-aware pipelines. The examples below use safe toy commands and captured output so the control flow is visible instead of implied.


flowchart TD
    A([Command A runs]) --> B{Exit code?}
    B -->|0 — success| C["&& → runs Command B"]
    B -->|non-zero — failure| D["|| → runs Command B"]
    B -->|ignored| E["; → always runs Command B"]
    A --> F["| → streams stdout to Command B's stdin<br>(runs concurrently)"]

    style A fill:#292e42,color:#c0caf5
    style B fill:#1a1b26,color:#c0caf5
    style C fill:#24283b,color:#c0caf5
    style D fill:#24283b,color:#c0caf5
    style E fill:#24283b,color:#c0caf5
    style F fill:#24283b,color:#c0caf5

Linux command chaining operators

Linux | && | fail-fast chaining

Bash uses && for dependent steps that must not continue after a failure. This is the operator that keeps later work from running on missing files, half-built artifacts, or bad input.

Stop after the first failed step

This chain prints two successful stages, fails deliberately at false, and never reaches deploy. The final printf captures the numeric exit code that the chain returned.

Run the commands in this section to stop after the first failed step.

printf 'build\n' && printf 'test\n' && false && printf 'deploy\n'
printf 'exit=%s\n' "$?"
build
test
exit=1

Linux | || | fallback branching

Use || when the fallback is short, local, and safe to run only after a failure. It is a good fit for default values, alternate data sources, and small recovery actions.

Run a fallback command after a failure

The left-hand false simulates a failed primary command. Because it fails, Bash runs the fallback printf, and the overall expression finishes successfully.

Run the commands in this section to run a fallback command after a failure.

false || printf 'fallback\n'
printf 'exit=%s\n' "$?"
fallback
exit=0

Linux | ; | unconditional sequencing

The semicolon is just a separator. Bash executes the next statement whether the previous one succeeded or failed, so it is dangerous in dependent workflows and useful only for independent work.

Show why a semicolon masks failures

The failed command does not stop execution. still-ran proves the second statement executed anyway, and the final exit code is 0 because the last command succeeded.

Run the commands in this section to show why a semicolon masks failures.

false; printf 'still-ran\n'
printf 'exit=%s\n' "$?"
still-ran
exit=0

Linux | | and pipefail | streaming pipelines

Bash pipelines connect text streams. They are powerful, but their exit semantics need extra care because the default pipeline status only reflects the last command.

Stream stdout into the next command

This pipeline sends two lines into grep, keeps only the line that begins with b, and counts the result. The pipeline succeeds because every stage completed successfully.

Run the commands in this section to stream stdout into the next command.

printf 'alpha\nbeta\n' | grep '^b' | wc -l
printf 'exit=%s\n' "$?"
1
exit=0

Enable and verify pipefail

set -o pipefail produces no output, so you have to verify the setting explicitly. The sed filter confirms that pipefail is on before you rely on it in a script.

Run the commands in this section to enable and verify pipefail.

set -o pipefail
set -o | sed -n '/pipefail/p'
pipefail       	on

Inspect ${PIPESTATUS[@]} after a failed pipeline stage

This pipeline prints no matches from grep, so the middle stage exits with 1 even though wc -l still prints 0. Capturing ${PIPESTATUS[@]} immediately shows which stage failed.

Run the commands in this section to inspect ${PIPESTATUS[@]} after a failed pipeline stage.

set -o pipefail
printf 'alpha\n' | grep z | wc -l
statuses=("${PIPESTATUS[@]}")
printf 'grep-exit=%s\n' "${statuses[1]}"
printf 'stages=%s\n' "${statuses[*]}"
0
grep-exit=1
stages=0 1 0

PowerShell command chaining operators

PowerShell | && | fail-fast chaining

PowerShell 7 added Bash-style chain operators. They are useful for native tools and short pipelines, but they do not exist in Windows PowerShell 5.1.

Stop after the first failed native command

This chain prints build and test, then calls cmd /c exit 1. Because that native command fails, deploy never runs, $? becomes $false, and $LASTEXITCODE records the native exit code.

Run the commands in this section to stop after the first failed native command.

Write-Output 'build' &&
Write-Output 'test' &&
cmd /c exit 1 &&
Write-Output 'deploy'
"success=$?"
"exit=$LASTEXITCODE"
build
test
success=False
exit=1

PowerShell | || | fallback branching

In PowerShell 7+, || runs the right-hand command only when the left-hand command failed. It is a direct analogue of Bash ||.

Run a fallback after a non-zero exit

The first native command exits with code 1, so the fallback echo runs. After the fallback succeeds, the overall chain reports success and $LASTEXITCODE reflects the last native command that ran.

Run the commands in this section to run a fallback after a non-zero exit.

cmd /c exit 1 || cmd /c echo fallback
"success=$?"
"exit=$LASTEXITCODE"
fallback
success=True
exit=0

PowerShell | ; | unconditional sequencing

The semicolon keeps going regardless of failure, just as it does in Bash. That makes it appropriate for independent diagnostics and unsafe for dependent steps.

Show why a semicolon keeps going

cmd /c exit 1 fails, but Write-Output still runs because the semicolon does not inspect the previous status. $LASTEXITCODE still remembers the native failure even though the last cmdlet succeeded.

Run the commands in this section to show why a semicolon keeps going.

cmd /c exit 1; Write-Output 'still-ran'
"success=$?"
"exit=$LASTEXITCODE"
still-ran
success=True
exit=1

PowerShell | | and *> | object pipeline and stream capture

PowerShell pipelines move structured objects between cmdlets. When you need to persist every output stream, *> captures them into one file for later inspection.

Pass objects through the pipeline

This pipeline starts with integers, filters them as integers, and formats the surviving values. The output proves that PowerShell is piping objects, not text columns.

Run the commands in this section to pass objects through the pipeline.

1..5 | Where-Object { $_ -gt 3 } | ForEach-Object { "item=$_" }
item=4
item=5

Capture every stream with *> and verify the file

*> itself is silent, so the proof comes from reading the file after the block runs. The resulting file contains standard output, a warning, and information output in one place.

Run the commands in this section to capture every stream with *> and verify the file.

$temp = Join-Path $env:TEMP 'chain-streams-demo.txt'
Remove-Item $temp -ErrorAction SilentlyContinue
& {
  Write-Output 'stdout'
  Write-Warning 'warning'
  Write-Information 'info' -InformationAction Continue
} *> $temp
Get-Content $temp
Remove-Item $temp
stdout
warning
info

These Bash patterns cover the scenarios that show up most often in scripts: safe defaults, dependent steps, cleanup, and long pipelines.

cmd && ok || fail is not an if/else

The || branch reacts to the exit status of the entire left-hand expression, not only to the primary command. If the ok branch fails, the fail branch still runs even after the primary command succeeded. Use if ...; then ... else ... fi for true branching.

Start script entrypoints with set -euo pipefail

This is the standard Bash safety header: -e stops on failures, -u catches unset variables, and pipefail exposes failed pipeline stages. The verification command confirms that all three settings are enabled.

Run the commands in this section to start script entrypoints with set -euo pipefail.

set -euo pipefail
set -o | sed -n '/errexit/p;/nounset/p;/pipefail/p'
errexit        	on
nounset        	on
pipefail       	on

Use && between causally dependent steps

When step 3 depends on step 2, and step 2 depends on step 1, chain them with &&. The missing deploy line proves that the failure stopped the chain before the unsafe step.

Run the commands in this section to use && between causally dependent steps.

printf 'build\n' && printf 'test\n' && false && printf 'deploy\n'
printf 'exit=%s\n' "$?"
build
test
exit=1

Use primary || fallback for short default paths

This pattern is appropriate when the fallback is simple and local. The failed file read falls through to a safe default value, and the chain exits successfully.

Run the commands in this section to use primary || fallback for short default paths.

db_host=$(cat /tmp/chain-missing 2>/dev/null) || db_host='localhost'
printf 'db_host=%s\n' "$db_host"
printf 'exit=%s\n' "$?"
db_host=localhost
exit=0

Use trap ... EXIT for cleanup that must always run

Cleanup belongs in trap, not in ||, because cleanup must run on every shell exit path. This script fails deliberately after creating a temp directory, and the trap still prints cleanup.

Run the commands in this section to use trap ... EXIT for cleanup that must always run.

rm -rf /tmp/chain-trap-demo
mkdir /tmp/chain-trap-demo
trap "rm -rf /tmp/chain-trap-demo; printf 'cleanup\n'" EXIT
set -e
touch /tmp/chain-trap-demo/demo
false
cleanup

Inspect ${PIPESTATUS[@]} after long pipelines

pipefail tells you that the pipeline failed; ${PIPESTATUS[@]} tells you which stage failed. That is the fastest way to isolate a bad stage in a long text-processing chain.

Run the commands in this section to inspect ${PIPESTATUS[@]} after long pipelines.

set -o pipefail
printf 'alpha\n' | grep z | wc -l
statuses=("${PIPESTATUS[@]}")
printf 'grep-exit=%s\n' "${statuses[1]}"
printf 'stages=%s\n' "${statuses[*]}"
0
grep-exit=1
stages=0 1 0

Keep pipefail in scripts, not in ad hoc interactive searches

With pipefail enabled, a normal grep miss becomes a failed pipeline. That is usually what you want in automation and usually not what you want when you are exploring interactively.

Run the commands in this section to keep pipefail in scripts, not in ad hoc interactive searches.

set -o pipefail
printf 'alpha\n' | grep z | wc -l
printf 'pipeline=%s\n' "$?"
0
pipeline=1

PowerShell recommendations depend on the version you are targeting. PowerShell 7 can use Bash-like chain operators; Windows PowerShell 5.1 needs explicit control flow.

$? and $LASTEXITCODE answer different questions

$? reports whether the last PowerShell pipeline succeeded. $LASTEXITCODE reports what the last native executable returned. Inspect both when a chain mixes cmdlets and native tools, because a successful cmdlet can leave an old native exit code in place.

Use && and || in PowerShell 7+ when you want Bash-like chaining

This is the shortest readable form for dependent steps and simple fallbacks in modern PowerShell. The fallback runs only after the deliberate failure, and the chain finishes successfully.

Run the commands in this section to use && and || in PowerShell 7+ when you want Bash-like chaining.

cmd /c exit 1 || cmd /c echo fallback
"success=$?"
"exit=$LASTEXITCODE"
fallback
success=True
exit=0

Use try/catch/finally with $LASTEXITCODE when you must support Windows PowerShell 5.1

Windows PowerShell 5.1 has no && or ||, so you have to inspect native exit codes yourself. This example throws when the native command fails, reports the reason in catch, and still runs cleanup in finally.

Run the commands in this section to use try/catch/finally with $LASTEXITCODE when you must support Windows PowerShell 5.1.

try {
  cmd /c exit 1
  if ($LASTEXITCODE -ne 0) { throw 'step failed' }
} catch {
  $_.Exception.Message
} finally {
  'cleanup-ran'
}
step failed
cleanup-ran

Command Chaining Troubleshooting

Linux | command chaining troubleshooting | semicolons and pipeline failures

These Bash failure modes are common because they look harmless in code review while changing runtime behavior in important ways.

A script keeps going after a failed step

If the line uses ;, Bash treats the next command as unconditional. The first line prints because the semicolon does not care about failure; the second line never prints because && does.

Run the commands in this section to a script keeps going after a failed step.

false; printf 'ran-with-semicolon\n'
false && printf 'ran-with-and\n'
ran-with-semicolon

A pipeline returns success but the result is empty

Without pipefail, the pipeline status comes from wc -l, not from grep. 0 lines were counted, but the pipeline still reports success because the last stage succeeded.

Run the commands in this section to a pipeline returns success but the result is empty.

printf 'alpha\n' | grep z | wc -l
printf 'exit=%s\n' "$?"
0
exit=0

cmd && ok || fail triggers the fail branch even though the primary command succeeded

The left-hand true succeeds, but the grouped ok branch returns failure because it ends with false. That failure is enough to trigger the || branch, which is why this idiom is unsafe for critical logic.

Run the commands in this section to cmd && ok || fail triggers the fail branch even though the primary command succeeded.

true && { printf 'primary-succeeded\n'; false; } || printf 'fallback-ran\n'
printf 'exit=%s\n' "$?"
primary-succeeded
fallback-ran
exit=0

PowerShell | command chaining troubleshooting | version and exit-code pitfalls

PowerShell adds a second axis of complexity: version support and the difference between cmdlet failures and native exit codes.

&& is a syntax error in Windows PowerShell 5.1

Windows PowerShell 5.1 never learned the chain operators, so the parser fails before execution starts. If you need 5.1 compatibility, replace this syntax with explicit if, try/catch, and $LASTEXITCODE checks.

Run the commands in this section to && is a syntax error in Windows PowerShell 5.1.

Write-Output 'ok' && Write-Output 'later'
At line:1 char:19
+ Write-Output 'ok' && Write-Output 'later'
+                   ~~
The token '&&' is not a valid statement separator in this version.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : InvalidEndOfLine

$? is $false even though the last native command exited 0

This happens when a cmdlet fails after a native executable succeeded. The output shows the cmdlet error first, then proves that $? tracks the cmdlet failure while $LASTEXITCODE still holds the native process result.

Run the commands in this section to $? is $false even though the last native command exited 0.

cmd /c exit 0
Write-Error 'cmdlet failure' -ErrorAction Continue
"success=$?"
"lastnative=$LASTEXITCODE"
cmdlet failure
success=False
lastnative=0

Command Chaining Cross-References

Command Chaining References