Guide contents

Lesson 5 of 14

Viewing, Sorting, and Counting Text

Most work on the command line is reading text, cutting it into pieces, and counting what is in it. This lesson covers the small commands that do this: they read standard input or files, write standard output, and combine through pipes (pipes and redirection). After it, a reader can look at any file, extract one column, rank the most frequent values, and compare two versions of a file.

Command Purpose
cat Print files, or join several files
less Page through a long file on screen
head, tail Print the first or last lines or bytes; tail -f follows a growing file
wc Count lines, words, and bytes
sort Order lines (-n, -r, -k, -t, -u, -h)
uniq Collapse or count repeated adjacent lines (-c, -d)
cut Select columns by delimiter or character position
tr Translate, delete, or squeeze characters
paste Join files line by line, side by side
column -t Align text into a table
diff, diff -u Show the differences between two files

All outputs below were produced by GNU coreutils on Linux in the C (POSIX) locale. Where macOS (BSD) behaves differently, the example says so. Every example creates its own files in a demo directory, so each one can be repeated on its own. The sample web server log used at the end is created in example 21.

Example 1: cat prints and joins files

cat (short for concatenate) writes the contents of the files it is given, in order, to standard output. This example creates two small files and prints them in three ways.

Terminal
mkdir -p demo && cd demo
printf 'apple\nbanana\ncherry\n' > fruits.txt
printf 'date\nelderberry\n' > more.txt
cat fruits.txt
echo '--- two files ---'
cat fruits.txt more.txt
echo '--- numbered ---'
cat -n fruits.txt
Output
apple
banana
cherry
--- two files ---
apple
banana
cherry
date
elderberry
--- numbered ---
     1	apple
     2	banana
     3	cherry

printf writes each \n as a line break, and > sends its output into a file. cat fruits.txt prints the three lines. cat fruits.txt more.txt prints the first file and then the second, with nothing between them, which is the original meaning of the name: it concatenates. The echo lines are only separators for reading the output. cat -n adds a line number and a tab in front of every line. The number is part of the output only; the file is unchanged.

Other options seen often: -A shows hidden characters (tabs as ^I, line ends as $), and -s squeezes repeated blank lines into one. cat with no file name reads standard input, which is why cat > notes.txt followed by typed text and Ctrl+D creates a file.

Example 2: less pages through a long file

less shows a file one screen at a time and lets the reader scroll and search without loading everything into the terminal history. It is interactive, so the command below is not run here; the table lists the keys used inside it.

Terminal
less /var/log/syslog
Key Action
Space or f Forward one screen
b Back one screen
Down, Up, j, k One line forward or back
g / G Go to the first / last line
/text then Enter Search forward for text
?text then Enter Search backward
n / N Next / previous match of the last search
F Follow the end of the file, like tail -f; Ctrl+C stops following
q Quit

Options given on the command line change the display:

Option Meaning
-N Show line numbers
-S Cut long lines at the screen edge instead of wrapping them (scroll sideways with the arrow keys)
-F Quit at once if the whole file fits on one screen
-R Pass color escape codes through (needed for colored output piped into less)
+G Start at the end of the file
+/text Start at the first match of text

When standard output is not a terminal, less stops being interactive and behaves like cat. This makes its behavior visible in a script:

Terminal
mkdir -p demo && cd demo
printf 'apple\nbanana\ncherry\n' > fruits.txt
less fruits.txt | head -n 2
Output
apple
banana

The pipe means less does not write to a terminal, so it prints the file unchanged and head -n 2 keeps the first two lines. On macOS the same less command and keys apply; the installed version is older, so check man less for newer options.

Example 3: head prints the first lines

head prints the beginning of its input. Without options it prints 10 lines. This example uses a file with the numbers 1 to 20.

Terminal
mkdir -p demo && cd demo
seq 1 20 > numbers.txt
head numbers.txt
echo '--- -n 3 ---'
head -n 3 numbers.txt
echo '--- -n -17 ---'
head -n -17 numbers.txt
echo '--- -c 10 on text ---'
printf 'apple\nbanana\ncherry\n' | head -c 10
echo
Output
1
2
3
4
5
6
7
8
9
10
--- -n 3 ---
1
2
3
--- -n -17 ---
1
2
3
--- -c 10 on text ---
apple
bana

seq 1 20 prints the integers from 1 to 20, one per line. The plain head prints lines 1 to 10. -n 3 limits the output to three lines. A negative count, -n -17, means "everything except the last 17 lines", so the output is lines 1 to 3. This negative form is a GNU extension: BSD head on macOS rejects it. -c 10 counts bytes instead of lines: the first 10 bytes of the text are apple, a line break, and bana, so the output stops in the middle of a word. The final echo adds the line break that -c did not print. The old form head -3 is accepted by both versions, but -n 3 is the documented spelling.

Example 4: tail prints the last lines

tail is the mirror image of head: it prints the last 10 lines by default, which is the usual way to look at the newest entries of a log.

Terminal
mkdir -p demo && cd demo
seq 1 20 > numbers.txt
tail numbers.txt
echo '--- -n 3 ---'
tail -n 3 numbers.txt
echo '--- -n +18 ---'
tail -n +18 numbers.txt
echo '--- -c 7 ---'
tail -c 7 numbers.txt
Output
11
12
13
14
15
16
17
18
19
20
--- -n 3 ---
18
19
20
--- -n +18 ---
18
19
20
--- -c 7 ---

19
20

Plain tail prints lines 11 to 20. -n 3 prints the last three lines. A plus sign changes the meaning: -n +18 means "start at line 18 and print everything after it", so the output is lines 18, 19, and 20. This form is used to skip a header line: tail -n +2 data.csv prints a file without its first line, and it works on macOS as well. -c 7 prints the last 7 bytes. The file ends with the bytes 1, 8, line break, 1, 9, line break, 2, 0, line break. The last 7 are a line break (the end of line 18), 19, a line break, 20, and a line break. The output therefore starts with an empty line, followed by 19 and 20.

Example 5: tail -f follows a growing file

tail -f keeps running after it prints the last lines and prints every line that is appended to the file later. It is the standard way to watch a log while a program runs. The example appends two lines from the same script and then stops tail with kill, so the output can be captured.

Terminal
mkdir -p demo && cd demo
printf 'starting\nready\n' > app.log
tail -n 2 -f app.log > followed.txt &
pid=$!
sleep 1
echo 'request 1' >> app.log
echo 'request 2' >> app.log
sleep 1
kill $pid
wait $pid 2>/dev/null
cat followed.txt
Output
starting
ready
request 1
request 2

tail -n 2 -f app.log prints the last two lines at once (starting and ready) and then waits. The trailing & runs it in the background, and > followed.txt collects what it prints. pid=$! stores the process id of the background job. >> appends a line to the file, and tail notices the change and copies the new line to its output. kill $pid stops tail (in a terminal, Ctrl+C does this), and wait collects the finished job. The final cat shows all four lines in the order they were written.

tail -f follows the file by its open file handle. When a log is rotated (the file is renamed and a new file with the old name is created), -f keeps reading the renamed file. tail -F (GNU and BSD) follows the name instead and reopens the file when it is replaced. Use -F for logs that are rotated.

Example 6: wc counts lines, words, and bytes

wc (word count) reads files or standard input and prints counts. It answers how many lines a file has, which is the most common use.

Terminal
mkdir -p demo && cd demo
printf 'apple\nbanana\ncherry\n' > fruits.txt
printf 'date\nelderberry\n' > more.txt
wc fruits.txt
echo '--- -l -w -c ---'
wc -l fruits.txt
wc -w fruits.txt
wc -c fruits.txt
echo '--- two files ---'
wc fruits.txt more.txt
echo '--- standard input ---'
cat fruits.txt | wc -l
Output
 3  3 20 fruits.txt
--- -l -w -c ---
3 fruits.txt
3 fruits.txt
20 fruits.txt
--- two files ---
 3  3 20 fruits.txt
 2  2 16 more.txt
 5  5 36 total
--- standard input ---
3

The first command prints three numbers followed by the file name: lines (3), words (3), and bytes (20). Each line holds one word here, so lines and words match. The byte count is 20 because apple, banana, and cherry have 5, 6, and 6 letters (17) plus 3 line breaks. wc -l prints only the line count, -w only the words, and -c only the bytes. -m counts characters instead of bytes, which differs for text such as é, which takes two bytes in UTF-8. With two files, wc prints one row for each file and a total row. When the input comes from a pipe, wc has no file name to print, so the last command prints only the number.

wc -l counts line breaks, not lines. A last line without a final line break is not counted. On macOS the numbers are padded with leading spaces, so compare values with $(wc -l < file) rather than text matching; the redirect form wc -l < file prints no file name on both systems.

Example 7: sort orders lines, and -n orders numbers

sort reads lines and prints them in order. The default order compares text character by character, which gives surprising results for numbers. The option -n compares numeric values.

Terminal
mkdir -p demo && cd demo
printf '10\n9\n100\n1\n' > nums.txt
sort nums.txt
echo '--- -n ---'
sort -n nums.txt
echo '--- words ---'
printf 'banana\nApple\ncherry\n' | sort
Output
1
10
100
9
--- -n ---
1
9
10
100
--- words ---
Apple
banana
cherry

The default order places 10 before 9, because it compares the first characters and 1 is smaller than 9. The sequence 1, 10, 100, 9 is correct as text and wrong as numbers. With -n, sort reads the leading number of each line and compares the values, which gives 1, 9, 10, 100. The last command shows the default order for words. In the C locale, sort compares byte values, so uppercase letters come before lowercase letters and Apple is first. In a UTF-8 locale such as en_US.UTF-8, the comparison follows locale collation rules instead, and the order of mixed-case words can differ from the C locale. Set LC_ALL=C before sort when a script needs the same order on every machine.

Example 8: sort -r reverses the order

-r reverses the comparison, so the largest or last item comes first. It combines with every other sort option.

Terminal
mkdir -p demo && cd demo
printf '10\n9\n100\n1\n' > nums.txt
printf 'apple\nbanana\ncherry\n' > fruits.txt
sort -r fruits.txt
echo '--- -n -r ---'
sort -n -r nums.txt
echo '--- -nr (combined) ---'
sort -nr nums.txt
Output
cherry
banana
apple
--- -n -r ---
100
10
9
1
--- -nr (combined) ---
100
10
9
1

sort -r on the fruit names prints cherry, banana, apple: the text order reversed. sort -n -r gives the numbers from largest to smallest: 100, 10, 9, 1. Single-letter options can be written together, so -nr means the same as -n -r. The pattern sort -nr | head prints the largest values and appears in many pipelines, including the one in example 21.

Example 9: sort -k sorts by a column

-k (key) chooses which part of each line is compared. Fields are separated by runs of blanks by default. The key -k2,2 means "start at field 2 and stop at field 2".

Terminal
mkdir -p demo && cd demo
cat > people.txt <<'EOF'
alice eng 5200
bob ops 4100
carol eng 6100
dave ops 3900
erin hr 4700
frank eng 4800
EOF
echo '--- by department ---'
sort -k2,2 people.txt
echo '--- by salary, numeric, highest first ---'
sort -k3,3nr people.txt
echo '--- department, then salary highest first ---'
sort -k2,2 -k3,3nr people.txt
Output
--- by department ---
alice eng 5200
carol eng 6100
frank eng 4800
erin hr 4700
bob ops 4100
dave ops 3900
--- by salary, numeric, highest first ---
carol eng 6100
alice eng 5200
frank eng 4800
erin hr 4700
bob ops 4100
dave ops 3900
--- department, then salary highest first ---
carol eng 6100
alice eng 5200
frank eng 4800
erin hr 4700
bob ops 4100
dave ops 3900

The file has three columns: name, department, and salary. -k2,2 sorts by the department only. Lines with the same department (alice, carol, and frank for eng) appear in the order alice, carol, frank. When the keys are equal, GNU sort compares the whole lines as a last step, and here the names happen to be in alphabetical order. -k3,3nr sorts by the salary field as a number, in reverse. The letters after the field numbers (n, r) apply to that key only. The third command gives two keys: the first compares departments, and the second breaks ties by salary, highest first. Without the ,2 in -k2, the key would run from field 2 to the end of the line, so the salary would take part in the comparison as text. Writing both the start and the end field avoids that.

Example 10: sort -t sets the field separator

-t names the character that separates fields. It is needed for CSV files and for files such as /etc/passwd that use :.

Terminal
mkdir -p demo && cd demo
cat > scores.csv <<'EOF'
id,name,score
1,alice,88
2,bob,95
3,carol,72
10,dave,95
EOF
tail -n +2 scores.csv | sort -t, -k3,3nr -k2,2
echo '--- by id, numeric ---'
tail -n +2 scores.csv | sort -t, -k1,1n
Output
2,bob,95
10,dave,95
1,alice,88
3,carol,72
--- by id, numeric ---
1,alice,88
2,bob,95
3,carol,72
10,dave,95

tail -n +2 removes the header line (example 4), so the header does not take part in the sort. -t, makes the comma the separator, and -k3,3nr sorts the third field (the score) as a number, highest first. The two entries with a score of 95 tie, and the second key -k2,2 orders them by name: bob, then dave. The second command sorts by the id as a number, so 10 comes last. With a text sort, 10 would come before 2. The same option sorts /etc/passwd by user id with sort -t: -k3,3n /etc/passwd.

Example 11: sort -u removes duplicates

-u (unique) prints only the first line from each group of lines that compare equal. It combines the sort and the removal of duplicates in one step.

Terminal
mkdir -p demo && cd demo
cat > people.txt <<'EOF'
alice eng 5200
bob ops 4100
carol eng 6100
dave ops 3900
erin hr 4700
frank eng 4800
EOF
printf 'b\na\nb\nc\na\n' | sort -u
echo '--- one line per department ---'
sort -u -k2,2 people.txt
Output
a
b
c
--- one line per department ---
alice eng 5200
erin hr 4700
bob ops 4100

The first command reads five lines with repeats and prints a, b, c: each value once, in sorted order. The second command applies -u to a key. Two lines are "equal" when the compared key is equal, so -u -k2,2 prints one line for each department. The line kept is the first in the input among those with the same department: alice for eng, bob for ops, and erin for hr. sort -u is not the same as uniq in one respect: uniq also counts (example 13), while sort -u cannot.

Example 12: sort -h sorts human-readable sizes

-h understands the suffixes that tools such as du -h and ls -lh print: K, M, G, and so on. -n cannot, because it stops reading at the letter.

Terminal
mkdir -p demo && cd demo
printf '1.5K\n200M\n2G\n10K\n512\n3M\n' > sizes.txt
sort -n sizes.txt
echo '--- -h ---'
sort -h sizes.txt
Output
1.5K
2G
3M
10K
200M
512
--- -h ---
512
1.5K
10K
3M
200M
2G

sort -n reads only the leading number and ignores the letter, so 2G is treated as 2 and 512 is treated as the largest number. The result is a wrong order. sort -h multiplies by the suffix: no suffix is bytes, K is kibibytes (1024 bytes), M is mebibytes, and G is gibibytes. The result is 512, 1.5K, 10K, 3M, 200M, 2G, which is the real order of size.

The common use is to rank directories by size. This example creates five files of known sizes and sorts the output of du:

Terminal
mkdir -p demo/sizes && cd demo/sizes
head -c 12288 /dev/zero > twelve-k.bin
head -c 204800 /dev/zero > twohundred-k.bin
head -c 3145728 /dev/zero > three-m.bin
head -c 1048576 /dev/zero > one-m.bin
head -c 8192 /dev/zero > eight-k.bin
du -sh * | sort -h
Output
8.0K	eight-k.bin
12K	twelve-k.bin
200K	twohundred-k.bin
1.0M	one-m.bin
3.0M	three-m.bin

du -sh * prints the disk usage of each file in human-readable units (-s summarizes, -h uses K, M, and G). The output sorted with -h runs from the smallest to the largest. du reports disk blocks, so sizes are rounded up to the file system block size (4 KiB here). The -h option of sort is a GNU feature; check man sort on macOS to see whether the installed version supports it.

Example 13: uniq -c counts repeated lines

uniq collapses adjacent identical lines into one. It compares only neighbors, so the input must be sorted first for it to find all repeats. With -c it puts the repeat count in front of each line.

Terminal
mkdir -p demo && cd demo
printf 'error\nok\nok\nerror\nok\nwarn\nok\n' > levels.txt
uniq levels.txt
echo '--- sort first ---'
sort levels.txt | uniq
echo '--- -c ---'
sort levels.txt | uniq -c
echo '--- most frequent first ---'
sort levels.txt | uniq -c | sort -rn
Output
error
ok
error
ok
warn
ok
--- sort first ---
error
ok
warn
--- -c ---
      2 error
      4 ok
      1 warn
--- most frequent first ---
      4 ok
      2 error
      1 warn

The plain uniq merges only the two ok lines that touch, and error, ok, and warn still appear more than once. After sort, equal lines are adjacent, and uniq prints each value once. uniq -c prefixes each line with its count, right-aligned in a column of width 7: ok appears 4 times, error 2 times, and warn once. The last pipeline sorts the counted lines by number in reverse, which puts the most frequent value first. The pattern sort | uniq -c | sort -rn is the standard way to build a frequency table from a column of values.

Example 14: uniq -d shows only duplicated lines

-d prints one copy of each line that appears more than once, which answers which values are repeated. Its counterpart -u prints the lines that appear exactly once.

Terminal
mkdir -p demo && cd demo
printf 'error\nok\nok\nerror\nok\nwarn\nok\n' > levels.txt
sort levels.txt | uniq -d
echo '--- -u ---'
sort levels.txt | uniq -u
Output
error
ok
--- -u ---
warn

error and ok occur more than once, so -d prints them, one time each. warn occurs once, so only -u prints it. A common use is finding duplicate entries in a list, for example sort emails.txt | uniq -d. -i makes the comparison ignore case, and -D prints every copy of each duplicated line instead of one.

Example 15: cut selects columns

cut prints chosen parts of every line. -d sets the delimiter, -f picks fields, and -c picks character positions.

Terminal
mkdir -p demo && cd demo
cat > scores.csv <<'EOF'
id,name,score
1,alice,88
2,bob,95
3,carol,72
EOF
cut -d, -f2 scores.csv
echo '--- fields 1 and 3 ---'
cut -d, -f1,3 scores.csv
echo '--- from field 2 to the end ---'
cut -d, -f2- scores.csv
echo '--- characters 1 to 3 ---'
cut -c1-3 scores.csv
Output
name
alice
bob
carol
--- fields 1 and 3 ---
id,score
1,88
2,95
3,72
--- from field 2 to the end ---
name,score
alice,88
bob,95
carol,72
--- characters 1 to 3 ---
id,
1,a
2,b
3,c

-d, makes the comma the delimiter, and -f2 prints the second field of each line, including the header name. -f1,3 prints fields 1 and 3, joined with the same delimiter. -f2- prints from field 2 to the end of the line. -c1-3 ignores delimiters and prints characters 1 to 3 of each line. The default delimiter is a tab. cut has two limits: it cannot reorder fields (-f3,1 still prints field 1 first), and it treats every delimiter as a separator, so a comma inside quotes in a real CSV file splits the field. For columns separated by runs of spaces, use awk (sed and awk) because cut treats each space as its own delimiter. GNU cut has --complement to print all fields except the selected ones; BSD cut does not.

Example 16: tr translates, deletes, and squeezes characters

tr reads standard input and replaces or removes single characters. It takes character sets, not file names, so it is always used with a pipe or a redirect.

Terminal
mkdir -p demo && cd demo
echo 'Hello, World' | tr 'a-z' 'A-Z'
echo '--- spaces to line breaks ---'
echo 'one two three' | tr ' ' '\n'
echo '--- delete digits ---'
echo 'a1b22c333' | tr -d '0-9'
echo '--- squeeze repeated spaces ---'
echo 'too     many    spaces' | tr -s ' '
echo '--- keep only letters ---'
echo 'abc-123_def!' | tr -cd 'a-zA-Z\n'
Output
HELLO, WORLD
--- spaces to line breaks ---
one
two
three
--- delete digits ---
abc
--- squeeze repeated spaces ---
too many spaces
--- keep only letters ---
abcdef

tr 'a-z' 'A-Z' maps each character in the first set to the character at the same position in the second set, so every lowercase letter becomes uppercase. The second command replaces each space with a line break, which turns a sentence into one word per line. -d deletes the characters in the set: all digits are removed. -s squeezes each run of the listed character into a single one, which collapses the runs of spaces. -c complements the set, meaning all characters not in it. -cd 'a-zA-Z\n' deletes everything that is not a letter or a line break, which leaves abcdef. The \n keeps the final line break. tr works on single bytes, so a multi-byte character such as é is not handled correctly by ranges on most systems.

Example 17: paste joins files side by side

paste prints line 1 of every file on one output line, then line 2, and so on. Columns are separated by a tab unless -d says otherwise.

Terminal
mkdir -p demo && cd demo
printf 'alice\nbob\ncarol\n' > names.txt
printf '88\n95\n72\n' > scores.txt
paste names.txt scores.txt
echo '--- -d, ---'
paste -d, names.txt scores.txt
echo '--- -s (serial) ---'
paste -s -d, names.txt
Output
alice	88
bob	95
carol	72
--- -d, ---
alice,88
bob,95
carol,72
--- -s (serial) ---
alice,bob,carol

The first command prints each name and its score separated by a tab. -d, changes the separator to a comma, which produces CSV lines. -s (serial) pastes the lines of one file into a single row instead of using several files as columns, so paste -s -d, names.txt prints alice,bob,carol. This is a short way to turn a list into a comma-separated line. When the files have different lengths, paste leaves the missing cells empty.

Example 18: column -t aligns text into a table

column -t reads lines, splits them into fields at blanks, and pads each column to the width of its longest entry, so the output lines up.

Terminal
mkdir -p demo && cd demo
printf 'name dept salary\nalice eng 5200\nbob operations 4100\ncarol eng 6100\n' > table.txt
cat table.txt
echo '--- column -t ---'
column -t table.txt
echo '--- CSV with -s, ---'
printf 'id,name,score\n1,alice,88\n10,dave,95\n' | column -t -s,
Output
name dept salary
alice eng 5200
bob operations 4100
carol eng 6100
--- column -t ---
name   dept        salary
alice  eng         5200
bob    operations  4100
carol  eng         6100
--- CSV with -s, ---
id  name   score
1   alice  88
10  dave   95

The raw file is hard to read because operations is longer than the other departments. column -t (table mode) measures each column and pads it, so every row lines up. By default, any run of blanks separates fields. -s, sets the input separator to a comma, which makes the command usable on CSV output. Fields that contain spaces need a different separator, because the default splits on every blank. BSD column on macOS supports -t and -s the same way.

Example 19: diff compares two files

diff prints the lines that differ between two files and says how to change the first one into the second. Its exit status is 0 when the files are identical, 1 when they differ, and 2 on an error.

Terminal
mkdir -p demo && cd demo
printf 'host=localhost\nport=8080\nmode=dev\nlog=info\n' > old.conf
printf 'host=localhost\nport=9090\nmode=dev\nlog=info\ncache=on\n' > new.conf
diff old.conf new.conf
echo "exit status: $?"
diff old.conf old.conf
echo "exit status: $?"
Output
2c2
< port=8080
---
> port=9090
4a5
> cache=on
exit status: 1
exit status: 0

The first output line, 2c2, reads "line 2 of the first file is changed (c) into line 2 of the second file". The lines starting with < come from the first file, and the lines starting with > come from the second. --- separates the two groups. 4a5 means "after line 4 of the first file, add (a) line 5 of the second file", and the added line cache=on follows with >. A deletion appears as d, for example 3d2. echo $? prints the exit status: 1 for the two different files. Comparing a file with itself prints nothing and returns status 0, which makes diff -q a b (report only whether the files differ) usable in an if statement.

Example 20: diff -u prints a unified diff

-u prints the differences in the unified format, which shows changed lines with unchanged lines around them. Git, code reviews, and the patch command use this format.

Terminal
mkdir -p demo && cd demo
printf 'host=localhost\nport=8080\nmode=dev\nlog=info\n' > old.conf
printf 'host=localhost\nport=9090\nmode=dev\nlog=info\ncache=on\n' > new.conf
diff -u --label old.conf --label new.conf old.conf new.conf
Output
--- old.conf
+++ new.conf
@@ -1,4 +1,5 @@
 host=localhost
-port=8080
+port=9090
 mode=dev
 log=info
+cache=on

The two header lines --- and +++ name the old and the new file. Normally they also show the modification time of each file, which changes between runs, so --label replaces them here with fixed names. The line @@ -1,4 +1,5 @@ is a hunk header: the old file contributes 4 lines starting at line 1, and the new file contributes 5 lines starting at line 1. Below it, a line that starts with a space is unchanged context, a line that starts with - exists only in the old file, and a line that starts with + exists only in the new file. Three context lines are shown before and after each change by default; -U 1 reduces this to one. Saving the output with diff -u old new > change.patch produces a file that patch can apply. diff -ru dir1 dir2 compares two directories recursively. BSD diff accepts -u with the same format; the label option is documented in man diff.

Example 21: a log-analysis pipeline

Sorting, counting, and cutting combine into short pipelines that answer questions about a log. This example creates a web server log with 30 requests and builds a "top clients" report step by step. Each log line holds the client address in field 1, the request path in field 7, and the status code in field 9, when fields are split at spaces.

Terminal
mkdir -p demo && cd demo
cat > access.log <<'EOF'
10.0.0.9 - - [03/Oct/2026:10:00:00 +0000] "GET /index.html HTTP/1.1" 500 350
10.0.0.5 - - [03/Oct/2026:10:00:15 +0000] "GET /missing HTTP/1.1" 404 2048
10.0.0.14 - - [03/Oct/2026:10:00:30 +0000] "GET /login HTTP/1.1" 200 512
10.0.0.5 - - [03/Oct/2026:10:00:45 +0000] "GET /api/orders HTTP/1.1" 200 1024
10.0.0.5 - - [03/Oct/2026:10:01:00 +0000] "GET /api/orders HTTP/1.1" 200 8192
10.0.0.5 - - [03/Oct/2026:10:01:15 +0000] "GET /index.html HTTP/1.1" 200 8192
10.0.0.21 - - [03/Oct/2026:10:01:30 +0000] "GET / HTTP/1.1" 200 512
10.0.0.14 - - [03/Oct/2026:10:01:45 +0000] "GET /index.html HTTP/1.1" 301 4096
10.0.0.5 - - [03/Oct/2026:10:02:00 +0000] "GET / HTTP/1.1" 301 8192
10.0.0.33 - - [03/Oct/2026:10:02:15 +0000] "GET /index.html HTTP/1.1" 200 8192
10.0.0.14 - - [03/Oct/2026:10:02:30 +0000] "GET /index.html HTTP/1.1" 404 512
10.0.0.14 - - [03/Oct/2026:10:02:45 +0000] "GET / HTTP/1.1" 200 8192
10.0.0.5 - - [03/Oct/2026:10:03:00 +0000] "GET /static/app.js HTTP/1.1" 500 2048
10.0.0.9 - - [03/Oct/2026:10:03:15 +0000] "GET /api/orders HTTP/1.1" 404 2048
10.0.0.5 - - [03/Oct/2026:10:03:30 +0000] "GET /index.html HTTP/1.1" 200 512
10.0.0.14 - - [03/Oct/2026:10:03:45 +0000] "GET /login HTTP/1.1" 304 0
10.0.0.52 - - [03/Oct/2026:10:04:00 +0000] "GET /static/app.js HTTP/1.1" 304 0
10.0.0.9 - - [03/Oct/2026:10:04:15 +0000] "GET / HTTP/1.1" 200 8192
10.0.0.9 - - [03/Oct/2026:10:04:30 +0000] "GET /missing HTTP/1.1" 404 1024
10.0.0.60 - - [03/Oct/2026:10:04:45 +0000] "GET /api/orders HTTP/1.1" 200 350
10.0.0.5 - - [03/Oct/2026:10:05:00 +0000] "GET /login HTTP/1.1" 404 2048
10.0.0.21 - - [03/Oct/2026:10:05:15 +0000] "GET /login HTTP/1.1" 304 0
10.0.0.14 - - [03/Oct/2026:10:05:30 +0000] "GET /api/orders HTTP/1.1" 200 512
10.0.0.88 - - [03/Oct/2026:10:05:45 +0000] "GET /api/orders HTTP/1.1" 200 512
10.0.0.33 - - [03/Oct/2026:10:06:00 +0000] "GET /api/users HTTP/1.1" 304 0
10.0.0.9 - - [03/Oct/2026:10:06:15 +0000] "GET /api/orders HTTP/1.1" 404 512
10.0.0.60 - - [03/Oct/2026:10:06:30 +0000] "GET /api/users HTTP/1.1" 200 8192
10.0.0.5 - - [03/Oct/2026:10:06:45 +0000] "GET / HTTP/1.1" 200 2048
10.0.0.5 - - [03/Oct/2026:10:07:00 +0000] "GET /index.html HTTP/1.1" 500 4096
10.0.0.60 - - [03/Oct/2026:10:07:15 +0000] "GET /api/orders HTTP/1.1" 200 1024
EOF
wc -l access.log
head -n 3 access.log
Output
30 access.log
10.0.0.9 - - [03/Oct/2026:10:00:00 +0000] "GET /index.html HTTP/1.1" 500 350
10.0.0.5 - - [03/Oct/2026:10:00:15 +0000] "GET /missing HTTP/1.1" 404 2048
10.0.0.14 - - [03/Oct/2026:10:00:30 +0000] "GET /login HTTP/1.1" 200 512

The file has 30 lines. Each line follows the common log format: client address, two unused fields (-), a timestamp in brackets, the quoted request, the status code, and the response size in bytes. The first stage extracts one column.

Terminal
cd demo
cut -d' ' -f1 access.log | head -n 5
echo '--- sorted ---'
cut -d' ' -f1 access.log | sort | head -n 5
Output
10.0.0.9
10.0.0.5
10.0.0.14
10.0.0.5
10.0.0.5
--- sorted ---
10.0.0.14
10.0.0.14
10.0.0.14
10.0.0.14
10.0.0.14

cut -d' ' -f1 prints the first space-separated field, which is the client address. Equal addresses are not yet adjacent, so uniq could not count them. Adding sort groups them. The next stages count the groups and rank them.

Terminal
cd demo
echo '--- count per address ---'
cut -d' ' -f1 access.log | sort | uniq -c
echo '--- top 10 clients ---'
cut -d' ' -f1 access.log | sort | uniq -c | sort -rn | head -n 10
Output
--- count per address ---
      6 10.0.0.14
      2 10.0.0.21
      2 10.0.0.33
     10 10.0.0.5
      1 10.0.0.52
      3 10.0.0.60
      1 10.0.0.88
      5 10.0.0.9
--- top 10 clients ---
     10 10.0.0.5
      6 10.0.0.14
      5 10.0.0.9
      3 10.0.0.60
      2 10.0.0.33
      2 10.0.0.21
      1 10.0.0.88
      1 10.0.0.52

uniq -c writes one line per address with the number of requests. sort -rn orders these lines by the leading count, largest first, and head -n 10 keeps at most ten. The log has only eight distinct clients, so all of them appear: 10.0.0.5 made 10 requests and is the busiest. The same pipeline changes the question by changing the column. Field 9 gives the status codes, and field 7 gives the paths:

Terminal
cd demo
echo '--- status codes ---'
cut -d' ' -f9 access.log | sort | uniq -c | sort -rn
echo '--- paths with a 404 ---'
grep ' 404 ' access.log | cut -d' ' -f7 | sort | uniq -c | sort -rn
echo '--- number of server errors (5xx) ---'
cut -d' ' -f9 access.log | grep -c '^5'
Output
--- status codes ---
     15 200
      6 404
      4 304
      3 500
      2 301
--- paths with a 404 ---
      2 /missing
      2 /api/orders
      1 /login
      1 /index.html
--- number of server errors (5xx) ---
3

The status table shows how many responses of each kind the server sent. In the third pipeline, grep ' 404 ' keeps only the lines with that status, cut -f7 takes the path, and the rest ranks the paths, which shows the missing pages that clients ask for most. The last command counts the lines that start with 5 after the cut: grep -c prints the number of matching lines instead of the lines. Each stage reads the output of the previous stage, so a stage can be checked on its own by running the pipeline up to that point and looking at the output.

Interview points

  • sort compares text by default, so 10 comes before 9; -n compares numbers and -h understands K, M, and G suffixes.
  • uniq only merges adjacent duplicates, so it is almost always preceded by sort; sort | uniq -c | sort -rn builds a frequency table.
  • sort -k2,2 uses one field as the key; -k2 runs to the end of the line. -t sets the field separator.
  • head -n -N (all but the last N lines) works with GNU only, while tail -n +N (from line N on) works on GNU and BSD.
  • tail -f follows the open file; tail -F follows the name and survives log rotation.
  • wc -l counts line breaks, so a final line without a line break is not counted.
  • cut cannot reorder fields and splits on every delimiter; awk handles runs of spaces and reordering.
  • diff exits with 0 when files are equal, 1 when they differ, and 2 on an error; diff -u produces the format used by Git and patch.
  • less acts like cat when its output is not a terminal; -S stops line wrapping and +G starts at the end.
  • Setting LC_ALL=C makes sort use byte order, which gives the same result on every machine.

Summary

  • cat prints and joins files; cat -n numbers lines.
  • less pages through a file; / searches, G jumps to the end, q quits.
  • head -n N and tail -n N print the first and last N lines; tail -n +N starts at line N.
  • tail -f prints appended lines as they arrive; -F also follows a replaced file.
  • wc -l, -w, and -c count lines, words, and bytes.
  • sort -n, -r, -k, -t, -u, and -h set numeric, reverse, key, separator, unique, and human-size ordering.
  • uniq -c counts and uniq -d lists duplicates among adjacent lines.
  • cut -d -f selects fields and cut -c selects character positions.
  • tr translates (a-z A-Z), deletes (-d), and squeezes (-s) single characters.
  • paste joins files line by line; paste -s turns a column into a row.
  • column -t aligns whitespace-separated fields into a table.
  • diff old new lists changes, diff -u adds context in unified format, and the exit status reports whether the files differ.