原文为 《The AWK Programming Language》,GitHub上有中译版,不过有些内容翻译的比较奇怪,建议跟原版对照着看 https://github.com/wuzhouhui/awk
本篇的小案例基本均基于文件 emp.data,三个字段分别为:员工名、每小时工资、工作时长,每一行代表一个雇员的记录
- Beth 4.00 0
- Dan 3.75 0
- Kathy 4.00 10
- Mark 5.00 20
- Mary 5.50 22
- Susie 4.25 18
一、 AWK入门
1. 入门小案例
打印每位雇员的名字以及他们的报酬 (每小时工资*工作时长),雇员的工作时长必须大于0。
- awk '$3 > 0 { print $1, $2 * $3 }' emp.data
- # $n 代表第n列,$0为整行数据
- # 过滤 $3>0 的行,输出$1及$2*$3
被单引号包围的部分是一个完整的 awk 程序,一般由 模式–动作 (pattern-action) 组成,当然模式可以没有。模式 $3 > 0 扫描每一个输入行, 如果该行的第三列大于0,则执行动作,为每一个匹配行打印第一个字段以及第二与第三个字段的乘积。
想知道哪些员工在偷懒(工作时长为0)
awk '$3 == 0 { print $1 }' emp.data
2. 运行 AWK 程序
awk 程序有多种运行方式:
- 读取输入文件 awk 'program' input files。输入文件可以有多个,例如
- # 按顺序打印文件emp.data,emp.data2符合条件的每一行的第一个字段
- awk '$3 == 0 { print $1 }' emp.data emp.data2
- 读取终端输入内容 awk 'program',awk 会将 program 应用到你在终端输入的内容,直到你输入文件结束标志 (ctrl+d)
awk '$3 == 0 { print $1 }'
橙色部分为输出,其余为输入,不符合条件则无返回结果
- 将awk程序放入文件,并用-f参数指定 awk -f progfile optional list of files,注意progfile中程序不用加''
二、 AWK输出
awk 的数据只有两种类型:数值与字符串。emp.data 是很典型的待处理数据,它既有单词也包括数值,且字段之间通过制表符或空格分隔。
1. 打印每一行
{ print }或者{ print $0 },即:
- awk '{ print }' emp.data
- #or
- awk '{ print $0 }' emp.data
2. NF,字段的数量
{ print NF, $1, $NF },打印每行字段数、第一个字段、最后一个字段。
awk ' { print NF, $1, $NF } ' emp.data
3. NR,打印行号
可以使用 NR 和 $0 为 emp.data 的每一行加上行号。
awk ' { print NR, $0 } ' emp.data
4. 列计算
可以用字段的值进行计算,并将计算得到的结果放在输出语句中,{ print $1, $2 * $3 } 就是最开始的例子。
5. 拼文本
可以把单词放在字段与算术表达式之间,例如 { print "total pay for", $1, "is", $2 * $3 }
awk '{ print "total pay for", $1, "is", $2*$3 }' emp.data
三、 更精美的输出
格式化输出需要使用 printf 语句,printf 几乎可以产生任何种类的输出
printf(format, value1, value2, ... , valuen)
format 是一个字符串,它包含按字面打印的文本,中间为格式说明符,格式说明符是%加上后面几个字符,这些字符控制 value 的输出格式。每个格式说明符对应一个value值,因此,格式说明符的数量应该和被打印的 value 一样多。
1. 打印每位雇员的报酬
awk ' { printf("total pay for %s is %.2f\n", $1, $2 * $3) } ' emp.data
这个 printf 语句包含两个格式说明符:%s 将第一个值 $1 以字符串的形式打印;%.2f 将第二个值 $2*$3 按照浮点数格式打印且保留两位小数。其他内容按照字面值打印,\n 表示换行符。printf 不会自动产生空格符或换行符,用户必须自己创建。
来看另一个打印每位雇员的名字与报酬程序:{ printf("%-8s %6.2f\n", $1, $2 * $3) }
%-8s -代表左对齐输出,8占用 8 个字符的宽度,s代表字符串;6.2f,6代表至少占用 6 个字符的宽度,.2f跟前例相同。
awk '{ printf ("%-8s %6.2f\n",$1,$2*$3) }' emp.data
为每一位雇员打印所有的数据,包括报酬,报酬按照升序排列。最简单的办法是使用awk 在每一位雇员的记录前加上报酬,然后将 awk 的输出通过管道传递给 sort 命令。
awk '{ printf("%6.2f %s\n",$2 * $3, $0) }' emp.data | sort -n
四、 选择(过滤)
1. 数值选择
比如过滤报酬大于50的员工信息,模式部分应该是 $2 * $3 > 50,结合前面的格式化:
awk ' $2 * $3 > 50 { printf("%.2f for %s\n"),$2*$3,$1 }' emp.data
2. 通过文本内容选择
也可以选择那些包含特定单词或短语的输入行,下面程序打印所有第一个字段是Susie 的行:
awk ' $1 == "Susie" { printf("%.2f for %s\n"),$2*$3,$1 }' emp.data
也可以通过正则表达式完成,下面程序打印所有包含 Susie 的行:
awk ' /Susie/ { printf("%.2f for %s\n"),$2*$3,$1 }' emp.data
3. 模式的组合
模式可以使用逻辑运算符(&&, ||, 和 !,)进行组合,例如 $2 >= 4 || $3 >= 20
- awk ' $2 >= 4 || $3 >= 20 { printf("%.2f for %s\n"),$2*$3,$1 }' emp.data
- # or
- awk ' !($2 < 4 && $3 < 20) { printf("%.2f for %s\n"),$2*$3,$1 }' emp.data
这两个程序是逻辑上是等价的,但第二个可读性比较差
五、 BEGIN 与 END
有两个特殊的模式 BEGIN 和 END。BEGIN 在第一个输入文件的第一行之前被匹配,END 在最后一个输入文件的最后一行被处理之后匹配。下面程序使用 BEGIN 打印一个标题:
awk 'BEGIN { print"NAME RATE HOURS"; print "" } { print } ' emp.data
BEGIN {}构造标题,{ print }输出文件内容,相当于
awk 'BEGIN { print"NAME RATE HOURS"; print "" } ' emp.data; cat emp.data
BEGIN 与END 分别提供了一种控制初始化与扫尾的方式,BEGIN 与 END 不能与其他模式作组合。如果有多个 BEGIN,会按照它们在程序中出现的顺序执行,多个 END 同样适用。
BEGIN 的一个常见用途是更改输入行的分隔符,分隔符由内建变量FS 控制,默认由空格分割(FS=' ')。将 FS设置为什么值,就会使其成为字段分隔符。
下面程序在 BEGIN 里将字段分隔符设置为 \t,并在输出之前打印标题。第二个 printf 语句将输出格式化成一张表格,使得每一列都刚好与标题的列表头对齐,END 打印总和。
- BEGIN { FS = "\t" # make tab the field separator
- printf("%10s %6s %5s %s\n\n",
- "COUNTRY", "AREA", "POP", "CONTINENT")
- }
- { printf("%10s %6d %5d %s\n", $1, $2, $3, $4)
- area = area + $2
- pop = pop + $3
- }
- END { printf("\n%10s %6d %5d\n", "TOTAL", area, pop) }
六、 用 AWK 计算
1. count
下面程序计算工作时长超过 15 个小时的员工人数
- awk ' $3 > 15 { emp = emp + 1 }
- END { print emp, "employees worked more than 15 hours" } ' emp.data
在 awk 中,用户变量不需要事先声明,数值默认初始化为0。对每个第三列>15的行,对emp加1,统计结束后利用END最后输出结果。
2. 求总和与平均数
为了计算雇员的人数,可以使用内建变量 NR(当前行数),当所有输入都处理完毕时,它的值就是读取到的行数。
- # 计算平均报酬
- awk ' { pay = pay + $2*$3 }
- END { print NR, "employees"
- print "total pay is", pay
- print "average pay is", pay / NR
- } ' emp.data
显然如果NR 的值为0,程序会报错,后面再看如何处理。
七、 操作文本
Awk 的长处之一是它可以非常方便地操作字符串。
1. 搜索每小时工资最高的雇员
- awk ' $2 > maxrate { maxrate = $2; maxemp = $1 }
- END { print "highest hourly rate:",maxrate, "for", maxemp } ' emp.data
在这个程序里,变量 maxrate 保存的是数值,maxemp 保存的是字符串,如果有多个雇员都拥有相同的最高每小时工资,这个程序只会打印第一个人的名字。
2. 字符串拼接
- awk ' { name = name $1 " " }
- END { print name } ' emp.data
将所有雇员名存储到一个单独的字符串中,每一次拼接都是把名字与一个空格符添加到变量 name 的值的末尾,最后在 END 动作中被打印出来。存储字符串的变量的初始值默认为空字符串,因此 name 不需要显式地初始化。
3. 打印最后一行
在 END 里 NR 的值会被保留下来,但是 $0 不会,需要通过用户变量存储。
- awk ' { last = $0 }
- END { print last } ' emp.data
如果在END前面执行,print $0会打印每行的值,如果放在END里,由于已经遍历完了,就只打印最后一行。
4. 内建函数
awk 提供一些内建函数,例如求平方根、取对数、随机数,还有用来操作文本的函数,比如 length 用来计算字符串中字符的个数。下面程序计算每个名字的长度:
awk ' { print $1, length($1) } ' emp.data
八、 流程控制语句
Awk 提供了 if-else 语句以及循环语句 while和for,这些都来源于 C 语言,只能用在Action部分。
1. if-else 语句
下面程序计算每小时工资>6 的雇员的总报酬与平均报酬。在计算平均数时,它用到了 if 语句,避免用 0 作除数。
- awk ' $2 > 6 { emp_num = emp_num + 1; pay = pay + $2*$3 }
- END { if (emp_num > 0)
- print emp_num,"employees, total pay is", pay,"avg pay is", pay/emp_num
- else
- print "no employees are paid more than $6/hour"
- } ' emp.data
改为判断每小时工资>2 的雇员
2. while 语句
while 含有一个条件判断与一个循环体。当条件为真时,执行循环体。
下面程序展示了一笔钱在特定的利率下如何随着投资时间的增长而增加,假设计算的公式是
- { i = 1
- while ( i <= $3 ) {
- printf("\t%.2f\n",$1*(1+$2)^i )
- i = i + 1
- }
- }
- # \t 表示一个制表符; ^ 是指数运算符
3. for 语句
大多数循环都包括初始化、判断、递增值,for 语句将这三者压缩成一行。
这里是前一个计算投资回报的程序, 不过这次用 for 循环:
- { for (i = 1; i <= $3; i = i + 1)
- printf("\t%.2f\n", $1 * (1 + $2) ^ i)
- }
九、 数组
awk 提供数组用来存储一组相关的值,数组功能强大,,但是我们在这里只展示一个简单的例子。
下面程序按行逆序显示输入数据
- # reverse - print input in reverse order by line
- { line[NR] = $0 } # 顺序记录每行内容
- END { i = NR # 逆序输出
- while (i > 0) {
- print line[i]
- i = i - 1
- }
- }
用 for 循环实现的等价的程序
- # reverse - print input in reverse order by line
- { line[NR] = $0 }
- END { for (i = NR; i > 0; i = i - 1)
- print line[i]
- }
十、 实用 “一行” 手册
虽然 awk 可以写出非常复杂的程序,,但是许多实用的程序并不比我们目前为止看到的复杂多少。这里有一些小程序集合,对读者应该会有一些参考价值,大多数是我们已经讨论过的程序的变形。
1. 输入行的总行数
END { print NR }
2. 打印第 10 行
NR == 10
3. 打印每一个输入行的最后一个字段
{ print $NF }
4. 打印最后一行的最后一个字段
- { field = $NF }
- END { print field }
5. 打印字段数多于 4 个的输入行
NF > 4
6. 打印最后一个字段值大于 4 的输入行
$NF > 4
7. 打印所有输入行的字段数的总和
- { nf = nf + NF }
- END { print nf }
8. 打印包含 Beth 的行的数量
- /Beth/ { nlines = nlines + 1 }
- END { print nlines }
9. 打印具有最大值的第一个字段, 以及包含它的行 (假设 $1 总是 正的) 18
- $1 > max { max = $1; maxline = $0 }
- END { print max, maxline }
10. 打印至少包含一个字段的行
NF > 0
11. 打印长度超过 80 个字符的行
length($0) > 80
12. 在每一行的前面加上它的字段数
{ print NF, $0 }
13. 打印每一行的第 1 与第 2 个字段, 但顺序相反
{ print $2, $1 }
14. 交换每一行的第 1 与第 2 个字段, 并打印该行
{ temp = $1; $1 = $2; $2 = temp; print }
15. 将每一行的第一个字段用行号代替
{ $1 = NR; print }
16. 打印删除了第 2 个字段后的行
{ $2 = ""; print }
17. 将每一行的字段按逆序打印
- { for (i = NF; i > 0; i = i - 1) printf("%s ", $i)
- printf("\n")
- }
18. 打印每一行的所有字段值之和
- { sum = 0
- for (i = 1; i <= NF; i = i + 1) sum = sum + $i
- print sum
- }
19. 将所有行的所有字段值累加起来
- { for (i = 1; i <= NF; i = i + 1) sum = sum + $i }
- END { print sum }
20. 将每一行的每一个字段用它的绝对值替换
- { for (i = 1; i <= NF; i = i + 1) if ($i < 0) $i = -$i
- print}