โ† Back to Golu.Net
๐Ÿ”Ž

Linux Search & Pipes

Finding text and files the Unix way โ€” grep, awk, sort, and the pipe that ties them together.

pipes grep awk sort ยท uniq wc redirection
๐Ÿ“„

The practice file

Everything below runs on one sample file: a fake web server log called access.log. A log is ideal because it has repeated IPs and status codes, so counting and ranking commands produce real, interesting output.

Each line looks like this:

access.log
203.0.113.42 - - [10/Oct/2025:09:14:28 +0000] "GET /admin HTTP/1.1" 200 47045 "-" "PostmanRuntime/7.29"

The whitespace-separated fields you'll use most:

Grab the ready-made file straight onto your Linux box and follow along โ€” no setup, no scripts:

bash
wget https://golu.net/access.log
# or, if you prefer curl:
curl -O https://golu.net/access.log

It's 500 lines of realistic traffic โ€” repeated IPs, mixed status codes and response sizes โ€” so every command below produces real, interesting output. โฌ‡ Download access.log directly

๐Ÿ”—

The pipe |

The pipe is the whole trick. It takes the output of one command and feeds it as the input of the next. Instead of one giant command, you chain small ones, each doing one job:

bash
command1 | command2 | command3

Read it left to right: do this, then pass it to that, then to that. This one idea powers every recipe on this page.

๐Ÿ“

head & tail โ€” top and bottom

head shows the start of a file, tail shows the end. Both default to 10 lines.

bash
head access.log          # first 10 lines (default)
head -n 5 access.log     # first 5
tail access.log          # last 10 lines
tail -n 20 access.log    # last 20
tail -f app.log          # "follow" โ€” stream new lines as they arrive

Your "top 10" is literally head โ€” the default count. tail -f is the one to remember for live logs: it keeps printing new lines until you hit Ctrl-C.

โš  Gotcha: the number needs -n

head 10 fails โ€” head reads 10 as a filename and errors out. Use head -n 10, or the shorthand head -10. Only the bare number with no dash is broken.

๐ŸŽฏ

grep โ€” search inside files

This is the one you'll use most. It keeps only the lines that match a pattern.

bash
grep "404" access.log        # lines containing 404
grep -i "post" access.log    # -i: case-insensitive (matches POST)
grep -n "500" access.log     # -n: show line numbers
grep -v "200" access.log     # -v: invert โ€” lines that DON'T match
grep -c "404" access.log     # -c: count matching lines
grep -r "TODO" ./src/        # -r: search a folder recursively
โš  grep patterns are regex, not filename wildcards

In grep, * means "zero or more of the previous character" โ€” not "anything." So grep 203* means 20 followed by zero-or-more 3s, which also matches plain 200. And . means "any single character." To match a literal IP, escape the dots: grep '203\.0\.113\.42'. When in doubt, drop the star: grep 203 just finds the substring.

โœ“ Quote your patterns

Wrap any pattern containing *, ., ?, [, or $ in single quotes so the shell doesn't try to expand it as a filename before grep runs.

๐Ÿ—‚๏ธ

find โ€” search for files

Different job from grep. find looks for files by name, size, or type โ€” it doesn't look at what's inside them.

bash
find . -name "*.log"          # all .log files under here
find . -type f -size +10M     # files bigger than 10 MB
find /etc -name "ssh*"        # files starting with "ssh"
โœ“ The one-line rule

grep searches inside files. find searches for files.

โ†”๏ธ

Pipeline order changes the meaning

The single most important habit. Whatever comes first in the pipe runs first โ€” so where you put head relative to grep decides what you actually get.

bash
head -n 10 access.log | grep 404   # 404s WITHIN the first 10 lines only
grep 404 access.log | head -n 10   # search the whole file, then first 10 matches

Same tools, opposite meaning. Rule of thumb: head before grep = "search only the top of the file." grep before head = "find matches everywhere, then trim to 10." The second is almost always what you want.

โœ“ Bonus: drop the cat

cat file | grep x can just be grep x file โ€” grep reads files directly. Harmless either way, but good to know.

๐Ÿ”€

sort

Reorders lines. Alphabetical by default; the flags are where the power is.

bash
sort access.log          # alphabetical (text)
sort -r access.log       # -r: reverse
sort -n numbers.txt      # -n: NUMERIC sort
sort -k9 -n access.log   # -k9: sort by the 9th field
sort -u names.txt        # -u: sort + drop duplicates
โš  Text vs numeric sort

By default sort compares character by character, so it thinks "100" comes before "9" (it sees 1 < 9). Use -n to sort by actual numeric value. This is why the top-10 recipe uses sort -rn โ€” reverse and numeric, so the biggest counts land on top.

Find the five heaviest responses by sorting on the size field:

bash
awk '{print $10, $7}' access.log | sort -rn | head -n 5
๐Ÿ†

uniq & the top-10 pattern

uniq -c collapses duplicate lines and prepends a count. But there's a catch that trips everyone up:

โš  Always sort before uniq

uniq only collapses duplicates that are next to each other โ€” it compares each line to the one before it, nothing more. If identical lines are scattered, it misses them. sort first pulls them together so uniq can see them as a block.

Put it together and you get the classic "top N most frequent" idiom โ€” here, the most active IPs:

bash
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 10

Reading it stage by stage:

output
    131 203.0.113.42
     93 192.168.1.10
     71 10.0.0.5
     69 198.51.100.7
     46 8.8.8.8
     31 172.16.0.1
     30 192.168.1.55
     29 10.0.0.99

Notice this uses two different sorts doing two different jobs: the first groups identical lines; the last ranks the counts. Swap $1 for $9 to rank status codes instead.

โšก

awk โ€” the field processor

The one idea

awk reads a file one line at a time and runs your instructions on each. The whole model is:

bash
awk 'PATTERN { ACTION }' file

"For every line where PATTERN is true, do ACTION." Leave out the pattern and the action runs on every line. Leave out the action and it just prints the line.

Fields โ€” why awk beats cut

awk splits each line on whitespace and hands you numbered pieces:

bash
awk '{print $1}' access.log        # the IP
awk '{print $1, $9}' access.log    # IP and status (comma = space between)

Patterns โ€” filtering by value

This is where awk replaces grep: it can compare a column to a number.

bash
awk '$9 == 404 { print $0 }' access.log        # status equals 404
awk '$10 > 40000 { print $7, $10 }' access.log # responses over 40 KB
awk '$9 == 404 && $10 > 20000' access.log      # two conditions
awk '/admin/ { print $1 }' access.log          # regex match, like grep

Math โ€” BEGIN and END

BEGIN runs once before any lines; END runs once after all of them. Perfect for totals.

bash
awk '{ total += $10 } END { print total }' access.log          # sum all bytes
awk '{ t += $10; n++ } END { print t/n }' access.log          # average size
awk '$9 == 404 { c++ } END { print c }' access.log            # count 404s

Arrays โ€” replacing sort | uniq -c

An associative array is a tally sheet keyed by anything you choose:

bash
# bytes sent to each IP โ€” grep/cut simply can't do this
awk '{ bytes[$1] += $10 } END { for (ip in bytes) print bytes[ip], ip }' access.log | sort -rn
โš  Always single-quote the awk program

awk {print $1} fails two ways: the shell splits on the space (awk sees a broken {print โ†’ "missing }"), and it expands $1 to the shell's own first argument (usually empty) before awk sees it. Single quotes tell the shell "hands off," so awk gets the intact program and reads $1 its own way โ€” as the first field. Use single quotes, never double (double still expands $).

โœ“ When to reach for what

grep: does this line contain X?  ยท  cut: give me column N.  ยท  awk: column N, but only when column M > 500, and sum column K. The moment you compare a column's value or do math, it's awk's job.

๐Ÿ“Š

column -t โ€” spreadsheet-style output

Ragged output is hard to read. column -t pads every column to line up like a table.

bash
awk '{print $1, $9}' access.log | column -t | head

Add a header row via awk's BEGIN and let column align it with the data:

bash
awk 'BEGIN {print "IP","STATUS"} {print $1,$9}' access.log | column -t | head
output
IP            STATUS
198.51.100.7  404
203.0.113.42  200
10.0.0.5      404

For fixed widths you control yourself, use printf inside awk โ€” %-16s means "string, 16 wide, left-aligned":

bash
awk '{printf "%-16s %-8s\n", $1, $9}' access.log | head
โœ“ Human vs machine

Use column -t only when a person reads the output. Drop it before wc, sort, or another grep โ€” those don't care about alignment, so it's wasted work.

๐Ÿ”ข

wc -l โ€” counting

There is no count command in Linux. The line counter is wc -l (word count, lines).

bash
grep 404 access.log | wc -l    # how many lines matched
โœ“ The core distinction

Without wc -l โ†’ you see the actual lines (the content). With wc -l โ†’ you see one number, the count. Same pipeline, two questions: "what are they?" vs "how many?"

Since counting matches is so common, grep does it in one step with -c:

bash
grep -c 404 access.log         # same result, no extra pipe
โš  What you count depends on what you feed in

grep -c '203' access.log scans whole lines, so it also catches 203 inside timestamps or byte sizes. Narrowing with awk first ($1 only) counts fewer. To count real IP matches: grep -c '203\.0\.113\.42' access.log.

Workflow: look first, then count. Run without wc -l to confirm you're matching the right stuff, then add | wc -l once you trust it.

๐Ÿ’พ

Redirection > >> 2>

Pipes send output to another command. Redirection sends it to a file.

bash
grep 404 access.log > errors.txt    # write to file (OVERWRITES)
grep 500 access.log >> errors.txt   # APPEND to the end
head 10 2> whoops.txt               # send ERRORS to a file (stream 2)
โš  > overwrites, >> appends

> wipes the file and writes fresh; >> adds to the end. Mixing these up is how people erase things โ€” burn it in early.

Normal output is "stream 1," errors are "stream 2" โ€” hence 2> for capturing error messages separately. You can redirect the end of any pipe:

bash
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head > top10.txt
๐Ÿงฐ

grep power-ups ยท cut ยท less

grep context & extraction

bash
grep -A 2 500 access.log   # match + 2 lines After
grep -B 2 500 access.log   # 2 lines Before + match
grep -C 2 500 access.log   # 2 lines of Context both sides
grep -oE 'HTTP/1.1" [0-9]+' access.log  # -o: print only the matching part

cut โ€” a lighter awk for simple columns

bash
cut -d' ' -f1 access.log | head   # -d' ': split on space, -f1: field 1

Same as awk '{print $1}' here. Use cut to slice a column; use awk when you need logic or math.

less โ€” read long output like a document

bash
grep GET access.log | less   # space=scroll, /word=search, q=quit
๐Ÿชค

Gotchas we hit โ€” the real lessons

These mistakes teach more than the commands. Every one traces back to the same few causes.

What brokeWhyFix
head 1010 read as a filenamehead -n 10 or head -10
grep 203* matched too much* = zero-or-more of prev char (regex), so it matched 20grep 203 or escape: '203\.0'
awk {print $1}shell split the space & ate $1single-quote it: awk '{print $1}'
... | countno such command... | wc -l
head | grep found nothingonly searched the first 10 linesput grep before head
โœ“ The three habits that prevent all of these

1. Quote your patterns and awk programs (single quotes).   2. Mind the order of the pipe โ€” first runs first.   3. Know whether the next stage is a human (align it) or a machine (keep it raw).

๐Ÿ“‹

Cheat sheet

TaskCommand
First / last 10 lineshead file ยท tail file
Follow a live logtail -f file
Find matching linesgrep 'pat' file
Count matchesgrep -c 'pat' file
Lines that don't matchgrep -v 'pat' file
Find files by namefind . -name '*.log'
Pull out a columnawk '{print $1}' file
Filter by column valueawk '$9==404' file
Sum a columnawk '{s+=$10} END{print s}' file
Top 10 most frequentโ€ฆ | sort | uniq -c | sort -rn | head
Align as a tableโ€ฆ | column -t
Count linesโ€ฆ | wc -l
Save (overwrite / append)โ€ฆ > f ยท โ€ฆ >> f
Scroll long outputโ€ฆ | less
๐Ÿ—“๏ธ

This week's practice

One small challenge a day, all on access.log. Try each yourself first, then reach for a hint only if stuck.

โœ“ How to actually learn a pipeline

Build it left to right and delete one stage at a time from the right end. Watching what changes at each step is the fastest way to understand why every piece is there.

โ†‘