Text Filters: Reshaping Lines with cut, sort, uniq, wc & tr
- Explain the filter model: a tool that reads lines on stdin, transforms them, and writes lines on stdout — chained with pipes
- Pull columns out of each line with cut (-d delimiter, -f fields, -c characters)
- Order data with sort (-n numeric, -r reverse, -k by field) and collapse duplicates with uniq — and know why uniq needs sorted input
- Count with wc, translate/delete characters with tr, and slice the top or bottom with head/tail — then combine them into a count-and-rank pipeline
- Compare two sorted files with comm (-23 = only in the first, -12 = in both) and merge them on a shared key with join
A filter reads lines and writes lines
See the shared shape of every text filter — and where it fits after finding files and searching them.
You can now locate files (
find) and search inside them (grep). The third everyday skill is reshaping the lines you get back — pulling out a column, sorting, counting, deduping. The tools that do this are filters.A filter follows one simple contract: it reads lines on standard input, does one small transformation, and writes lines on standard output. Because input and output are both just streams of lines, filters snap together with pipes — the output of one becomes the input of the next. This is the Unix philosophy in action: instead of one giant do-everything command, you chain a few tools that each do one job well.
The filters worth knowing:
- `cut` — pull out columns (fields or character ranges) from each line.
- `sort` — put the lines in order.
- `uniq` — collapse adjacent duplicate lines (and count them).
- `wc` — count lines, words, or characters.
- `tr` — translate or delete individual characters.
- `head` / `tail` — take just the first or last few lines.
Each reads lines and writes lines, so any of them drops into a pipeline. The art is picking the right small tools and ordering them.
printf 'banana\napple\ncherry\napple\n' | sort | uniqapple banana cherry
Three filters in a row: printf produces four lines, sort orders them, and uniq collapses the now-adjacent duplicate 'apple'. Each tool did one job and passed its lines along — that's the whole model. cut: pull columns out of each line
Extract specific fields or character ranges from structured lines.
Lots of data is columnar — each line is fields separated by a delimiter (a comma in a CSV, a colon in
/etc/passwd, spaces in a log). `cut` pulls out the columns you want:- `-d` sets the delimiter (the separator character).
-d','for CSV,-d':'for/etc/passwd. The default is a tab. - `-f` picks the fields by number, starting at 1.
-f1is the first field;-f1,3is fields 1 and 3;-f2-4is a range. - `-c` picks character positions instead of fields:
-c1-8is the first eight characters of each line.
So
cut -d':' -f1 /etc/passwdprints just the username from each line — the text before the first colon. The mental model:cutslices every line at the delimiter and keeps the pieces you name.printf 'ada:1000:/home/ada\nlinus:1001:/home/linus\n' | cut -d':' -f1,3ada:/home/ada linus:/home/linus
Split each line on ':' (-d':') and keep fields 1 and 3 (-f1,3) — the name and the home directory, dropping the number in the middle. cut re-joins the kept fields with the same delimiter. Check yourself
To print only the first field of a comma-separated line, which two cut options do you need?
- `-d` sets the delimiter (the separator character).
sort and uniq: order, then dedupe and count
Order lines the right way and collapse duplicates — and internalize why uniq needs sorted input.
`sort` puts lines in order. By default it sorts lexically (as text), which surprises people with numbers: as text,
10comes before2because it compares character by character. The flags fix this:- `-n` sorts numerically (so
2before10). - `-r` reverses the order (largest/last first).
- `-k N` sorts by field N, not the whole line (pair with
-tto set the field separator, likecut's-d).
`uniq` collapses duplicate lines — but with a catch that trips up everyone: it only removes duplicates that are adjacent. It compares each line to the one right before it. So
uniqon unsorted data misses duplicates that aren't next to each other. The fix is a rule: `sort` first, then `uniq`. Once the data is sorted, all copies of a line are adjacent, anduniqcan collapse them.uniq's star flag is `-c`: it prefixes each line with a count of how many times it occurred.sort | uniq -cis the canonical "tally up how many of each" move.When all you want is the distinct values, `sort -u` (unique) sorts and drops the duplicates in one step: the same lines as
sort | uniq, with one less process. Keepuniqfor when you need what only it can do, like-ccounts.printf '2\n10\n1\n2\n' | sort -n | uniq -c1 1 2 2 1 10sort -n orders the numbers correctly (1, 2, 2, 10 — not text order), which puts the two 2's next to each other. Then uniq -c collapses them and prints a count in front: '2 2' means the value 2 appeared twice. Without sort -n first, uniq would never see those 2's as adjacent. printf 'banana\napple\nbanana\ncherry\napple\n' | sort -uapple banana cherry
- -u
- unique: output only the first of each run of equal lines
Five lines in, three out: sort -u put them in order and kept one copy of each. This is the one-flag answer to 'give me the distinct values, sorted', the same result as sort | uniq without the second process. Check yourself
Sorting the lines 2, 10, 1 with plain sort (no -n) puts 10 before 2. Which flag makes sort treat them as numbers?
- `-n` sorts numerically (so
wc, tr, head and tail: count, translate, slice
Round out the toolkit with counting, character translation, and taking the top or bottom.
Three more filters finish the everyday set:
- `wc` counts. `wc -l` counts lines (the one you'll use most — 'how many matches?'),
-wcounts words,-ccounts bytes.grep … | wc -lanswers 'how many lines matched?'. - `tr` translates or deletes individual characters.
tr 'a-z' 'A-Z'upper-cases;tr -d '\r'deletes carriage returns (handy for cleaning Windows line endings);tr -s ' 'squeezes repeated spaces into one. Notetrworks on characters from stdin only — it takes no filename. - `head` and `tail` take a slice. `head -n 5` prints the first 5 lines; `tail -n 5` the last 5.
tailhas a superpower: `tail -f` follows a file, printing new lines as they're appended — the standard way to watch a live log.
These are the 'ends and edges' tools: count how much, tidy the characters, or grab just the top or bottom of a stream.
printf 'INFO ok\nERROR bad\nERROR worse\nWARN meh\n' | grep ERROR | wc -l2
A two-filter pipeline: grep keeps the ERROR lines, then wc -l counts them. This 'grep X | wc -l' pattern — how many lines match? — is one of the most common one-liners you'll write. printf 'hello world\n' | tr 'a-z' 'A-Z'HELLO WORLD
tr maps every character in the first set to the character at the same position in the second set, so a→A, b→B, and so on — upper-casing the line. tr reads from stdin and writes to stdout; it never takes a filename. printf 'Too MANY spaces\n' | tr -s ' ' | tr 'A-Z' 'a-z'too many spaces
- -s
- squeeze: collapse each run of a repeated character in the set to one
Two more tr jobs in one pipe. tr -s ' ' squeezes every run of repeated spaces down to a single space, and tr 'A-Z' 'a-z' is the mirror of the last example: it lower-cases. No regex and no fields, because these are pure character jobs. - `wc` counts. `wc -l` counts lines (the one you'll use most — 'how many matches?'),
comm and join: compare and combine two sorted files
Answer 'what's in one list but not the other?' with comm, and merge two tables on a shared key with join.
Every filter so far reads one stream. Two classic tools read two sorted files side by side, which answers a family of questions nothing else here can:
- `comm A B` compares the files line by line and prints three columns: lines only in A, lines only in B (indented one tab), and lines in both (indented two tabs). You almost never want all three, so you suppress the ones you don't:
-1,-2and-3hide columns 1, 2 and 3.comm -23 A Bleaves only column 1, the lines only in A (a set difference).comm -12 A Bleaves only column 3, the lines in both (an intersection).comm -13 A Bis what B has that A lacks. - `join A B` is a relational join: for each key (the first field, by default) that appears in both files, it prints the key followed by the rest of A's line and the rest of B's. Keys found in only one file are dropped.
Both tools walk the two files in step, which only works if both are sorted on the part they compare: the same rule as
uniq, for the same reason. Sort first if you aren't sure.printf 'alice\nbob\ncarol\ndave\n' > all.txt && printf 'bob\ndave\nerin\n' > active.txt && comm all.txt active.txtalice bob carol dave erin
- comm A B
- column 1 = only in A, column 2 = only in B, column 3 = in both
Two sorted rosters and all three columns. alice and carol sit at the left margin (only in all.txt), erin is indented once (only in active.txt), and bob and dave are indented twice (in both). Read the indentation, not the order: comm interleaves the columns as it walks both files together. comm -23 all.txt active.txt; echo '--- in both ---'; comm -12 all.txt active.txtalice carol --- in both --- bob dave
- -23
- hide columns 2 and 3: lines only in the first file
- -12
- hide columns 1 and 2: lines in both files
Suppress columns to ask a precise question. -23 hides 'only in active' and 'in both', leaving the accounts that are on the full roster but not active: a set difference in one command. -12 hides the two 'only' columns and leaves the intersection. printf '1 ada\n2 linus\n3 grace\n' > names.txt && printf '1 ops\n3 dev\n4 sec\n' > teams.txt && join names.txt teams.txt1 ada ops 3 grace dev
- join A B
- pair lines whose first fields match; print key + rest of A + rest of B
Two tables keyed on an id in field 1, both sorted by that id. join pairs the lines whose keys match and prints key, then names.txt's other fields, then teams.txt's: '1 ada ops'. Id 2 has no team and id 4 has no name, so neither appears. That is a database-style inner join with no database. Check yourself
Two sorted files, old.txt and new.txt. Which comm command prints only the lines that appear in BOTH?
- `comm A B` compares the files line by line and prints three columns: lines only in A, lines only in B (indented one tab), and lines in both (indented two tabs). You almost never want all three, so you suppress the ones you don't:
Now do it — build a count-and-rank pipeline
Chain the filters into the classic 'which values are most common?' one-liner.
One continuous experiment building the most useful pipeline in all of text processing: cut a field, sort it, count each value, then rank them. This 'top-N' shape answers 'which IP hit us most?', 'which status code is most common?', 'who ran the most commands?' — all the same pattern.
printf 'GET /a\nGET /b\nPOST /a\nGET /a\nPOST /c\n' > reqs.txt && cut -d' ' -f1 reqs.txtGET GET POST GET POST
Make a small request log, then cut the first field (the HTTP method, split on a space). We've isolated just the column we want to count — the first stage of the pipeline. cut -d' ' -f1 reqs.txt | sort | uniq -c3 GET 2 POSTAdd sort (so identical methods sit together) and uniq -c (to collapse and count). Now we have a tally: GET appeared 3 times, POST 2. The sort is essential — without it uniq couldn't see the GETs as adjacent. cut -d' ' -f1 reqs.txt | sort | uniq -c | sort -rn | head -n 1; echo '--- cleaning up ---'; rm reqs.txt3 GET --- cleaning up ---
The final two stages: sort -rn ranks the tally by count, highest first (numeric, reversed), and head -n 1 keeps only the top line. The whole pipeline — cut | sort | uniq -c | sort -rn | head — is the count-and-rank idiom you'll reuse constantly. Five small filters, each doing one job.
- A filter reads lines on stdin, does one transformation, and writes lines on stdout — so pipes chain them. Pick small tools and order them, rather than reaching for one big command.
- cut pulls columns from each line: -d sets the delimiter, -f picks fields (from 1), -c picks character positions.
- sort orders lines (-n numeric, -r reverse, -k by field); uniq collapses ADJACENT duplicates and -c counts them — which is why you almost always sort before uniq. sort -u sorts and dedupes in one step.
- comm compares two sorted files into three columns (only in A, only in B, in both); suppress the ones you don't want: -23 = only in A, -12 = in both. join merges two sorted files on their first field. Both need sorted input, just like uniq.
- wc -l counts lines, tr translates/deletes characters (stdin only), head/tail take the first/last lines (tail -f follows a live file). The count-and-rank idiom is cut | sort | uniq -c | sort -rn | head.
| You write | Effect |
|---|---|
| cut -d',' -f2 file | print the 2nd comma-separated field of each line |
| sort -n / sort -r | order numerically / in reverse |
| sort -k2 -t: | sort by the 2nd colon-separated field |
| sort | uniq | remove duplicate lines (sort makes copies adjacent) |
| sort -u | sort and remove duplicates in one step |
| sort | uniq -c | count how many times each line occurs |
| grep X | wc -l | count how many lines match X |
| tr 'a-z' 'A-Z' | upper-case every character |
| tr -s ' ' | squeeze runs of spaces into one |
| comm -23 a b | lines only in a (both files sorted) |
| join a b | merge lines that share a first-field key |
| cut … | sort | uniq -c | sort -rn | head | the count-and-rank pipeline |
