使用

1.动作(Action)

动作写在{ }中,可以包含输出语句、赋值、条件判断和循环等。

1
echo 'this is a test' | awk '{print $1,$4}'
1
this test
  • { }围出一个动作块
  • print $1,$4是动作块中的输出语句
  • 同一动作块可以写多条语句,用分号或换行分隔

if条件语句

看日志时,常见的需求是根据HTTP状态码给请求分类。这里用一份简化的日志演示,格式为路径 状态码 耗时毫秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat > requests.log <<'EOF'
/ 200 18
/missing 404 3
/api 503 1200
EOF
awk '{
if ($2 >= 500) {
result="服务端错误"
} else if ($2 >= 400) {
result="客户端错误"
} else {
result="未命中4xx/5xx"
}
print $1,$2,result
}' requests.log
1
2
3
/ 200 未命中4xx/5xx
/missing 404 客户端错误
/api 503 服务端错误

这里假设输入是有效的HTTP状态码,只演示4xx、5xx的分类。其他状态不直接标成“成功”,例如3xx还可能表示重定向。

日志不是越长越有用,先把值得看的几行捞出来,眼睛也能少加点班。

while循环

打印数字1~10。

1
awk 'BEGIN {i=1; while (i<=10) {print i; i++}}'
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

i++让变量每次加1,避免一直停留在同一个条件中。

do…while循环

1
echo 'this is a test' | awk '{i=1; do {print $i; i++} while (i<=NF)}'
1
2
3
4
this
is
a
test

do...while会先执行一次,再判断条件。因此遇到空行、NF=0时,上述写法仍会输出一次空内容。

如果需要逐个输出字段,并跳过空行,使用下面的for更直接。

for循环

1
awk 'BEGIN {for (i=1; i<=5; i++) {print i}}'
1
2
3
4
5
1
2
3
4
5

逐个输出每行的字段

1
echo 'this is a test' | awk '{for (i=1; i<=NF; i++) {print $i}}'

输出同上面的do...while示例,但遇到空行时不会进入循环。

补充:格式化输出

printf可以控制列宽、小数位和换行。

1
printf '%s\n' 'apple 3.5' 'banana 12' | awk '{printf "%-8s %6.2f\n",$1,$2}'
1
2
apple      3.50
banana 12.00
  • %-8s:字符串按至少8个字符宽度左对齐
  • %6.2f:浮点数按至少6个字符宽度输出,保留2位小数
  • printf不会自动添加ORS,这里用\n手动换行

2.数组

awk中的数组是关联数组,可以用字符串作为下标,不需要提前声明大小。

例如,按服务名称保存状态,比记住“第1个是谁、第2个是谁”更直观。

1
2
3
下标(key)      值(value)
"blog" "running"
"api" "stopped"

创建数组

格式:数组名[下标]=值

访问:数组名[下标]

1
2
3
4
5
awk 'BEGIN {
service["blog"]="running"
service["api"]="stopped"
print service["blog"] "\n" service["api"]
}'
1
2
running
stopped

删除数组元素

格式:delete 数组名[下标]

1
2
3
4
5
6
7
awk 'BEGIN {
service["blog"]="running"
service["api"]="stopped"
delete service["api"]
print service["blog"]
print ("api" in service)
}'
1
2
running
0

"api" in service用于判断下标是否存在,返回0表示不存在。直接输出一个不存在的元素,通常只会显示空内容,不能清楚地说明是否已删除。

补充:按日志级别统计

1
printf '%s\n' 'INFO' 'ERROR' 'INFO' | awk '{count[$1]++} END {for (key in count) print key,count[key]}' | sort
1
2
ERROR 1
INFO 2

for (key in count)遍历数组下标,遍历顺序不保证固定。这里通过sort让结果按名称排序。

3.函数

内置函数大致包括算术函数、字符串函数等;awk还支持自定义函数。

算术函数

rand()产生大于等于0、小于1的伪随机数。调用多次会得到一个序列,并不是每次调用都返回相同的值。

通常需要配合srand()设置种子。

1
awk 'BEGIN {srand(); print int(rand()*100)}'

输出为0~99之间的整数,实际结果不固定。

💡提示

不设种子时,同一awk实现的不同运行往往从同一个默认种子开始,因此容易得到相同的序列。

srand()常按时间设置种子,短时间内重复启动也可能得到相同序列。

截取整数部分

1
awk 'BEGIN {print int(3.8),int(-3.8)}'
1
3 -3

注意,截取整数部分的函数名是int()。

字符串函数

1.sub()和gsub()

  • sub(正则, 替换内容, 目标):替换第一个匹配
  • gsub(正则, 替换内容, 目标):替换所有匹配
  • 省略目标时,默认修改$0;返回值是替换次数
1
echo 'this is a test' | awk '{gsub(/t/,"T",$1); print $0}'
1
This is a test

这里只有第1列中的t被替换。对字段赋值后,awk会用OFS重新拼接$0,原有多余空白可能随之改变。

2.length()

1
echo 'this is a test' | awk '{print $0,length($0)}'
1
this is a test 14

空格也计算在长度中,但记录末尾作为输入分隔符的换行不包含在$0里。

3.index()

1
echo 'this is a test' | awk '{print index($0,"a")}'
1
9

位置从1开始计数,没有找到时返回0。

4.substr()

1
echo 'this is a test' | awk '{print substr($0,11,4)}'
1
test

从第11个位置开始,取4个字符。

补充:自定义函数

1
awk 'function square(x) {return x*x} BEGIN {print square(5)}'
1
25

总结

(1)动作块可以包含输出、赋值、条件判断和循环

(2)awk数组无需预先声明大小,可以用字符串作为下标

(3)sub()替换第一个匹配,gsub()替换所有匹配

(4)print默认追加ORS,printf需要自己安排换行

(5)统计总和、平均值等结果,通常放在END中输出

练习

基础使用

为了让结果便于核对,先创建几份小文件。nums.txt里的数字可以看作请求耗时(毫秒),用它练习总和、平均值和最大值。

1
2
3
printf '%s\n' '114' '191' '666' '888' > nums.txt
printf '%s\n' 'bash' 'cat' 'vim is a tool for editing' > tools.txt
printf '%s\n' 'berry null' 'cherry' 'berry null' 'null' 'whoami' > words.txt

1.统计文件中的行数

1
awk 'END {print NR}' nums.txt
1
4

默认情况下NR是行数;自定义RS后,应理解为记录数。

2.计算数字总和

1
awk '{sum+=$1} END {print sum+0}' nums.txt
1
1859

sum+0让空文件也能输出数字0。

3.查找包含关键字的行

1
awk '/vim/' tools.txt
1
vim is a tool for editing

4.替换文本

1
awk '{gsub(/vim/,"vi")} 1' tools.txt
1
2
3
bash
cat
vi is a tool for editing

结尾的1是一个始终为真的模式,没有动作时默认打印当前记录。

5.删除指定行

1
awk 'NR != 2' tools.txt
1
2
bash
vim is a tool for editing

6.提取第1列

1
awk '{print $1}' tools.txt
1
2
3
bash
cat
vim

7.计算第1列的平均值

1
awk '{sum+=$1} END {if (NR>0) print sum/NR; else print "没有数据"}' nums.txt
1
464.75

空文件时不计算,避免除以0。这个练习假设每行都有一个有效数字。

8.找到第1列的最大值

1
awk 'NR==1 || $1>max {max=$1} END {if (NR>0) print "最大值是:",max; else print "没有数据"}' nums.txt
1
最大值是: 888

先使用第1条记录初始化,避免数据全为负数时错误地输出0。请求耗时通常非负,但换成余额或温度数据时,这个细节就会派上用场。

9.删除重复行,保留第一次出现的顺序

1
awk '!seen[$0]++' words.txt
1
2
3
4
berry null
cherry
null
whoami

seen[$0]以整行为下标。后置++先返回原值再递增:第一次出现时原值为0,取反后为真,因此输出;再次出现时就不会输出。

10.打印第5~10行

1
awk 'NR>=5 && NR<=10' /proc/cpuinfo

/proc/cpuinfo属于Linux系统文件,实际内容随机器变化。

11.计算每行字符串的长度

1
awk '{print length($0)}' tools.txt
1
2
3
4
3
25

12.使用冒号分隔字段,输出结果的最后10行

1
awk -F ':' '{print $1,$3}' /etc/passwd | tail -n 10

13.将小写字母转换为大写

1
awk '{print toupper($0)}' tools.txt
1
2
3
BASH
CAT
VIM IS A TOOL FOR EDITING

14.打印用户名为root的行

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

15.统计文本长度,以及文件字节数

1
awk '{sum+=length($0)} END {print sum+0}' tools.txt
1
32

这里统计的是每条记录的字符串长度之和,不包含记录分隔符。字符与字节的关系还受编码和awk实现影响,因此不能把这个结果当成文件字节大小。

如果需要准确的文件字节数,使用:

1
wc -c < tools.txt
1
35

这份文件全部是ASCII字符,另有3个换行字节,所以共35字节。wc输出数字前可能带有空格。

小应用

1.格式化空白

移除每行前后空白,并统一字段之间的分隔符。

1
2
3
4
5
6
cat > 1.txt <<'EOF'
aaaa bbb ccc
bbb aaa ccc
ddd fff eee gg hh ii jj
EOF
awk 'BEGIN {OFS="\t"} {$1=$1; print}' 1.txt
1
2
3
aaaa	bbb	ccc
bbb aaa ccc
ddd fff eee gg hh ii jj
  • BEGIN设置输出字段分隔符为制表符
  • $1=$1给第1个字段重新赋值,触发$0的重建
  • print输出重建后的记录,字段之间使用OFS

💡提示

这会统一空白,但不保证视觉上各列一定对齐。制表符的显示与终端制表位有关;需要固定列宽时可使用printf。

2.获取网卡的IPv4地址

如果直接写{print "ens33:"},没有模式条件,会对每行重复输出标题。可以将标题放入BEGIN,也可以把网卡名和地址放在同一行输出。

使用Linux的ip命令

1
ip -4 -o addr show dev ens33 | awk '{split($4,addr,"/"); print $2 ":" addr[1]}'

模拟输入

1
printf '%s\n' '2: ens33    inet 192.168.10.20/24 brd 192.168.10.255 scope global ens33' | awk '{split($4,addr,"/"); print $2 ":" addr[1]}'
1
ens33:192.168.10.20
  • -4只查看IPv4,-o让每个地址占一条输出记录
  • $4包含地址和前缀长度,例如192.168.10.20/24
  • split()按斜线拆分,addr[1]就是地址
  • ens33需要替换成实际网卡名称

如果使用Linux中常见的ifconfig输出格式,也可以写:

1
ifconfig ens33 | awk '$1=="inet" {print "ens33:" $2}'

旧版inet addr:...格式需要另外处理,不能假定所有环境的第2列都是纯IP地址。

3.统计TCP连接状态数量

先提取状态字段,再排序、计数。

1
netstat -tna | awk '$1 ~ /^tcp/ {print $6}' | sort | uniq -c

这条命令针对Linux中常见的netstat输出:第1列为协议,第6列为状态。

也可以直接使用awk数组计数。

1
netstat -tna | awk '$1 ~ /^tcp/ {count[$6]++} END {for (state in count) print state,count[state]}' | sort

Linux中使用ss时,状态通常是第1列,不能照搬$6。

1
ss -tanH | awk '{count[$1]++} END {for (state in count) print state,count[state]}' | sort

模拟状态统计

1
printf '%s\n' 'ESTAB' 'LISTEN' 'ESTAB' 'TIME-WAIT' | awk '{count[$1]++} END {for (state in count) print state,count[state]}' | sort
1
2
3
ESTAB 2
LISTEN 1
TIME-WAIT 1

4.生成随机数

1
2
3
4
5
awk 'BEGIN {
srand()
num=int(rand()*100)
print "随机数:",num
}'
  • srand()初始化种子
  • rand()返回[0,1)范围内的伪随机数
  • 乘以100,再通过int()截去小数,得到0~99之间的整数

这个方法适合简单的随机演示,不适合生成密码或密钥。

参考:GNU awk数值函数、GNU awk使用手册。