Standard Streams, Pipes, Redirection, and Exit Codes
The three data streams every command has by default, redirecting them to files, chaining commands with pipes, and the exit-code convention that lets one command react to another's success or failure.
What you'll learn
- Explain the difference between stdout and stderr, and why the distinction matters
- Chain commands with a pipe so one command's output becomes another's input
- Use a command's exit code to make a shell decision (&&, ||, or an explicit check)
Prerequisites
Explanation
Every real command below runs only in your own terminal — this lesson's exercises model streams and exit codes as data, never executing anything.
Every command has three standard streams by default: stdin (standard input, where it reads input from, usually the keyboard unless redirected), stdout (standard output, where normal results go), and stderr (standard error, a separate stream specifically for error/diagnostic messages). This separation is genuinely useful, not incidental: command > output.txt redirects only stdout into the file, so real error messages still appear in your terminal even while normal output is being captured elsewhere — exactly the behavior you want when a long-running command's actual results should go to a file, but you still need to see if something goes wrong while it runs. command 2> errors.txt redirects only stderr; command > out.txt 2>&1 redirects both into the same file (the 2>&1 specifically means "make stream 2 (stderr) go wherever stream 1 (stdout) is currently going," and the order relative to > out.txt genuinely matters — this must come after).
A pipe (|) connects one command's stdout directly to the next command's stdin, without ever touching a file in between: cat access.log | grep "ERROR" | wc -l reads the log, filters to lines containing "ERROR," and counts them — three small, focused commands composed into one pipeline, each doing one job well, which is the actual Unix philosophy this pattern embodies rather than an arbitrary syntax choice.
Every command, when it finishes, sets an exit code — a number from 0 to 255, where 0 conventionally means success and any non-zero value means some kind of failure (different non-zero values can mean different specific failure reasons, tool-dependent). $? holds the most recently finished command's exit code, readable immediately after. command1 && command2 runs command2 only if command1 exited 0; command1 || command2 runs command2 only if command1 exited non-zero. This convention is precisely what lets shell scripts (and CI pipelines built around them) make real, automated decisions based on whether a previous step actually succeeded — mvn test && echo "deploying" || echo "tests failed, aborting" is a genuine, common pattern built entirely on this one convention.
Example
Modeling stdout/stderr separation, a pipeline's data flow, and exit-code-based branching, as pure data -- no shell is invoked.
function runCommand(name, output, exitCode) {
return { name, stdout: exitCode === 0 ? output : "", stderr: exitCode !== 0 ? output : "", exitCode };
}
const result = runCommand("build", "build succeeded", 0);
console.log(result.stdout, "| exit:", result.exitCode); // "build succeeded" | exit: 0
function pipeline(commands, initialInput) {
// Models a shell pipeline: each command's stdout becomes the next command's stdin.
let data = initialInput;
for (const cmd of commands) {
data = cmd(data); // each function represents one stage's transformation of the data
}
return data;
}
const countErrorLines = (log) => log.split("\n").filter((line) => line.includes("ERROR")).length;
console.log(pipeline([countErrorLines], "INFO: ok\nERROR: disk full\nERROR: timeout")); // 2
function andThen(exitCode, ifSuccess, ifFailure) {
return exitCode === 0 ? ifSuccess() : ifFailure(); // models command1 && command2 || command3
}
console.log(andThen(0, () => "deploying", () => "tests failed, aborting")); // "deploying"Try it yourself
Call andThen with exitCode 1 (a failure) and confirm the ifFailure branch runs instead.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
This models exit-code branching only -- no shell command is executed. Write andThenOrElse(exitCode, ifSuccess, ifFailure) implementing command1 && ifSuccess() || ifFailure() semantics: call and return ifSuccess() when exitCode is 0, otherwise call and return ifFailure().
Checks: exit code 0 runs the success branch · a non-zero exit code runs the failure branch · any non-zero value (not just 1) counts as failure
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
This models a pipeline's data transformation only -- no real process or shell is involved. Write countMatchingLines(text, keyword) that splits text on newlines and returns how many lines contain keyword -- modeling exactly what `grep keyword file | wc -l` computes.
Checks: counts matching lines correctly · handles text with no matches · handles empty text
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Redirecting only stdout (>) when error messages also need to be captured, then being confused when errors still appear on screen -- stdout and stderr are genuinely separate streams; capturing one doesn't capture the other unless you explicitly combine them (2>&1).
- Writing `2>&1 > out.txt` instead of `> out.txt 2>&1` -- the order matters: '2>&1' must come AFTER the stdout redirection to correctly send stderr to the SAME place stdout was just redirected to.
- Assuming a non-zero exit code always means exactly the same thing across every tool -- 0 reliably means success everywhere, but different non-zero values can carry tool-specific meanings; treating 'non-zero' as simply 'failure' (without needing to know which specific number) is usually the safe, correct level of detail for a script's own branching logic.
Knowledge check
Takeaway
stdout and stderr are genuinely separate streams, redirectable independently; pipes connect one command's stdout directly to the next's stdin with no file involved; and exit codes (0 for success, non-zero for failure) are the precise, universal mechanism that lets a shell script or CI pipeline branch automatically based on whether a previous command actually succeeded.
Summary
stdin/stdout/stderr are a command's three default streams; > redirects stdout, 2> redirects stderr, 2>&1 combines them (order matters). Pipes (|) connect stdout directly to the next command's stdin. Exit codes (0 = success, non-zero = failure) drive && (run next on success) and || (run next on failure) — the foundation of automated shell/CI decision-making.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.