Pipes and Redirection

Reviewed & published by Brayan K

By the end of this lesson you'll be able to chain small commands into one powerful pipeline, send output and errors exactly where you want, and combine commands so they run in the right order — the skills that turn the terminal from a toy into a tool.

Part of the free Command Line course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1. The Three Streams

Before pipes make sense, you need to know that every command has three channels. stdin (channel 0) is where input arrives. stdout (channel 1) is where normal results go. stderr (channel 2) is a separate channel just for error messages. On screen, stdout and stderr look the same, but keeping them apart is what lets you save real results while still seeing warnings — and it's the reason a later trick (2>&1) exists at all.

# Every command talks through THREE streams (numbered channels):
#   stdin  (0) — where INPUT comes from   (your keyboard by default)
#   stdout (1) — where normal OUTPUT goes (your screen by default)
#   stderr (2) — where ERROR output goes  (your screen by default)

# 'echo' writes to stdout. This text is going to channel 1:
echo "this line is stdout (channel 1)"

# 'ls' on a missing file writes the complaint to stderr (channel 2),
# NOT stdout. On screen they look identical, but they are separate pipes:
ls no-such-file.txt

2. The Pipe Operator |

The pipe (|) connects the stdout of the command on its left to the stdin of the command on its right. That lets you build a chain of tiny, single-purpose tools — list | filter | count — where data flows left to right. This is the Unix philosophy in action: "do one thing well," then combine. Read this worked example, run it, and watch each stage shrink the data.

# A pipe (|) connects the stdout of one command to the stdin of the
# next, so data flows left -> right through a chain of small tools.
# Build a sample log first so every example below has real data:
cat > server.log <<'EOF'
[INFO]  GET /api/users
[ERROR] database timeout
[INFO]  POST /api/login
[ERROR] database timeout
[WARN]  slow query
[ERROR] invalid token
[ERROR] database timeout
EOF

# ls's list -> grep keeps the matches -> wc -l counts the lines.
# This is the classic three-stage pipe:  list | filter | count
ls | grep server | wc -l            # 1  (one file matches "server")

echo "--- only the ERROR lines ---"
grep ERROR server.log               # grep filters stdin/the file

echo "--- count the ERROR lines ---"
grep ERROR server.log | wc -l       # 4  (pass matches into wc -l)

echo "--- most frequent errors ---"
# sort groups duplicates, uniq -c counts each group, sort -rn ranks them:
grep ERROR server.log | sort | uniq -c | sort -rn

Pro Tip — Build pipelines one stage at a time

Don't write a five-command pipe in one go. Run grep ERROR server.log, check it looks right, then add | sort, check again, then | uniq -c, and so on. Each | you add only sees the output of the stage before it, so building up gradually makes mistakes obvious immediately.

Your turn. The pipeline below is almost complete — fill in the blank marked ___ using the hint in the comment, then run it.

3. Redirection: >, >>, <, 2>, 2>&1

Redirection points a stream at a file instead of the screen or keyboard. > writes output to a file and overwrites it; >> appends to the end. < feeds a file in as stdin. To capture errors you must name the channel: 2> redirects stderr on its own, and 2>&1 means "send stderr to wherever stdout is going" — so > file 2>&1 puts both into one file. The order matters: redirect stdout first, then merge stderr into it.

# Redirection sends a stream somewhere other than the screen/keyboard.

# >  WRITES output to a file and OVERWRITES whatever was there.
echo "Hello" > file.txt
# >> APPENDS output to the end of a file (keeps existing contents).
echo "World" >> file.txt
cat file.txt                         # Hello / World on two lines

# 2> redirects ONLY stderr (channel 2). Here stdout still hits the screen,
# but the error is captured into errors.txt instead:
ls file.txt no-such-file.txt 2> errors.txt
echo "--- captured stderr ---"
cat errors.txt

# 2>&1 means "send channel 2 to wherever channel 1 is going" — i.e.
# merge errors INTO normal output. Order matters: redirect stdout first.
echo "--- merge both streams into one file ---"
ls file.txt no-such-file.txt > all.txt 2>&1
cat all.txt

# < feeds a file in as stdin. sort reads its input from fruit.txt:
echo "--- input redirection (sort < file) ---"
printf 'banana\napple\ncherry\n' > fruit.txt
sort < fruit.txt

4. /dev/null, tee, and Command Chaining

/dev/null is a special file that throws away anything written to it — use it to silence output you don't care about. tee does the opposite of hiding: it splits a stream so it goes to a file and the screen at once. Finally, command chaining sequences whole commands: ; runs the next one regardless, && runs it only if the previous command succeeded, and || runs it only if the previous command failed.

# /dev/null is the "trash can" — anything written to it disappears.
# Run a command but silence its normal output, keep only errors:
ls file.txt no-such-file.txt > /dev/null
echo "(stdout was thrown away; the error above came from stderr)"

# Silence EVERYTHING (stdout and stderr) — a common, quiet check:
grep ERROR server.log > /dev/null 2>&1
echo "grep finished; we showed nothing from it"

# tee splits a stream: it writes to a file AND passes it on to the screen.
echo "--- tee: save and show at once ---"
echo "saved and shown" | tee output.txt

# Command chaining operators run commands in sequence:
#   ;   run the next command no matter what
#   &&  run the next command ONLY if the previous one SUCCEEDED
#   ||  run the next command ONLY if the previous one FAILED
echo "--- chaining ---"
mkdir build && echo "made build/ (only printed because mkdir worked)"
ls missing-dir || echo "ls failed, so this fallback ran"
echo "first" ; echo "second (always runs)"

Now you try. Fill in the two blanks with the right chaining operators using the hints, then run it.

# 🎯 YOUR TURN — replace each ___ then run it.
# Goal: print "ok" ONLY if the file server.log exists, and run a
# fallback message ONLY if the previous check failed.

# 'test -f FILE' succeeds when the file exists (exit code 0).
# 1) Run echo "ok" only if the test SUCCEEDS  -> use the AND operator
test -f server.log ___ echo "ok"        # 👉 the "only if it worked" operator

# 2) Run the fallback only if the test FAILS -> use the OR operator
test -f ghost.log ___ echo "missing"     # 👉 the "only if it failed" operator

# ✅ Expected output:
#    ok
#    missing

Common Errors (and the fix)

📋 Quick Reference

OperatorWhat it does
cmd1 | cmd2Send cmd1's stdout into cmd2's stdin
> fileRedirect stdout to a file (overwrite)
>> fileRedirect stdout to a file (append)
< fileFeed a file in as stdin
2> fileRedirect only stderr to a file
> file 2>&1Redirect both stdout and stderr to a file
> /dev/null 2>&1Discard all output (silence everything)
| tee fileWrite to a file AND show on screen
cmd1 && cmd2Run cmd2 only if cmd1 succeeded
cmd1 || cmd2Run cmd2 only if cmd1 failed
cmd1 ; cmd2Run cmd2 after cmd1, regardless

Mini-Challenge: A Tiny Error Report

No blanks this time — just a brief and an outline to keep you on track. Combine a pipe, tee, and && into a single line, run it, and check your output against the comments. This is exactly the kind of one-liner you'll reach for on real servers.

# 🎯 MINI-CHALLENGE: build a tiny error report
# Using the server.log from earlier, in ONE chained line:
# 1. Keep only the ERROR lines            (hint: grep)
# 2. Count how many there are             (hint: | wc -l)
# 3. Save that number to error-count.txt  AND show it on screen (hint: | tee)
# 4. Then, ONLY if that worked, echo "report saved"  (hint: && )
#
# ✅ Expected output (screen):
#    4
#    report saved
# ✅ error-count.txt should contain: 4

# your one-line pipeline here

🎉 Lesson Complete!

Practice quiz

What does the pipe | operator do?

  • Deletes output
  • Sends one command's stdout into the next command's stdin
  • Renames a file
  • Runs a command as root

Answer: Sends one command's stdout into the next command's stdin. A pipe connects one command's stdout to the next command's stdin.

Which channel number is stderr?

  • 0
  • 1
  • 2
  • 3

Answer: 2. stdin is 0, stdout is 1, and stderr is 2.

What does > do to an existing file?

  • Appends to the end
  • Overwrites it
  • Reads it as input
  • Deletes only errors

Answer: Overwrites it. > overwrites the file, erasing the old contents.

Which operator appends output to the end of a file?

  • >>
  • <
  • |
  • 2>

Answer: >>. >> appends and keeps the existing contents.

What does 2> file.txt capture?

  • Both streams
  • Only stdout
  • Only stderr
  • Nothing

Answer: Only stderr. 2> redirects only stderr (channel 2) to the file.

What does 2>&1 mean?

  • Send stderr to wherever stdout is going
  • Delete both streams
  • Send stdout to a file
  • Run two commands

Answer: Send stderr to wherever stdout is going. 2>&1 merges stderr into wherever stdout currently points.

What is /dev/null used for?

  • Saving backups
  • Listing files
  • Discarding output you do not care about
  • Counting lines

Answer: Discarding output you do not care about. /dev/null is the trash can; anything written to it disappears.

What does the && operator do?

  • Runs the next command only if the previous one succeeded
  • Always runs the next command
  • Runs the next command only if the previous failed
  • Pipes output

Answer: Runs the next command only if the previous one succeeded. && runs the next command only if the previous one succeeded (exit 0).

What does the || operator do?

  • Runs the next command regardless
  • Runs the next command only if the previous succeeded
  • Pipes stderr
  • Runs the next command only if the previous failed

Answer: Runs the next command only if the previous failed. || runs the next command only if the previous one failed.

What does the tee command do?

  • Discards a stream
  • Sorts input
  • Counts words
  • Writes to a file AND shows it on screen

Answer: Writes to a file AND shows it on screen. tee splits a stream: it saves to a file and passes it on to the screen.

Continue this course

Frequently asked questions

What is the difference between a pipe (|) and redirection (>)?

A pipe sends one command's output to ANOTHER command as its input (cmd1 | cmd2). Redirection sends output to or from a FILE (cmd > file.txt, or cmd < file.txt). Rule of thumb: a pipe always has a command on both sides; redirection has a filename on one side.

Why didn't > capture my error message?

Plain > only redirects stdout (channel 1). Error messages travel on stderr (channel 2), so > leaves them on screen. Capture them with 2> file.txt, or merge both into one place with > file.txt 2>&1.

When do I use > versus >>?

> overwrites the file — it erases the old contents first, so it's a common cause of accidental data loss. >> appends to the end and keeps what was already there. If in doubt and the file matters, use >>.

What does 2>&1 actually mean?

It means 'send channel 2 (stderr) to wherever channel 1 (stdout) is currently going.' Put it AFTER you redirect stdout: command > file.txt 2>&1 sends both to the file. The shorthand &> file.txt does the same in Bash.

What is /dev/null for?

It's a special file that discards everything written to it — the system 'trash can.' Use it to silence output you don't care about: command > /dev/null hides normal output, and command > /dev/null 2>&1 silences both output and errors.

What is the difference between && and ; when chaining commands?

; runs the next command no matter what happened before. && runs the next command only if the previous one SUCCEEDED (exit code 0). Use && to stop a chain when a step fails, e.g. cd build && make.