awk文本处理器

概念

awk是一种用于处理文本和数据的编程语言。名称来自三位创建者Alfred Aho、Peter Weinberger和Brian Kernighan的姓氏首字母。

  • 可以处理标准输入、一个或多个文件,以及其他命令的输出
  • 支持变量、条件判断、循环、数组和函数
  • 既可以直接写在命令行中,也可以保存为脚本使用

如何理解

如果说sed擅长“按行修改”,那么awk很适合“把每行分成几列,再按条件处理”。

常用文本工具对应的使用场景

  • grep:搜索文本,输出匹配的行
  • sed:对文本进行替换、删除、插入等编辑
  • awk:按字段提取内容,进行判断、统计和格式化输出

原理

flowchart TD A["开始"] --> B["执行 BEGIN:初始化"] B --> C{"还有下一条记录?"} C -->|有| D["按 RS 读取记录,保存为 $0"] D --> E["按 FS 分割字段,更新 NF、NR、FNR"] E --> F["从第一条普通规则开始"] F --> G{"当前规则的模式匹配?"} G -->|是| H["执行对应动作"] G -->|否| I{"还有下一条规则?"} H --> I I -->|"有:切换到下一条规则"| G I -->|没有| C C -->|没有| J["执行 END:输出统计或收尾"] J --> K["结束"]

同一条记录可以匹配多条规则,awk会按书写顺序逐条判断。默认会检查所有普通规则;使用next时,会跳过当前记录剩余的规则,直接读取下一条记录。

默认使用换行符分隔记录,因此可以先把“记录”理解为一行,把“字段”理解为一列。

  • 没有指定模式时,对每条记录执行动作
  • 普通模式没有指定动作时,默认执行print $0
  • BEGIN和END是特殊模式,必须有动作块,不能单独写成一个裸模式

使用说明

基本语法:awk [选项] '模式 {动作}' [文件名 ...]

选项

选项 说明
-F 分隔符 指定输入字段分隔符
-v 变量=值 在程序执行前设置变量,可重复使用
-f 脚本文件 从文件读取awk程序

脚本可以写成:'BEGIN {初始化} 模式 {处理} END {收尾}'。

三个部分按需要选用,不必同时存在。程序通常放在单引号内,避免Shell提前展开$1等内容。

内置变量

变量 说明
$0 当前整条记录
$n 当前记录的第n个字段,例如$1
NF 当前记录的字段数量
$NF 当前记录的最后一个字段
NR 从所有输入文件累计读取的记录数
FNR 当前文件已读取的记录数,每个新文件从1开始
FILENAME 当前正在处理的文件名
FS 输入字段分隔符,默认值是一个空格,具有特殊分割规则
OFS print多个参数之间的分隔符,默认是一个空格
RS 输入记录分隔符,默认是换行符\n
ORS print每次输出末尾的分隔符,默认是换行符\n
ARGC、ARGV 命令行参数数量和参数数组
ENVIRON 环境变量组成的关联数组
CONVFMT 数值转为字符串时的格式,默认%.6g
OFMT print输出数值时使用的格式,默认%.6g
RSTART、RLENGTH match()匹配的位置和长度
SUBSEP 多维数组组合下标时使用的分隔符,默认"\034"

GNU awk扩展变量

变量 说明
ARGIND 当前文件在ARGV中的下标;ARGV[0]通常为程序名称
ERRNO 某些输入输出错误的描述
FIELDWIDTHS 按固定宽度分割字段的宽度列表
IGNORECASE 控制大小写是否参与字符串和正则比较

💡提示

awk可能是GNU awk、mawk,也可能是BSD awk。上面的扩展变量不能假定所有实现都支持,本文主体示例使用常见的通用语法。

内置函数

函数 说明
rand() 返回大于等于0、小于1的伪随机数
srand([种子]) 设置随机种子,不传参数时通常使用当前时间
int(x) 向0截断小数部分,例如int(-2.8)为-2
sqrt(x) 平方根
sin(x)、cos(x) 正弦、余弦,角度使用弧度
toupper(s)、tolower(s) 字符串转为大写、小写
length(s) 字符串长度;省略参数时使用$0
substr(s, 起点, 长度) 提取子串,起点从1开始,长度可以省略
index(s, 子串) 返回子串首次出现的位置,未找到时返回0
sub()、gsub() 替换第一个匹配、替换所有匹配
split(s, 数组, 分隔符) 将字符串拆分到数组,返回字段数

基本用法

1.基础打印

先不使用选项,也不指定模式,只写最简单的动作。

普通打印

1
echo aaa | awk '{print}'
1
aaa

{print}与{print $0}效果相同,都输出当前整条记录。

单列打印

1
echo 'this is a test' | awk '{print $3}'
1
a

$3表示第3列。默认的FS=" "会将连续的空格和制表符作为分隔,并忽略记录前后的空格和制表符,并不是每个空格都分出一个字段。

NF和$NF

1
echo 'this is a test' | awk '{print NF; print $NF}'
1
2
4
test
  • NF表示有多少个字段
  • $NF表示第NF个字段,也就是最后一列

一个回答“有几列”,一个回答“最后一列是什么”。只差一个$,工作内容可没少差。

多列打印

1
echo 'this is a test' | awk '{print $1,$3}'
1
this a

用逗号分隔多个输出参数,awk会在它们之间插入输出字段分隔符OFS。

组合打印

1
echo 'this is a test' | awk '{print "Hello," $NF}'
1
Hello,test

字符串和变量相邻时表示拼接,不会自动插入空格。如果想显示Hello, test,可以写成print "Hello,", $NF。

💡提示

$1外面不要加双引号。print "$1"输出的是字面文本$1。

2.特殊模式

BEGIN

读取文本之前执行的操作。

1
awk 'BEGIN {print "Hello"}'
1
Hello

只有BEGIN动作时,不需要提供输入文件。

如果想先执行BEGIN,再逐行处理文本,可以这样写。

1
echo 'this is a test' | awk 'BEGIN {print "Hello"} {print $0}'
1
2
Hello
this is a test

不要在上述管道命令后再随意加一个test文件名:指定文件后,正常读取的是文件,管道中的内容不会自动与文件内容合并。

END

输入处理结束后执行的操作。

1
echo 'this is a test' | awk '{print $0} END {print "Hello"}'
1
2
this is a test
Hello

扩展:两种模式结合使用

1
echo 'this is a test' | awk 'BEGIN {print "Now"} {print $0} END {print "Hello"}'
1
2
3
Now
this is a test
Hello

3.分隔符

输入字段分隔符

通过-F指定分隔符,把每条记录切成多个字段。

例如,一份简单的服务清单,按服务名#环境#位置#端口排列,我们只想取服务名和端口。

1
printf '%s\n' 'blog#dev#local#3000' 'api#test#local#8080' | awk -F '#' '{print $1,$4}'
1
2
blog 3000
api 8080

也可以使用-v设置内置变量FS。

1
printf '%s\n' 'blog#dev#local#3000' 'api#test#local#8080' | awk -v FS='#' '{print $1,$4}'

FS还可以在BEGIN中设置,不是必须通过-v赋值。例如:awk 'BEGIN {FS="#"} {print $1,$4}'。

输出字段分隔符

1
echo 'this is a test' | awk -v OFS='#' '{print $1,$4}'
1
this#test

输入和输出分隔符结合使用

1
printf '%s\n' 'blog#dev#local#3000' 'api#test#local#8080' | awk -v FS='#' -v OFS='_' '{print $1,$4}'
1
2
blog_3000
api_8080

补充:连接在一起输出

1
echo 'this is a test' | awk '{print $1,$4; print $1 $4}'
1
2
this test
thistest

有逗号时使用OFS分隔;没有逗号、表达式相邻时表示拼接。

4.变量

内置变量举例

NR和FNR

1
2
3
printf '%s\n' 'apple red' 'banana yellow' > a.txt
printf '%s\n' 'cherry red' > b.txt
awk '{print FILENAME,NR,FNR,$1}' a.txt b.txt
1
2
3
a.txt 1 1 apple
a.txt 2 2 banana
b.txt 3 1 cherry

NR跨文件累加,FNR在新文件中重新计数。

RS:输入记录分隔符

1
printf '%s' 'this is a test' | awk -v RS=' ' '{print $0}'
1
2
3
4
this
is
a
test

这里每遇到一个空格,就结束一条记录。RS分割记录,FS分割记录内部的字段。

ORS:输出记录分隔符

1
printf '%s\n' 'aaa' 'bbb' | awk -v ORS='++' '{print $0}'
1
aaa++bbb++

print每次输出末尾都会添加ORS。这里没有结尾换行,终端提示符可能会紧接在++后面。

自定义变量

方法一:使用-v

1
echo 'this is a test' | awk -v name='Shell' '{print name,$NF}'
1
Shell test

方法二:在程序中赋值

1
echo 'this is a test' | awk 'BEGIN {name="Shell"} {print name,$NF}'

变量名区分大小写,不需要像Shell那样用$name取值。同一代码块内的多条语句,可以用分号或换行隔开。

5.模式(Pattern)

模式相当于条件:满足条件的记录才执行后面的动作。

关系表达式

1
2
3
4
printf '%s\n' '3 2' '2 2' '1 2' > numbers.txt
awk '$1 > $2 {print}' numbers.txt
awk '$1 == $2 {print}' numbers.txt
awk '$1 < $2 {print}' numbers.txt
1
2
3
3 2
2 2
1 2

上面的三行输出分别对应三个命令。

输出奇数行

1
awk 'NR % 2 == 1 {print NR,$1}' numbers.txt
1
2
1 3
3 1

输出第6行以后的行

1
awk 'NR > 6 {print $1}' /etc/os-release

/etc/os-release属于Linux系统文件,内容随发行版变化。

运算符 含义 示例
<、<= 小于、小于等于 $1 < 10
==、!= 等于、不等于 $1 == "root"
>、>= 大于、大于等于 $3 >= 1000
~ 匹配正则 $1 ~ /^root/
!~ 不匹配正则 $1 !~ /^root/
&& 两个条件同时满足 NR >= 5 && NR <= 10

💡提示

==是比较,=是赋值。筛选第5行应该写NR == 5,不要写成NR = 5。

正则模式

awk使用扩展正则表达式,正则常量写在两个斜线之间。

1
awk '/^root/ {print $0}' /etc/passwd

对应的grep写法是:grep '^root' /etc/passwd。

没有写动作时,默认输出整条记录,因此可以简写为:

1
awk '/^root/' /etc/passwd

如果只想精确匹配用户名root,按字段判断更清楚。

1
awk -F ':' '$1 == "root" {print}' /etc/passwd

补充

现代GNU awk默认支持{m,n}次数匹配,不需要额外添加--posix或--re-interval。较旧的awk实现可能存在差异。

小结

(1)awk默认按行读取、按空白分割字段

(2)$0是整条记录,$1是第1列,NF是字段数,$NF是最后一列

(3)BEGIN在读取前执行,END在输入处理结束后执行

(4)FS和OFS处理字段分隔,RS和ORS处理记录分隔

参考:GNU awk内置变量、GNU awk使用手册。