第七章 编写awk脚本

本章讲 POSIX‑awk(标准awk,不是gawk特有扩展)。

遵守规则

  • awk有完整语法规则,参考附录二快速参考。
  • 学习方式:先写脚本实践,遇到问题再回头看语法手册。
  • 常见坑:漏掉空格、括号、引号,语法报错。
  • 注释:#开头,到行尾结束;awk脚本任意位置都可以写注释,这点和sed不一样。

Hello,World

awk是输入驱动型语言:没有输入行,主循环代码不会执行。

  1. 每一行输入都执行print输出字符串
1
awk '{print "Hello World"}' test

输入行会被读取,但不会自动打印;只打印写死的字符串。

  1. 无参数print,原样输出输入行
1
awk '{print}' test2
  1. BEGIN块:在读入任何输入行之前执行,但仅执行一次,不需要输入文件也能运行
1
awk 'BEGIN {print "Hello,World"}'

如果整个脚本只有BEGIN,awk不会去读取任何输入文件。就算后边跟着test文件,也不会去读test文件。

awk程序设计模型

awk内置主输入循环,不需要你手写读文件、读行代码。 整个awk脚本分为3部分:

模块 执行时机
BEGIN {…} 处理输入之前,读取任何输入行之前执行一次
模式/动作(pattern {action}) 主输入循环:对每一行输入循环执行
END {…} 全部输入读完之后,退出前执行一次

流程:BEGIN → 循环读每一行,执行模式动作 → END。 模式:条件判断;动作:大括号{}里面的代码; 如果只写模式不写动作,默认动作是打印匹配行。

示例:匹配空行就打印提示

1
/^$/ {print "This is a blank line."}

一行输入可以匹配多条模式规则,多条动作都会执行。

程序脚本的注释

  • # 行注释,从#到行末尾。
1
2
# blank.awk -- 为每个空行打印消息
/^$/ { print "This is a blank line." }

脚本文件里可以随意加注释;命令行直接写awk单行脚本时,注意shell引号干扰,慎用#注释。

记录和字段

awk默认:

  • 记录(record):一行输入,$0代表整条记录
  • 字段(field):被分隔符切分出来的单词;默认分隔符:连续空格/制表符
  • $1第1字段,$2第2字段……$NF最后一个字段。

字段引用

示例names文件内容

1
John Robinson 666-555-1111
1
2
awk '{print $2, $1, $3}' names
#输出 Robinson John 666-555-1111

print里面的逗号,输出时自动用 输出字段分隔符OFS(默认空格) 隔开。

字段编号可以是表达式计算结果:

1
2
echo a b c | awk 'BEGIN{one=1;two=2}{print $(one+two)}'
#输出 c

修改输入字段分隔符FS

  1. 命令行参数 -F
1
2
#以逗号作为分隔符
awk -F, '{print $1,$6}' names
  1. 在BEGIN块设置内置变量FS(推荐写在脚本内部)
1
2
BEGIN { FS = "," }
{ print $1 ", " $6 }

FS的三种工作模式(重点)

  1. FS = " "(默认,一个空格)
    • 前导、末尾空格制表全部忽略;
    • 任意多个连续空格、tab都当做一个分隔符。
  2. FS设置单个字符,例如 FS=":"、FS="\t"
    • 每一个该字符都是分隔符;两个连续分隔符之间产生空字段。
    • 例:abc\t\tdef,FS="\t" → 切分成3段:abc、空字符串、def
  3. FS设置为正则表达式(长度>1)
1
BEGIN{FS="\t+"} #一个或多个制表符作为分隔符
  • 把匹配正则的子串作为分隔符。

字段匹配操作符

  • ~:字段匹配正则;!~:字段不匹配正则
1
2
3
4
#第5个字段包含MA则打印
$5 ~ /MA/ { print $1, $6 }
#第5个字段不包含MA则打印
$5 !~ /MA/ { print $1, $6 }

表达式

表达式由:常量、变量、运算符、函数、正则组成。

常量

  • 字符串常量:必须双引号 "hello"
  • 数字常量:100、3.14
  • 字符串拼接:直接放一起,不需要运算符,空格代表拼接
1
z = "Hello" "World"  # z得到 HelloWorld

变量

  1. 变量命名:字母数字下划线,不能数字开头,区分大小写。
  2. 不需要声明定义,不用指定类型。变量同时保存字符串值和数字值,awk自动根据上下文选用。
  3. 未初始化的变量:字符串是空;当做数字时值等于0。
1
2
3
x = 1          #数字赋值
z = "Hello"    #字符串赋值
w = $1         #把第一个字段赋值给w

表7‑1 转义序列

序列 描述 示例
\a 报警BEL字符 蜂鸣
\b 退格
\f 换页
\n 换行
\r 回车
\t 水平制表
\v 垂直制表
\ddd 1‑3位八进制字符 \141 代表a
\xhex 十六进制字符(非POSIX扩展) \x61代表a
\c 字面输出c

\xhex不属于POSIX标准,部分awk版本不支持。

表7‑2 算术运算符

操作符 描述 备注
+ 加
- 减
* 乘
/ 除
% 取模(余数)
^ 取幂 POSIX标准
** 取幂 扩展,不可移植,不推荐

表7‑3 赋值运算符

操作符 定义
++ 变量自增1
-- 变量自减1
+= x += n 等价 x = x + n
-= x -= n 等价 x = x - n
*= x *= n 等价 x = x * n
/= x /= n 等价 x = x / n
%= x %= n 等价 x = x % n
^= x ^= n 等价 x = x ^ n
**= 取幂赋值(扩展,不可移植)

++ --区分前缀、后缀:

  • ++x:先自增,再返回值(前缀)
  • x++:先返回原值,再自增(后缀)

示例:统计空行总数

1
2
3
#统计空行数量
/^$/ { ++x }
END { print x }

示例:学生成绩求平均值 输入grades文件

1
2
3
John 85 92 78 94 88
andrea 89 90 75 90 86
jasper 84 88 80 92 84

脚本grades.awk

1
2
3
4
5
{
    total = $2 + $3 + $4 + $5 + $6
    avg = total / 5
    print $1, avg
}

awk内置系统变量

变量 含义
FS 输入字段分隔符,默认空格;BEGIN中修改生效,只影响下一行
OFS 输出字段分隔符,默认空格;print逗号输出用这个隔开
RS 输入记录分隔符,默认换行\n;RS=""代表按空行分割多条记录
ORS 输出记录分隔符,默认换行\n
NF 当前这一行切割出来的字段的数量;$NF取最后一个字段
NR 行号计数器。读到第 1 行 NR=1;读到第 2 行 NR=2;跨多个文件会持续累加。
FNR 当前文件内的记录行号;每个新文件从1重新计数
FILENAME 当前正在读取的输入文件名
CONVFMT POSIX,数字转字符串格式,默认%.6g
OFMT print输出数字格式(老版本awk)

重要:POSIX awk中修改FS,不会改变当前已经读入的行,只会对下一行生效。

示例,打印总行数

1
2
3
BEGIN{FS=","}
{print $1,$6}
END{print NR,"records processed."}

多行记录(记录之间空行分隔)

场景:一条记录跨多行,记录与记录之间用空行隔开。 设置:

1
2
3
4
5
BEGIN {
    FS = "\n"   #字段分隔符为换行,一行内容=一个字段
    RS = ""     #空行作为记录分隔符
}
{ print $1, $NF } #打印第一条、最后一条字段

还可以修改OFS、ORS改变多行记录输出格式。

支票簿结算示例

输入:第一行为初始余额,后续每行:支票号、商户、金额(负数代表支出)

1
2
3
1000
125 Market -125.45
126 Hardware Store -34.95

脚本 checkbook.awk

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
BEGIN { FS = "\t" }
NR == 1 {
    print "Beginning Balance:\t" $1
    balance = $1
    next   #next:立刻读取下一行,不再执行本记录后续代码
}
{
    print $1,$2,$3
    print balance += $3
}

next:立刻结束当前行处理,直接跳到文本读取第 2 行,下面大括号的代码完全跳过,不执行。

关系操作符与布尔操作符

表7‑4 关系操作符

操作符 描述
< 小于
> 大于
<= 小于等于
>= 大于等于
== 等于判断;不要和赋值=搞混
!= 不等于
~ 匹配正则
!~ 不匹配正则

示例:只打印恰好6个字段的记录

1
NF == 6 { print $1,$6 }

注意:=是赋值;==才是相等判断,新手高频错误。

表7‑5 布尔操作符

操作符 定义
&& 逻辑与(两边条件全部为真才成立)
| 逻辑或(任意一个条件为真就成立)
! 逻辑非,取反

示例复合条件

1
2
3
4
5
#字段数等于6,并且行号大于1
NF == 6 && NR >1

# 括号改变运算优先级
NR>1 && (NF>=2 || $1 ~ /\t/)

&&优先级高于||,复杂条件建议写括号明确顺序。

ls‑l输出处理示例 filesum脚本

需求:读取ls -l输出,统计普通文件大小总和,区分普通文件、目录,忽略total汇总行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ls -l | awk '
BEGIN { print "BYTES","\t","FILE" }
#普通文件:9个字段,行首-
NF ==9 && /^-/ {
    sum += $5
    ++filenum
    print $5,"\t",$9
}
#目录:9个字段,行首d
NF ==9 && /^d/ {
    print "<dir>","\t",$9
}
#匹配 ls -lR输出的 ./xxx: 这种标题行
$1 ~ /^\..*:$/ { print $0 }
END {
    print "Total: ", sum, "bytes (" filenum " files)"
}
'

格式化打印 printf

  • print:自动追加ORS换行;
  • printf:不会自动输出换行,必须手动写\n。

语法:

1
2
3
printf(格式字符串, 参数1, 参数2 …)
#括号可以省略
printf 格式字符串, 参数1, 参数2 …

表7‑6 printf格式格式符

字符 定义
c ASCII字符
d 十进制整数
i 十进制整数(POSIX新增)
e 科学计数法e形式
E 科学计数法E大写形式
f 浮点数小数形式
g 自动选用e/f,去除多余末尾0
G 自动选用E/f,去除多余末尾0
o 无符号八进制
s 字符串
u 无符号十进制
x 小写十六进制 a‑f
X 大写十六进制 A‑F
%% 输出字面%符号

修饰符完整格式: %[-][宽度][.精度]格式符

  1. -:不加-默认右对齐;写-代表左对齐
  2. 宽度:输出占多少字符位,不足补空格
  3. .精度:对浮点数是小数点位数;字符串代表最多输出多少字符

示例

1
2
3
4
5
6
#右对齐,占10位
printf("|%10s|\n","hello")
#左对齐,占10位
printf("|%-10s|\n","hello")
#%*.* 宽度、精度可以由参数动态传入
printf("%*.*f\n",8,2,3.14159)

%*.*f 中 * 的含义,* 代表:宽度 / 精度不从格式字符串写死,从后面的参数取数值。

filesum使用printf对齐输出示例

1
2
printf("%-15s %10d\n",$9,$5)
END{ printf("Total: %d bytes (%d files)\n",sum,filenum) }

向脚本传递参数

1. 命令行直接赋值变量(写在脚本后,文件名之前)

1
awk -f script.awk high=10 low=20 data.txt

限制:这种方式的变量,BEGIN块里面拿不到值!
原理:awk把high=10当成伪文件名;处理到这条赋值的时候才生效,此时已经开始处理输入。
awk 做了一条特殊判断:如果某个参数格式是 变量名=值,里面包含=,不把它当做真正要打开的磁盘文件,当做「伪文件 / 赋值参数」。不会去磁盘找名叫high=10的文件,所以不会报 “文件不存在” 错误。

2. -v参数(解决BEGIN拿不到变量的问题,推荐)

-v 变量=值,写在脚本/程序代码前面;在BEGIN执行前完成赋值。

1
awk -v search="MA" -f script.awk data.txt

多个变量就写多个‑v。

shell脚本把shell参数传给awk

shell脚本acro示例(acro缩写查询工具)

1
2
3
#!/bin/sh
# $1 shell的第一个参数传给awk变量search
awk '$1 == search' search=$1 acronyms

调用:acro CICS

检索多行文本块实战

原始问题:输入文件多个文本块,块之间空行分隔;需要搜索关键词,打印整个匹配的块。
利用 RS=""(空行做记录分隔符)实现:

1
awk 'BEGIN{FS="\n";RS=""} $0 ~ search {print $0}' search=$1 $2

第七章 核心速记清单

  1. awk三大块:BEGIN {}(处理输入前)、主循环模式/动作、END{}(全部读完后);awk自带读行主循环,不用手写读文件。
  2. $0完整记录;$1/$2…字段;$NF最后字段;NF字段总数;NR全局行号;FNR单文件行号。
  3. FS输入分隔符、OFS输出分隔符;RS输入记录分隔符,RS=""空行分割多条记录。
  4. 变量不用声明,未初始化数字为0,字符串为空;++区分前后缀。
  5. 模式:~ /正则/匹配;!~不匹配;==判断相等,不要写成=赋值。
  6. 布尔:&&与,||或,!非;&&优先级高于||。
  7. next:立刻结束当前行,直接读下一行。
  8. printf不会自动换行;格式:%[-]宽度.精度类型。
  9. 传参:普通变量赋值BEGIN不可见;必须用‑v才能在BEGIN拿到外部变量。
  10. 只写模式不写动作:默认动作是打印整行。