Text Processing · about 50 min

awk: fields, patterns and actions

Why this matters

cut breaks on space-aligned columns, grep cannot compare numbers, and none of the tools so far can add things up. awk can do all of that in one line. It is a small programming language designed for exactly one job: reading text records split into fields, and doing something with the ones you care about. "Sum the sizes of all files owned by bob", "average response time per URL", "print the lines where column 4 is over 10" are all one-liners. (The name comes from its authors, Aho, Weinberger and Kernighan, in 1977.)

The model: pattern { action }

An awk program is a list of rules of the form

pattern { action }

awk reads its input one record (by default, one line) at a time, splits it into fields, and for each rule whose pattern is true for this record, runs the action. Like sed, it is an automatic loop over lines; you only write the body.

$ awk '{ print $1 }' /etc/passwd | head -n 2
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
$ awk -F: '{ print $1 }' /etc/passwd | head -n 2
root
daemon
$ awk -F: '$3 >= 1000' /etc/passwd
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
learner:x:1000:1000::/home/learner:/bin/bash

The last one is a pattern without an action: print lines whose third field (the user ID) is at least 1000. On Debian that means "real people" rather than system accounts, plus the special nobody account ($3 >= 1000 && $3 < 65534 would exclude it).

Always put the program in single quotes: $1 means something to bash too, and you want awk to see it.

Fields and built-in variables

awk splits each record at the field separator. By default that is "any run of spaces and tabs", with leading and trailing whitespace ignored, which is exactly right for aligned output like ls -l or ps, where cut fails. -F sets another separator: -F, for CSV, -F: for /etc/passwd, -F'\t' for tabs.

NameMeaning
$0the whole record (line)
$1, $2, ...fields 1, 2, ...
NFnumber of fields in this record
$NFthe last field ($(NF-1) the one before it)
NRnumber of the current record, counting from 1 across all input
FS, OFSinput and output field separators
$ echo 'alice 34 Lisbon PT' | awk '{ print NF, $NF, $1 }'
4 PT alice
$ ls -l /etc | awk 'NR > 1 { print $5, $NF }' | head -n 3
1108 alternatives
32 apparmor.d
138 apt

print a, b prints its arguments separated by OFS (a space by default) and ends with a newline. print a b (no comma) concatenates them with nothing in between.

Patterns

A pattern can be:

Open the shell for this lesson; sales.csv is in /work.

$ awk -F, '$2 == "east"' sales.csv
2026-10-02,east,widget,20,2.50
2026-10-04,east,doohickey,1,49.00
2026-10-06,east,sprocket,30,0.75
2026-10-08,east,doohickey,3,49.00
$ awk -F, '$3 ~ /^g/ && $4 >= 8 { print $1, $3, $4 }' sales.csv
2026-10-03 gadget 8
2026-10-05 gadget 11

Strings and numbers

awk has no separate number type to declare. A value is treated as a number when used with arithmetic or compared with a number, and as a string otherwise. Field values that look like numbers compare numerically: $4 > 10 compares 12 and 10 as numbers. But the header field units does not look like a number, so "units" > 10 becomes a string comparison (and u sorts after 1, so it is true). That is why data scripts start with NR > 1.

Variables, arithmetic and END

Variables need no declaration. An unset variable is the empty string, which counts as 0 in arithmetic. This makes accumulating natural:

$ awk -F, 'NR > 1 { units += $4 } END { print units }' sales.csv
136
$ awk -F, 'NR > 1 { n++; sum += $4 } END { print "average:", sum / n }' sales.csv
average: 9.06667

The main rule runs for every line; END runs once at the end, when the totals are complete. BEGIN is for setup before the first line, such as printing a header or setting OFS:

$ awk -F, 'BEGIN { OFS = "\t" } NR > 1 { print $3, $4 }' sales.csv | head -n 2
widget    12
gadget    5

Arithmetic works as you expect: + - * / % and ^ for powers, with floating-point results. x++, x += 3 and friends exist too.

printf: controlled formatting

print uses a default number format (9.06667 above). printf gives control, using the same format language as C and the shell's printf command:

$ awk -F, 'NR > 1 { t += $4 * $5 } END { printf "%.2f\n", t }' sales.csv
832.19
$ awk -F, 'NR > 1 { printf "%-10s %5d\n", $3, $4 }' sales.csv | head -n 3
widget        12
gadget         5
widget        20

%s string, %d integer, %.2f two decimals, %-10s left-aligned in 10 columns, %5d right- aligned in 5. Unlike print, printf adds no newline: you write \n yourself.

Associative arrays: grouping

awk arrays are indexed by strings (associative arrays, also called maps or dictionaries). They make "per group" totals a one-liner:

$ awk -F, 'NR > 1 { units[$3] += $4 } END { for (p in units) print p, units[p] }' sales.csv | sort
doohickey 4
gadget 31
gizmo 11
sprocket 30
widget 60

units[$3] += $4 creates the entry for that product on first use (as empty, so 0) and adds to it. In END, for (p in units) visits each key. The order of for (k in arr) is unspecified (it depends on awk's internal hash table), so pipe the result through sort when order matters.

Count occurrences the same way: awk '{ count[$1]++ } END { for (k in count) print count[k], k }' is the awk version of sort | uniq -c, in one pass and without sorting the input.

When awk, when the others?

awk programs can be put in a file and run with awk -f prog.awk, and support if, while, for, functions and string functions (length, substr, split, tolower, sub, gsub). This lesson covers the one-liner use, which is most of what you will need.

Common mistakes

Exercises

A quiz on fields, a summing script with an edge case, two tasks on sales.csv, and a per-group report script.

Recap

Going further: ps -eo user,rss | awk 'NR > 1 { m[$1] += $2 } END { for (u in m) print u, m[u] }' shows memory use per user. GNU awk's manual is extensive; awk --help lists its options here.

Exercises

Exercise 1 · quiz

Fields and records

Given the input line alice 34 Lisbon PT, which statements are true? Select all that apply.

Answer
  • awk '{print $NF}' prints PT.

  • awk '{print NF}' prints 4.

  • awk '{print $2 + 1}' prints 35.

NF is the number of fields (4), and $NF is the field with that number, the last one (PT). $0 is the whole line, not the first field. awk converts strings that look like numbers automatically, so $2 + 1 is 35. And $1$3 concatenates with nothing in between, aliceLisbon; to get a space, separate them with a comma: print $1, $3.

Exercise 2 · write code

Add up a column

Write a script that reads lines of the form name amount from standard input (separated by spaces) and prints the sum of all amounts as a bare number.

If the input is empty, it must print 0, not an empty line.

File: script.sh

Starting point

awk '{ total += $2 } END { print total }'

Example

input

rent 950
food 312
power 74

expected output

1336

Example

input

  a   10
b 5

expected output

15
Hint 1

An awk variable that was never assigned is an empty string. In a numeric context it counts as 0, but print total prints it as a string.

Hint 2

Force a number: print total + 0.

Reference solution
awk '{ total += $2 } END { print total + 0 }'

Exercise 3 · task in a shell

Filter rows by a number

Open the shell. /work/sales.csv is the same sales file as in the previous lesson (date,region,product,units,price, with a header).

Using one awk command, write into /work/big-sales.txt every sale with more than 10 units, printing for each one the date, product and units separated by single spaces, e.g. 2026-10-01 widget 12, in file order. The header must not appear.

Hint 1

-F, splits on commas. A pattern before { } selects lines: $4 > 10.

Hint 2

awk -F, 'NR > 1 && $4 > 10 { print $1, $3, $4 }' sales.csv > big-sales.txt

Reference solution
awk -F, 'NR > 1 && $4 > 10 { print $1, $3, $4 }' sales.csv > big-sales.txt

Exercise 4 · task in a shell

Total revenue

Still with /work/sales.csv: compute the total revenue, the sum over all sales of units * price, and save it in /work/revenue.txt formatted with exactly two decimals (for example 123.40), on one line. Use awk's printf in an END block.

Hint 1

In the main block accumulate total += $4 * $5 for lines after the header; print in END.

Hint 2

awk -F, 'NR > 1 { total += $4 * $5 } END { printf "%.2f\n", total }' sales.csv > revenue.txt

Reference solution
awk -F, 'NR > 1 { total += $4 * $5 } END { printf "%.2f\n", total }' sales.csv > revenue.txt

Exercise 5 · write code

Totals per group

Write a script that reads a sales CSV with a header (date,region,product,units,price) from standard input and prints the total units per region, one region per line as region total (single space), sorted alphabetically by region.

Example output:

east 21
north 12

Use an awk associative array, and pipe the result through sort (awk's for (k in arr) visits keys in no particular order).

File: script.sh

Starting point

awk -F, 'NR > 1 { print $2, $4 }'

Example

input

date,region,product,units,price
2026-10-01,north,widget,12,2.50
2026-10-02,east,widget,20,2.50
2026-10-03,east,gizmo,1,14.00

expected output

east 21
north 12

Example

input

date,region,product,units,price
2026-10-01,west,a,3,1
2026-10-01,south,a,5,1
2026-10-02,west,b,4,1
2026-10-02,north,c,10,1
2026-10-03,east,a,7,1
2026-10-03,south,c,1,1

expected output

east 7
north 10
south 6
west 7
Hint 1

units[$2] += $4 adds the units to an entry keyed by the region name, creating it on first use.

Hint 2

In END, loop with for (r in units) print r, units[r], then | sort outside awk.

Reference solution
awk -F, 'NR > 1 { units[$2] += $4 } END { for (r in units) print r, units[r] }' | sort