Pipes, redirection and quoting in Linux

Linux pipes and redirection are how you join small commands into one useful answer, and how you decide where output and errors go: to the screen, into a file, into another command, or nowhere. Almost every one-liner an experienced admin types has a | or a > in it, and most broken cron jobs and scripts come from getting one of them, or the quoting around them, slightly wrong.

You’ll learn the three standard streams, the redirection operators and why the order of 2>&1 matters, how to write into a root-owned file with sudo tee, how xargs turns a list into arguments, and the quoting rules that keep spaces and dollar signs from surprising you. The outputs were run for real wherever the result doesn’t depend on your machine.

Where this lesson fits

Lesson 13 of 82 in Linux Administration · Phase 1: The origins and OS architecture · Before this: Lesson 12, reading and editing files · Next: Lesson 14, text processing with grep, sed and awk

TimeYou needBy the end you can
About 60 minutesrocky01 or ubu01 from Lesson 2Send output and errors where you want them, chain commands with pipes, and quote variables safely

What it is, in one line

Every command reads from standard input and writes to standard output and standard error, and redirection and pipes let you connect those three streams to files or to other commands.

In Simple Words

A railway station with three tracks

Think of a small railway station. One track brings trains in, one sends passenger trains out, and a separate siding takes the damaged coaches away so they don’t block the main line.

A command is that station. Input comes in on track 0, normal output leaves on track 1 and errors leave on track 2. The signalman (your shell) can switch any track to another station (a pipe), to a goods yard (a file) or to the scrapyard where things disappear (/dev/null).

Engineer's View

File descriptors and the operators

Each process gets numbered file descriptors. By default 0, 1 and 2 all point at your terminal, which you can see in /proc:

$ ls -l /proc/$$/fd
lrwx------ 1 student student 64 Oct 27 12:05 0 -> /dev/pts/0
lrwx------ 1 student student 64 Oct 27 12:05 1 -> /dev/pts/0
lrwx------ 1 student student 64 Oct 27 12:05 2 -> /dev/pts/0
OperatorWhat it doesExample
>Send stdout to a file, emptying it firstls > list.txt
>>Append stdout to the end of a filedate >> run.log
2>Send stderr to a filefind / -name x 2> err.txt
2>&1Point stderr at wherever stdout points right nowcmd > all.log 2>&1
&>Bash shortcut for both streams to one filecmd &> all.log
<Read stdin from a filewc -l < servers.txt
|Connect stdout of one command to stdin of the nextls /etc | wc -l
|&Bash shortcut for 2>&1 |cmd |& grep -i error
/dev/nullA device that throws away whatever is written to itcmd 2> /dev/null

The shell sets all of this up before the command starts, so the command itself doesn’t know or care whether it’s writing to a screen, a file or a pipe. That’s also why sudo in front of a command doesn’t help a redirection: the > is done by your own shell, as your user, before sudo even runs.

QuotesWhat the shell does inside themExample and result
Double quotes “…”Expands $variables and $(commands), keeps spaces“Hello $name” gives Hello Priya
Single quotes ‘…’Nothing at all: every character is literal‘Hello $name’ gives Hello $name
Backslash \Makes the next character literal“Cost: \$5” gives Cost: $5
No quotesExpands variables, then splits on spaces and expands wildcards$v with v=”a b” gives a b

How it works, step by step

A pipeline looks like one line, but the shell does a fair bit of work before any command runs. The diagram below follows grep -i error app.log 2>/dev/null | sort | uniq -c > summary.txt.

1The shell splits the line at each pipe

It sees three commands: grep, sort and uniq. For each | it creates a pipe, a small buffer in the kernel (64 KiB by default on Linux), with a writing end and a reading end.

2It wires the streams, then starts every command at once

grep’s stdout goes into the first pipe and its stderr goes to /dev/null. sort reads from the first pipe and writes into the second. uniq reads from the second and its stdout goes to summary.txt. All three processes run at the same time, and data flows through as it’s produced.

Linux pipes and redirection: how bash wires grep, sort and uniq with pipes and files

3Redirections are read left to right

For cmd > file 2>&1 the shell first points stdout at the file, then points stderr at “wherever stdout points now”, which is the file. For cmd 2>&1 > file it first points stderr at the terminal (where stdout points at that moment), and only then moves stdout to the file. Same characters, different result, and the second one is a classic cron job bug.

4Exit status of a pipeline

A pipeline’s exit status is the status of the last command. So cat missing.txt | wc -l “succeeds” because wc worked, even though cat failed. Bash keeps every status in ${PIPESTATUS[@]}, and set -o pipefail makes the pipeline fail if any part fails, which is what you want in scripts (Lesson 39).

5Expansion happens before the command runs, in a fixed order

On every line, bash expands variables and $( ) first, then splits unquoted results on spaces, then expands wildcards, and only then runs the command with the final list of words. Quotes switch off some of those steps. The picture underneath shows the same variable going through with and without quotes.

Linux pipes and redirection: quoted and unquoted variables during bash expansion

6Command substitution and xargs

$(command) runs a command and puts its output in place, so echo "Kernel: $(uname -r)" prints the kernel version inside your sentence. The old backtick form does the same but is hard to read and to nest, so use $( ).

Some commands don’t read stdin at all: rm, ls and mkdir want names as arguments. xargs reads a list from stdin and turns it into arguments, so find ... | xargs rm works where find ... | rm does nothing. Because a plain list breaks on spaces in names, pair find -print0 with xargs -0, which separate names with a null byte that can’t appear in a file name.

Hands-on lab: route output, errors and pipes in ~/lab13

Before you run any command, double-check it

Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.

Everything happens in a new folder, ~/lab13. You’ll make one real file and ask for one that doesn’t exist, so every command has something to say on both stdout and stderr. The outputs below are exact, except times, dates and counts that depend on your machine.

1See the two output streams

Paste this whole block:

mkdir -p ~/lab13 && cd ~/lab13
printf 'web01\nweb02\ndb01\n' > servers.txt
ls servers.txt nofile.txt

You should see this:

ls: cannot access 'nofile.txt': No such file or directory
servers.txt

Both lines look the same on screen, but the first came through stderr (fd 2) and the second through stdout (fd 1). The next steps pull them apart.

✅ Checkpoint: You see one error line and one file name.

2Send stdout to a file

Run this:

ls servers.txt nofile.txt > out.txt
cat out.txt

You should see this:

ls: cannot access 'nofile.txt': No such file or directory
servers.txt

The error still hit your screen because > only moved fd 1. The file name went into out.txt, which cat then printed.

✅ Checkpoint: out.txt contains only servers.txt.

3Overwrite or append

Run this:

echo one > n.txt
echo two > n.txt
cat n.txt
echo three >> n.txt
cat n.txt

You should see this:

two
two
three

> empties the file before writing, so “one” is gone. >> adds to the end. Logs and reports you build over time always use >>.

Protect yourself from accidental overwrites with set -o noclobber. Then echo x > n.txt fails with “cannot overwrite existing file”, and >| forces it when you mean it.

✅ Checkpoint: n.txt ends with the lines two and three.

Linux pipes and redirection lab output: 2> err.txt and > all.txt 2>&1 on rocky01″ class=”wp-image-8227″ width=”1200″ height=”332″ style=”width:100%;height:auto;border-radius:10px;” /><figcaption style=Example output from rocky01: 2> keeps only the error, and > all.txt 2>&1 puts both streams in one file

4Split errors from output, or join them

Run this:

ls servers.txt nofile.txt 2> err.txt
cat err.txt
ls servers.txt nofile.txt > all.txt 2>&1
cat all.txt

You should see this:

servers.txt
ls: cannot access 'nofile.txt': No such file or directory
ls: cannot access 'nofile.txt': No such file or directory
servers.txt

The first command printed servers.txt on screen and saved only the error. The second put both streams in all.txt, which is the form you want for a cron job log. &> all.txt does the same in bash.

✅ Checkpoint: all.txt contains both the error line and servers.txt.

5Throw output away with /dev/null

Run this:

ls nofile.txt 2>/dev/null
echo "exit status: $?"

You should see this:

exit status: 2

The error is gone from the screen, but the command still failed, and $? (the exit status of the last command) says so. Hiding errors is fine in a quick check; in a script, check the status, or you’ll hide real problems.

✅ Checkpoint: Nothing printed except the exit status 2.

6Pipe one command into another

Run this:

ls /etc | wc -l
wc -l < servers.txt
ls servers.txt nofile.txt 2>&1 | wc -l

You should see something like this:

178
3
2

The first line counts the entries in /etc (your number will differ). wc -l < servers.txt prints only the number, because wc never saw a file name. The last one counts 2 lines because 2>&1 sent the error into the pipe as well; without it the error skips the pipe and wc counts 1.

✅ Checkpoint: wc -l < servers.txt prints 3.

7Write to a root-owned file with sudo tee

Run this:

sudo echo "10.10.10.31 kafka01.saatlab.test kafka01" >> /etc/hosts

You’ll get:

-bash: /etc/hosts: Permission denied

sudo ran echo as root, but the >> was opened by your own shell as student, before sudo started. Use tee, which reads stdin and writes it both to the screen and to a file, and run tee itself with sudo:

Run this:

grep -q kafka01 /etc/hosts || echo "10.10.10.31 kafka01.saatlab.test kafka01" | sudo tee -a /etc/hosts
tail -n 1 /etc/hosts

You should see this:

10.10.10.31 kafka01.saatlab.test kafka01
10.10.10.31 kafka01.saatlab.test kafka01

The first line is tee echoing what it wrote, the second is tail proving it’s in the file. The -a matters: without it tee overwrites the file, and an /etc/hosts with one line breaks name lookups on the server. The grep -q ... || part only adds the line if it isn’t already there, so running the block twice is safe. kafka01 is the VM you’ll build in Lesson 58, and this entry matches the lab plan.

✅ Checkpoint: tail -n 1 /etc/hosts shows the kafka01 line.

8Save and watch at the same time

Run this:

df -h / | tee disk-report.txt
wc -l disk-report.txt

You should see something like this:

Filesystem           Size  Used Avail Use% Mounted on
/dev/mapper/rl-root   17G  1.9G   16G  11% /
2 disk-report.txt

This is the everyday use of tee: you see the output and you keep a copy for the ticket.

✅ Checkpoint: disk-report.txt has the same two lines you saw on screen.

9Turn a list into arguments with xargs

Paste this whole block:

mkdir -p logs && touch logs/a.log logs/b.log "logs/my app.log"
find logs -name "*.log" | xargs ls -l

You’ll get:

ls: cannot access 'logs/my': No such file or directory
ls: cannot access 'app.log': No such file or directory
-rw-rw-r--. 1 student student 0 Oct 27 12:14 logs/a.log
-rw-rw-r--. 1 student student 0 Oct 27 12:14 logs/b.log

xargs split “my app.log” into two words. Now do it the safe way, with null separators:

Run this:

find logs -name "*.log" -print0 | xargs -0 ls -l
echo a b c d | xargs -n 2 echo

You should see something like this:

-rw-rw-r--. 1 student student 0 Oct 27 12:14 logs/a.log
-rw-rw-r--. 1 student student 0 Oct 27 12:14 logs/b.log
-rw-rw-r--. 1 student student 0 Oct 27 12:14 logs/my app.log
a b
c d

-n 2 passes two arguments per run. For deleting files you’ll usually use find’s own -delete after a dry run with -print (Lesson 25), but xargs is what you reach for when the next command isn’t find.

✅ Checkpoint: The -print0 / -0 version lists all three files, including my app.log.

10Command substitution and quoting

Paste this whole block:

name="Priya"
echo "Hello $name"
echo 'Hello $name'
echo "Cost: \$5"
count=$(wc -l < servers.txt)
echo "$count servers, kernel $(uname -r)"
v="a   b"
echo $v
echo "$v"

You should see something like this:

Hello Priya
Hello $name
Cost: $5
3 servers, kernel 6.12.0-211.16.1.el10_2.0.1.x86_64
a b
a   b

Double quotes expand, single quotes don’t, and a backslash protects one character. $( ) dropped the output of two commands into the sentence. The last pair is the one that matters most: without quotes, bash split $v into two words and echo joined them with one space. That same splitting is what turns a file called “my app.log” into two files in a script.

✅ Checkpoint: echo “$v” keeps the three spaces and echo $v does not.

Break It, Then Fix It

These three bugs show up in real cron jobs and scripts every week. Snapshot first if you like, although none of them touches anything outside ~/lab13.

12>&1 in the wrong order

Run this:

ls servers.txt nofile.txt 2>&1 > order.txt
cat order.txt

You’ll get:

ls: cannot access 'nofile.txt': No such file or directory
servers.txt

The error went to the screen and only servers.txt went to the file. Bash read left to right: 2>&1 copied “stdout is the terminal” into stderr, and then > order.txt moved only stdout. In a cron job the screen doesn’t exist, so those errors are mailed to a mailbox nobody reads, or lost, and the log looks clean.

Fix: file first, then 2>&1.

ls servers.txt nofile.txt > order.txt 2>&1
cat order.txt

You should see this:

ls: cannot access 'nofile.txt': No such file or directory
servers.txt

✅ Checkpoint: This time nothing printed while the command ran, and order.txt holds both lines.

2Sorting a file into itself

Run this:

printf 'web02\ndb01\nweb01\n' > s2.txt
sort s2.txt > s2.txt
wc -c s2.txt

You’ll get:

0 s2.txt

The file is empty. The shell opened s2.txt for > and emptied it before sort started, so sort read an empty file. The same thing happens with grep ... file > file and sed ... file > file, and people have wiped a config this way on a live server.

Fix: write to a new file and move it into place, or use the tool’s own in-place option.

printf 'web02\ndb01\nweb01\n' > s2.txt
sort s2.txt > s2.sorted && mv s2.sorted s2.txt
cat s2.txt
sort -o s2.txt s2.txt

You should see this:

db01
web01
web02

✅ Checkpoint: s2.txt has three sorted lines.

3A pipeline that hides a failure

Run this:

cat nofile.txt | wc -l
echo "status: $?  all: ${PIPESTATUS[@]}"

You’ll get:

cat: nofile.txt: No such file or directory
0
status: 0  all: 1 0

The pipeline reports success (status 0) because wc, the last command, worked. A script that checks $? would carry on with a count of 0 and maybe report “no errors found” to the team. PIPESTATUS shows cat failed with 1.

Fix: turn on pipefail in scripts.

set -o pipefail
cat nofile.txt | wc -l
echo "status: $?"
set +o pipefail

You should see this:

cat: nofile.txt: No such file or directory
0
status: 1

✅ Checkpoint: With pipefail on, the status is 1.

This happened at work

The nightly job that “never failed”

A colleague on the platform team at a video streaming service in Singapore found this one during a quarterly restore test. The team pulled last night’s copy of the content catalogue database from the backup bucket, and the restore failed because the file was 20 bytes, which is the size of an empty gzip file. The night before was the same, and so was every night for six weeks.

The backup job had a log and the log was clean. Every night it said “upload complete”, and the monitoring check only looked for that line. Her first idea was a storage permission problem, so she spent a morning checking bucket policies, which were fine.

Then she read the cron line properly: pg_dump catalog | gzip | upload.sh 2>&1 >> /var/log/backup.log. Two things were wrong. The 2>&1 sat before the >>, so every error went to cron’s mail, which went nowhere.

And without pipefail, the pipeline only reported upload.sh’s status, so when pg_dump started failing after a password rotation six weeks earlier, gzip compressed an empty stream and the upload “succeeded”.

The fix was a small script with set -euo pipefail, the redirection written as >> /var/log/backup.log 2>&1, and a size check that fails the job if the dump is under a sensible minimum. The bigger change was cultural: a backup only counts as working after a restore test, and the team now runs one every month.

Common mistakes and troubleshooting

MistakeWhat happensWhat to do instead
Writing 2>&1 > fileErrors go to the terminal or cron mail, not the fileWrite > file 2>&1, or &> file in bash
Using > when you meant >>The log or config is emptied and replacedUse >> for logs and turn on noclobber in your shell
sudo echo ... > /etc/filePermission denied, because your shell opens the fileUse echo ... | sudo tee -a /etc/file
sudo tee without -aThe whole file is replaced by one lineUse tee -a to append, and keep a backup of the file
Reading and writing the same file in one lineThe file ends up emptyWrite to a new file, then mv, or use sort -o / sed -i
Unquoted variablesNames with spaces split and wildcards expandQuote every variable: "$file"
find | xargs with plain newlinesNames with spaces break into piecesUse find -print0 | xargs -0, or find’s own -exec ... {} +

Interview questions

Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.

Fresher level

1. What are stdin, stdout and stderr, and how would you save only the errors of a command to a file?

Why they ask this: They want the basic model with the file descriptor numbers. Saying 2> confidently shows you’ve used it.

How you might answer: They’re the three standard streams every process starts with: stdin is file descriptor 0 for input, stdout is 1 for normal output and stderr is 2 for errors. By default all three are the terminal.

To keep only errors I redirect descriptor 2, like find / -name app.conf 2> errors.txt. The normal output still shows on screen.

2. What’s the difference between > and >>, and when have you used each?

Why they ask this: A simple question that tells them whether you’ve ever emptied a file by accident. They want a concrete example.

How you might answer: A single > empties the file and writes from the start, and >> adds to the end. I use > for a fresh report, like df -h > disk.txt before attaching it to a ticket.

For anything that builds up over time, like a cron job log, I always use >>, together with 2>&1 after it so errors land in the same log.

2–5 years experience

3. Your colleague runs sudo echo “line” >> /etc/hosts and gets Permission denied. Explain why and fix it.

Why they ask this: This checks that you understand the shell does the redirection before sudo runs. It’s a favourite because the error looks illogical.

How you might answer: The redirection is opened by the user’s own shell before sudo runs, so it’s student trying to open /etc/hosts for writing, not root. sudo only applies to the echo.

I’d use echo "line" | sudo tee -a /etc/hosts, where tee runs as root and opens the file. The other way is sudo sh -c 'echo line >> /etc/hosts', but tee is easier to read and the -a is the part people forget.

4. A cron job writes to a log, but when it fails the log shows nothing. What would you check?

Why they ask this: They want to see that you know redirection order, cron’s environment and exit codes. A structured answer beats a guess.

How you might answer: First I’d read the crontab line for the redirection order: >> log 2>&1 catches errors, but 2>&1 >> log sends them to cron mail. Then I’d check for pipelines without pipefail, because they report only the last command’s status.

I’d also check whether the script relies on PATH or relative paths that differ under cron, and look at journalctl -u crond on RHEL or -u cron on Ubuntu to confirm it ran at all.

Senior level

5. How do you write a shell pipeline in a production script so failures can’t be silently ignored?

Why they ask this: They want concrete habits: strict mode, checking statuses, and not trusting a log that says “done”. Experience with a real incident helps.

How you might answer: I start scripts with set -euo pipefail so an error, an unset variable or a failing part of a pipeline stops the script. Output goes to a log with >> "$LOG" 2>&1, and the script exits with a non-zero code that monitoring can alert on.

I don’t trust success messages alone, so I check the result: the backup file isn’t empty, the row count is in range, and a restore test runs on a schedule. If part of the pipeline is allowed to fail, I handle that case on purpose with PIPESTATUS instead of switching pipefail off.

6. When would you use xargs instead of find -exec, and how do you make either one safe with odd file names?

Why they ask this: They’re checking that you know the trade-offs and the null-separator habit. Bringing up the dry run shows maturity.

How you might answer: I use find’s own -exec cmd {} + or -delete when the input comes from find, because it hands names over directly. xargs is useful when the list comes from somewhere else, like grep -l or a file, or when I want parallel runs with -P.

For safety I always use -print0 with xargs -0 so spaces and newlines in names can’t split them, and I run the same command with echo or -print first and read the list before anything gets deleted.

Quick quiz

Q1. Which file descriptor number is stderr?

  1. 0
  2. 1
  3. 2
  4. 3
Show answer

C — 0 is stdin, 1 is stdout and 2 is stderr.

Q2. Which line saves both normal output and errors in run.log?

  1. cmd 2>&1 > run.log
  2. cmd > run.log 2>&1
  3. cmd > run.log 2> /dev/null
  4. cmd | run.log
Show answer

B — Redirections are read left to right: stdout goes to the file first, then stderr follows it.

Q3. Why does sudo echo x >> /etc/hosts fail for a normal user?

  1. echo can’t write files
  2. Your own shell opens /etc/hosts before sudo runs
  3. sudo blocks redirection on purpose
  4. /etc/hosts is immutable
Show answer

B — The redirection belongs to the shell. Use echo x | sudo tee -a /etc/hosts.

Q4. What does sort data.txt > data.txt leave you with?

  1. A sorted file
  2. An unchanged file
  3. An empty file
  4. An error message
Show answer

C — The shell empties data.txt for writing before sort reads it. Use sort -o or a temporary file.

Q5. With v=”a b”, what does echo “$v” print?

  1. a b
  2. a b
  3. $v
  4. Nothing
Show answer

B — Double quotes keep the spaces; without them bash splits the value into two words.

Quick recap

  • Every process has stdin (0), stdout (1) and stderr (2); the shell connects them to files, pipes or /dev/null before the command runs.
  • > overwrites, >> appends, 2> catches errors, and > file 2>&1 (in that order) puts both in one file.
  • sudo doesn’t cover your shell’s redirection, so write root-owned files with | sudo tee -a.
  • xargs turns a list into arguments; use -print0 and xargs -0 for names with spaces.
  • Double quotes expand $variables and $( ), single quotes don’t, and unquoted variables split on spaces; use pipefail in scripts.

Next lesson

Lesson 14: Text processing with grep, cut, sort, uniq, tr, sed and awk → You’ll put these pipes to work: filter log lines with grep, cut and count fields, and fix text with sed and awk, the way admins summarise logs during an incident.

Further reading: Linux pipes and redirection

Want to go deeper into Linux pipes and redirection? The bash manual’s pages on redirections, pipelines and quoting are short and exact. For the kernel side, read pipe(7), and for xargs, the GNU findutils manual.

You can also see the full plan for this course on the Linux Administration course page.

Scroll to Top

This website uses cookies

We use a few cookies to keep this site working. If you allow it, we also use Google Analytics to count visits so we can improve our lessons. You can accept all, deny the optional ones, or choose what to allow.