@Author: Basil Guo @Date: Jan. 19, 2021 @Description: The AWK Programming Language @Keyword: Linux, awk @Type: tutorial
《The AWK Programming Language》——Alfred V. Aho, Brian W. Kernighan,and Peter J. Weinberger
本文无意于重新翻译一遍这本作者做的书籍,有关翻译在GitHub上已经有人做了awk_zh_cn,有PDF可以下载。
本文会根据常用的工作取舍书中例子。本文如有后续,会更新新的gawk(GNU awk)的内容。
1. 简介
一个awk
程序由“模式+动作”组成,可以简单的修改数据格式、验证数据有效性、搜索特定数据项、求和、打印报表等。“模式”指定搜索哪些数据,“动作”表示对搜索到的数据进行哪些处理。模式可以不存在代表会对每一行都执行后续动作,模式存在则是对匹配行执行后续动作,动作及其{}
都可以不存在表示会打印每一个匹配的输入行,但是不能两个都不存在,不然虽然语法通过,但是没有意义。
awk
是针对文本文件逐行处理的,可以应用在多种不同的计算和数据处理任务中,awk
会自动将输入行切分成字段,awk
缺省的行分隔符是换行,缺省的列分隔符是连续的空格和Tab,但是行分隔符和列分隔符都可以自定义。但是一般使用awk
主要是逐列处理数据的,这也是awk
相比sed更加强大的地方。
awk
程序基本格式是:awk [OPTIONS] '[PATTERN]{ACTION}' /PATH/TO/FILE
,这里必须使用''
单引号,以防止其中的$
被Shell解释,如果程序长度多于一行,也需要使用单引号。多个模式-动作语句或者动作内语句可以使用;
或者换行分隔。
pattern {action}
[pattern {action}; ... ] # 我是行注释
可以使用\
换行,也可以使用,
换行。每一行的字段数可以不尽相同,但是默认将字段数上限设置为100。
emp.data
保存了当前一些工人单位工资和工时数据:
现在我们想查看下工时不是0的工人他们的薪酬情况:
$ awk '$3 > 0 {print $1,$2 * $3}' emp.data
Kathy 40
Mark 100
Mary 121
Susie 76.5
这里awk
不会修改源文件的数据,而且这里表明ACTION
也支持算术表达式。
如果语法错误了,awk
可以检测出来,书中给的例子使用>>> <<<
包裹,但是可能gawk
做出了改变。例如:
$ awk -F: '$3 > 500 [print $1,$3}' /etc/passwd
awk: cmd. line:1: $3 > 500 [print $1,$3}
awk: cmd. line:1: ^ syntax error
awk: cmd. line:1: $3 > 500 [print $1,$3}
awk: cmd. line:1: ^ syntax error
2. 选项
-f /PATH/TO/FILE
指定脚本程序文件,这个文件只包含''
内的模式+动作部分,不包含别的;
3. 模式
3.1 分类
模式位于{}
之外,有六种基本的模式,其中BEGIN
和END
不能省略后续动作,不与其它模式组合。范围模式不能是其它模式的一部分。
BEGIN{statements}
表示在第一个输入文件的第一行之前被匹配,通常放在程序开头,可以提供初始化的功能,多个BEGIN
会按照出现顺序依次执行。- 可用来输出一个标题
awk 'BEGIN {printf("Name\t$/h\thours\n"); print} {print}' emp.data
; - 常见用途用于搭配内置变量
FS
更改输入行被分割位字段的方式,就是更改分割符;
- 可用来输出一个标题
END{statements}
在最后一个输入文件的最后一行被处理之后匹配,通常放在程序结尾,可用于扫尾工作,多个END
会按照出现顺序依次执行。awk '$3>15{emp+=1} END{print emp, "employees worked more than 15 hours."}' emp.data
;expression{statements}
表示每碰到一个使得expression为真(非零非空)的输入行,statements就执行一次;- 简单比较:
$2 > 0
; - 简单计算:
$2*$3 > 100 {printf("$%.2f for %s\n", $2*$3, $1)}
; - 文本比较:
$1 == "Susie"
,包含Susie
的行;
- 简单比较:
/regular expression/{statements}
正则表达式,/Susie/{print}
,这里和sed很像;有关正则表达式的具体内容,参见正则表达式expression ~ /regexpr/
:expression
中字符串匹配regexpr
模式,/regexpr/
就是$0 ~ /regexpr/
的简写;expression !~ /regexpr/
:expression
中字符串不匹配regexpr
模式,如$1 !~ /C/
;
compound pattern{statements}
使用&&(AND), ||(OR), !(NOT)
以及括号组合起来形成复杂模式;- 模式组合:
$2 >= 4 || $3 >= 20
;
- 模式组合:
pattern1, pattern2{statements}
范围模式匹配多个输入行,范围模式不能是其它模式的一部分。/Canada/, /USA/
匹配的行从包含Canada的行开始,到包含USA的行结束。如果一直都没有匹配到要么不开始,要么到结束。
3.2 运算符
-
关系运算符
< <= > >= == !=
。数值比较使用相对大小,字符串比较使用字典序。只有在两个比较的对象都是数值时,比较才会以数值的形式进行,否则就是字符串形式的比较。 -
算术运算符:
+ - * / % ^
。最后一个是指数运算符。所有的算术运算符皆使用浮点数。 -
赋值运算符:
= += -= *= /= %= ^=
。 -
条件表达式:
?:
-
逻辑运算符:
&& || !
。 -
匹配运算符:
~ !~
。分别是匹配和不匹配,可以指定字段匹配不匹配。 -
拼接运算符:没有显示的拼接运算符,通过陆续写出字符串常量, 变量, 数组元素, 函数返回值, 与其他表达式, 就可以创建一 个字符串。
-
单目运算符:
+ -
-
自增和自减运算符:
++ --
-
括号:用于分组
4. 动作
动作都是包围在{}
之内的,动作也可以是一个表达式,例如开篇简介所写的算术运算。对于同一模式的动作可以有多个,位于同一个{}
之内,动作之间使用;
分隔,[模式+]动作
也可以有多个之间只要是可以识别即可。如下两个结果相同:
$ awk 'BEGIN {printf("Name\t$/h\thours\n"); print;}{print $0}' emp.data
$ awk 'BEGIN {printf("Name\t$/h\thours\n"); print;}''{print $0}' emp.data
{print}
:相当于{print $0}
,其中$0
代表的是整行数据,真正分割的数据是从1
开始编号的,打印参数之间使用,
逗号分开,这样子默认两个值间会打印一个空格,否则输出的两个值之间就没有空格分隔,print默认是换行符结束;{printf}
:相比于{print}
的简单快速输出,{printf}
可以更多格式输出。它的基本格式是{printf(format, value1, value2,...,valuen)}
。如awk '{printf("Total pay for %-6s is $%6.2f\n", $1, $2*$3)}' emp.data
;- 变量:awk可以设置变量,不需要实现声明就可以使用:
$3 > 15 {emp = emp+1}
; - 拼接:针对字符串来说,
{names=nams $1 " "} END{print names}
,这里不需要显示初始化字符串names
; if (expression) statements
if (expression) statements else statements
while (expression) statements
for (expression; expression; expression) statements
for (expression in array) statements
do statements while (expression)
break
continue
5. 内建变量
变量 | 意义 | 默认值 |
---|---|---|
ARGC |
命令行参数的个数 | — |
ARGV |
命令行参数数组 | — |
FILENAME |
当前输入文件名 | — |
FNR |
当前输入文件的记录个数 | — |
FS |
控制着输入行的字段分割符,如果设置FS 为一个非空格字符,就可以使该字符成为字段分割符。 |
" " |
NF |
当前记录的字段个数。The Number of Fields in the current input record. | — |
NR |
到目前为止读取的记录数量,即行数。The total Number of input Records seen so far. | — |
OFMT |
数值的输出格式 | %.6g |
OFS |
输出字段分割符 | " " |
ORS |
输出的记录的分割符 | "\n" |
RLENGTH |
被函数match匹配的字符串的长度 | — |
RS |
控制着输入行的记录分隔符 | — |
RSTART |
被函数match匹配的字符串的开始 | |
SUBSEP |
下标分割符 | "\034" |
6. 内建函数
6.1 数学类
6.1.1 算术函数
函数 | 返回值 |
---|---|
atan2(y,x) |
y/x 的反正切值,定义域位于(-π,π) |
cos(x) |
x 的余弦值,x 以弧度为单位 |
exp(x) |
x 的指数函数,e^x |
int(x) |
x 的整数部分;当x>0 时,向0取整 |
log(x) |
x 的自然对数,以e 为底 |
rand(x) |
返回一个随机数r ,0<=r<1 |
sin(x) |
x 的正弦值,x 以弧度为单位 |
sqrt(x) |
x 的方根 |
srand(x) |
x 是rand() 新的随机数种子 |
6.2 文本类
length()
:求长度,awk '{print $1, length($1)}' emp.data
;printf()
:就类似于C语言中的printf(format, args...)
函数;
7. 流程控制
-
if-else
$ awk '$2 > 6 { n = n + 1; pay = pay + $2} END{if (n>0) { if (n>0) print n, "employees, total pay is", pay, "average pay is", pay/n else print "no employees are paid more than $6/hour" }' emp.data
-
while
# interest1 - compute compound interest: value = amount * (1 + rate) ^ years # input: amount rate years # output: compounded value at the end of each year $ awk '{ i = 1 while (i <= $3) { printf("\t%.2f\n", $1 * (1 + $2) ^ i) i = i + 1 } }'
-
for
# interest2 - compute compound interest # input: amount rate years # output: compounded value at the end of each year $ awk '{ for (i=1; i<=$3; i=i+1) printf("\t%.2f\n", $1 * (1 + $2) ^ i) }'
8. 数组
逆序输出:
# reverse - print input in reverse order by line
$ awk '{line[NR]=$0} # remember each input line
END {
i=NR # print lines in reverse order
while (i>0) {
print line[i]
i = i-1
}
}' emp.data
9. 例子
-
输入行的总行数
$ awk 'END{print NR}' /PATH/TO/FILE
-
打印第10行
$ awk 'NR == 10' /PATH/TO/FILE
-
打印每一个输入行的最后一个字段
$ awk '{print $NF}' /PATH/TO/FILE
-
打印最后一行的最后一个字段
$ awk '{field = $NF} END{print field}' /PATH/TO/FILE
-
打印字数多于4个的输入行
$ awk 'NF > 4' /PATH/TO/FILE
-
打印最后一个字段值大于4的行
$ awk '$NF > 4' /PATH/TO/FILE
-
打印所有输入行的字段数的总和
$ awk '{nf = nf + NF} END {printf nf}' /PATH/TO/FILE
-
打印包含Beth的行的数量
$ awk '/Beth/ {nlines = nlines + 1} END {print nlines}' /PATH/TO/FILE
-
打印具有最大值的第一个字段,以及包含它的行(假设$1总是正数)
$ awk '$1 > max{max = $1; maxline=$0} END{print max, maxline}' /PATH/TO/FILE
-
打印至少包含一个字段的行
$ awk 'NF > 0' /PATH/TO/FILE
-
打印长度超过80个字符的行
$ awk 'length($0) > 80' /PATH/TO/FILE
-
在每一行前加入它的字段数
$ awk '{print NF, $0}' /PATH/TO/FILE
-
打印每一行的第一个与第二个字段,但是顺序相反
$ awk '{print $2, $1}' /PATH/TO/FILE
-
交换每一行的第1和第二个字段,并打印该行
$ awk '{tmp = $1; $1 = $2; $2 = tmp; print}' /PATH/TO/FILE
-
将每一行的第一个字段使用行号代替
$ awk '{$1 = NR; print}' /PATH/TO/FILE
-
打印删除了第二个字段后的行
$ awk '{$2 = ""; print}' /PATH/TO/FILE
-
将每一行的字段按照逆序打印输出
$ awk ' { for (i=NF; i>0; i=i-1) printf("%s\t", $i) printf("\n") } ' /PATH/TO/FILE
-
打印每一行的所有字段值之和
$ awk ' { total = 0 for (i=1; i<=NF; ++i) total = total + $i print total }' /PATH/TO/FILE
-
将所有行的所有字段值累加起来
$ awk ' { for(i=1; i<=NF; ++i) sum = sum + $i } END{print sum} ' /PATH/TO/FILE
-
将每一行的每个字段用它的绝对值替换
$ awk ' { for (i=1; i<=NF; ++i) if ($i < 0) $i = -1 * $i print }' /PATH/TO/FILE
-
实现
wc
的功能:分别输出“行 单词 字符”的个数:# +1是因为length()不包含换行符号 $ awk '{nc=nc+length($0)+1; nw=nw+NF} END{print NR, nw, nc}' /PATH/TO/FILE
数据集
-
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
-
countries
USSR 8649 275 Asia Canada 3852 25 North America China 3705 1032 Asia USA 3615 237 North America Brazil 3286 134 South America India 1267 746 Asia Mexico 762 78 North America France 211 55 Europe Japan 144 120 Asia Germany 96 61 Europe England 94 56 Europe