主题
awk 命令使用详解
简介
awk 是一门专门用于文本处理的编程语言。它由贝尔实验室的 Alfred Aho、Peter Weinberger 和 Brian Kernighan 在 1977 年设计,名字取自三位作者姓氏的首字母。awk 最初用于生成报表,后来成为 POSIX 标准的一部分,几乎所有 Unix/Linux 系统都会预装。
它不像 grep 只能筛选行,也不像 cut 只能按固定分隔符取列。awk 把「读取、分列、判断、计算、格式化输出」放在一次扫描里完成,非常适合处理结构化文本。
一句话理解
awk = 按记录读取 + 自动分列 + 模式过滤 + 计算输出。
核心模型
awk 的处理单位是记录(record),默认一条记录就是一行;记录会被字段分隔符(FS)切分成若干字段(field),分别用 $1、$2 …… $NF 表示,$0 表示整条记录。
程序由「模式 + 动作」组成:
awk
pattern { action }pattern为真时,执行对应的actionpattern省略时,默认匹配每一行action省略时,默认执行print $0
此外还有两个特殊模式:BEGIN 在读取输入前执行一次,END 在所有输入读完后执行一次。
text
BEGIN
│
▼
┌──────────────────────────────────┐
│ 读取一条记录(默认一行) │
│ 按 FS 切分成 $1、$2 … $NF │
│ 依次检查每个 pattern │
│ 匹配则执行对应 action │
└──────────────────────────────────┘
│ 重复直到输入结束
▼
END适用场景
- 提取某几列、调整列顺序
- 按条件过滤行,支持正则和数值比较
- 对列求和、求平均、计数、分组统计
- 分析日志:状态码、IP、响应大小、时间范围
- 生成报表、SQL、Shell 命令等格式化文本
- 替代
grep + cut + sed + sort的复杂组合
awk 不擅长 JSON、XML 和带引号、换行的复杂 CSV,也不适合做超出内存的大规模聚合。这类任务交给 jq、Python、Miller 或数据库更合适。
常见实现
| 实现 | 说明 |
|---|---|
| gawk | GNU awk,Linux 上最常见,扩展功能最丰富 |
| mawk | 体积小、速度快,Debian/Ubuntu 的精简环境常用 |
| nawk | 原始 awk(BWK awk),BSD/macOS 上常见 |
| busybox awk | 嵌入式环境中的精简实现,功能有限 |
本文以 GNU Awk 5.4.1 为例。涉及 gensub、asort、--csv、BEGINFILE 等特性时会特别标注,这些是 gawk 扩展,换到 mawk 或 busybox awk 上可能不可用。
安装与版本
bash
sudo apt update
sudo apt install gawkbash
sudo dnf install gawkbash
sudo pacman -S gawkbash
apk add gawk查看当前使用的实现和版本:
bash
awk --version # gawk
awk -W version # mawk 等实现
# 查看 awk 实际指向哪个程序
readlink -f "$(command -v awk)"Debian/Ubuntu 上 awk 可能指向 mawk。需要切换默认实现时:
bash
sudo update-alternatives --config awk示例数据
后面的例子主要使用下面这份 employee.txt:
bash
cat > employee.txt <<'EOF'
张三 研发 12000
李四 测试 9000
王五 研发 15000
赵六 运维 8000
EOF每行三个字段:姓名、部门、月薪。
快速入门
打印指定列
bash
awk '{print $1, $3}' employee.txt输出:
text
张三 12000
李四 9000
王五 15000
赵六 8000说明:
$1是第一个字段,$3是第三个字段print中的逗号会使用输出字段分隔符OFS,默认是一个空格- 整个程序用单引号包住,避免 Shell 提前展开
$1
按条件过滤
bash
awk '$3 > 10000 {print $1, $3}' employee.txt输出:
text
张三 12000
王五 15000$3 > 10000 是模式,只有月薪大于 10000 的行才执行后面的动作。
模式匹配也可以直接用正则:
bash
awk '/研发/ {print $1}' employee.txt输出:
text
张三
王五统计求和
bash
awk '{sum += $3} END {print sum}' employee.txt输出:
text
44000sum += $3 对每一行累加,END 在全部行处理完后输出结果。
awk '{print}' file 基本等价于 cat file;awk '1' file 也基本等价,因为模式 1 恒为真,而默认动作是打印整行。两者都会按 ORS 补齐每条记录的换行。
运行方式与引号
三种常见写法
bash
# 1. 程序直接写在命令行
awk '{print $1}' file.txt
# 2. 从脚本文件读取程序
awk -f report.awk file.txt
# 3. 从标准输入读取
cat file.txt | awk '{print $1}'常用选项
| 选项 | 说明 | 示例 |
|---|---|---|
-F fs | 指定输入字段分隔符 | awk -F: '{print $1}' /etc/passwd |
-v var=val | 在 BEGIN 之前定义变量 | awk -v n=3 '{print $n}' file |
-f file | 从文件读取 awk 程序 | awk -f report.awk data.txt |
-i inplace | 原地修改文件,gawk 扩展 | gawk -i inplace '{...}' file |
--csv | 按 CSV 规则解析,gawk 5.3+ | gawk --csv '{print $2}' data.csv |
--posix | 严格遵循 POSIX 行为 | awk --posix '...' file |
--dump-variables | 结束时输出所有变量,gawk | gawk --dump-variables '...' file |
单引号很重要
bash
# 正确:$1 交给 awk 解释
awk '{print $1}' file.txt
# 错误:Shell 会先把 $1 展开成位置参数,通常变成空
awk "{print $1}" file.txt需要把 Shell 变量传给 awk 时,用 -v:
bash
name=张三
awk -v n="$name" 'BEGIN {print n}'也可以直接读取环境变量:
bash
awk 'BEGIN {print ENVIRON["HOME"]}'如果 awk 程序本身需要单引号,可以用 '\'' 拼接,或者用 awk 的八进制转义 \047:
bash
awk 'BEGIN {print "it'\''s ok"}'
awk 'BEGIN {print "it\047s ok"}'命令行变量赋值的时机
-v 在 BEGIN 之前生效;写在文件名后面的 var=value 则在 BEGIN 之后、处理第一条记录之前生效:
bash
awk 'BEGIN {print "BEGIN x=[" x "]"} {print "record x=[" x "]"; exit}' x=1 employee.txt输出:
text
BEGIN x=[]
record x=[1]需要 BEGIN 里就能用,必须用 -v x=1。
写成脚本文件
awk
# report.awk
BEGIN {
FS = ":"
OFS = "\t"
}
$3 >= 1000 {
print $1, $3
}
END {
print "total", NR
}bash
awk -f report.awk /etc/passwd也可以加 shebang 直接执行:
awk
#!/usr/bin/awk -f
BEGIN { print "hello" }bash
chmod +x report.awk
./report.awk处理模型与内置变量
默认规则
- 默认记录分隔符
RS是换行符,所以一条记录就是一行 - 默认字段分隔符
FS是空白,连续空格或 Tab 算一个分隔符,行首行尾空白会被忽略 - 每个模式都会对每条记录依次判断;没有模式的动作对每条记录都执行
常用内置变量
| 变量 | 含义 |
|---|---|
$0 | 当前整条记录 |
$1 … $NF | 第 1 到第 NF 个字段 |
NF | 当前记录的字段数 |
NR | 已读取的记录总数(跨文件累计) |
FNR | 当前文件内已读取的记录数 |
FS | 输入字段分隔符,默认空白 |
OFS | 输出字段分隔符,默认空格 |
RS | 输入记录分隔符,默认换行 |
ORS | 输出记录分隔符,默认换行 |
FILENAME | 当前输入文件名 |
ARGIND | 当前文件在命令行中的序号,gawk |
ARGC / ARGV | 命令行参数个数与数组 |
ENVIRON | 环境变量数组 |
RSTART / RLENGTH | match() 匹配到的位置和长度 |
SUBSEP | 多维数组下标分隔符,默认 \034 |
CONVFMT / OFMT | 数字转字符串 / 输出数字的格式 |
IGNORECASE | 忽略大小写,gawk |
RT | 实际匹配到的记录分隔符,gawk |
PROCINFO | gawk 运行时信息与数组排序控制 |
NR 与 FNR
处理多个文件时,NR 是累计行号,FNR 是当前文件内的行号:
bash
awk 'FNR == 1 {print "--- " FILENAME " ---"} {print FNR, $0}' employee.txt employee.txt输出中每个文件都会从 1 重新开始。FNR == 1 常用来给每个文件加表头,或者在文件切换时做初始化。
只统计所有文件的总行数:
bash
awk 'END {print NR}' file1 file2 file3ARGV 与 ARGC
bash
awk 'BEGIN {for (i = 0; i < ARGC; i++) print i, ARGV[i]}' a.txt b.txtARGV[0] 是程序名(通常是 awk 或 gawk),后面是文件名。awk 允许在 BEGIN 中修改 ARGV 来跳过某些文件:
bash
awk 'BEGIN {delete ARGV[1]} {print FILENAME, $0}' a.txt b.txt这样 a.txt 会被跳过。
FILENAME 在 BEGIN 中的值
FILENAME 在处理第一条记录前才被设置,BEGIN 中通常是空字符串。如果需要在 BEGIN 中知道文件名,可以读 ARGV[1]。
字段与分隔符
默认按空白分列
bash
printf ' a b\t c \n' | awk '{print NF, "[" $1 "]", "[" $2 "]", "[" $3 "]"}'输出:
text
3 [a] [b] [c]行首、行尾空白被忽略,中间连续空白只算一个分隔符。
指定分隔符
bash
# 冒号分隔,例如 /etc/passwd
awk -F: '{print $1, $3}' /etc/passwd
# Tab 分隔的 TSV
awk -F'\t' '{print $2}' data.tsv
# 逗号分隔的 CSV
awk -F, '{print $2}' data.csv
# 多个分隔符:逗号或分号
awk -F'[,;]' '{print $1}' data.txt
# 连续空白作为一个分隔符
awk -F'[[:space:]]+' '{print $1}' data.txt也可以用在 BEGIN 里设置:
bash
awk 'BEGIN {FS = ":"; OFS = "\t"} {print $1, $3}' /etc/passwd单字符分隔符与空字段
默认的 FS = " " 有特殊行为:连续空白算一个分隔符,并忽略行首行尾空白。但 -F, 这类单字符分隔符会严格按每个逗号切分,空字段会保留:
bash
printf 'a,,b\n' | awk -F, '{print NF}'
# 3如果用正则 -F',+',连续逗号会被当成一个分隔符,结果为 2 个字段。
FS 的设置时机
FS 在读取记录时生效。处理第一条记录时再修改 FS,不会重新切分当前记录:
bash
printf 'a:b\nc:d\n' | awk 'NR == 1 {FS = ":"} {print $1}'输出:
text
a:b
c第一行仍按默认空白切分,第二行才使用冒号。正确做法是用 -F: 或在 BEGIN 中设置。
OFS 与字段重建
print $1, $2 使用 OFS 作为分隔符;print $1 $2 是字符串拼接,没有分隔符:
bash
awk 'BEGIN {OFS = "|"} {print $1, $2; print $1 $2}' employee.txt以第一行为例,输出:
text
张三|研发
张三研发修改某个字段会触发 $0 重建,重建时使用 OFS:
bash
awk 'BEGIN {OFS = "|"} {$2 = "X"; print}' employee.txt输出:
text
张三|X|12000
李四|X|9000
王五|X|15000
赵六|X|8000反过来,给 $0 赋值会让 awk 在下次访问字段时按 FS 重新切分。
NF、$NF 与倒数第二个字段
bash
awk '{print NF, $NF, $(NF - 1)}' employee.txt输出:
text
3 12000 研发
3 9000 测试
3 15000 研发
3 8000 运维注意优先级
$NF - 1 表示「最后一个字段的值减 1」,不是倒数第二个字段。倒数第二个字段必须写成 $(NF - 1)。
修改 NF 可以截断字段,并重建 $0;gawk 中也支持增大 NF 来补空字段:
bash
echo 'a b c d' | awk '{NF = 2; print}'
# a b
echo 'a b' | awk 'BEGIN {OFS = "|"} {NF = 4; print}'
# a|b||RS、ORS 与段落模式
RS 控制如何切分记录。默认是换行,所以一行一条记录。RS = "" 是特殊的段落模式:以空行分隔记录,并自动忽略记录开头和结尾的空行。
bash
cat > poem.txt <<'EOF'
床前明月光
疑是地上霜
举头望明月
低头思故乡
EOF
awk 'BEGIN {RS = ""; ORS = "\n---\n"} {print NR ": " $1}' poem.txt输出:
text
1: 床前明月光
---
2: 举头望明月
---段落模式下,换行符也会被视为字段分隔符,无论 FS 是什么。
也可以把任意字符作为记录分隔符:
bash
printf 'a;b;c' | awk 'BEGIN {RS = ";"} {print NR, $0}'输出:
text
1 a
2 b
3 cgawk 支持用正则作为 RS,并用 RT 查看实际匹配到的分隔符:
bash
printf 'a1b22c' | gawk 'BEGIN {RS = "[0-9]+"} {print NR, "[" $0 "]", "[" RT "]"}'输出:
text
1 [a] [1]
2 [b] [22]
3 [c] []ORS 控制 print 输出的记录分隔符,默认是换行:
bash
awk 'BEGIN {ORS = "|"} {print $1}' employee.txt
# 张三|李四|王五|赵六|模式 Pattern
正则模式
bash
# 包含 error 的行
awk '/error/' app.log
# 不包含 debug 的行
awk '!/debug/' app.log
# 字段匹配正则
awk '$6 ~ /GET/' access.log
awk '$1 !~ /^10\.0\.0\./' access.log/regex/ 等价于 $0 ~ /regex/,只要整行中任意位置匹配就算命中。要匹配整个字段,记得加锚点:
bash
awk '$2 ~ /^研发$/' employee.txt表达式模式
bash
# 数值比较
awk '$3 >= 10000' employee.txt
# 字符串比较
awk '$2 == "研发"' employee.txt
# 逻辑组合
awk '$2 == "研发" && $3 > 10000' employee.txt
# 行号条件
awk 'NR % 2 == 0' employee.txt
awk 'NR >= 2 && NR <= 3' employee.txt模式也可以是单纯的数字:1 恒真,0 恒假。awk '1' file 因此等价于 cat file。
范围模式
bash
# 从包含「王五」的行到包含「赵六」的行,包含首尾
awk '/王五/, /赵六/' employee.txt范围模式在开始行匹配后进入「打开」状态,直到结束行匹配后关闭;如果结束行一直没出现,会一直打印到文件末尾。同一个文件里可以匹配多个区间。
常见用途是提取配置文件中的某个段落。但如果结束模式也会匹配开始行本身,范围会立即关闭:
bash
# 只输出 [server] 这一行,因为 /^\[/ 也匹配了起始行
awk '/^\[server\]/, /^\[/' nginx.conf这种情况更适合用状态变量:
bash
awk '/^\[/ {in_section = ($0 ~ /^\[server\]$/)} in_section' nginx.confBEGIN 与 END
bash
awk 'BEGIN {print "开始"} {print $1} END {print "结束,共 " NR " 行"}' employee.txtBEGIN 适合初始化 FS、OFS、表头、计数器;END 适合输出汇总结果。
BEGINFILE 与 ENDFILE
gawk 扩展,在每个文件开始和结束时执行:
bash
gawk 'BEGINFILE {print ">>> " FILENAME} ENDFILE {print "<<< " FILENAME}' a.txt b.txtnext 与 nextfile
next 跳过当前记录,直接进入下一条记录;nextfile 跳过当前文件的剩余部分,直接处理下一个文件。
bash
# 跳过空行和注释行
awk 'NF == 0 || /^#/ {next} {print}' config.conf
# 每个文件只打印第一条匹配 error 的记录
awk '/error/ {print FILENAME ": " $0; nextfile}' *.lognext 和 nextfile 只能出现在动作里,不能写在 BEGIN 或 END 中。
动作与流程控制
if/else
bash
awk '{
if ($3 >= 12000)
level = "高"
else if ($3 >= 9000)
level = "中"
else
level = "低"
print $1, level
}' employee.txt输出:
text
张三 高
李四 中
王五 高
赵六 低循环
bash
# for 循环遍历字段
awk '{for (i = 1; i <= NF; i++) print NR, i, $i}' employee.txt
# while
awk 'BEGIN {i = 1; while (i <= 3) {print i; i++}}'
# do-while
awk 'BEGIN {i = 0; do {print i; i++} while (i < 3)}'
# for-in 遍历数组
awk '{count[$2]++} END {for (dept in count) print dept, count[dept]}' employee.txtbreak 跳出循环,continue 进入下一轮。
exit
exit 会立即停止处理输入,转到 END 块;如果 END 中再次 exit,则直接结束。
bash
awk '/ERROR/ {print; exit 1}' app.log
echo $?没有匹配到 ERROR 时正常结束,退出码为 0;匹配到时退出码为 1。可以用退出码配合 Shell 脚本做检查。
语句分隔
awk 的语句可以用换行或分号分隔。多条语句需要用 {} 包起来,单条语句可以省略 {}:
bash
awk '$3 > 10000' employee.txt # 省略动作,默认 print
awk '$3 > 10000 {print $1}' employee.txt
awk '$3 > 10000 {print $1; print $2}' employee.txt运算符与表达式
| 类别 | 运算符 |
|---|---|
| 算术 | + - * / % ^ |
| 比较 | < <= > >= == != |
| 逻辑 | && || ! |
| 正则 | ~ !~ |
| 三元 | ? : |
| 拼接 | 相邻表达式(空格) |
| 数组成员 | in |
| 赋值 | = += -= *= /= %= ^= |
| 自增自减 | ++ -- |
bash
awk 'BEGIN {print 2^10, 17 % 5, 1 / 3}'
# 1024 2 0.333333
awk 'BEGIN {print (1 > 2) ? "yes" : "no"}'
# no
awk '{print $1 ":" $3}' employee.txt
# 张三:12000字符串拼接
awk 用相邻表达式拼接字符串,不需要 + 或 .:
bash
awk '{print $1 $2}' employee.txt但要注意歧义:$1 - $2 是减法,不是用 - 拼接。拼接负号或容易误读的表达式时加括号:
bash
awk '{print $1 ":" $3}' employee.txt
awk '{print ($1 - $2)}' employee.txt数值与字符串比较
这是 awk 最容易踩的坑之一。来自输入的字段如果看起来像数字,会被当作数值比较;而字符串常量之间按字典序比较:
bash
# 输入字段是 strnum,按数值比较:10 > 9
echo '10 9' | awk '{print ($1 > $2)}'
# 1
# 两个字符串常量,按字典序比较:"10" < "9"
awk 'BEGIN {print ("10" > "9")}'
# 0需要强制按数值比较时,加 0 或做算术运算:
bash
awk 'BEGIN {a = "10"; b = "9"; print (a + 0 > b + 0)}'
# 1需要强制按字符串比较时,可以拼接空字符串:
bash
awk 'BEGIN {a = 10; b = 9; print (a "" > b "")}'
# 0正则匹配
bash
awk '$1 ~ /^192\.168\./' access.log
awk '$9 !~ /^(200|304)$/' access.log动态正则可以把模式放在变量里:
bash
awk -v pat='^研发' '$2 ~ pat' employee.txt正则中的 .、*、+、?、(、)、[、]、{、}、^、$、|、\ 都有特殊含义,匹配字面量时要转义。
字符串与数学函数
字符串函数
| 函数 | 说明 | 示例 |
|---|---|---|
length(s) | 字符串长度,省略参数时为 $0 | length($0) |
substr(s, m, n) | 从第 m 个字符开始取 n 个,m 从 1 开始 | substr($0, 1, 4) |
index(s, t) | t 在 s 中的位置,找不到返回 0 | index($0, "ll") |
split(s, a, fs) | 按 fs 切分到数组 a,返回元素个数 | split($0, a, ":") |
sub(re, repl, s) | 替换第一个匹配,返回替换次数 | sub(/a/, "b") |
gsub(re, repl, s) | 替换全部匹配,返回替换次数 | gsub(/a/, "b") |
gensub(re, repl, how, s) | 返回替换后的新字符串,gawk | gensub(/a/, "b", "g") |
match(s, re) | 匹配并设置 RSTART、RLENGTH | match($0, /[0-9]+/) |
tolower(s) / toupper(s) | 转小写 / 大写 | tolower($1) |
sprintf(fmt, ...) | 按格式返回字符串 | sprintf("%05d", $1) |
strtonum(s) | 按进制解析数字,gawk | strtonum("0x10") |
substr 与 index
bash
echo '2026-10-19' | awk '{print substr($0, 1, 4), substr($0, 6, 2), substr($0, 9, 2)}'
# 2026 10 19
echo 'hello' | awk '{print index($0, "ll")}'
# 3substr 的起始位置从 1 开始;省略第三个参数表示取到末尾。
sub 与 gsub
sub 只替换第一个匹配,gsub 替换全部匹配,默认操作对象是 $0,返回值是替换次数:
bash
echo '2026-10-19' | awk '{gsub(/-/, "/"); print}'
# 2026/10/19
echo 'foo bar foo' | awk '{n = gsub(/foo/, "baz"); print n, $0}'
# 2 baz bar baz替换文本中的 & 代表被匹配到的内容:
bash
echo 'abc' | awk '{sub(/b/, "[&]"); print}'
# a[b]c要输出字面量 &,写成 \\&:
bash
echo 'abc' | awk '{sub(/b/, "[\\&]"); print}'
# a[&]cgensub 不修改原字符串,而是返回新字符串,支持反向引用:
bash
echo 'abc123' | awk '{print gensub(/([a-z]+)([0-9]+)/, "\\2-\\1", "g")}'
# 123-abc第三个参数 "g" 表示全部替换,也可以是数字表示替换第几个匹配。
match 与捕获
bash
echo 'abc123' | awk '{
if (match($0, /[0-9]+/))
print RSTART, RLENGTH, substr($0, RSTART, RLENGTH)
}'
# 4 3 123gawk 支持第三个参数,把捕获组保存到数组:
bash
echo '2026-10-19' | gawk '{
match($0, /([0-9]+)-([0-9]+)-([0-9]+)/, m)
print m[1], m[2], m[3]
}'
# 2026 10 19split
bash
echo 'a:b:c' | awk '{n = split($0, a, ":"); print n, a[1], a[n]}'
# 3 a c不指定分隔符时使用 FS。split(s, a, "") 在 gawk 中会把字符串拆成单个字符。
数学函数
| 函数 | 说明 |
|---|---|
int(x) | 向零取整 |
sqrt(x) | 平方根 |
exp(x) / log(x) | 指数 / 自然对数 |
sin(x) / cos(x) | 正弦 / 余弦 |
atan2(y, x) | 反正切 |
rand() | 返回 [0, 1) 的随机数 |
srand([seed]) | 设置随机种子,返回旧种子 |
bash
awk 'BEGIN {print int(3.9), int(-3.9), sqrt(16), 2^10, 17 % 5}'
# 3 -3 4 1024 2
awk 'BEGIN {srand(); for (i = 1; i <= 5; i++) printf "%d ", int(rand() * 100); print ""}'数组
awk 的数组是关联数组,下标是字符串,不需要提前声明大小。
计数与求和
bash
# 按部门计数
awk '{count[$2]++} END {for (d in count) print d, count[d]}' employee.txt
# 按部门求和
awk '{sum[$2] += $3} END {for (d in sum) print d, sum[d]}' employee.txt
# 计数、求和、平均一起算
awk '{
count[$2]++
sum[$2] += $3
}
END {
for (d in count)
printf "%s\t%d人\t平均%.2f\n", d, count[d], sum[d] / count[d]
}' employee.txt判断与删除
bash
awk '!($1 in seen) {seen[$1] = 1; print}' file.txtin 用来判断下标是否存在。delete a[key] 删除单个元素;gawk 还支持用 delete a 删除整个数组,需要可移植时可以循环逐个删除。
bash
awk 'BEGIN {
a["x"] = 1
a["y"] = 2
delete a["x"]
for (k in a) print k, a[k]
}'遍历顺序
for (k in a) 的顺序是未定义的,不要依赖它。需要排序时,可以:
- 输出后交给
sort - 使用 gawk 的
PROCINFO["sorted_in"] - 使用 gawk 的
asort/asorti
bash
gawk 'BEGIN {
a["b"] = 2
a["a"] = 1
PROCINFO["sorted_in"] = "@ind_str_asc"
for (k in a) print k, a[k]
}'输出:
text
a 1
b 2常用的排序值还有 @ind_num_asc、@val_num_desc、@val_str_asc 等。
多维数组
awk 没有真正的多维数组,a[i, j] 实际使用 SUBSEP 把下标拼成一个字符串:
bash
awk 'BEGIN {
a[1, 2] = "x"
for (k in a) {
split(k, p, SUBSEP)
print p[1], p[2], a[k]
}
}'
# 1 2 x内存提醒
awk 的数组保存在内存中。统计的 key 数量很大时(例如几千万个唯一 IP),内存会明显增长。超大基数聚合应交给数据库或外部排序。
格式化输出
print 自动追加 ORS(默认换行),printf 不追加换行,需要自己写 \n。
printf 格式符
| 格式 | 说明 |
|---|---|
%s | 字符串 |
%d / %i | 十进制整数 |
%f | 浮点数 |
%e / %E | 科学计数法 |
%g / %G | 自动选择 %f 或 %e |
%x / %X | 十六进制 |
%o | 八进制 |
%c | 字符 |
%% | 字面量 % |
宽度、精度和对齐:
bash
awk 'BEGIN {printf "|%10s|%-10s|%05d|%.2f|\n", "right", "left", 42, 3.14159}'输出:
text
| right|left |00042|3.14|%10s:右对齐,宽度 10%-10s:左对齐,宽度 10%05d:宽度 5,不足补 0%.2f:保留两位小数
实际例子:
bash
awk -F, 'NR > 1 {printf "%-6s %8.2f\n", $2, $3}' sales.csvprint 与数字格式
print 输出数字时受 OFMT 影响,默认是 %.6g;数字参与字符串拼接或作为数组下标时受 CONVFMT 影响。
bash
awk 'BEGIN {
print 1 / 3
OFMT = "%.2f"
print 1 / 3
x = 1 / 3
print "x=" x
CONVFMT = "%.3f"
print "x=" x
}'输出:
text
0.333333
0.33
x=0.333333
x=0.333需要精确控制小数位时,优先用 printf。
重定向、管道与 getline
输出到文件
bash
# 覆盖写入:awk 启动后第一次打开时截断
awk '{print $1 > "names.txt"}' employee.txt
# 追加写入
awk '{print $1 >> "names.txt"}' employee.txt按字段拆分到不同文件时,文件名需要用括号包起来,避免 > 被解析成比较运算符:
bash
awk '{print $0 > ("out_" $2 ".txt")}' employee.txt同一个 awk 运行期间,> 只在第一次打开时截断,之后继续写入同一个文件句柄。要在中途重新截断,需要先 close()。
管道
bash
# 把输出交给 sort
awk '{print $1}' employee.txt | sort
# 在 awk 内部启动管道
awk 'BEGIN {print "b\na\nc" | "sort"; close("sort")}'awk 内部的管道会启动一个子进程,close("sort") 会关闭管道并等待子进程结束。文件句柄和管道用完后都应该关闭,处理大量文件时尤其重要,否则可能耗尽文件描述符。
输出到标准错误
bash
awk '{print "bad line: " $0 > "/dev/stderr"}' file.txtgawk 也支持 print ... | "cat 1>&2" 这类写法,但可读性较差。
system 与 fflush
bash
awk 'BEGIN {system("mkdir -p output")}'system() 执行 Shell 命令并返回退出状态。不要在逐行处理中频繁调用 system(),进程创建开销很大。
fflush() 强制刷新输出缓冲区,常用于需要及时看到输出的场景:
bash
awk '{print; fflush()}' file.txtgetline
getline 用于在 awk 程序内部主动读取数据,常见形式如下:
| 形式 | 读取内容 | 更新的变量 |
|---|---|---|
getline | 下一条记录到 $0 | $0、NF、NR、FNR |
getline var | 下一条记录到 var | var、NR、FNR |
getline < file | 文件下一行到 $0 | $0、NF |
getline var < file | 文件下一行到 var | var |
cmd | getline | 命令输出下一行到 $0 | $0、NF |
cmd | getline var | 命令输出下一行到 var | var |
返回值:成功为 1,读到 EOF 为 0,出错为 -1。
读取文件:
bash
awk 'BEGIN {
while ((getline line < "employee.txt") > 0)
print ++n, line
close("employee.txt")
}'读取命令输出:
bash
awk 'BEGIN {
"date +%F" | getline today
close("date +%F")
print today
}'getline 不会自动更新 NR
getline < file 和 cmd | getline 不会增加 NR / FNR,因为它们不是主输入流。需要行号时自己维护计数器。
自定义函数
函数定义在程序的顶层,可以带参数和返回值:
awk
function max(a, b) {
return a > b ? a : b
}
function trim(s) {
sub(/^[ \t]+/, "", s)
sub(/[ \t]+$/, "", s)
return s
}使用:
bash
awk '
function max(a, b) { return a > b ? a : b }
{ m = max(m, $3) }
END { print m }
' employee.txt输出:
text
15000局部变量
awk 没有专门的 local 关键字,局部变量通过在参数列表末尾多写几个参数来实现,调用时不传值:
awk
function average(sum, n, result) {
if (n == 0) return 0
result = sum / n
return result
}result 是局部变量,sum 和 n 是调用时传入的参数。
数组参数
标量按值传递,数组按引用传递:
awk
function add(count, key, value) {
count[key] += value
}
{
add(total, $2, $3)
}
END {
for (d in total) print d, total[d]
}递归
awk
function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1)
}gawk 可以用 @include "lib.awk" 引入函数库,但这不是 POSIX 特性。
实战案例
案例数据
日志 access.log:
bash
cat > access.log <<'EOF'
192.168.1.10 - - [19/Oct/2026:10:00:01 +0800] "GET /index.html HTTP/1.1" 200 512
10.0.0.5 - - [19/Oct/2026:10:00:03 +0800] "POST /api/login HTTP/1.1" 401 64
192.168.1.10 - - [19/Oct/2026:10:00:05 +0800] "GET /style.css HTTP/1.1" 200 1024
10.0.0.8 - - [19/Oct/2026:10:00:07 +0800] "GET /missing HTTP/1.1" 404 0
10.0.0.5 - - [19/Oct/2026:10:00:09 +0800] "GET /index.html HTTP/1.1" 200 512
EOF销售数据 sales.csv:
bash
cat > sales.csv <<'EOF'
部门,姓名,薪资
研发,张三,12000
测试,李四,9000
研发,王五,15000
运维,赵六,8000
EOF系统用户与磁盘
列出普通用户:
bash
awk -F: '$3 >= 1000 && $3 != 65534 && $7 !~ /(nologin|false)$/ {
printf "%-12s %6s %s\n", $1, $3, $7
}' /etc/passwd检查磁盘使用率是否超过 80%:
bash
df -hP | awk 'NR > 1 {
u = $5
sub(/%/, "", u)
if (u + 0 >= 80) print $6, $5
}'df -hP 的 $5 形如 85%,先用 sub 去掉百分号,再用 u + 0 强制按数值比较。
按用户统计内存占用:
bash
ps aux | awk 'NR > 1 {
mem[$1] += $4
}
END {
for (u in mem) printf "%-10s %6.2f%%\n", u, mem[u]
}' | sort -k2,2nr | head日志过滤
筛选 5xx 状态码:
bash
awk '$9 ~ /^5[0-9][0-9]$/' access.log筛选某个时间范围:
bash
awk '$4 >= "[19/Oct/2026:10:00:03" && $4 < "[19/Oct/2026:10:00:08"' access.log这里利用了时间格式固定、字典序与时间顺序一致的特点。如果日志时间格式不统一,应该先解析成时间戳再比较。
排除注释和空行:
bash
awk 'NF && $1 !~ /^#/' config.conf求和、平均、最值
bash
awk '{
sum += $3
if (NR == 1 || $3 < min) min = $3
if (NR == 1 || $3 > max) max = $3
}
END {
printf "行数=%d 总额=%d 平均=%.2f 最小=%d 最大=%d\n", NR, sum, sum / NR, min, max
}' employee.txt输出:
text
行数=4 总额=44000 平均=11000.00 最小=8000 最大=15000统计满足条件的行数时,记得处理「一条都没匹配」的情况:
bash
awk '$3 >= 20000 {n++} END {print n + 0}' employee.txt
# 0如果直接 print n,没有匹配时 n 未初始化,会输出空行。
分组聚合
按部门统计人数和平均薪资:
bash
awk -F, 'NR > 1 {
count[$1]++
sum[$1] += $3
}
END {
for (d in count)
printf "%s\t%d人\t平均%.0f\n", d, count[d], sum[d] / count[d]
}' sales.csv | sort输出:
text
测试 1人 平均9000
研发 2人 平均13500
运维 1人 平均8000按状态码统计请求数和响应字节数:
bash
awk '{
count[$9]++
bytes[$9] += $10
}
END {
for (s in count)
printf "%s\t%d\t%d\n", s, count[s], bytes[s]
}' access.log | sort -k1,1n日志字段位置
这里按空白切分日志,请求行 "GET /index.html HTTP/1.1" 被拆成了多个字段,状态码刚好在 $9。如果 URL 中含空格,后面的字段位置会整体偏移。生产环境解析访问日志时,建议使用专门的日志解析工具,或者用正则直接提取需要的部分,而不是依赖固定字段号。
输出:
text
200 3 2048
401 1 64
404 1 0词频统计
bash
printf 'the quick brown fox\njumps over the lazy dog\nThe quick fox\n' > words.txt
awk '{
for (i = 1; i <= NF; i++) {
w = tolower($i)
gsub(/[^a-z0-9-]/, "", w)
if (w) count[w]++
}
}
END {
for (w in count) print count[w], w
}' words.txt | sort -k1,1nr -k2,2 | head输出:
text
3 the
2 fox
2 quick
1 brown
1 dog
1 jumps
1 lazy
1 overgsub(/[^a-z0-9-]/, "", w) 去掉单词两边的标点。中文没有空格分词,这个例子只适用于英文或以空格分隔的文本。
去重与重复计数
保留第一次出现的行,保持原顺序:
bash
printf 'a\nb\na\nc\nb\n' | awk '!seen[$0]++'输出:
text
a
b
c按第一列去重,保留每个 key 的第一行:
bash
awk '!seen[$1]++' file.txt统计重复行:
bash
awk '{count[$0]++} END {for (line in count) if (count[line] > 1) print count[line], line}' file.txt!seen[$0]++ 的原理:seen[$0]++ 返回自增前的值。第一次遇到某行时值为 0,取反为真,执行默认的 print;之后值非 0,取反为假,跳过。
Top N
统计访问次数最多的 IP:
bash
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -k1,1nr -k2,2r | head -3输出:
text
2 192.168.1.10
2 10.0.0.5
1 10.0.0.8只保留出现至少两次的 IP:
bash
awk '{count[$1]++} END {for (ip in count) if (count[ip] >= 2) print count[ip], ip}' access.log | sort -rn两个文件关联
users.txt:
text
1001 张三
1002 李四orders.txt:
text
1001 键盘 2
1002 鼠标 1
1003 显示器 1用用户 ID 关联两个文件:
bash
awk 'NR == FNR {name[$1] = $2; next} $1 in name {print $1, name[$1], $2, $3}' users.txt orders.txt输出:
text
1001 张三 键盘 2
1002 李四 鼠标 1NR == FNR 只在读取第一个文件时为真:第一个文件的记录被存入数组后 next,第二个文件再用 $1 in name 做查找。
第一个文件为空时
如果第一个文件为空,第二个文件的第一条记录也会满足 NR == FNR,导致数据被误当成映射表。更稳妥的写法是用 gawk 的 ARGIND == 1,或者用 FILENAME == ARGV[1]。
bash
gawk 'ARGIND == 1 {name[$1] = $2; next} $1 in name {print $1, name[$1], $2, $3}' users.txt orders.txt区间提取
提取证书内容(首尾标记不同,适合范围模式):
bash
awk '/BEGIN CERTIFICATE/, /END CERTIFICATE/' cert.pem范围模式会包含开始行和结束行,一个文件里可以匹配多个区间。
提取配置文件中的某个 section:
bash
awk '/^\[/ {in_section = ($0 ~ /^\[server\]$/)} in_section' nginx.conf这里没有使用范围模式,因为 /^\[/ 会匹配 [server] 自身,导致区间刚开始就结束。状态变量写法遇到下一个 section 时自动关闭,不会把下一段的内容带进来。
段落与多行记录
按空行分隔的段落,统计每段行数:
bash
awk 'BEGIN {RS = ""} {
n = split($0, lines, "\n")
print NR, n
}' poem.txtRS = "" 时每条记录是一个段落,$0 中包含段落内部的换行,用 split 按换行拆开即可得到行数。
把段落中的多行合并成一行:
bash
awk 'BEGIN {RS = ""; FS = "\n"} {print $1, $2}' poem.txt行列转置
bash
printf 'a b c\nd e f\ng h i\n' > grid.txt
awk '{
for (i = 1; i <= NF; i++) a[NR, i] = $i
if (NF > max) max = NF
}
END {
for (i = 1; i <= max; i++) {
for (j = 1; j <= NR; j++)
printf "%s%s", a[j, i], (j < NR ? OFS : ORS)
}
}' grid.txt输出:
text
a d g
b e h
c f iCSV 处理
简单 CSV,字段内没有逗号:
bash
awk -F, 'NR > 1 {print $2, $3}' sales.csv字段内有逗号时,-F, 会把一个字段拆成多个。gawk 5.3+ 可以用 --csv:
bash
printf 'name,note\n张三,"hello, world"\n' > quoted.csv
gawk --csv 'NR > 1 {print $1 " | " $2}' quoted.csv输出:
text
张三 | hello, world旧版 gawk 可以用 FPAT 定义字段本身的样子:
bash
gawk -v FPAT='([^,]*)|("[^"]*")' 'NR > 1 {print $2}' quoted.csvFPAT 方式会保留字段两侧的引号,需要额外去掉。真正复杂的 CSV(转义引号、字段内换行)建议用 Python 的 csv 模块、Miller 或 csvkit。
原地修改文件
删除 Windows 换行中的 \r:
bash
gawk -i inplace '{gsub(/\r$/, ""); print}' file.txt保留备份:
bash
gawk -i inplace -v INPLACE_SUFFIX=.bak '{gsub(/\r$/, ""); print}' file.txt-i inplace 是 gawk 扩展。其他实现可以用临时文件:
bash
awk '{gsub(/\r$/, ""); print}' file.txt > file.tmp && mv file.tmp file.txt生成 SQL
bash
awk -F, 'NR > 1 {
printf "INSERT INTO staff(name, salary) VALUES (\047%s\047, %d);\n", $2, $3
}' sales.csv输出:
sql
INSERT INTO staff(name, salary) VALUES ('张三', 12000);
INSERT INTO staff(name, salary) VALUES ('李四', 9000);
INSERT INTO staff(name, salary) VALUES ('王五', 15000);
INSERT INTO staff(name, salary) VALUES ('赵六', 8000);\047 是单引号的八进制转义,避免和外层 Shell 单引号冲突。真实数据中的单引号需要额外转义,复杂场景建议用专门的导出工具。
性能建议
- 一次扫描完成多个统计:尽量在一个 awk 程序里完成过滤、计数、求和,避免多次遍历文件。
- 用
next提前跳过:不关心的行尽早next,减少后续判断。 - 用
nextfile跳过整个文件:只需要每个文件的第一条匹配时很有效。 - 设置
LC_ALL=C:处理纯 ASCII 数据时,LC_ALL=C awk ...通常能减少 locale 带来的开销。 - 优先用单字符分隔符:
-F,比复杂的正则分隔符更快。 - 避免逐行
system():每次调用都会创建子进程,开销远大于 awk 内部计算。 - 注意数组基数:计数和分组的内存量与唯一 key 数量成正比。
- 大文件仍然流式处理:awk 默认逐条读取,不会把整个文件载入内存;只有数组会占用内存。
- 排序交给
sort:awk 的for-in无序,外部sort通常更简单,也更省内存。 - 选择合适的实现:简单脚本 mawk 往往更快;需要
gensub、asort、--csv时用 gawk。 - 调试用 gawk 工具:
gawk --dump-variables查看变量,gawk --profile生成执行剖面。
常见坑
1. 双引号导致 $1 被 Shell 展开
bash
# 错误
awk "{print $1}" file.txt
# 正确
awk '{print $1}' file.txt2. 忘记指定分隔符
bash
echo 'a,b,c' | awk '{print $2}'
# 输出 a,b,c,因为默认按空白切分
echo 'a,b,c' | awk -F, '{print $2}'
# b3. $NF - 1 不是倒数第二个字段
bash
awk '{print $(NF - 1)}' file.txt # 倒数第二个字段
awk '{print $NF - 1}' file.txt # 最后一个字段的值减 14. NR 与 FNR 混用
NR 跨文件累计,FNR 在每个文件内重新计数。给每个文件加表头要用 FNR == 1,统计总行数才用 NR。
5. 字符串比较与数值比较
bash
awk 'BEGIN {print ("10" > "9")}' # 0,字典序
echo '10 9' | awk '{print ($1 > $2)}' # 1,数值不确定时用 +0 强制转数值。
6. > 会覆盖文件
bash
awk '{print $1 > "out.txt"}' file.txt # 第一次打开时截断
awk '{print $1 >> "out.txt"}' file.txt # 追加同一个 awk 运行中,> 后续写入不会反复截断;但换一次运行就会重新截断。
7. 数组遍历顺序不固定
for (k in a) 的顺序由实现决定。需要稳定输出时,用 sort 或 gawk 的 PROCINFO["sorted_in"]。
8. sub/gsub 会修改原字符串
bash
echo 'aaa' | awk '{sub(/a/, "b"); print}' # baa
echo 'aaa' | awk '{gsub(/a/, "b"); print}' # bbb不想修改原值时,用 gensub(gawk)或先复制一份。
9. FS 设置太晚
在动作里设置 FS 只影响下一条记录。要在第一条记录就生效,用 -F 或 BEGIN {FS = ...}。
10. 空字段被忽略或保留的规则不同
-F, 会保留空字段:
bash
printf 'a,,b\n' | awk -F, '{print NF}'
# 3默认空白分隔则会合并连续空白。写脚本时要先确认数据中是否存在空字段。
11. print 与 printf 的换行
bash
awk '{print $1}' file.txt # 自动换行
awk '{printf "%s", $1}' file.txt # 不换行12. 正则 \d 不可移植
POSIX awk 的 ERE 不支持 \d,用 [0-9]:
bash
awk '$1 ~ /^[0-9]+$/' file.txtgawk 支持 \w、\s 等扩展,但换实现就可能失效。
13. 大整数与浮点精度
awk 内部用双精度浮点数表示数字,超过 53 个二进制位(约 15~16 位十进制)的整数可能丢失精度。处理超长 ID、金额时,尽量按字符串处理,或改用专门工具。
14. 文件句柄与管道未关闭
处理大量输出文件或管道时,及时 close(),否则可能遇到 "too many open files"。
15. getline 的返回值
getline 成功返回 1,EOF 返回 0,出错返回 -1。循环条件要写 > 0,不要只判断非零,否则出错时 -1 也被当成真。
与其他工具对比
| 工具 | 擅长 | 不擅长 |
|---|---|---|
| grep | 按行筛选、正则搜索 | 字段计算、聚合 |
| sed | 流式替换、删除、插入 | 数组、统计、复杂逻辑 |
cut | 按固定分隔符取列 | 条件判断、正则、计算 |
sort / uniq | 排序、相邻去重 | 按字段聚合、保留原顺序去重 |
| awk | 字段处理、过滤、统计、报表 | JSON/XML、复杂 CSV |
perl / python | 复杂逻辑、库生态、JSON/CSV | 简单一行命令的启动成本 |
jq | JSON 查询和转换 | 普通文本表格处理 |
选择建议:
- 只是筛选行:
grep - 只是替换文本:
sed - 只是取固定列:
cut - 需要按列判断、求和、分组、生成报表:
awk - 需要解析 JSON、复杂 CSV、调用 API:Python / jq / Miller
速查表
bash
# 打印某列
awk '{print $2}' file.txt
# 打印多列
awk '{print $1, $3}' file.txt
# 指定分隔符
awk -F: '{print $1}' /etc/passwd
# 按数值过滤
awk '$3 > 100' file.txt
# 按字符串过滤
awk '$2 == "研发"' file.txt
# 正则过滤
awk '/error/' file.txt
# 正则取反
awk '!/debug/' file.txt
# 带行号
awk '{print NR, $0}' file.txt
# 最后一个字段
awk '{print $NF}' file.txt
# 倒数第二个字段
awk '{print $(NF - 1)}' file.txt
# 总行数
awk 'END {print NR}' file.txt
# 列求和
awk '{sum += $1} END {print sum}' file.txt
# 列平均
awk '{sum += $1} END {print sum / NR}' file.txt
# 最大值
awk 'NR == 1 || $1 > max {max = $1} END {print max}' file.txt
# 整行去重并保持顺序
awk '!seen[$0]++' file.txt
# 按第一列去重
awk '!seen[$1]++' file.txt
# 分组计数
awk '{count[$1]++} END {for (k in count) print k, count[k]}' file.txt
# 分组求和
awk '{sum[$1] += $2} END {for (k in sum) print k, sum[k]}' file.txt
# 两文件关联
awk 'NR == FNR {map[$1] = $2; next} $1 in map {print $1, map[$1], $2}' f1 f2
# 区间提取
awk '/start/, /end/' file.txt
# 替换第一个匹配
awk '{sub(/old/, "new"); print}' file.txt
# 替换全部匹配
awk '{gsub(/old/, "new"); print}' file.txt
# 指定输出分隔符
awk 'BEGIN {OFS = "\t"} {print $1, $2}' file.txt
# 生成 Top N
awk '{count[$1]++} END {for (k in count) print count[k], k}' file.txt | sort -rn | head -10总结
awk 的核心可以概括为三句话:
- 一条记录一条记录地读:默认一行一条,按
RS切分。 - 自动切成字段:默认按空白,按
FS切分,用$1…$NF访问。 - 模式匹配后执行动作:
pattern { action },BEGIN做初始化,END做汇总。
掌握字段、内置变量、模式、数组和 printf 这五部分,就能覆盖绝大多数日常文本处理需求。遇到复杂 CSV、JSON 或超大规模数据时,再把任务交给更合适的工具。