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).
Table of Contents
How to use this article
- Create a test folder with the
access.logfrom the practice (the 16 lines are at the start of the practice). - 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.
- In “What’s wrong here?”, predict what will come out before reading the explanation. That’s where you learn the most.
- 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:
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.
| Step | Stream 1 | Stream 2 |
|---|---|---|
| at the start | screen | screen |
2>&1 | screen | screen (copy of 1) |
> all.txt | all.txt | screen |
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. JustFAILwould also work here (there’s no otherFAIL), but the full pattern makes clear what you’re looking for and won’t break if aLOGOUT FAILturns up tomorrow.> fails.txtbecause we want a fresh file each time; with>>, repeating the exercise would duplicate the lines.wc -l < fails.txtinstead ofwc -l fails.txt: with<bash opens the file andwcdoesn’t know its name, so it only prints5. The obvious alternative would give5 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:
| Stage | What comes out | Lines |
|---|---|---|
grep "LOGIN FAIL" | the 5 failure lines | 5 |
cut -d' ' -f4 | laura, ana, ana, dani, laura | 5 |
sort | ana, ana, dani, laura, laura | 5 |
uniq -c | 2 ana, 1 dani, 2 laura | 3 |
sort -nr | 2 laura, 2 ana, 1 dani | 3 |
tee fails-per-user.txt | the same, on screen and in the file | 3 |
- Without the
sortin the third stage, laura would come out split (that’s snippet 1). sort -nrand notsort -r: here all the numbers have one digit, but with a 10 the text order would fail.teegoes at the end because we want to save the full ranking; if there were aheadafter it, it would have to go before that.- laura comes before ana because they tie at 2 and
sort -rbreaks 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
<< EOFand>. The here-document givescattwo lines and>createsusage.txtwith them. Without quotes onEOF, bash replaces$USERwith your user (sergioin my run). It’s the only command with>: everything else appends. cat access.log tuesday.log 2> /dev/null.catreads both files one after the other; sincetuesday.logdoesn’t exist, it complains on stderr, and2> /dev/nullswallows the complaint. What it could read (Monday’s 16 lines) carries on through the pipe. The daytuesday.logexists, 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 +3starts 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.
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:
- Without
sort,uniq -cgives three groups:laura,ana anaandlauraagain, because the twolauralines aren’t adjacent. - With
sort, two groups:2 anaand2 laura. Same data, different order, different result. - In the last command,
wc -lcounts 0: the> x.txttook thehellointo the file (check withcat x.txt). It’s snippet 3. - Change the last one to
echo hello | tee x.txt | wc -l: now it counts 1 andx.txtalso containshello.
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. Nevercommand f > f.2>&1copies 1’s current destination: first> f, then2>&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… | Code | Result |
|---|---|---|
| save the output (from scratch) | command > f | f only has the new output |
| append at the end | command >> f | f grows |
| read a file through stdin | command < f | the command doesn’t see the name (wc -l < f → 16) |
| save only the errors | command 2> err.txt | normal output stays on screen |
| save everything | command > f 2>&1 or command &> f | output and errors, in order |
| throw errors away | command 2> /dev/null | clean screen; $? still tells you |
| write a warning to stderr | echo "WARNING" >&2 | can be separated with 2> |
| input text inside the command | cat << EOF > f … EOF | $USER is replaced (with 'EOF', it isn’t) |
Pipes and filters
| I want… | Code | Result with access.log |
|---|---|---|
| lines that contain something | grep "LOGIN FAIL" f | 5 lines |
| how many | grep -c "LOGIN FAIL" f | 5 |
| one column | cut -d' ' -f4 f | the users |
| distinct values | cut -d' ' -f3 f | sort -u | pc01 … pc04 |
| count repetitions | cut -d' ' -f4 f | sort | uniq -c | sort -nr | 5 laura first |
| the first / last N | head -n N / tail -n N | |
| from line N on | tail -n +N | no header |
| save and carry on | … | tee f | … | f with everything that went through |
| errors in the pipe too | command 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
| Symptom | Likely cause |
|---|---|
| The file has ended up empty (0 bytes) | You read and wrote the same file with > |
| My report only has the last figure | You used > instead of >> |
| The error still appears on screen | You only redirected 1 (>), or wrote 2>&1 > f |
grep -c counts 0 errors with a screen full of them | Errors go on stderr: 2>&1 is missing before | |
uniq -c repeats a value with split counts | sort is missing before uniq |
| “100” comes before “9” | sort without -n |
wc at the end of a pipeline gives 0 | There’s a > in the middle of the pipeline: use tee |
wc -l prints the file name | Use wc -l < f |
Before you hand it in, ask yourself
- Is there any
>that will overwrite a file I need, or the very file I’m reading? - In each command, where does stream 1 go and where does stream 2 go?
- If I use
uniq, is there asortbefore it? - 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 articlesDid this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)