Redirection and pipes in Linux — exercises with failed logins, rankings that split apart and a 2>&1 in the wrong place (and a grep that counted zero errors with a screen full of them)

One afternoon, compiling my C assignment, I wanted to know how many errors gcc was giving me without counting them with my finger. I typed gcc programa.c | grep -c error and the terminal answered with a screen full of errors… and, at the very end, a 0. Zero errors, said grep, while I was looking at fifteen. It took me a while to get it: gcc writes its errors on stderr, and only stdout travels through the pipe. grep received nothing and counted, quite rightly, zero. With 2>&1 before the bar, the real number came out. These exercises are full of traps like that, so before you look at the solution, predict.

These are the exercises for redirection and pipes (if you haven’t done the practice, do it first). We work once more with access.log, the log of lab 1.3: 16 lines with date, time, computer, user, event (LOGIN/LOGOUT) and result (OK/FAIL).

How to use this article

  1. Create a test folder with the access.log from the practice (the 16 lines are at the start of the practice).
  2. Read each level’s statement and the expected output, and write your commands without looking at the solution. If you get stuck, read the hint.
  3. In “What’s wrong here?”, predict what will come out before reading the explanation. That’s where you learn the most.
  4. Compare with the solutions and read the reasoning behind each decision. At the end there’s the cheat sheet to review in 30 seconds.

Level 1 (basic): the failed attempts

Save in fails.txt the lines of access.log that are failed login attempts and show their content. Then print only the number of failures (without the file name after it).

Expected output (the content of fails.txt and then the number):

2026-10-19 08:05 pc02 laura LOGIN FAIL
2026-10-19 08:30 pc04 ana LOGIN FAIL
2026-10-19 08:31 pc04 ana LOGIN FAIL
2026-10-19 10:02 pc01 dani LOGIN FAIL
2026-10-19 10:20 pc02 laura LOGIN FAIL
5

grep filters and > saves. For wc -l not to print the name, don’t give it the name: give it the data on stdin.

Level 2 (intermediate): who fails the most

Work out how many failed attempts each person has, from most to fewest, and make them appear on screen and be saved in fails-per-user.txt at the same time, with a single pipeline. Then check how many lines that file has.

Expected output:

      2 laura
      2 ana
      1 dani
3 fails-per-user.txt

The counting pattern is “extract the column → sort → count → sort by number”. To see and save at once there’s a T-shaped command.

Level 3 (challenge): usage of each computer

Create usage.txt with a two-line header (Usage of the computers in lab 1.3 and Generated by <your user>, with the user filled in by bash, not typed by hand) and below it the number of entries for each computer. Tuesday’s log, tuesday.log, should be read too, but it doesn’t exist yet: its error must not appear on screen. Show usage.txt and, at the end, only the line of the most used computer.

Expected output:

Usage of the computers in lab 1.3
Generated by sergio
      4 pc01
      5 pc02
      3 pc03
      4 pc04
      5 pc02

(On your computer you’ll see your user instead of sergio.)

The header, with a here-document (<< EOF, without quotes so that $USER gets replaced). The data, with >>. To find the maximum, skip the two header lines with tail -n +3.

What’s wrong here?

Three snippets that look right. For each one: read the command, predict the output and only then read on.

Snippet 1: counting each person’s failures

# What's wrong here? 1: counting each person's failures
grep FAIL access.log | cut -d' ' -f4 | uniq -c

What do you think comes out? This is what really came out:

$ grep FAIL access.log | cut -d' ' -f4 | uniq -c
      1 laura
      2 ana
      1 dani
      1 laura

laura appears twice, with a 1 each time. It isn’t a bug in uniq: uniq compares each line only with the previous one. The users of the failures, in log order, are laura, ana, ana, dani, laura: the two ana lines are adjacent and get merged, but the two laura lines aren’t. Look at the diagram:

uniq only merges adjacent duplicatesOn the left, the users of the five failures in log order: laura, ana, ana, dani, laura. uniq -c makes four groups and laura appears twice with a 1. On the right, after sort we get ana, ana, dani, laura, laura: three groups and laura with a 2.Without sortuniq groupslaura1 lauraanaana2 anadani1 danilaura1 lauraWith sortuniq groupsanaana2 anadani1 danilauralaura2 laura
uniq compares each line only with the previous one: if equal lines are not next to each other, it counts them separately.

Using uniq -c without a sort before it. With small or already grouped data it seems to work, and the day it isn’t grouped you get split counts without any warning.

The fix is to put sort in front:

# Fix: sort before uniq, so that equal lines end up together
grep FAIL access.log | cut -d' ' -f4 | sort | uniq -c

And now it’s right:

$ grep FAIL access.log | cut -d' ' -f4 | sort | uniq -c
      2 ana
      1 dani
      2 laura

2 + 1 + 2 = 5 failures, as in the log.

If you only want the list of distinct values (without counting), sort -u does sort | uniq in one go.

Snippet 2: saving everything in all.txt

We use the check.sh from the practice: it writes three good lines on stdout and two warnings on stderr.

#!/bin/bash
# check.sh: checks the four computers in the lab (simulated with echo)
echo "pc01: correct"
echo "pc02: correct"
echo "WARNING pc03: disk almost full" >&2   # >&2: this message goes out on stderr
echo "pc04: correct"
echo "WARNING pc04: no network" >&2

We want to save all five lines in all.txt:

# What's wrong here? 2: saving the correct lines and the warnings in all.txt
bash check.sh 2>&1 > all.txt
cat all.txt

What appears on screen and what’s in the file?

$ bash check.sh 2>&1 > all.txt
WARNING pc03: disk almost full
WARNING pc04: no network
$ cat all.txt
pc01: correct
pc02: correct
pc04: correct

The warnings appear on screen and all.txt only has the three good lines. Redirections are processed from left to right: 2>&1 makes 2 point to wherever 1 points at that moment, which is still the screen. Then > all.txt moves 1 to the file, but 2 already had its destination and doesn’t follow.

StepStream 1Stream 2
at the startscreenscreen
2>&1screenscreen (copy of 1)
> all.txtall.txtscreen

Reading 2>&1 as “2 always goes with 1”. It’s a copy of 1’s current destination, not a permanent link.

The fix is to swap the order:

# Fix: first > all.txt and then 2>&1
bash check.sh > all.txt 2>&1
cat all.txt

Now 1 goes to all.txt first and 2>&1 copies that destination:

$ bash check.sh > all.txt 2>&1
$ cat all.txt
pc01: correct
pc02: correct
WARNING pc03: disk almost full
pc04: correct
WARNING pc04: no network

A clean screen and all five lines in all.txt, in the order the script wrote them.

If you struggle to remember the order, use &> all.txt: it does the same thing and there’s no order to mix up (it’s bash-specific).

Snippet 3: saving the failures and counting them at once

# What's wrong here? 3: saving the failures and counting them on the same line
grep FAIL access.log > fails.txt | wc -l
cat fails.txt | wc -l

What does the first wc -l count?

$ grep FAIL access.log > fails.txt | wc -l
0
$ cat fails.txt | wc -l
5

It counts 0, even though fails.txt has 5 lines (the second wc proves it). The > fails.txt takes all of grep‘s stdout to the file, so there’s nothing left for the pipe to pass on: wc gets an empty input. One output can’t go to two places at once with just > and |.

Mixing > and | in the same command and expecting the data to go to both places. The redirection wins and the pipe stays empty.

The fix is tee, which does exactly that: copies to the file and lets the data through.

# Fix: tee saves the copy and lets the data through to wc
grep FAIL access.log | tee fails.txt | wc -l

The real output:

$ grep FAIL access.log | tee fails.txt | wc -l
5

5, and fails.txt with its 5 lines.

When a command gives you “nothing” in a pipeline, look for a > hiding in the middle of the line.

Solutions

Level 1 solution

# Level 1: the failed attempts, saved and counted
grep "LOGIN FAIL" access.log > fails.txt  # > creates fails.txt with only the matching lines
cat fails.txt
wc -l < fails.txt                         # < : just the number, without the file name

The real output:

$ grep "LOGIN FAIL" access.log > fails.txt
$ cat fails.txt
2026-10-19 08:05 pc02 laura LOGIN FAIL
2026-10-19 08:30 pc04 ana LOGIN FAIL
2026-10-19 08:31 pc04 ana LOGIN FAIL
2026-10-19 10:02 pc01 dani LOGIN FAIL
2026-10-19 10:20 pc02 laura LOGIN FAIL
$ wc -l < fails.txt
5

Why it’s done this way:

  • grep "LOGIN FAIL" in quotes because the pattern has a space. Just FAIL would also work here (there’s no other FAIL), but the full pattern makes clear what you’re looking for and won’t break if a LOGOUT FAIL turns up tomorrow.
  • > fails.txt because we want a fresh file each time; with >>, repeating the exercise would duplicate the lines.
  • wc -l < fails.txt instead of wc -l fails.txt: with < bash opens the file and wc doesn’t know its name, so it only prints 5. The obvious alternative would give 5 fails.txt.
  • Hand count: laura at 08:05 and 10:20, ana at 08:30 and 08:31, dani at 10:02: 2 + 2 + 1 = 5.

Level 2 solution

# Level 2: who fails the most, saved and on screen at the same time
grep "LOGIN FAIL" access.log | cut -d' ' -f4 | sort | uniq -c | sort -nr | tee fails-per-user.txt
wc -l fails-per-user.txt                # one line per person who failed

The real output:

$ grep "LOGIN FAIL" access.log | cut -d' ' -f4 | sort | uniq -c | sort -nr | tee fails-per-user.txt
      2 laura
      2 ana
      1 dani
$ wc -l fails-per-user.txt
3 fails-per-user.txt

Why it’s done this way: the pipeline has five stages, and each one leaves the data ready for the next:

StageWhat comes outLines
grep "LOGIN FAIL"the 5 failure lines5
cut -d' ' -f4laura, ana, ana, dani, laura5
sortana, ana, dani, laura, laura5
uniq -c2 ana, 1 dani, 2 laura3
sort -nr2 laura, 2 ana, 1 dani3
tee fails-per-user.txtthe same, on screen and in the file3
  • Without the sort in the third stage, laura would come out split (that’s snippet 1).
  • sort -nr and not sort -r: here all the numbers have one digit, but with a 10 the text order would fail.
  • tee goes at the end because we want to save the full ranking; if there were a head after it, it would have to go before that.
  • laura comes before ana because they tie at 2 and sort -r breaks the tie with the rest of the line, reversed.

Level 3 solution

# Level 3: usage report for each computer, with a header and no error noise
cat << EOF > usage.txt
Usage of the computers in lab 1.3
Generated by $USER
EOF
cat access.log tuesday.log 2> /dev/null | cut -d' ' -f3 | sort | uniq -c >> usage.txt  # tuesday.log does not exist yet
cat usage.txt
tail -n +3 usage.txt | sort -nr | head -1     # from line 3 (no header): the most used

The real output:

$ cat << EOF > usage.txt
> Usage of the computers in lab 1.3
> Generated by $USER
> EOF
$ cat access.log tuesday.log 2> /dev/null | cut -d' ' -f3 | sort | uniq -c >> usage.txt
$ cat usage.txt
Usage of the computers in lab 1.3
Generated by sergio
      4 pc01
      5 pc02
      3 pc03
      4 pc04
$ tail -n +3 usage.txt | sort -nr | head -1
      5 pc02

Why it’s done this way:

  • The header with << EOF and >. The here-document gives cat two lines and > creates usage.txt with them. Without quotes on EOF, bash replaces $USER with your user (sergio in my run). It’s the only command with >: everything else appends.
  • cat access.log tuesday.log 2> /dev/null. cat reads both files one after the other; since tuesday.log doesn’t exist, it complains on stderr, and 2> /dev/null swallows the complaint. What it could read (Monday’s 16 lines) carries on through the pipe. The day tuesday.log exists, the same command will add it without changing anything.
  • cut -d' ' -f3 | sort | uniq -c >> usage.txt. The counting pattern, per computer, appended under the header. Hand count: pc01 4 + pc02 5 + pc03 3 + pc04 4 = 16.
  • tail -n +3 usage.txt | sort -nr | head -1. tail -n +3 starts at line 3, skipping the header (with +, “from line N”; without +, “the last N”). Then we sort by number and keep the first one: pc02.
Usage of each computer in lab 1.3Bars with the records of each computer: pc01 4, pc02 5, pc03 3 and pc04 4. pc02 is the most used, the one returned by the last command of level 3. The error from tuesday.log, which does not exist, is discarded with 2> /dev/null.Usage of each computer in lab 1.3pc014 recordspc025 records ← the most usedpc033 recordspc044 recordstuesday.log does not exist → 2> /dev/null
The bars are the lines of usage.txt without the header; tail -n +3 | sort -nr | head -1 keeps the longest one.

If you don’t skip the header, sort -nr treats lines without a number as 0 and puts them last, so it would also work here. But relying on that is fragile: with tail -n +3 you say exactly what you mean.

Try it in your terminal

Snippet 1 in miniature, so you can see it with your own eyes without the log. Type these commands and compare:

$ printf 'laura\nana\nana\nlaura\n' | uniq -c
      1 laura
      2 ana
      1 laura
$ printf 'laura\nana\nana\nlaura\n' | sort | uniq -c
      2 ana
      2 laura
$ echo hello > x.txt | wc -l
0

What to watch for:

  1. Without sort, uniq -c gives three groups: laura, ana ana and laura again, because the two laura lines aren’t adjacent.
  2. With sort, two groups: 2 ana and 2 laura. Same data, different order, different result.
  3. In the last command, wc -l counts 0: the > x.txt took the hello into the file (check with cat x.txt). It’s snippet 3.
  4. Change the last one to echo hello | tee x.txt | wc -l: now it counts 1 and x.txt also contains hello.

Cheat sheet: redirection and pipes

The essentials in 30 seconds

  • There are three streams: 0 stdin, 1 stdout and 2 stderr. By default, 0 is the keyboard and 1 and 2 are the screen.
  • > empties and writes; >> appends. Never command f > f.
  • 2>&1 copies 1’s current destination: first > f, then 2>&1.
  • Only stdout travels through |. To include errors: 2>&1 |.
  • To count: sort | uniq -c | sort -nr. To save and carry on: tee.

Redirections

I want…CodeResult
save the output (from scratch)command > ff only has the new output
append at the endcommand >> ff grows
read a file through stdincommand < fthe command doesn’t see the name (wc -l < f → 16)
save only the errorscommand 2> err.txtnormal output stays on screen
save everythingcommand > f 2>&1 or command &> foutput and errors, in order
throw errors awaycommand 2> /dev/nullclean screen; $? still tells you
write a warning to stderrecho "WARNING" >&2can be separated with 2>
input text inside the commandcat << EOF > f … EOF$USER is replaced (with 'EOF', it isn’t)

Pipes and filters

I want…CodeResult with access.log
lines that contain somethinggrep "LOGIN FAIL" f5 lines
how manygrep -c "LOGIN FAIL" f5
one columncut -d' ' -f4 fthe users
distinct valuescut -d' ' -f3 f | sort -upc01 … pc04
count repetitionscut -d' ' -f4 f | sort | uniq -c | sort -nr5 laura first
the first / last Nhead -n N / tail -n N
from line N ontail -n +Nno header
save and carry on… | tee f | …f with everything that went through
errors in the pipe toocommand 2>&1 | filter

Templates from the exercises

# Level 1: filter, save and count without the name
grep "LOGIN FAIL" access.log > fails.txt
wc -l < fails.txt                                   # 5

# Level 2: ranking shown and saved
grep "LOGIN FAIL" access.log | cut -d' ' -f4 | sort | uniq -c | sort -nr | tee fails-per-user.txt

# Level 3: header + data + maximum
cat << EOF > usage.txt
Usage of the computers in lab 1.3
Generated by $USER
EOF
cat access.log tuesday.log 2> /dev/null | cut -d' ' -f3 | sort | uniq -c >> usage.txt
tail -n +3 usage.txt | sort -nr | head -1           # 5 pc02

Symptom → likely cause

SymptomLikely cause
The file has ended up empty (0 bytes)You read and wrote the same file with >
My report only has the last figureYou used > instead of >>
The error still appears on screenYou only redirected 1 (>), or wrote 2>&1 > f
grep -c counts 0 errors with a screen full of themErrors go on stderr: 2>&1 is missing before |
uniq -c repeats a value with split countssort is missing before uniq
“100” comes before “9”sort without -n
wc at the end of a pipeline gives 0There’s a > in the middle of the pipeline: use tee
wc -l prints the file nameUse wc -l < f

Before you hand it in, ask yourself

  1. Is there any > that will overwrite a file I need, or the very file I’m reading?
  2. In each command, where does stream 1 go and where does stream 2 go?
  3. If I use uniq, is there a sort before it?
  4. If I silence errors with /dev/null, do I really not care about them?

That wraps up redirection and pipes. Next in Linux come shell scripts: saving all these commands in a file and running them in one go.

Keep goingAll Linux articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *