Module 4 — Streams, Redirection, and Pipes

Updated 3 September 2026

Module 4 — Streams, Redirection, and Pipes. Logs, /dev/null, 2>&1, and pipelines — how data actually flows between commands, and how to send it where you want.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Modules 1–3: scripts, quoted variables, and exit codes (this module keeps using verdicts, and finally explains where error messages live). Tools: nothing new. Keep ~/bash-course — the users.txt file from Module 3 gets reused.

Part A — Output is two separate channels

A1. stdout and stderr: the two mouths of every command

Every running command is born holding three open channels, numbered like phone extensions: 0 — standard input (stdin), where it reads from; 1 — standard output (stdout), where it sends its results; 2 — standard error (stderr), where it sends complaints and commentary. In a terminal, 1 and 2 both happen to point at your screen — which is why you have never had to tell them apart. But they are two genuinely separate channels, and every command you have run in this track has been using both: ls hello.sh /nonexistent sends hello.sh out of mouth 1 and the cannot access complaint out of mouth 2, interleaved on the same screen.

Why two mouths? So that results stay clean even when complaints happen. A command's stdout may be feeding a file or another program (Part C); its grumbling must not contaminate the data. The entire Unix toolset is built on this discipline: results → 1, diagnostics → 2.

Real-world analogy — the invoice and the phone call. A supplier fulfilling your order sends two kinds of communication: the paperwork (invoice, delivery note — the result, filed neatly), and the phone call ("your usual brand was out of stock" — the commentary). You would never want the phone call transcribed into the middle of the invoice; accounting software reading the invoice would choke on it. Separate channels exist so machines can read the paperwork while humans hear the phone calls.

Where the analogy stops working. On your terminal screen the two channels arrive visually identical — nothing marks a line as stdout or stderr, and their interleaving order isn't even guaranteed (the channels are buffered differently). You cannot tell them apart by looking; you tell them apart by redirecting one and seeing what moves. That experiment is Exercise 4.1, and it is the fastest stderr diagnosis there is.

🧪 Exercise 4.1 — split the invisible

The ls fails on purpose — half of it.

bash
cd ~/bash-course
ls hello.sh /nonexistent              # both channels hit the screen, mixed
ls hello.sh /nonexistent > /tmp/out.txt   # > captures channel 1 into a file
cat /tmp/out.txt
Expected result — click to reveal (contains a deliberate failure)
plain text
ls: cannot access '/nonexistent': No such file or directory
hello.sh
ls: cannot access '/nonexistent': No such file or directory
hello.sh

What to read out of it: run 1 shows both lines on screen — indistinguishable. Run 2 is the revelation: the complaint still hit your screen while the file received only hello.sh. The > moved channel 1 and only channel 1; stderr never heard about it. That split is the diagnostic trick: whatever still prints after > file is stderr. It is also the production bug in miniature — "I redirected the output to the log, but errors are still spraying onto the console" — which Part A3 fixes properly.

A2. > and >>: overwrite vs append

command > file sends stdout into a file — and if the file exists, its old contents are destroyed first, before the command even runs. command >> file appends to the end instead, creating the file if needed. That single-character difference is the difference between a log and an amnesiac: a nightly job writing > report.log keeps only the last night forever.

Counter-intuitive: the truncation happens before the command runs — bash prepares the redirection first, then starts the command (it must: the command needs its channels already connected when it is born). Two consequences worth engraving. First, wc -l data.txt > data.txt destroys data.txt before wc reads a single line — the file is emptied by the redirect, then counted: 0. Never redirect a command's output onto its own input file. Second, > file with no command at all is a legal line that just empties a file — occasionally useful, frequently an accident.
Real-world analogy — the whiteboard and the logbook. > writes on the meeting-room whiteboard: whoever writes next erases you first — the board only ever shows the latest meeting. >> writes in the ship's logbook: every entry goes below the previous one, and the history is the point.

Where the analogy stops working. A human erases a whiteboard when they need the space. Bash erases at setup time, every time — even if the command then produces no output at all, the old contents are already gone. An erased-but-never-rewritten whiteboard (an empty log after a failed job) is a state the analogy never predicts, and it is a real thing you will meet in production.

🧪 Exercise 4.2 — the amnesiac and the historian

The second command destroys data on purpose.

bash
cd ~/bash-course
echo "first" > notes.log
echo "second" > notes.log     # ← overwrites: "first" is gone forever
cat notes.log
echo "third" >> notes.log     # appends
cat notes.log
Expected result — click to reveal (contains deliberate data loss)
plain text
second
second
third

What to read out of it: the first cat prints only secondfirst was truncated away before the second echo ran; no error, no warning, no recycle bin. The second cat shows append doing what logs need: second still there, third below it. Rule of thumb for scripts: > for reports (each run replaces the last), >> for logs (history accumulates) — and say which you meant in a comment, because the next maintainer cannot tell your intent from one character.

A3. Redirecting the complaints: 2>, 2>&1, and the order trap

Channel numbers unlock the rest. > is really shorthand for 1> — so 2> is the same tool aimed at the other mouth: command 2> errors.log captures the complaints and leaves results on screen. To send both channels to one file — the everyday need for cron jobs and CI logs — the idiom is:

bash
command > everything.log 2>&1

Read it left to right, as bash does: "point 1 at everything.log; now point 2 at wherever 1 currently points." 2>&1 means "make 2 a copy of 1" — a copy of where 1 points right now, not a live link to wherever 1 may point later. That's why the order matters, and why the reversed spelling is a famous bug: command 2>&1 > everything.log copies 2 from 1 while 1 still points at the screen (so errors go to the screen), and only then moves 1 to the file. Bash also offers the shorthand &> everything.log — both channels, one token — fine interactively; the portable, greppable > file 2>&1 remains the convention in scripts.

Real-world analogy — mail forwarding. 2>&1 is filing a forwarding order at the post office: "deliver my mail wherever Alex's mail goes today." If Alex moves next week, your mail keeps going to the old address — the order copied a destination, it did not subscribe you to Alex. Filing your order before Alex moves (the 2>&1 > file mistake) forwards you to a place Alex is about to leave.

Where the analogy stops working. Real forwarding orders take effect over days; redirections are wired up in strict left-to-right sequence in a microsecond, before the command starts. There is no window in which "both moved" — the sequence is the whole semantics, which is why a one-token swap changes the meaning entirely.

🧪 Exercise 4.3 — both channels, and the trap sprung

Both ls runs half-fail on purpose; the second line demonstrates the bug.

bash
cd ~/bash-course
ls hello.sh /nonexistent > both.log 2>&1     # correct order
cat both.log
ls hello.sh /nonexistent 2>&1 > only-out.log # ← the trap: reversed order
cat only-out.log
Expected result — click to reveal (contains a deliberate failure)
plain text
ls: cannot access '/nonexistent': No such file or directory
hello.sh
ls: cannot access '/nonexistent': No such file or directory
hello.sh

What to read out of it — the four lines come from different places and that is the entire lesson. Lines 1–2: cat both.log shows both channels captured (error first here; ordering within the file can vary — buffering). Line 3: springing the trap, the error printed straight to your screen at run time. Line 4: cat only-out.log contains only hello.sh. Same two tokens, opposite outcomes, purely from order. ShellCheck flags the reversed form as SC2069 — one more reason Module 14 puts that linter in your CI.

Interview questions — Part A

🎯 "How to redirect both standard output and standard error to the same location?" — asked verbatim at Edureka; Hirist asks "How do you redirect both stdout and stderr to a file?"; Zero To Mastery asks "How do you redirect output in Bash?"

The direct answer: command > file 2>&1 — stdout to the file first, then stderr copied to stdout's destination. Bash shorthand: command &> file. Append variants: >> file 2>&1 and &>>.

Going deeper: explain the left-to-right mechanics and why the reversed order fails2>&1 copies where 1 points at that moment; reversed, stderr is copied to the terminal before stdout moves to the file. Interviewers love this follow-up because it separates memorized idiom from understood mechanism.

The details that separate candidates: knowing &> is bash-only (a script with #!/bin/sh may hit a shell where it means something else entirely — Module 1's sh-vs-bash trap resurfacing); mentioning ShellCheck SC2069 catches the reversed form mechanically; and the one-liner mental model — "file descriptors are values, 2>&1 is assignment, not aliasing."

🎯 "What is the difference between > and >>?" — a staple in every screening round; rarely published with exact wording, but Zero To Mastery's redirection answer covers both operators

The direct answer: > truncates the target to empty and writes fresh; >> appends to the end, preserving contents. Both create the file if absent.

Going deeper: the truncation happens at redirection-setup time, before the command runs — which yields the classic self-destruction sort data.txt > data.txt (empty result) and the empty-log-after-crash phenomenon. Logs and anything history-shaped get >>; regenerated reports get >.

The details that separate candidates: explaining the setup-time truncation mechanism rather than just the behavior difference; and one operational note — long-running services should write logs via append mode so log rotation (moving the file aside) behaves predictably.

Part B — Silence, script voices, and feeding input

B1. /dev/null: the place where output goes to disappear

Official docs: null(4) — man7.org

/dev/null is a special file provided by the kernel that discards everything written to it and is always empty when read — a wastebasket that is always empty because it incinerates on contact. Redirect a channel there and that channel goes silent: command 2>/dev/null hides complaints; command >/dev/null hides results (keeping only complaints); command >/dev/null 2>&1 hides everything — the classic wrapper for commands run only for their exit code, which after Module 3 you know is a whole category. Note what survives total silencing: the verdict. $? still tells the truth; /dev/null eats words, not numbers.

Trap: 2>/dev/null is a chainsaw sold as a comb. It silences the complaint you are sick of seeing — and every future complaint you have not met yet: permission errors, missing files, disk full. Debugging a script whose author sprinkled 2>/dev/null everywhere means removing them one by one to let the machine speak again. Production rule: silence a specific expected error at a specific spot, comment why, and never as a reflex. If you find yourself silencing stderr to make a script "clean," the script is trying to tell you something.
🧪 Exercise 4.4 — three levels of silence

Every command here half-fails on purpose; watch what each silencing keeps.

bash
cd ~/bash-course
ls hello.sh /nonexistent 2>/dev/null    # complaints gone, results stay
ls hello.sh /nonexistent >/dev/null     # results gone, complaints stay
ls hello.sh /nonexistent >/dev/null 2>&1  # total silence...
echo "$?"                                 # ...except the verdict
Expected result — click to reveal (contains deliberate failures)
plain text
hello.sh
ls: cannot access '/nonexistent': No such file or directory
2

What to read out of it: three runs, three silhouettes. Run 1 kept the result channel only; run 2 the complaint channel only; run 3 printed nothing at all — yet the final line shows the exit code 2 arrived intact. That last pairing — total silence plus a live verdict — is the shape of every "check quietly, act on the code" idiom: Module 3's grep -q did the silencing internally; >/dev/null 2>&1 is how you impose it on commands that lack a quiet flag.

B2. Your script's own two mouths: >&2

Your scripts are commands (Module 3 C2), so they too own channels 1 and 2 — and everything they echo currently leaves through mouth 1, complaints included. That breaks the Unix discipline: when someone runs ./yourscript.sh > results.txt, your error messages get filed into the results. The fix is one token: echo "error: config missing" >&2 — "send this echo's output out through channel 2." Now results stay results, and whoever runs your script can split the channels exactly as they did with ls.

🧪 Exercise 4.5 — a script that grumbles properly
bash
cd ~/bash-course
nano twovoices.sh     # four lines:
bash
#!/bin/bash
echo "report: all systems nominal"
echo "WARN: disk almost full" >&2
exit 0
bash
chmod +x twovoices.sh
./twovoices.sh > report.txt     # capture results only
cat report.txt
Expected result — click to reveal
plain text
WARN: disk almost full
report: all systems nominal

What to read out of it: the WARN line hit your screen at run time — it went out mouth 2, which the > did not touch — while cat report.txt shows the report landed clean in the file. Your script now behaves like a well-mannered Unix citizen: ./twovoices.sh 2>warnings.log > report.txt splits it fully. From this module on, every error message in every script you write ends with >&2 — it is two keystrokes, and it is the difference between logs you can parse and logs you must apologize for.

B3. Feeding input: <, here-documents, here-strings

Channel 0 — stdin — is redirectable too. Meet it with a tiny new tool: wc -l counts lines (word count, -l = lines only). wc -l users.txt opens the file itself and reports 3 users.txt — count plus name. But wc -l < users.txt connects the file to wc's stdin: wc reads from channel 0 without ever knowing a file was involved, and prints just 3 — no name, because it never had one. That anonymous-input distinction matters in scripts that go on to use the number.

When the input is short and lives in your script rather than in a file, a here-document embeds it directly: cat <<EOF … lines … EOF feeds everything between the markers to the command's stdin (the marker word is your choice; EOF is convention; the closing marker must sit alone, unindented, at line start). Config snippets, MOTD banners, multi-line messages piped to mail — here-docs are how scripts carry small documents inside themselves. The one-line version is the here-string: command <<< "one line of input".

Real-world analogy — the dictation. < file hands the typist a document to copy from. A here-document is dictation: the material comes out of your own mouth, inline, framed by "quote … end quote" (the EOF markers). The typist cannot tell the difference — words arrive either way — which is precisely the design: commands read stdin without caring who feeds it.

Where the analogy stops working. Dictation is literal; a here-doc is not — by default, $variables and $(commands) inside it expand (double-quote rules, Module 2). To dictate literally — a config full of $ that must survive untouched — quote the opening marker: <<'EOF'. That one-character habit (quote the marker when the content owns its dollars) prevents a whole family of mangled-config bugs.

🧪 Exercise 4.6 — three ways to feed a command
bash
cd ~/bash-course
wc -l users.txt        # wc opens the file: count + name
wc -l < users.txt      # the FILE feeds stdin: count only
cat <<EOF > motd.txt
Welcome to the staging server.
Deploys are frozen on Fridays.
EOF
cat motd.txt
grep -c o <<< "hello world"    # here-string; grep -c counts MATCHING LINES
Expected result — click to reveal
plain text
3 users.txt
3
Welcome to the staging server.
Deploys are frozen on Fridays.
1

What to read out of it: the first pair shows the fingerprint difference — with <, no filename in the output, because wc never saw one. The here-doc landed two lines in motd.txt (note the redirect on the opening line: cat <<EOF > motd.txt — stdin from the here-doc, stdout to the file, both channels rewired in one line). The final 1 is worth a squint: grep -c counts matching lines, not occurrences — "hello world" is one line containing o twice, and the answer is 1. Reading the man page's exact words for -c ("print a count of matching lines") is the kind of precision this track keeps rewarding.

Interview questions — Part B

🎯 "What is a here document in shell scripting?" — asked verbatim at Hirist

The direct answer: a redirection that feeds multiple literal lines from the script itself into a command's stdin: command <<EOF … content … EOF. Used for embedding config snippets, banners, SQL, or API payloads directly in scripts.

Going deeper: name the two behaviors that get tested — variables and command substitutions expand inside an unquoted here-doc (making them templates), and quoting the marker (<<'EOF') turns expansion off (making them literal); plus the syntax rule that the terminator must be alone at the start of a line. The <<-EOF variant strips leading tabs, allowing indented here-docs inside structured code.

The details that separate candidates: the template-vs-literal decision stated as a rule (content owns its $? quote the marker); knowing the here-string <<< as the one-line sibling; and one production use that shows experience — feeding a here-doc to ssh host 'bash -s' to run a small script on a remote machine without copying a file (Module 15 territory).

🎯 "Why do scripts write errors with >&2, and what breaks if they don't?" — the underlying question behind every redirection interview; asked in practice as a follow-up to the 2>&1 question

The direct answer: >&2 sends a message out the script's own stderr, keeping results (stdout) machine-readable. Without it, error text lands in whatever file or pipe is consuming the script's results — corrupting reports, breaking parsers, and hiding the error from anyone watching the console.

Going deeper: the discipline is symmetric — a script reads clean because its tools obey it (ls, grep and friends all complain on 2), and writes clean so its own callers can rely on the same split. One echo without >&2 in an error path undoes the whole contract.

The details that separate candidates: pairing it with exit codes — a proper failure path is echo "error: …" >&2 ; exit 1, words on 2 and a number in $?, so both humans and machines get told; and noting that usage messages for bad arguments belong on stderr too (the Module 3 convention: usage error → message on 2, exit 2).

Part C — Pipes: wiring mouths to mouths

C1. | — stdout of the left becomes stdin of the right

Everything so far wired channels to files. The pipe | wires them to each other: a | b connects a's stdout directly to b's stdin — no file in between, both commands running at once, data flowing through as it is produced. This is the composing move of the entire Unix toolbox: small programs, each doing one job well, snapped together like hose segments. ls /etc | wc -l — ls lists names, wc counts lines, and suddenly you have "how many entries are in /etc" without either program knowing the question.

One more segment for your kit: head -3 passes through only the first 3 lines of its input (its sibling tail takes the last; both get real work in Module 11). And when you need to watch data mid-pipeline while still passing it on, tee copy.txt writes its stdin both to a file and onward to stdout — a T-junction in the hose, named after the letter's shape.

Two properties to hold onto. Only stdout enters the pipe — stderr from any stage sprays straight to the terminal, bypassing the hose entirely (your 2>&1 skills apply per-stage when you need otherwise). And the pipeline's exit status is the last command's — a fact with a sting in its tail, in C2.

Real-world analogy — the sushi conveyor. A pipeline is a kitchen line at a conveyor-belt restaurant: the rice station passes to the fish station passes to the plating station, all working simultaneously on different pieces — nobody waits for the previous station to finish the whole batch. tee is the food photographer standing mid-belt, snapping each plate as it passes without stopping the line.

Where the analogy stops working. If the plating station walks off shift early, a real rice station keeps cooking. In a pipeline, a dead downstream eventually stops upstream: writing into a pipe nobody reads raises SIGPIPE and terminates the writer (Module 13 explains the mechanism). That is also the promised answer to Module 3's "echo can fail" teaser — echo into a closed pipe is precisely how echo fails.

🧪 Exercise 4.7 — snap the segments together
bash
cd ~/bash-course
ls /etc | wc -l                 # how many entries in /etc?
grep bob users.txt | wc -l      # how many lines mention bob?
ls /etc | head -3               # just the first three
wc -l users.txt | tee count.txt # watch AND save
cat count.txt
Expected result — click to reveal
plain text
161
1
ImageMagick-6
LatexMk
PackageKit
3 users.txt
3 users.txt

What to read out of it: your /etc count and first-three names will differ — that is a directory listing of your machine, not a constant (the count was 161 on the test machine). The grep|wc segment answers a question neither tool asks alone. The tee pair is the one to study: the same 3 users.txt appears twice — once live on screen (tee passing it through) and once from the file (tee's copy). In production this becomes long_job | tee build.log — humans watch live, the log still gets everything.

C2. Pipeline verdicts: the last word wins (and why that bites)

Module 3 taught you to trust exit codes; pipelines are where that trust gets tested. The exit status of a | b is b's status — a's is discarded. A pipeline whose first stage exploded reports success as long as the final stage coped: false | true exits 0. Real shape of the bug: dump_database | compress > backup.gz — the dump fails halfway, compress happily compresses the fragment it received, verdict 0, and Module 3's Ticket-1 backup horror story has a new author.

Bash does keep the evidence: the array PIPESTATUS holds every stage's code (read slot N with "${PIPESTATUS[N]}", counting from 0 — proper array syntax is Module 10's; treat this as a recipe until then). And Module 14 brings the production switch set -o pipefail, which makes the whole pipeline report the first failure. For now, the lesson is awareness: a pipeline's green can be a lie about every stage but the last.

Counter-intuitive: false | true exits 0. The most failure-prone stage of a pipeline is usually the first (the producer touching disk, network, database), yet the verdict comes from the last (often a formatter that succeeds on any input, even empty). The default is exactly backwards from what operations wants — which is why set -o pipefail (Module 14) appears near the top of virtually every production bash script written this decade.
🧪 Exercise 4.8 — the lying pipeline, caught

The pipeline lies on purpose.

bash
false | true
echo "pipeline verdict: $?"
false | true
echo "the evidence: ${PIPESTATUS[0]} ${PIPESTATUS[1]}"
Expected result — click to reveal (contains a deliberate lie)
plain text
pipeline verdict: 0
the evidence: 1 0

What to read out of it: verdict 0 — the failure pill vanished from the record, exactly as C2 warned. The second run repeats the crime so PIPESTATUS is fresh (it is as perishable as $?, Module 3 D1), and the evidence reads 1 0: stage one failed, stage two succeeded, the pipeline reported stage two. When a pipeline's green feels suspicious, PIPESTATUS is your audit trail — and pipefail (Module 14) is how you stop needing one.

Now imagine this at 500 hosts. Pipelines are the fleet's daily bread — journalctl | grep | wc health checks, dump | compress | ship backups, curl | jq API probes — almost always running unattended, where the only thing anyone ever sees is the exit code. The last-word-wins default therefore scales into a fleet-wide blind spot: 500 backup jobs can all be green while 40 dumps fail nightly. Fleet practice: set -o pipefail in every script template (Module 14), and for the transition period, log "${PIPESTATUS[@]}" on every critical pipeline so the per-stage truth lands in the logs even where the verdict still lies.

Part D — The whole module in one picture

Note: the diagram below is a Mermaid code block. Notion does not render it automatically — click the block and switch it from "Code" to "Preview" (or "Split") to see the flowchart.
Diagram source
flowchart LR
    K["keyboard /<br>file via <  /<br>here-doc"] -->|"0 stdin"| CMD["command"]
    CMD -->|"1 stdout<br>results"| OUT{"where?"}
    CMD -->|"2 stderr<br>complaints"| ERR{"where?"}
    OUT -->|"default"| S1["screen"]
    OUT -->|"> file<br>>> file"| F1["file"]
    OUT -->|"pipe to next"| P["next command's<br>stdin"]
    OUT -->|"> /dev/null"| N1["discarded"]
    ERR -->|"default"| S1
    ERR -->|"2> file"| F2["errors file"]
    ERR -->|"2>&1"| OUT
    ERR -->|"2> /dev/null"| N1

Read any redirection line by tracing this map: find the channel, follow the arrow you wrote. The one arrow that surprises people is 2>&1 pointing at the stdout decision node — it inherits whatever choice 1 already made, which is the order trap in picture form.

Interview questions — Parts C–D

🎯 "How to use pipe commands?" — asked verbatim at InterviewBit

The direct answer: a | b connects a's stdout to b's stdin, running both concurrently with data streaming between them; chains extend naturally (a | b | c). Classic examples: ls | wc -l, cat access.log | grep 500 | wc -l — though grep takes files directly, making the cat unnecessary (the famous "useless use of cat").

Going deeper: state the three mechanics that questions probe — only stdout enters the pipe (stderr bypasses it); stages run simultaneously, not sequentially (a pipeline handles gigabytes in constant memory because nothing is stored between stages); and the pipeline's exit status is the last stage's unless pipefail is set.

The details that separate candidates: the constant-memory streaming point, stated crisply, signals systems understanding beyond syntax; naming SIGPIPE as what stops an abandoned producer shows depth; and volunteering tee for the watch-and-log pattern (job | tee log) shows operational habit. Mentioning that variables set in a pipeline stage don't survive it (each stage is its own process — Module 12) preempts the next trick question.

🎯 "What is the exit status of a pipeline, and how do you catch a failure in the middle of one?" — the operational follow-up to every pipe question; LinuxTeck's FAQ asks the sibling "What is PIPESTATUS in bash and when should I use it?"

The direct answer: by default, the last command's status — earlier failures are discarded (false | true exits 0). To catch mid-pipeline failures: set -o pipefail makes the pipeline return the last non-zero status, and the PIPESTATUS array exposes every stage's code individually.

Going deeper: explain why the default exists (each stage is a separate process; the shell had to pick one verdict and chose the final consumer's) and the operational consequence — producer failures are precisely the ones hidden. pipefail changes the verdict; PIPESTATUS preserves the detail; serious scripts use pipefail globally and consult PIPESTATUS where stages need individual handling.

The details that separate candidates: knowing PIPESTATUS is overwritten by the very next command (copy it immediately — same rule as $?); that pipefail is one of the trio set -euo pipefail (Module 14); and the subtle grep case — with pipefail, a pipeline containing grep that finds nothing "fails" with 1, so pipefail forces you to decide whether no-match is an error, typically via grep pattern || true when it is not.

Part E — Production

E1. 🏭 Production practices

These are the practices in production — how the things this module taught are actually done on real systems:

Unattended jobs capture both channels, in the correct order. Cron jobs and CI steps end in >> /var/log/jobname.log 2>&1 (append, both channels) — a job whose errors go nowhere is a job that fails invisibly.

Logs append, reports overwrite — and the choice is commented. >> for history, > for regenerated artifacts; the intent is written down because one character cannot carry it.

Script errors go out stderr, always. Every failure path is echo "error: …" >&2 ; exit N — words on channel 2 for humans, a number in $? for machines. Usage messages included.

2>/dev/null requires a justification comment. Blanket stderr-silencing is treated in review like a disabled smoke alarm; silence is scoped to one specific, expected, documented error.

Pipelines run under pipefail, and critical ones log their stage codes. set -o pipefail in every template (Module 14); "${PIPESTATUS[@]}" logged where a stage-by-stage audit trail matters.

Long jobs go through tee. ./build.sh 2>&1 | tee build.log — the human watches live, the log is complete, and nobody has to choose between the two.

Here-doc markers are quoted when content owns its dollars. <<'EOF' for literal configs; unquoted <<EOF only when template expansion is the point — and the choice is deliberate, not accidental.

E2. Production-practice table

SymptomWhat is really happeningWhat to runThe fix
"I redirected to the log, but errors still spray on the console"> moved only stdout; stderr never heard about itcommand > f and watch what still prints — that remainder is stderrcommand > f 2>&1 (or &>> f for append-both in bash)
Errors ended up on screen even though 2>&1 is right there in the commandThe order trap: 2>&1 came before > file, so stderr copied the terminalRead the line left to right, tracing each channel; ShellCheck flags it as SC2069Put the file redirect first: > file 2>&1 — always
The nightly log contains only the last run; history is goneThe job writes with > — every run truncates the file at startupls -l the log over two runs — size resets instead of growingSwitch to >> for logs; keep > only for regenerated reports
A file processed "onto itself" is now emptycmd file > samefile — truncation happens at setup, before cmd reads a bytewc -c samefile → 0; check shell history for the self-redirectWrite to a temp file and move it over (mktemp, Module 14), or use a tool's in-place mode
Script "works" but its report file has WARN/error lines mixed into the dataThe script echoes complaints out stdout — no >&2 on its error paths./script.sh >/dev/null — whatever disappears was wrongly on stdoutAdd >&2 to every diagnostic echo; re-test with the /dev/null probe
Backup pipeline green; backups partial or emptyLast-word-wins: the producer failed, the final stage succeeded, verdict 0false | true ; echo $? to see the mechanism; check "${PIPESTATUS[@]}" on the real pipelineset -o pipefail (Module 14); log stage codes on critical pipelines

E3. 🎓 Capstone — four tickets from the queue

Work each ticket yourself before opening the answer. Everything needed was taught in Modules 1–4.
🎓 Ticket 1 — "Our cron job's log file is always empty, but the job clearly runs — and when it breaks, we get no clue why. The crontab line ends with: ... > /var/log/sync.log 2>&1 — wait, no: ... 2>&1 > /var/log/sync.log."

Diagnosis. The order trap (A3), running unattended. 2>&1 executed first, while stdout still pointed at cron's default destination — so stderr goes to cron's mail/void, not the log. Then > moved stdout to the log. Result: the log gets stdout only (this quiet job prints little — hence "always empty"), and every error message vanishes into wherever cron sends unclaimed output.

Work the steps: reproduce interactively: ls /nonexistent 2>&1 > /tmp/t.log — error on screen, empty file. Same shape.

Fix and prevention: swap the order: >> /var/log/sync.log 2>&1 (append while you're at it — E1's log rule). Prevention is mechanical: ShellCheck (SC2069) in CI would have flagged the crontab's script; and a five-minute team convention — "the file redirect always comes first" — retires the whole bug class.

🎓 Ticket 2 — "A teammate's cleanup script ran fine for months. This week it silently stopped cleaning. Every command in it ends with 2>/dev/null — 'to keep the output tidy,' the author says."

Diagnosis. B1's chainsaw. Something changed on the host (a permissions tightening, a moved directory — an error of the new kind), the script's commands began failing, and their explanations have been incinerating on contact ever since. The script still exits, cron still shows it ran, and the one channel that would have named the problem is gagged at every line.

Work the steps: copy the script, strip every 2>/dev/null, run it manually: the machine speaks immediately (Permission denied on the new directory, say). Check $? at the failing line to pair message with verdict.

Fix and prevention: remove the blanket silencers. Where one specific, expected error genuinely needs hiding, silence exactly that spot with a comment naming the expected error. Then give errors somewhere to live: exec 2>> /var/log/cleanup.err at the top of the script (or the cron-line 2>&1 into the log) — tidy console and preserved complaints. "Tidy" was never the requirement; quiet success, loud failure is.

🎓 Ticket 3 — "We run ./healthcheck.sh > /var/www/status.txt every minute and serve status.txt on the intranet. Since yesterday the page sometimes shows a curl warning line above the status JSON, and the dashboard that parses it errors out."

Diagnosis. B2 violated one line at a time: something inside healthcheck.sh — the new curl call added yesterday — is writing a warning, and either curl's stderr was redirected into stdout inside the script, or the script echoes its own warnings without >&2. Diagnostics leaked into channel 1, and channel 1 is the intranet page.

Work the steps: the /dev/null probe from E2: ./healthcheck.sh >/dev/null — anything that still prints is correctly on stderr; then ./healthcheck.sh 2>/dev/null | head — the warning showing here proves it is traveling on stdout. Grep the script for the new curl line and for echoes missing >&2.

Fix and prevention: route diagnostics to 2 (curl ... 2>>/var/log/healthcheck.err, warnings via >&2), keep stdout JSON-only. Add a CI check that runs the script and validates stdout parses as JSON — machine-enforcing the two-mouth discipline this module taught.

🎓 Ticket 4 — "Database backup: pg_dump appdb | gzip > /backups/appdb.gz, checked by ... && touch /backups/.success. The success marker updates nightly. A restore test today found the last nine backups are truncated mid-table."

Diagnosis. C2's lying pipeline, with stakes. pg_dump has been failing partway (disk, timeout, permissions — its complaint went to stderr, which nobody captured); gzip faithfully compressed the fragment and exited 0; the pipeline's verdict is gzip's; the && saw green and stamped success. Nine days of confident, useless backups.

Work the steps: run the pipeline manually and read the evidence: pg_dump appdb | gzip > /tmp/test.gz ; echo "${PIPESTATUS[@]}" → something like 1 0. pg_dump's stderr (now visible on your terminal) names the real cause.

Fix and prevention: three layers, all from this track: set -o pipefail at the top of the backup script so the pipeline tells the truth (Module 14 formalizes it); capture stderr to a log (2>> /var/log/backup.err) so the cause is preserved; and make the success marker depend on evidence, not verdicts — a minimum-size / restore-test check on the artifact itself. Verdicts gate; evidence certifies.

E4. Documentation reference

TopicAuthoritative referenceWhat you'll find there
All redirection operatorsBash manual — Redirections>, >>, 2>, 2>&1, &>, <, here-documents, here-strings — the complete rulebook
Pipelinesbash(1) — man7.org (Pipelines section)Pipe mechanics, pipeline exit status, and the pipefail option
The order trap, mechanically detectedShellCheck SC2069The lint rule that catches 2>&1 > file, with explanation
The wastebasketnull(4) — man7.orgWhat /dev/null actually is: a kernel device, not a file convention
The new toolswc(1) · head(1) · tee(1)Line counting, first-N-lines, and the T-junction — options and exact semantics

E5. Self-assessment

Answer from memory, out loud or on paper. Every answer is in this module.

  1. Name the three standard channels by number and job. Why do results and complaints travel separately?
  2. You suspect a line of output is stderr. What is the one-command experiment that settles it?
  3. > vs >> — behavior, when each is right, and what exactly happens to the target file before the command runs?
  4. Why does wc -l data.txt > data.txt report 0? What is the safe pattern instead?
  5. Write the correct line to send both channels of a command into one log file — then write the broken order and explain, channel by channel, where each stream goes.
  6. What does 2>/dev/null cost you in six months? When is it legitimate?
  7. What survives >/dev/null 2>&1? Why does that make it useful rather than pointless?
  8. Why must a script's own error messages end with >&2? What is the /dev/null probe that audits a script for violations?
  9. wc -l users.txt vs wc -l < users.txt — why do the outputs differ, and what does the difference reveal about stdin?
  10. When do you quote a here-doc marker (<<'EOF'), and what changes when you do?
  11. What is the exit status of false | true, why, and what are the two tools that surface the hidden failure?
  12. In a | b, where does a's stderr go? And what stops a if b dies early?

E6. Sources

Interview questions in this module were captured verbatim from:

Edureka — Top 60 Shell Scripting Interview Questions and Answers — "How to redirect both standard output and standard error to the same location?" (page updated Dec 9, 2024)

Hirist — Top 30+ Shell Scripting Interview Questions and Answers — "How do you redirect both stdout and stderr to a file?", "What is a here document in shell scripting?" (published Jul 22, 2025; last modified Dec 31, 2025)

InterviewBit — Top Shell Scripting Interview Questions — "How to use pipe commands?" (no publication date shown on page)

Zero To Mastery — Bash Interview Prep — "How do you redirect output in Bash?" (published June 18, 2026)

LinuxTeck — Debugging Shell Scripts & Exit Status Explained — FAQ: "What is PIPESTATUS in bash and when should I use it?", "How do I log errors to a file in a bash script?" (published April 29, 2026)

A note on the corpus: the 2>&1 idiom and here-documents are richly represented in published questions; the > vs >> distinction and the >&2 script discipline are asked constantly in real screens but rarely published with exact wording — where a summary line lacks a verbatim source, it says so. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2; machine-dependent values (dates, /etc listings and counts) are flagged where they occur.

Next: verdicts (Module 3) plus clean channels (this module) make decisions possible — now bash gets its full decision grammar: if, the two test commands, file checks, string checks, and case. That is Module 5 — Conditionals: if, test, and case.
Spotted a mistake or want something added? Send me a note.