Why this matters
Data arrives as text all the time: CSV exports, logs, /etc/passwd, the output of other commands. A handful of tiny programs, each doing one simple thing to lines of text, can answer a surprising number of questions about such data: how many, which distinct values, the top ten, which column. This lesson covers the five most common ones. Combined with grep from the previous lesson and the pipes from module 5, they form the everyday Unix data toolkit.
All of them are filters: they read the files named as arguments, or standard input if there are none, and write to standard output. None of them modifies its input file.
Open the shell for this lesson; /work/sales.csv is ready:
$ head -n 4 sales.csv
date,region,product,units,price
2026-10-01,north,widget,12,2.50
2026-10-01,south,gadget,5,9.99
2026-10-02,east,widget,20,2.50
wc: counting
wc (word count) counts lines, words and bytes:
$ wc sales.csv
16 16 505 sales.csv
$ wc -l sales.csv
16 sales.csv
$ wc -l < sales.csv
16
-l lines, -w words (runs of non-whitespace), -c bytes, -m characters (differs from bytes for non-ASCII text). Precisely, -l counts newline characters, so a last line without a trailing newline is not counted. 16 lines here means 15 sales plus the header.
head and tail, as slicers
You know head -n 5 and tail -n 5. Two more forms are useful for data:
$ tail -n +2 sales.csv | head -n 2
2026-10-01,north,widget,12,2.50
2026-10-01,south,gadget,5,9.99
tail -n +2 means "starting at line 2", the standard way to drop a header line. (head -n -1, with a minus, means "all but the last line".)
cut: columns
cut extracts parts of each line. With -d (delimiter) and -f (fields):
$ cut -d, -f3 sales.csv | head -n 3
product
widget
gadget
$ cut -d, -f2,4 sales.csv | head -n 3
region,units
north,12
south,5
$ cut -d: -f1,7 /etc/passwd | head -n 2
root:/bin/bash
daemon:/usr/sbin/nologin
Fields are numbered from 1. Lists and ranges work: -f1,3, -f2-4, -f3- (3 to the end). cut -c1-10 cuts characters instead of fields, handy for fixed-width text such as the timestamp at the start of a log line.
cut is simple on purpose, and has two limits: the delimiter is a single character, and every occurrence counts, so two spaces in a row mean an empty field between them. It also knows nothing about CSV quoting: a field like "Smith, John" is split at its comma. For space-aligned output (like ps or ls -l) use awk instead (lesson 4).
sort: ordering
sort reads all its input (it has to, to sort it) and prints the lines in order.
$ printf '%s\n' banana Apple cherry apple | sort
Apple
apple
banana
cherry
The important options:
| Option | Effect |
|---|---|
-n | numeric: compare numbers by value (9 before 10) |
-r | reverse |
-u | unique: output each distinct line once |
-t, | field separator (like cut's -d) |
-k2,2 | sort by field 2 only (from field 2 to field 2) |
-f | fold case: ignore upper/lower |
-h | human sizes: 2K < 1M < 3G |
Sorting by a column, for example sales by units, highest first:
$ tail -n +2 sales.csv | sort -t, -k4,4nr | head -n 3
2026-10-06,east,sprocket,30,0.75
2026-10-02,east,widget,20,2.50
2026-10-03,south,widget,15,2.50
-k4,4nr means "the key is field 4 only, compared numerically, reversed". Always write the end of the key (-k4,4); -k4 alone means "from field 4 to the end of the line", which surprises people when the later fields differ. You can give several keys; later keys break ties: sort -t, -k2,2 -k4,4nr sorts by region, then by units within each region.
Why does text sorting put 100 before 20? It compares character by character: 1 sorts before 2, and the comparison is over. -n parses the leading number instead.
Note: The order of letters depends on the locale (language settings). This sandbox uses
C.UTF-8, which sorts by character code: all uppercase letters before lowercase. Other systems may sortappleandAppletogether. In scripts that must behave the same everywhere, people writeLC_ALL=C sort.
uniq: collapsing repeats
uniq removes a line if it is identical to the line just before it. It only looks at adjacent lines, so you almost always sort first:
$ cut -d, -f3 sales.csv | tail -n +2 | uniq | head -n 4
widget
gadget
widget
gizmo
$ cut -d, -f3 sales.csv | tail -n +2 | sort | uniq
doohickey
gadget
gizmo
sprocket
widget
Its real power is -c, which prefixes each line with how many times it occurred:
$ tail -n +2 sales.csv | cut -d, -f3 | sort | uniq -c
2 doohickey
4 gadget
3 gizmo
1 sprocket
5 widget
$ tail -n +2 sales.csv | cut -d, -f3 | sort | uniq -c | sort -nr
5 widget
4 gadget
3 gizmo
2 doohickey
1 sprocket
sort | uniq -c | sort -nr is the frequency-count idiom. You will type it hundreds of times. Also useful: uniq -d prints only lines that were repeated, uniq -u only lines that were not.
tr: translating characters
tr works on characters, not lines or fields. It reads only standard input (it takes no file names). tr SET1 SET2 replaces each character of SET1 with the corresponding one of SET2:
$ echo 'Hello World' | tr 'a-z' 'A-Z'
HELLO WORLD
$ echo '2026-10-30' | tr '-' '/'
2026/10/30
$ echo 'a,b,,c' | tr ',' '\n'
a
b
c
Options:
-d SETdeletes characters:tr -d '\r'removes the carriage returns of Windows text files,tr -d '[:digit:]'removes digits.-s SETsqueezes runs of repeated characters into one:tr -s ' 'collapses multiple spaces. With two sets, it squeezes after translating:tr -s ' ' '\n'turns any run of spaces into a single newline.-ccomplements SET1 ("every character not in"):tr -cs 'a-zA-Z' '\n'puts each word on its own line, treating every non-letter as a separator.
'\n' inside the quotes is tr's own escape for newline; bash passes the backslash through unchanged because of the single quotes.
Putting them together
How many distinct products, and how many units were sold in the north?
$ tail -n +2 sales.csv | cut -d, -f3 | sort -u | wc -l
5
$ grep ',north,' sales.csv | cut -d, -f4
12
8
6
4
Summing those numbers is beyond these tools (that is awk's job, lesson 4), but finding and counting is not.
Common mistakes
uniqwithoutsort. Duplicates that are not adjacent survive.uniq -cthen reports the same value several times with small counts.- Counting the header. Data files often start with a header line;
tail -n +2removes it. - Text sort on numbers. Use
-n(or-hfor sizes like4K). -k2instead of-k2,2. The key runs to the end of the line.trwith a file name.tr a-z A-Z file.txtfails withtr: extra operand 'file.txt'. Usetr a-z A-Z < file.txt.cuton space-aligned columns.ps aux | cut -d' ' -f2returns empty fields, because the columns are padded with several spaces. Useawk '{print $2}'.
Exercises
Two tasks on sales.csv, a quiz on uniq, and two filter scripts that read standard input.
Recap
wc -lcounts lines;head/tail -n +2slice;cut -d, -fNpicks columns.sortwith-n -r -u -t -k N,Norders lines; set-kranges explicitly.uniqcollapses adjacent duplicates;sort | uniq -c | sort -nrcounts frequencies.trtranslates (a-z A-Z), deletes (-d) and squeezes (-s) characters from stdin.
Going further: tr -cs 'A-Za-z' '\n' < /etc/services | sort | uniq -c | sort -nr | head shows the most common words in a real system file. Also look at paste and column -t -s, to join files side by side and pretty-print CSV.