Bash & the POSIX Shell Module 11 – Text processing: the Unix toolbox
awk programs: arrays, joins and reports
Group data with awk associative arrays, join two files with the FNR==NR idiom, format reports with printf and write multi-line programs with functions.
What you will learn
- Group values by key with associative arrays and handle the unspecified for-in order
- Join two files with the FNR==NR idiom and detect missing or duplicate keys
- Format tabular reports with printf and calculate totals and percentages
- Write output to files and commands and use close before reading back
- Create maintainable awk programs in files with comments and functions
Before you start
What you’ll learn · Before you start
What you will learn
- Group values by key with associative arrays and handle the unspecified for-in order
- Join two files with the FNR==NR idiom and detect missing or duplicate keys
- Format tabular reports with printf and calculate totals and percentages
- Write output to files and commands and use close before reading back
- Create maintainable awk programs in files with comments and functions
Before you start
On this page
A kirana shop has 12 items across three categories. One awk program, run once, groups the sales by category, computes each category’s share of the grand total, finds the top seller and prints a formatted report with printf. The shell alternative would need several pipelines, temporary files and a loop to do the arithmetic.
You will group data by key with associative arrays, detect the silent traps of the FNR==NR join idiom, format output with printf, send output to files and commands, and write multi-line awk programs with comments and functions. The previous lesson covers fields, patterns and actions; this one builds on them.
Group by key with associative arrays
An An awk array that stores values keyed by strings, accessed with array[key], created on first use. uses strings as keys, not numbers. Writing count[$2]++ creates an entry keyed by the second field and increments it; sum[$2] += $3 accumulates a running total. Neither operation needs a declaration: the array and its entry appear on first use, starting at zero for numbers and the empty string for text.
"""awk associative arrays: count, sum, for-in order, sorting the output.
The session shows:
1. count[$2]++ groups sales by category
2. sum[$2]+=$3 keeps running totals
3. for (k in count) visits keys in an unspecified order (different between awk versions)
4. Piping awk output to sort gives a deterministic order
5. gawk's PROCINFO["sorted_in"] sorts keys inside the for-in loop
"""
from session import run, write
write("sales.csv", """\
Milk,Dairy,45
Bread,Bakery,30
Cheese,Dairy,120
Paneer,Dairy,80
Rusk,Bakery,25
Butter,Dairy,55
Bun,Bakery,15
Ghee,Dairy,250
""")
run("""
cat sales.csv
# Count items per category with count[$2]++
awk -F, '{ count[$2]++ } END { for (k in count) print k, count[k] }' sales.csv
# The for-in order is unspecified: gawk may print differently than mawk
# Verify with /usr/bin/awk (macOS awk, not gawk)
/usr/bin/awk -F, '{ count[$2]++ } END { for (k in count) print k, count[k] }' sales.csv
# Sum amounts per category with sum[$2]+=$3
awk -F, '{ sum[$2]+=$3 } END { for (k in sum) print k, sum[k] }' sales.csv
# Combine count and sum in one pass
awk -F, '{ count[$2]++; sum[$2]+=$3 } END { for (k in count) print k, count[k], sum[k] }' sales.csv
# Pipe to sort for a deterministic order (the portable fix)
awk -F, '{ count[$2]++; sum[$2]+=$3 } END { for (k in count) print k, count[k], sum[k] }' sales.csv | sort
# gawk's PROCINFO["sorted_in"] sorts inside the for-in loop itself
awk -F, '{ count[$2]++; sum[$2]+=$3 } END { PROCINFO["sorted_in"]="@ind_str_asc"; for (k in count) print k, count[k], sum[k] }' sales.csv
# Check if a key exists with (key in array)
awk -F, '{ count[$2]++ } END { if ("Dairy" in count) print "Dairy:", count["Dairy"]; if ("Frozen" in count) print "Frozen found" }' sales.csv
# Delete an entry with delete array[key]
awk -F, '{ count[$2]++ } END { delete count["Bakery"]; for (k in count) print k, count[k] }' sales.csv
""") Output
$ cat sales.csv
Milk,Dairy,45
Bread,Bakery,30
Cheese,Dairy,120
Paneer,Dairy,80
Rusk,Bakery,25
Butter,Dairy,55
Bun,Bakery,15
Ghee,Dairy,250
# Count items per category with count[$2]++
$ awk -F, '{ count[$2]++ } END { for (k in count) print k, count[k] }' sales.csv
Dairy 5
Bakery 3
# The for-in order is unspecified: gawk may print differently than mawk
# Verify with /usr/bin/awk (macOS awk, not gawk)
$ /usr/bin/awk -F, '{ count[$2]++ } END { for (k in count) print k, count[k] }' sales.csv
Bakery 3
Dairy 5
# Sum amounts per category with sum[$2]+=$3
$ awk -F, '{ sum[$2]+=$3 } END { for (k in sum) print k, sum[k] }' sales.csv
Dairy 550
Bakery 70
# Combine count and sum in one pass
$ awk -F, '{ count[$2]++; sum[$2]+=$3 } END { for (k in count) print k, count[k], sum[k] }' sales.csv
Dairy 5 550
Bakery 3 70
# Pipe to sort for a deterministic order (the portable fix)
$ awk -F, '{ count[$2]++; sum[$2]+=$3 } END { for (k in count) print k, count[k], sum[k] }' sales.csv | sort
Bakery 3 70
Dairy 5 550
# gawk's PROCINFO["sorted_in"] sorts inside the for-in loop itself
$ awk -F, '{ count[$2]++; sum[$2]+=$3 } END { PROCINFO["sorted_in"]="@ind_str_asc"; for (k in count) print k, count[k], sum[k] }' sales.csv
Bakery 3 70
Dairy 5 550
# Check if a key exists with (key in array)
$ awk -F, '{ count[$2]++ } END { if ("Dairy" in count) print "Dairy:", count["Dairy"]; if ("Frozen" in count) print "Frozen found" }' sales.csv
Dairy: 5
# Delete an entry with delete array[key]
$ awk -F, '{ count[$2]++ } END { delete count["Bakery"]; for (k in count) print k, count[k] }' sales.csv
Dairy 5
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 arrays_session.py
The session shows 8 items across 2 categories. count[$2]++ counted 5 Dairy and 3 Bakery; sum[$2] += $3 totalled 550 and 70 rupees. The one-pass version { count[$2]++; sum[$2]+=$3 } does both in a single scan of the file, which matters on large logs.
The for (k in count) loop visits every key, but the order is unspecified. gawk printed Dairy first, then Bakery; macOS awk (run as /usr/bin/awk) printed Bakery first. A script that expects a fixed order breaks when it moves to another machine.
Portable sorted output
Pipe the awk output to sort for a deterministic order on every awk. gawk’s PROCINFO["sorted_in"] = "@ind_str_asc" sorts inside the loop itself, which is faster but gawk-only.
Two more operations complete the array toolbox: (key in array) tests whether a key exists, and delete array[key] removes it. The session’s ("Dairy" in count) returned true, ("Frozen" in count) returned false, and delete count["Bakery"] left only Dairy in the output.
Checkpoint
Check: Associative arrays and for-in order
A formatted report with printf
A grouped report needs column alignment, headers and computed values. awk’s printf is the C-style formatter: printf "%-10s %5d", name, qty prints a left-aligned string of at most 10 characters and a right-aligned integer of at least 5 digits. Percentages use %5.1f%% (5 characters, 1 decimal, literal %).
"""awk report: a kirana-shop sales summary with totals, averages, top category and percentage share.
The session shows:
1. A multi-line awk program (via awk -f) that groups sales by category
2. printf for formatted output with column alignment
3. Calculating the top category and percentage share in END
"""
from session import run, write
write("kirana.csv", """\
Milk,Dairy,45
Bread,Bakery,30
Cheese,Dairy,120
Paneer,Dairy,80
Rice,Staples,65
Rusk,Bakery,25
Butter,Dairy,55
Atta,Staples,48
Bun,Bakery,15
Ghee,Dairy,250
Dal,Staples,72
Sugar,Staples,40
""")
write("report.awk", """\
BEGIN { FS = "," }
{ count[$2]++; sum[$2] += $3; grand += $3 }
END {
printf "%-10s %5s %8s %6s\\n", "Category", "Items", "Total", "Share"
printf "%-10s %5s %8s %6s\\n", "--------", "-----", "-----", "-----"
PROCINFO["sorted_in"] = "@ind_str_asc"
for (k in sum) {
pct = (sum[k] / grand) * 100
printf "%-10s %5d %8d %5.1f%%\\n", k, count[k], sum[k], pct
}
printf "%-10s %5s %8s %6s\\n", "--------", "-----", "-----", "-----"
printf "%-10s %5d %8d\\n", "TOTAL", NR, grand
}
""")
write("topcategory.awk", """\
BEGIN { FS = "," }
{ sum[$2] += $3 }
END {
max_cat = ""; max_val = 0
for (k in sum) { if (sum[k] > max_val) { max_val = sum[k]; max_cat = k } }
print "Top category:", max_cat, "with", max_val, "rupees"
}
""")
write("avgstats.awk", """\
BEGIN { FS = "," }
{ count[$2]++; sum[$2] += $3 }
END {
PROCINFO["sorted_in"] = "@ind_str_asc"
printf "%-10s %5s %8s %7s\\n", "Category", "Items", "Total", "Average"
printf "%-10s %5s %8s %7s\\n", "--------", "-----", "-----", "-------"
for (k in sum) {
avg = sum[k] / count[k]
printf "%-10s %5d %8d %7.1f\\n", k, count[k], sum[k], avg
}
}
""")
run("""
cat kirana.csv
# A one-line summary: count and sum per category (unsorted)
awk -F, '{ count[$2]++; sum[$2]+=$3 } END { for (k in count) print k, count[k], sum[k] }' kirana.csv
# The formatted report using awk -f
cat report.awk
awk -f report.awk kirana.csv
# Find the top category by total sales
cat topcategory.awk
awk -f topcategory.awk kirana.csv
# Report with averages using PROCINFO for sorted output
cat avgstats.awk
awk -f avgstats.awk kirana.csv
""") Output
$ cat kirana.csv
Milk,Dairy,45
Bread,Bakery,30
Cheese,Dairy,120
Paneer,Dairy,80
Rice,Staples,65
Rusk,Bakery,25
Butter,Dairy,55
Atta,Staples,48
Bun,Bakery,15
Ghee,Dairy,250
Dal,Staples,72
Sugar,Staples,40
# A one-line summary: count and sum per category (unsorted)
$ awk -F, '{ count[$2]++; sum[$2]+=$3 } END { for (k in count) print k, count[k], sum[k] }' kirana.csv
Staples 4 225
Dairy 5 550
Bakery 3 70
# The formatted report using awk -f
$ cat report.awk
BEGIN { FS = "," }
{ count[$2]++; sum[$2] += $3; grand += $3 }
END {
printf "%-10s %5s %8s %6s\n", "Category", "Items", "Total", "Share"
printf "%-10s %5s %8s %6s\n", "--------", "-----", "-----", "-----"
PROCINFO["sorted_in"] = "@ind_str_asc"
for (k in sum) {
pct = (sum[k] / grand) * 100
printf "%-10s %5d %8d %5.1f%%\n", k, count[k], sum[k], pct
}
printf "%-10s %5s %8s %6s\n", "--------", "-----", "-----", "-----"
printf "%-10s %5d %8d\n", "TOTAL", NR, grand
}
$ awk -f report.awk kirana.csv
Category Items Total Share
-------- ----- ----- -----
Bakery 3 70 8.3%
Dairy 5 550 65.1%
Staples 4 225 26.6%
-------- ----- ----- -----
TOTAL 12 845
# Find the top category by total sales
$ cat topcategory.awk
BEGIN { FS = "," }
{ sum[$2] += $3 }
END {
max_cat = ""; max_val = 0
for (k in sum) { if (sum[k] > max_val) { max_val = sum[k]; max_cat = k } }
print "Top category:", max_cat, "with", max_val, "rupees"
}
$ awk -f topcategory.awk kirana.csv
Top category: Dairy with 550 rupees
# Report with averages using PROCINFO for sorted output
$ cat avgstats.awk
BEGIN { FS = "," }
{ count[$2]++; sum[$2] += $3 }
END {
PROCINFO["sorted_in"] = "@ind_str_asc"
printf "%-10s %5s %8s %7s\n", "Category", "Items", "Total", "Average"
printf "%-10s %5s %8s %7s\n", "--------", "-----", "-----", "-------"
for (k in sum) {
avg = sum[k] / count[k]
printf "%-10s %5d %8d %7.1f\n", k, count[k], sum[k], avg
}
}
$ awk -f avgstats.awk kirana.csv
Category Items Total Average
-------- ----- ----- -------
Bakery 3 70 23.3
Dairy 5 550 110.0
Staples 4 225 56.2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 report_session.py
The report program runs through three stages. During the main loop, { count[$2]++; sum[$2] += $3; grand += $3 } builds two arrays and a grand total. In the END block, the program prints headers with printf, iterates the categories in sorted order (via PROCINFO["sorted_in"]) and computes each category’s share as (sum[k] / grand) * 100. The result is a table that shows Dairy at 65.1 %, Bakery at 8.3 % and Staples at 26.6 %, then a footer with the grand total of 845 rupees across 12 items.
Finding the top category is a loop inside END: start with max_val = 0, compare each sum[k] and keep the winner. The session printed “Top category: Dairy with 550 rupees”.
Key idea
Arrays plus END let awk hold state across all lines, so one pass computes totals, averages, maximums and percentages without a second scan or a temporary file.
Checkpoint
Check: Formatted reports with printf
Join two files with FNR and NR
A common task is adding a lookup field: given a list of orders and a list of customers, print each order with its customer name. The The idiom FNR==NR { load; next } { use } in awk: the first file is loaded into an array, the second file is looked up against it. handles this in one awk call.
FNR is the record number within the current file; NR is the global record number. On the first file they are equal, so FNR==NR { name[$1] = $2; next } loads each customer into an array keyed by id and skips to the next line. On the second file FNR resets to 1 while NR keeps counting, so the condition is false and the second block runs, printing each order with its looked-up name.
The flow below follows awk through both files on the lesson’s own data: the four customers load into name[], then each order is looked up against it, and order O105 ends with a blank because its id was never loaded.
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Open the first file. awk opens customers.csv first. FNR counts records within the current file and NR counts across all files, so on the first file they are always equal and FNR==NR is true.
- Load a customer. The record C001,Asha Patel has FNR and NR both 1, so FNR==NR is true. The block name[$1]=$2 stores name[C001] as Asha Patel, and next skips straight to the following record.
- Finish loading file 1. awk repeats for all four customers, so name[] holds C001 to C004. C005 is never in customers.csv, so it is never loaded: that gap is what makes order O105 print a blank later.
- Switch to the second file. When awk opens orders.csv, FNR resets to 1 but NR keeps counting (5, 6, ...). Now FNR==NR is false, so the load block is skipped and the lookup block runs for every order.
- Look up a matched order. Order O101,C001,450 looks up name[C001], which is Asha Patel, so it prints the order line with Asha Patel appended. Orders O102 to O104 match the same way.
- The missing-key trap. Order O105,C005,500 looks up name[C005], which was never loaded, so awk returns the empty string and prints the order with a blank name. No error is raised; guard with ($2 in name).
"""awk join: the FNR==NR idiom for joining two files, and what goes wrong.
The session shows:
1. FNR==NR loads the first file into an array, then looks up the second file
2. Missing keys silently drop data (empty string)
3. Duplicate keys silently overwrite (last one wins)
4. A safer join that reports misses
"""
from session import run, write
write("customers.csv", """\
C001,Asha Patel
C002,Ravi Kumar
C003,Meera Iyer
C004,Kiran Shah
""")
write("orders.csv", """\
O101,C001,450
O102,C002,320
O103,C001,180
O104,C003,275
O105,C005,500
""")
run("""
cat customers.csv
cat orders.csv
# Join: add customer names to orders using FNR==NR (single line)
awk -F, 'FNR==NR { name[$1]=$2; next } { print $0, name[$2] }' customers.csv orders.csv
# What happened to order O105? Its customer C005 is missing, so name["C005"] is empty
# The join silently prints the order with a blank name field
# Duplicate keys: if a customer appears twice, the second name wins
printf 'C001,Asha Patel\\nC001,Asha P.\\n' > dup_customers.csv
cat dup_customers.csv
awk -F, 'FNR==NR { name[$1]=$2; next } { print $0, name[$2] }' dup_customers.csv orders.csv
# Check if a key exists before printing with (key in array)
awk -F, 'FNR==NR { name[$1]=$2; next } ($2 in name) { print $0, name[$2] }' customers.csv orders.csv
# Report missing keys to stderr
awk -F, 'FNR==NR { name[$1]=$2; next } { if ($2 in name) print $0, name[$2]; else print $0, "(unknown)" > "/dev/stderr" }' customers.csv orders.csv
# Use OFS to set the output field separator for comma-separated output
awk -F, -v OFS=, 'FNR==NR { name[$1]=$2; next } ($2 in name) { print $1, $2, $3, name[$2] }' customers.csv orders.csv
rm dup_customers.csv
""") Output
$ cat customers.csv
C001,Asha Patel
C002,Ravi Kumar
C003,Meera Iyer
C004,Kiran Shah
$ cat orders.csv
O101,C001,450
O102,C002,320
O103,C001,180
O104,C003,275
O105,C005,500
# Join: add customer names to orders using FNR==NR (single line)
$ awk -F, 'FNR==NR { name[$1]=$2; next } { print $0, name[$2] }' customers.csv orders.csv
O101,C001,450 Asha Patel
O102,C002,320 Ravi Kumar
O103,C001,180 Asha Patel
O104,C003,275 Meera Iyer
O105,C005,500
# What happened to order O105? Its customer C005 is missing, so name["C005"] is empty
# The join silently prints the order with a blank name field
# Duplicate keys: if a customer appears twice, the second name wins
$ printf 'C001,Asha Patel\nC001,Asha P.\n' > dup_customers.csv
$ cat dup_customers.csv
C001,Asha Patel
C001,Asha P.
$ awk -F, 'FNR==NR { name[$1]=$2; next } { print $0, name[$2] }' dup_customers.csv orders.csv
O101,C001,450 Asha P.
O102,C002,320
O103,C001,180 Asha P.
O104,C003,275
O105,C005,500
# Check if a key exists before printing with (key in array)
$ awk -F, 'FNR==NR { name[$1]=$2; next } ($2 in name) { print $0, name[$2] }' customers.csv orders.csv
O101,C001,450 Asha Patel
O102,C002,320 Ravi Kumar
O103,C001,180 Asha Patel
O104,C003,275 Meera Iyer
# Report missing keys to stderr
$ awk -F, 'FNR==NR { name[$1]=$2; next } { if ($2 in name) print $0, name[$2]; else print $0, "(unknown)" > "/dev/stderr" }' customers.csv orders.csv
O105,C005,500 (unknown)
O101,C001,450 Asha Patel
O102,C002,320 Ravi Kumar
O103,C001,180 Asha Patel
O104,C003,275 Meera Iyer
# Use OFS to set the output field separator for comma-separated output
$ awk -F, -v OFS=, 'FNR==NR { name[$1]=$2; next } ($2 in name) { print $1, $2, $3, name[$2] }' customers.csv orders.csv
O101,C001,450,Asha Patel
O102,C002,320,Ravi Kumar
O103,C001,180,Asha Patel
O104,C003,275,Meera Iyer
$ rm dup_customers.csv
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 join_session.py
Four orders printed their customer names; order O105 printed a blank because C005 is not in the customer file. awk never warns about a missing key: name["C005"] simply returns the empty string.
Two traps hide in this pattern. Missing keys silently drop data, as O105 did. Duplicate keys overwrite: when a customer appeared twice, the second row won, so “Asha Patel” became “Asha P.” for every order. The fix is ($2 in name) as a pattern before the action, which skips rows whose key is absent, or an if that prints to stderr when the key is missing.
A join that silently loses rows
Wrong: awk 'FNR==NR { a[$1]; next } { print $0, a[$2] }' keys vals prints every line of vals, even those with no match (their lookup is empty). Right: add ($2 in a) as a pattern, { if ($2 in a) ... } as a test, or print missing keys to stderr so you know they were dropped.
Use -v OFS=, when you need the output fields separated by commas. The session’s final command printed O101,C001,450,Asha Patel as four comma-separated values.
Checkpoint
Check: FNR==NR join idiom
Output to files and commands
Inside an awk program, print $1 > "file" writes to a file. The file is truncated once per awk run and appended to for every subsequent print in that run. print $1 >> "file" always appends. print $1 | "cmd" pipes to a command; each unique command string opens one pipe.
"""awk output: print to files and commands, and close() before reading back.
The session shows:
1. print > "file" truncates once per run, then appends within the run
2. print >> "file" always appends
3. print | "cmd" pipes to a command
4. close("file") before reading the file back in the same run
"""
from session import run, write
write("items.csv", """\
Milk,Dairy
Bread,Bakery
Cheese,Dairy
Rice,Staples
Paneer,Dairy
Rusk,Bakery
Atta,Staples
""")
write("split.awk", """\
BEGIN { FS = "," }
{ print $1 > ($2 ".txt") }
""")
write("readback_close.awk", """\
BEGIN { FS = "," }
{ print $1 > "sorted_items.txt" }
END {
close("sorted_items.txt")
while ((getline line < "sorted_items.txt") > 0)
print "Read back:", line
}
""")
write("readback_noclose.awk", """\
BEGIN { FS = "," }
{ print $1 > "sorted_items.txt" }
END {
while ((getline line < "sorted_items.txt") > 0)
print "Read back:", line
print "Lines read: none visible, file not fully flushed"
}
""")
run("""
cat items.csv
# print > "file" truncates once, then appends within the same awk run
awk -f split.awk items.csv
cat Dairy.txt
cat Bakery.txt
cat Staples.txt
# Running awk again truncates the files (not appending to old content)
awk -f split.awk items.csv
cat Dairy.txt
# print >> "file" always appends
echo "Biscuit" >> Bakery.txt
awk -F, '{ print $1 >> "Bakery.txt" }' items.csv
cat Bakery.txt
# Clean up for the next demo
rm -f Dairy.txt Bakery.txt Staples.txt
# print | "cmd" pipes output to a command
awk -F, '{ print $1 | "sort" }' items.csv
# close() before reading a file you wrote in the same run
cat readback_close.awk
awk -f readback_close.awk items.csv
# Without close(), the file may not be visible to getline yet
rm -f sorted_items.txt
cat readback_noclose.awk
awk -f readback_noclose.awk items.csv
# The file exists after awk finishes, even though getline saw nothing
cat sorted_items.txt
rm -f sorted_items.txt
""") Output
$ cat items.csv
Milk,Dairy
Bread,Bakery
Cheese,Dairy
Rice,Staples
Paneer,Dairy
Rusk,Bakery
Atta,Staples
# print > "file" truncates once, then appends within the same awk run
$ awk -f split.awk items.csv
$ cat Dairy.txt
Milk
Cheese
Paneer
$ cat Bakery.txt
Bread
Rusk
$ cat Staples.txt
Rice
Atta
# Running awk again truncates the files (not appending to old content)
$ awk -f split.awk items.csv
$ cat Dairy.txt
Milk
Cheese
Paneer
# print >> "file" always appends
$ echo "Biscuit" >> Bakery.txt
$ awk -F, '{ print $1 >> "Bakery.txt" }' items.csv
$ cat Bakery.txt
Bread
Rusk
Biscuit
Milk
Bread
Cheese
Rice
Paneer
Rusk
Atta
# Clean up for the next demo
$ rm -f Dairy.txt Bakery.txt Staples.txt
# print | "cmd" pipes output to a command
$ awk -F, '{ print $1 | "sort" }' items.csv
Atta
Bread
Cheese
Milk
Paneer
Rice
Rusk
# close() before reading a file you wrote in the same run
$ cat readback_close.awk
BEGIN { FS = "," }
{ print $1 > "sorted_items.txt" }
END {
close("sorted_items.txt")
while ((getline line < "sorted_items.txt") > 0)
print "Read back:", line
}
$ awk -f readback_close.awk items.csv
Read back: Milk
Read back: Bread
Read back: Cheese
Read back: Rice
Read back: Paneer
Read back: Rusk
Read back: Atta
# Without close(), the file may not be visible to getline yet
$ rm -f sorted_items.txt
$ cat readback_noclose.awk
BEGIN { FS = "," }
{ print $1 > "sorted_items.txt" }
END {
while ((getline line < "sorted_items.txt") > 0)
print "Read back:", line
print "Lines read: none visible, file not fully flushed"
}
$ awk -f readback_noclose.awk items.csv
Lines read: none visible, file not fully flushed
# The file exists after awk finishes, even though getline saw nothing
$ cat sorted_items.txt
Milk
Bread
Cheese
Rice
Paneer
Rusk
Atta
$ rm -f sorted_items.txt
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 output_session.py
The session split items into three files by category, then showed that print | "sort" sorted all items together. The key point is the close-before-read rule: if you write to a file in the main loop and read it back in END, call close("file") first. Without it, the output buffer may not be flushed, and getline sees nothing, as the second run proved.
Key idea
Call close("file") or close("cmd") before you read a file you wrote or before you reopen the same command with new input; otherwise the write is buffered and the read sees stale or missing data.
Checkpoint
Check: Output to files and commands
Multi-line programs with functions
A one-liner works for a quick filter, but a 20-line report belongs in a file. awk -f report.awk data.csv reads the program from report.awk. Comments start with # and run to the end of the line. User-defined functions use the syntax function name(params) { body }.
"""awk programs: awk -f, comments, functions with local variables, and getline.
The session shows:
1. awk -f report.awk for maintainable multi-line programs
2. Comments with # (to the end of the line)
3. User-defined functions with local variables via extra parameters
4. getline acceptable use and the "cmd" | getline trap
"""
from session import run, write
write("data.csv", """\
Milk,Dairy,45
Bread,Bakery,30
Cheese,Dairy,120
Paneer,Dairy,80
Rusk,Bakery,25
""")
write("report.awk", """\
# report.awk: generate a sales report by category
# Usage: awk -f report.awk sales.csv
# User-defined function with a local variable
# The extra parameter 'i' is a local (gawk and POSIX)
function print_line(cat, items, total, i) {
i = total / items
printf "%-10s %5d %8d %7.1f\\n", cat, items, total, i
}
BEGIN { FS = "," }
# For each input line, accumulate counts and sums
{ count[$2]++; sum[$2] += $3 }
END {
# Header
printf "%-10s %5s %8s %7s\\n", "Category", "Items", "Total", "Average"
printf "%-10s %5s %8s %7s\\n", "--------", "-----", "-----", "-------"
# Sort keys (gawk extension)
PROCINFO["sorted_in"] = "@ind_str_asc"
for (k in sum) {
print_line(k, count[k], sum[k])
}
}
""")
write("lines.txt", """\
header
data1
data2
""")
run("""
cat report.awk
cat data.csv
# Run the program file with awk -f
awk -f report.awk data.csv
# getline: read the next line into $0 (or into a variable)
cat lines.txt
awk 'NR==1 { getline; print "Skipped header, first data:", $0 }' lines.txt
# Read a line into a named variable
awk 'NR==1 { if ((getline nextline) > 0) print "Line 1:", $0, "| Line 2:", nextline }' lines.txt
# "cmd" | getline runs a command and reads its output
awk 'BEGIN { "date +%Y" | getline year; print "Year:", year }'
# The trap: "cmd" | getline complicates error handling
# If the command produces no output, getline returns 0; if it fails, -1
awk 'BEGIN { cmd="cat /nonexistent 2>/dev/null"; if ((cmd | getline line) <= 0) print "No output or failed"; else print "Got:", line; close(cmd) }'
# Preferred: pass data via -v or as files, not via "cmd" | getline
year=$(date +%Y)
awk -v year="$year" 'BEGIN { print "Year from shell:", year }'
rm lines.txt
""") Output
$ cat report.awk
# report.awk: generate a sales report by category
# Usage: awk -f report.awk sales.csv
# User-defined function with a local variable
# The extra parameter 'i' is a local (gawk and POSIX)
function print_line(cat, items, total, i) {
i = total / items
printf "%-10s %5d %8d %7.1f\n", cat, items, total, i
}
BEGIN { FS = "," }
# For each input line, accumulate counts and sums
{ count[$2]++; sum[$2] += $3 }
END {
# Header
printf "%-10s %5s %8s %7s\n", "Category", "Items", "Total", "Average"
printf "%-10s %5s %8s %7s\n", "--------", "-----", "-----", "-------"
# Sort keys (gawk extension)
PROCINFO["sorted_in"] = "@ind_str_asc"
for (k in sum) {
print_line(k, count[k], sum[k])
}
}
$ cat data.csv
Milk,Dairy,45
Bread,Bakery,30
Cheese,Dairy,120
Paneer,Dairy,80
Rusk,Bakery,25
# Run the program file with awk -f
$ awk -f report.awk data.csv
Category Items Total Average
-------- ----- ----- -------
Bakery 2 55 27.5
Dairy 3 245 81.7
# getline: read the next line into $0 (or into a variable)
$ cat lines.txt
header
data1
data2
$ awk 'NR==1 { getline; print "Skipped header, first data:", $0 }' lines.txt
Skipped header, first data: data1
# Read a line into a named variable
$ awk 'NR==1 { if ((getline nextline) > 0) print "Line 1:", $0, "| Line 2:", nextline }' lines.txt
Line 1: header | Line 2: data1
# "cmd" | getline runs a command and reads its output
$ awk 'BEGIN { "date +%Y" | getline year; print "Year:", year }'
Year: 2026
# The trap: "cmd" | getline complicates error handling
# If the command produces no output, getline returns 0; if it fails, -1
$ awk 'BEGIN { cmd="cat /nonexistent 2>/dev/null"; if ((cmd | getline line) <= 0) print "No output or failed"; else print "Got:", line; close(cmd) }'
No output or failed
# Preferred: pass data via -v or as files, not via "cmd" | getline
$ year=$(date +%Y)
$ awk -v year="$year" 'BEGIN { print "Year from shell:", year }'
Year from shell: 2026
$ rm lines.txt
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 program_session.py
The report.awk file defines print_line(cat, items, total, i). The extra parameter i after the required three is a local variable: awk fills only the arguments the caller passes, so i starts as zero and lives only inside the function. This convention is portable to every POSIX awk and avoids polluting the global namespace.
getline reads the next line into $0 (or into a named variable). The session’s NR==1 { getline; print ... } skipped a header and printed the first data line. The form "cmd" | getline var runs a command and reads its output, but it complicates error handling: a missing command or an empty result returns 0 or −1, and the program continues unless you check. The preferred alternative is passing data via -v var="$shell_value" or as a file, which keeps the control flow simple.
getline error handling
Wrong: "cmd" | getline var; print var assumes the command succeeded. Right: if (("cmd" | getline var) > 0) print var; else print "failed" > "/dev/stderr"; close("cmd"). Or avoid the issue by passing data with -v from the shell.
Checkpoint
Check: Multi-line programs with functions
Practise: build a report from a log
Try these on your own terminal, using the data files from the sessions. Each task is one awk program (possibly in a file), and the recorded sessions show every piece you need.
- From
sales.csv, print the grand total of all sales in a single number. Hint:grand += $3in the main loop,print grandin END. - From
kirana.csv, print only the category with the lowest total sales. Hint: start withmin_val = 99999999. - From
customers.csvandorders.csv, print orders whose customer id starts with “C00”. Hint: add a pattern/^C00/on the second file. - Split
items.csvinto files named<category>_items.txt(e.g.Dairy_items.txt). Hint:print $1 > ($2 "_items.txt"). - Write an awk program that reads a CSV, counts rows per category, and prints them sorted by count descending. Hint:
print count[k], kthen pipe tosort -rn.
Practice that adapts to you
Practise this lesson
6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): why does for (k in array) come out in a different order on different awk versions, and how do you fix it?
The POSIX standard says the order of keys in for (k in array) is unspecified, so each awk implementation chooses its own traversal. gawk and mawk both use hash tables internally, but their hashing differs, so the same keys appear in different sequences. A script that depends on a particular order works on one machine and breaks on another.
The portable fix is to pipe the output to sort: awk '...' | sort gives a deterministic order on every awk. gawk offers PROCINFO["sorted_in"] = "@ind_str_asc" (or "@ind_num_asc" for numeric keys) to sort inside the loop itself, which avoids the extra process but is not portable to mawk or the macOS awk.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A billing script joins orders to customers with awk 'FNR==NR { c[$1]=$2; next } { print $0, c[$2] }' customers orders. One day a customer complains that their invoice shows no name. What went wrong, and how would you make the script report the problem instead of hiding it?
-
Mid to senior
An awk script writes intermediate results to a temp file in the main loop, then reads them back with getline in END to produce a summary. The summary comes out empty on a small input and only partly filled on a large one. What is the likely cause, and what one-line fix makes it reliable?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does awk group lines by key in one pass?
Its arrays are associative: count[$2]++ counts lines per value of field 2, sum[$2] += $3 adds up field 3 per key, and an END block prints the totals.
-
How do you make an awk join print only the lines whose key was found?
Test membership with (k in a): FNR==NR { a[$1]; next } ($2 in a) { print } prints a line of the second file only when its key was loaded, while a bare lookup gives an empty value for a missing key.
-
In which order does for (k in array) visit the keys?
POSIX leaves it unspecified, and gawk and mawk differ, so pipe the output to sort or use gawk's PROCINFO["sorted_in"] for a fixed order.
-
How does the FNR==NR idiom join two files in awk?
While FNR equals NR, awk is still reading the first file, so FNR==NR { name[$1] = $2; next } loads it into an array; the second file then looks each key up. Missing keys give empty strings, and a repeated key overwrites.
-
What do print > "file", print >> "file" and close() do in awk?
> empties the file once per run and then keeps writing to it, >> always appends, and print | "cmd" feeds a command. close() flushes and closes it, which must come before reading a file the program wrote.
-
How are longer awk programs and their functions written?
In a file run with awk -f prog.awk; a function's local variables are declared as extra parameters after its real ones.
Key takeaways
- awk arrays are associative:
count[$2]++groups by field 2,sum[$2] += $3accumulates, and(k in a)tests membership. - The
for (k in array)order is unspecified: pipe tosortor use gawk’sPROCINFO["sorted_in"]for deterministic output. - The FNR==NR idiom joins two files: the first is loaded into an array, the second is looked up, but missing keys return empty and duplicate keys overwrite.
print > "file"truncates once per run;print >> "file"always appends;print | "cmd"pipes to a command; callclose()before you read a file you wrote.- Multi-line programs go in a file with
awk -f; local variables in functions are declared as extra parameters; prefer-vover"cmd" | getlinefor simpler error handling.
Terms in this lesson
- awk array key
- The string that identifies an entry in an awk associative array, written inside brackets: count["Dairy"] uses the key Dairy to store or look up a value.
- join key
- A field that appears in both files of a join, used to match rows: the FNR==NR idiom loads one file's key into an array and looks up the other file's key against it.
- output field separator
- The string awk prints between fields when you use print with commas; set with -v OFS=, or by assigning OFS in the program. The default is a single space.
References
- GNU awk manual: Arrays in awk (Free Software Foundation)
- GNU awk manual: Printing Output (Free Software Foundation)
- GNU awk manual: User-defined Functions (Free Software Foundation)
- POSIX.1-2024: awk utility (The Open Group and IEEE)
Report a problem with this lesson
Kept only in this browser. Your Learn progress