本来计划年前写完的,结果现在才搞定,手册的内容一边翻一边实验,还有awk版本问题,又参考Effective AWK Programming对语法和示例做了些补充。终于写完了,大家元宵节快乐!
AWK是一门解释型的编程语言,它的名字来源于它的三位作者的姓氏:Alfred Aho,Peter Weinberger和Brian Kernighan。AWK能够应用于广泛的计算和数据处理任务。所有的GNU/Linux发行版都自带GAWK,即GNU AWK,是AWK的扩展并且与AWK完全兼容。
和上一篇文章讲到的sed命令类似,AWK逐行读取输入流中的内容,并对读取的行执行所有命令,如此循环,直到输入流结束。
本文基于GAWK进行介绍,因为GAWK比原生AWK使用更普遍些。
AWK命令格式如下:
awk [options] 'program' input-file1 input-file2 ...
或者
awk [options] -f program-file input-file1 input-file2 ...
第一种格式中,awk从file中获取输入流,然后执行单引号内的程序。第二种格式则是从文件program-file中获取将要执行的程序。
上述AWK命令的program部分的结构可分为三块:BEGIN、BODY和END。
BEGIN:在AWK命令的一开始执行的动作,它只执行一次,可以把变量初始化放在这里。注意,BEGIN部分是可选的,并且一个AWK命令中可以有多个BEGIN块。另外,如果有-v选项的赋值操作,则-v的操作在BEGIN之前。
BEGIN块的写法为:BEGIN{…}
BODY:程序主体,对输入流的每一行执行动作。如果存在BEGIN或END,则这部分是可选的。一个AWK命令中可以有多个BODY块。
BODY块的写法为:{…}
END:在AWK命令的最后执行的动作,它只执行一次。注意,END部分是可选的,并且一个AWK命令中可以有多个END块。另外,使用END { }块时,awk的file参数不能省略。
END块的写法为:END{…}
例如下面的命令:[root@ubuntu]awk_test:$ awk 'BEGIN{print "Output start!"}; {print}; END{print "Output done!"}' awk_test.txt
Output start!
USER PID %MEM VSZ RSS STAT START TIME COMMAND
root 1 0.1 3652 1916 Ss Jan07 0:03 /sbin/init
root 2 0.0 0 0 S Jan07 0:00 [kthreadd]
root 3 0.0 0 0 S Jan07 0:02 [ksoftirqd/0]
root 26 0.0 0 0 S Jan07 0:40 [kswapd0]
user 495 0.1 3588 1092 Ss Jan07 0:00 /sbin/udevd --daemon
user 860 0.0 3584 908 S Jan07 0:01 /sbin/udevd --daemon
user 1137 0.0 4520 776 S Jan07 0:00 smbd -F
user 1550 0.1 4521 1816 Ss Jan07 0:15 nmbd -D
Output done!
可以看到这条命令在命令的开始打印一行输出“Output start!”,然后对文件中的每一行内容执行print操作,最后又打印出一行输出“Output done!”。
从句法结构上来讲,program由一条条规则组成,每条规则由模式和动作组成,即模式匹配后执行相应的动作。动作放在花括号内以与模式区分。所以,program一般的格式是这样的:
pattern { action }
pattern { action }
…
那么,下面这条命令(打印长度大于80字符的行):
awk 'length($0) > 80' data
可以看到这条awk命令只有pattern而没有action部分。如果没有action部分,则执行默认动作:打印整个record。
也可以把program写到文件里(不用加单引号),通过AWK的第二种格式来执行。[root@ubuntu]awk_test:$ cat progfile
BEGIN{print "Output start!"};
{print};
END{print "Output done!"};
[root@ubuntu]awk_test:$ awk –f progfile awk_test.txt
为了方便后期维护,建议将AWK的程序文件以.awk作为后缀名。
另外,可以利用Shell的#!机制,将progfile内容改为:#!/usr/bin/awk –f
BEGIN{print "Output start!"};
{print};
END{print "Output done!"};
这样的话,执行./progfile awk_test.txt即可。(通过type awk可以知道系统中awk命令的位置。)
注:使用#!机制的话,执行的shell命令./progfile实际上是执行:"#!后面的命令" +"./progfile脚本" + "./progfile脚本的参数"。另外,这种写法,awk后面最多只能跟一个参数;并且ARGV[0]的值在不同系统上可能表现不同,比如可能被解释为awk或/usr/bin/awk或./progfile。
比较特别的,如果要在program内使用或打印单引号,可以用其ASCII码'\47'表示。或者把程序写在文件中,这样就不用担心单引号和program外围的单引号混淆的问题。注意,在单引号中,反斜杠后接一个字符,会被解释成这个字符的字面意思,即和不加反斜杠是一样的含义。
你也可以通过下面两种方法来打印单引号:
awk 'BEGIN { print "Here is a single quote <'"'"'>" }'
或
awk 'BEGIN { print "Here is a single quote<'\''>" }'Program-file可以有多个,通常用来将通用的代码或函数做成库,以实现代码复用。
环境变量AWKPATH用来指定-f的搜索路径,如果不指定AWKPATH,则默认搜索“.:/usr/local/share/awk”,可以通过修改AWKPATH或ENVIRON["AWKPATH"]来修改搜索路径,每个路径之间用冒号隔开(.或::都可以表示当前路径)。如果-f选项后面跟的是包含“/”的文件名,就不会去额外搜索路径了。
[root@ubuntu]awk_test:$ awk -v name=jason 'BEGIN{printf("name=%s\n", name)}'
name=jason
注意变量的引用不需要加“$”,实际上AWK的语法很多跟ANSI C的语法类似。“$”在AWK中是用来引用field的,后面会讲到。
[root@ubuntu]awk_test:$ cat /etc/passwd | awk -F ':' '{print $1}'
root
daemon
bin
sys
sync
games
这个选项可以查看当前可用的内置全局变量。在编程的时候要注意,不要定义和这些内置变量重名的变量。
[root@ubuntu]awk_test:$ cat number.txt
0x12
0x32
012
10
[root@ubuntu]awk_test:$ awk '{sum+=($1)}; END{print sum}' number.txt
22
[root@ubuntu]awk_test:$ awk --non-decimal-data '{sum+=($1)}; END{print sum}' number.txt
88
可以看到,如果不加--non-decimal-data选项,就只识别出十进制的12和10。
不过man gawk中说“Use this option with great caution!”,一方面这个选项可能破坏旧的程序,另一方面这个选项可能在以后被摒弃。
[root@ubuntu]awk_test:$ awk --non-decimal-data --profile '{sum+=($1)}; END{print sum}' number.txt
[root@ubuntu]awk_test:$ cat awkprof.out
# gawk profile, created Sun Jan 8 23:20:48 2017
# Rule(s)
{
sum += $1
}
# END block(s)
END {
print sum
}
如果使用pgawk执行命令,则还会显示每条语句以及每个函数的调用次数。
传统的awk不支持间隔表达式,必须加上--re-interval选项或者--posix选项才能使用。
awk -f func_test.awk --source 'BEGIN{printadd_INT(1,2)}' awk_test.txt
这个例子中,func_test.awk文件里面定义了函数add_INT(),这里将-f指定的文件程序和--source指定的命令行程序混合在了一起。
这个选项应该在#!开头的脚本里使用,例如:
#! /usr/local/bin/gawk -E
awk program here …
这个选项可以防止向脚本里传递参数,因为所有的参数都先被awk识别并处理了。
这两个选项都是针对引用函数库的,也可以在文件中使用@include和@load来引用库文件。不过在我所用系统的gawk不支持这两个参数以及相应的AWKLIBPATH环境变量,我就不介绍了。我们就使用-f来引用库文件吧,只是-f的文件里面可以是任何程序内容,并不是只针对库文件而设计的。
这告诉awk,选项部分已经结束,可以用来传递以“-”开头的参数,而不被误认为是选项。
AWK的变量是动态生成的,在第一次被使用的时候开始存在。变量的值可以是下列数据类型:浮点型、字符串类型和一维数组。甚至一个变量既可以是浮点型也可以是字符串类型,这取决于代码中如何使用它。
AWK会将“var=value”形式的参数认为是变量赋值,例如下面这个命令,awk将“var=2”和“var=1”认为是给var赋值,而不是一个文件名,这个过程在awk顺序处理参数列表时进行的。
awk 'var == 1 {print 1} var == 2 { print 2}' var=2 awk_test2.txt var=1 awk_test1.txt一个record就是awk认为的一行输入,对一个输入流,默认以换行符分隔。不过可以通过内置变量RS来修改。例如,把RS赋值为Jan07:
[root@ubuntu]awk_test:$ awk -v RS=Jan07 '{print}' awk_test.txt
USER PID %MEM VSZ RSS STAT START TIME COMMAND
root 1 0.1 3652 1916 Ss
0:03 /sbin/init
root 2 0.0 0 0 S
0:00 [kthreadd]
root 3 0.0 0 0 S
0:02 [ksoftirqd/0]
root 26 0.0 0 0 S
0:40 [kswapd0]
user 495 0.1 3588 1092 Ss
0:00 /sbin/udevd --daemon
user 860 0.0 3584 908 S
0:01 /sbin/udevd --daemon
user 1137 0.0 4520 776 S
0:00 smbd -F
user 1550 0.1 4521 1816 Ss
0:15 nmbd -D
看效果就知道是什么意思了。注意“Jan07”本身没有打印出来。
如果RS被赋值为单个字符,则这个字符就是records的分隔符;如果被赋值为多个字符,那RS实际上是一个正则表达式。
如果RS被赋值为空,则以空行作为record的分隔符。AWK会把一个record再分隔成一个个的field依次进行处理,以变量FS的值作为分隔符。
如果FS被赋值为单个字符,则这个字符就是fields的分隔符;如果被赋值为多个字符,那就是一个正则表达式;如果FS是空,则record中的每个字符都是分隔符。
注意,如果FS是空格,则多个空格、tab和换行,都会被认为是分隔符。
一个record中的各个field可以通过各自的位置来引用,依次为$1,$2,$3,...。而$0表示整个record。
如果给FIELDWIDTHS变量赋值为一个数值列表(以空格隔开),如下面的例子,record就会按照字符串长度来分隔fields而不是按照FS的值。这时FS变量会被忽略。[root@ubuntu]awk_test:$ awk 'BEGIN{FIELDWIDTHS="2 4 4 4"}{print $1"||"$2"||"$3"||"$4}' awk_test.txt
US||ER || || PID
ro||ot || || 1
ro||ot || || 2
ro||ot || || 3
ro||ot || || 26
us||er || || 495
us||er || || 860
us||er || ||1137
us||er || ||1550
如果给FS重新赋值,就会切回到按照FS分隔fields。
NF变量用于获取 当前record共分了多少了fields。你可以给NF、$0以及某个field赋值,那么相应record会更新并重新划分fields。例如:[root@ubuntu]awk_test:$ awk '{NF-=1; if (FNR!=1) $1="json"; print}' awk_test.txt
USER PID %MEM VSZ RSS STAT START TIME
json 1 0.1 3652 1916 Ss Jan07 0:03
json 2 0.0 0 0 S Jan07 0:00
json 3 0.0 0 0 S Jan07 0:02
json 26 0.0 0 0 S Jan07 0:40
json 495 0.1 3588 1092 Ss Jan07 0:00 /sbin/udevd
json 860 0.0 3584 908 S Jan07 0:01 /sbin/udevd
json 1137 0.0 4520 776 S Jan07 0:00 smbd
json 1550 0.1 4521 1816 Ss Jan07 0:15 nmbd
这个例子中,把NF减1,则原来的每个record的最后一个field就没有打印出来。FNR表示当前已经输入了几个record,例子中将文件除了第一行之外的第一个field的值都改成了“jason”。
AWK有一些内置的全局变量可以直接使用,会使编程更方便。
内置变量 |
含义 |
ARGC |
Awk命令行的参数个数(不包括选项和选项的值以及program部分),awk自身是第0个参数。 |
ARGV |
Awk的参数列表,共ARGC个元素。 |
ARGIND |
当前正在处理的文件处于ARGV数组中的index。 |
BINMODE |
是否使用binmode打开文件。1("r"),2("w"),3("rw")分别表示输入文件、输出文件、所有文件需要使用binmode打开。 |
CONVFMT |
数字的格式化类型,默认是"%.6g"。 |
ENVIRON |
保存当前所有环境变量的数组。这个数组是以环境变量名作为下标的,例如ENVIRON["HOME"]的值是"/root"。 |
ERRNO |
当getline或close失败,ERRNO会保存字符串形式的错误描述。 |
FIELDWIDTHS |
一个以空格分隔的数值列表,设置这个值后,FS失效,record改以字符长度来划分fields。 |
FILENAME |
Awk当前处理的文件的名字。注意,在BEGIN{ }中由于还没开始处理文件,FILENAME是空(除非被getline设置)。 |
FNR |
表示正在处理的文件目前已经输入了几个record。 |
FS |
Field分隔符,默认是空格。 |
IGNORECASE |
是否忽略大小写,默认是0。用于控制正则表达式和字符串操作,例如会影响records和fields的分隔符。注意,数组下标不受影响。 |
LINT |
如果为true,则对可能不兼容的awk命令提示warning,如果为fatal,则提示为错误。默认是0:不提示。 |
NF |
当前record中的fields的数目。 |
NR |
已经处理的record的数目(包括当前record)。 |
OFMT |
输出中数字的格式,默认是“%.6g”。 |
OFS |
输出中fields的分隔符,默认是空格。例如下面的命令,就会以” : ”来打印fields(注意$1 $2 $3之间要加逗号): awk -v OFS=" : " '{print $1,$2,$3}' awk_test.txt |
ORS |
输出中records的分隔符,默认是新行。 |
PROCINFO |
一个包含正在执行的awk命令自身信息的数组,以元素名作为下标,例如PROCINFO[“egid”],PROCINFO[“pid”]等,下面的命令可以列举所有的元素: awk '{for (var in PROCINFO) print var;}' awk_test.txt |
RS |
records分隔符,默认是新行。 |
RT |
record的结束符,通常被赋值为RS。 |
RSTART |
match()匹配成功的子串的第一个字符在原始字符串中的index(从1开始)。0表示匹配失败。 |
RLENGTH |
match()匹配成功的子串的长度。-1表示匹配失败(可以匹配空串,此时长度为0)。 |
SUBSEP |
数组下标分隔符,默认为\034即0x1C的ASCII符号。在访问多维数组时有用。 |
TEXTDOMAIN |
文本域,本地化的时候用到。 |
对于FS,强调一个‘FS =" "’和‘FS = "[ \t\n]+"’的不同,这两种FS都是将多个空格、tab或newline作为分隔符。但是前者会先将record中的前导空格和尾部空格剥掉,再决定如何分割fields。例如下面两条命令的结果就不同:
[root@ubuntu]awk_test:$ echo ' a b c d ' | awk '{ print $2 }'
b
[root@ubuntu]awk_test:$ echo ' a b c d ' | awk 'BEGIN { FS = "[ \t\n]+" }{ print $2 }'
A
也可以据此特征来删除前导空格:
[root@ubuntu]awk_test:$ echo ' a b c d' | awk '{ print; $2 = $2; print }'
a b c d
a b c d
这条命令通过$2=$2,让awk重新划分fields,由于FS模式是空格,这时就会先把前导空格和尾部空格剥掉。
i = "A"; j = "B"; k = "C"
x[i, j, k] = "hello, world\n"
上面定义了数组x下标为"A\034B\034C"的值为"hello,world\n"。
操作符“ in”可以用来测试下标是否存在,例如:if (val in array)
print array[val]
如果是多维的下标,则写成 if ((i, j) in array) print array[i, j]。
也可以用“in”来遍历数组:
下面两个例子,前者定义了一个一维数组,数组下标是[“a,b”]的元素。后者用来定义多维数组,例子中定义了三维数组中下标为[”a”][“,”][”b”]的元素。[root@ubuntu]awk_test:$ awk 'BEGIN{array["a"",""b"]=1;for(i in array) print i}'
a,b
[root@ubuntu]awk_test:$ awk 'BEGIN{array["a",",","b"]=1;for(i in array) print i}'
a\034,\034b
记住,数组的下标总是字符串,如果以数值为下标,也会转换成字符串,也就是说,a[17]的下标是"17",并且和a[021]、a[0x11]是相同的下标。
使用delete可以删除一个数组元素,例如delete array[1],也可以delete array来删除整个数组。
变量和fields可以是字符串或浮点数类型,一个变量和field的值是什么类型依赖于它处于的上下文,例如,如果用于数学表达式,就被当为数值类型。
如果需要强制变量或field被当作数值,则写成var+0。如果需要强制被当作字符串,则连接一个空串,例如var ""。
虽然所有的数值都是浮点型的,但是对于一个整数,转换成字符串的结果就是整型,例如12会被转换为“12”而不是“12.00”。
在比较两个变量var1和var2时,如果两个变量都是浮点型,则按照数值来比较;如果一个浮点型,另一个是字符串,则按照数值来比较,这时,字符串变量被当做“数值字符串”,例如("12"==12)的值为1;如果两个变量都是字符串,则按照字符串来比较。
注意,只有在处理用户输入时,才会将形如“57”这种看起来像数值的字符串认为是“数值字符串”,例如getline的输入、FILENAME、ARGV的成员、ENVIRON的成员以及split()产生的数组元素。其他情况下,则只是一个字符串常量。
未初始化的变量,会有两个默认的初始值:数值0以及空字符串""。gawk识别八进制和十六进制,例如011、0x16。
字符串常量是指双引号内的字符序列。在字符串中,可以包含转义字符,如下:
转义字符 |
含义 |
\\ |
字面含义的反斜杠 |
\a |
“alert”字符,通常是ASCII中的BEL |
\b |
backspace,回车 |
\f |
form-feed,换页符 |
\n |
newline,换行符 |
\r |
carriage return,回车 |
\t |
horizontal tab |
\v |
vertical tab |
\xhex |
十六进制数表示的ASCII码字符。例如\x2A表示ASCII中的“*”号 |
\ddd |
一个、两个或三个数字组成的八进制数值表示的ASCII码字符。例如\052表示ASCII中的“*”号,注意\52同\052相同 |
\c |
字面含义的字符c,例如需要使用原意的*号,则需要写成\*。 |
AWK是一个line-oriented的语言,对每一行(record),先进行模式匹配,再执行动作。动作的语句放在花括号{ }里面。如果模式为空,则对所有record执行动作,例如 { print } ,就是无条件的打印整个record。
用“#”对一行程序进行注释,直到换行则注释内容结束。
正常情况下,一个完整的语句以换行结束,但一个语句也可以写成多行,如果一个语句在“,”、“{”、“?”、“:”、“&&”或“||”处换行,则认为该语句尚未完成;一个语句在一行中以do或else结尾,也会认为该语句未完成而继续读取下一行;其他情况下,可以在行末添加“\”来标记这一行尚未结束(建议不要在pattern或字符串以及注释的中间用反斜杠换行)。
也可以将多个语句写到一行中,语句之间用分号“;”隔开。AWK的模式可以是下面的其中一种:
BEGIN和END是两个比较特殊的模式,他们不对输入做模式匹配,因为BEGIN和END分别是在处理输入之前和之后。多个BEGIN块会被merge成一个BEGIN块,这个BEGIN里面的语句会在开始读取输入之前执行;多个END块会被merge成一个END块,这个END里面的语句会在处理完所有输入之后执行(或者执行exit)。注意, BEGIN和END必须是独立的,不能糅合在其他pattern表达式中。并且BEGIN和END不能没有action部分,也就是说BEGIN和END后面不能没有花括号{ }。
对于 /regular expression/模式,会对所有匹配到该正则表达式的record执行其关联的语句。例如,打印包含“root”的行:awk '/root/ {print $0}' awk_test.txt
一个relational expression可以使用后面将要讲到的任何运算符,这种模式通常用来测试一个field是否匹配某个正则表达式。关系表达式是指使用关系运算符(>,>=,<,<=,==,!=)连接一个或两个表达式组成的式子。
“&&”、“||”和“!”即我们熟知的与、或、非逻辑运算符,可以用来将多个patterns连接在一起,这时他们是short-circuite valuation(短路求值)的,例如对于&&操作,只要第一个值是false,那整个表达式就是false,就没必要计算后续的值了。圆括号( )可以改变运算符的运算顺序。
例如,打印第一个field不是“root”的行且第三个field等于“0.0”的行:awk '$1 != "root" && $3 == "0.0" {print}' awk_test.txt
“?:”运算符和C语言中的三目的条件运算符一样,pattern1为true则选用pattern2,否则选用pattern3。
pattern1, pattern2形式的表达式被称为范围样式,它将匹配pattern1的record,匹配pattern2的record以及这两个record之间的所有record都匹配出来。正则表达式由下表中一系列的字符集合组成。注:其中c表示character,r表示regular expression。
字符 |
含义 |
c |
匹配非元字符c |
\c |
匹配元字符c的字面含义。由于元字符有特殊含义,因此必须加反斜杠来匹配其原始的字符含义。这些字符包括"["、"]"、"\"、"^"、"$"、"."、"|"、"?"、"*"、"+"、"{"、"}"、"("、")"等。 |
. |
匹配一个字符(包括换行) |
^ |
匹配字符串的开头 |
$ |
匹配字符串的结尾(我系统上不支持) |
[abc...] |
匹配字符列表abc…中的任何一个字符 |
[^abc...] |
匹配字符列表abc…之外的任何一个字符 |
r1|r2 |
匹配r1或r2 |
r1r2 |
匹配r1且其后紧跟r2 |
r+ |
匹配一个或多个r |
r* |
匹配0个或多个r |
r? |
匹配0个或1个r |
(r) |
分组,匹配圆括号中的所有字符组合r |
r{n} r{n,} r{n,m} |
花括号内有1个或2个数值被称为区间表达式:只有一个数字n,则表示前面的正则表达式r重复n次;有一个数字n和一个逗号,表示r至少重复n次;有两个数字n和m,表示r重复n到m次。 区间表达式只有在指定--posix或--re-interval的时候才可用。 |
\y |
匹配一个单词开头和结尾的空串,例如‘\yballs?\y’匹配‘ball’或‘balls’,但不匹配‘football’ |
\B |
和\y相反 |
\s |
匹配空白字符,同[[:space:]] |
\S |
匹配非空白字符,同[^[:space:]] |
\< |
匹配一个单词开头的空串,例如/\ |
\> |
匹配一个单词结尾的空串 |
\w |
匹配word-constituent的字符(包括字母、数字和下划线),同‘[[:alnum:]_]’ |
\W |
匹配非word-constituent的字符,同‘[^[:alnum:]_]’ |
\` |
匹配一个buffer(字符串)开头的空串,同^ |
\' |
匹配一个buffer结尾的空串,同$ |
上表中红色标记的字符用于连接多个正则表达式,我们称之为“正则表达式运算符”或“元字符”。
在字符串常量中有效的转义字符,在正则表达式中同样有效。
正则表达式通常用两个斜杠“/ /”包裹起来作为pattern使用,我们称之为正则表达式常量。
在中括号表达式中引用‘\’、‘] ’、‘-’或‘^’,需要加反斜杠,例如“[d\]]”匹配字符'd'或字符']'。
正则表达式的匹配遵循最左开始最长的匹配的原则,例如下面表达式的结果是"bcd":
echo aaaabcd | awk '{ sub(/a+/, ""); print }'
字符类是POSIX标准中引入的特性,一个字符类表示了某个特定属性的字符的集合。POSIX标准定义的字符类包括:字符类 |
含义 |
[:alnum:] |
字母或数字 |
[:alpha:] |
字母 |
[:blank:] |
空格或tab |
[:cntrl:] |
控制字符 |
[:digit:] |
数字 |
[:graph:] |
可打印且可见的字符(例如space可打印但不可见,而字符a既可打印又可见) |
[:lower:] |
小写字母 |
[:print:] |
可打印的字符(非控制字符) |
[:punct:] |
标点符号(非字母、数字、控制字符和空白字符) |
[:space:] |
空白字符(例如空格、tab、换页符等等) |
[:upper:] |
大写字母 |
[:xdigit:] |
十六进制数字 |
[root@ubuntu]awk_test:$ awk '/[n[:digit:]]/' regexp.txt
我们有时将匹配字母和数字写成/[A-Za-z0-9]/,但在本地化的时候,一些国家的字母表的字符集并不是这些字符,这时我们使用/[[:alnum:]]/就可以准确的匹配而不必担心本地化后的差异。
还有两个特殊的字符序列:Collating Symbols和Equivalence Classes,应用在非英语国家的本地化,例如用一个字符来表示多个字符/[[.ch.]]/或者定义多个等价字符/[[=e=]]/,有兴趣的可以自己查阅相关资料。
注意,\y,\B,\s,\S,\<,\>,\w,\W,\`和\'这些正则表达式的运算符是gawk特有的,gawk的不同选项可能影响如何解析正则表达式:
无选项:上述所有运算符均有效(区间表达式除外)。
--posix:只支持POSIX标准(包括区间表达式)的,那么\w就只表示字面意思的'w'。
--traditional:只支持传统的UNIX awk的正则表达式,此时,GNU的运算符、区间表达式、字符类都不支持,八进制和十六进制的转义字符("\ddd","\xhex")也会按字面意思解析。
--re-interval:支持区间表达式(即使有--traditional选项)。
我们可以通过将IGNORECASE设置为一个非0的值,使AWK在匹配时忽略大小写。也可以用toupper()/tolower()或中括号表达式,来只对某一些规则忽略大小写。AWK中的动作语句(action statements)都放在花括号{ }中,由大部分语言都有的赋值、条件和循环语句组成。AWK中的运算符、控制语句以及输入输出语句都是仿照C语言来定义的。
AWK中运算符按照优先级由高到低,列举如下:
运算符 |
含义 |
(…) |
分组 |
$ |
field引用 |
++ -- |
自增、自减,均可前置和后置 |
^ |
幂运算(也可以用**,对应的幂运算赋值**=) |
+ - ! |
一元运算符的+、-、逻辑非 |
* / % |
乘、除、取模 |
+ - |
加、减 |
space |
字符串连接 |
| |& |
管道IO、协同进程的管道IO。getline,print和printf使用 |
< > <= >= != == |
关系运算符 |
~ !~ |
正则表达式的匹配、不匹配,一般用法是/exp/ ~ /regexp/,其中右侧可以是任何表达式(不需要必须是正则表达式常量)。例如$0 ~ /foo/意为查找匹配foo的$0,若匹配,则返回1,不匹配返回0。 |
in |
获取数组成员 |
&& |
逻辑与 |
|| |
逻辑或 |
?: |
条件表达式,格式为expr1 ? expr2 : expr3,如果expr1为true,则表达式的值为expr2,否则为expr3 |
= += -= *= /= %= ^= |
赋值运算符,均支持a=a+b和a+=b这两种形式。 |
控制语句列举如下:
这些控制语句和ANSI C中类似语句的语法相同。Switch的每个case分支通常要添加break语句以保证唯一匹配。
输入输出语句列举如下:
I/O statement |
解释 |
close(file [, how]) |
关闭文件、管道或协同进程。参数“how”只有在关闭协同进程的双向管道其中一端时才会用到,是字符串类型,可取"to"或"from"。 |
getline |
把$0赋值为下一个输入的record。同时会更新NF、NR和FNR。 |
getline |
把$0赋值为文件file的下一个record。同时会更新NF。 |
getline var |
把变量var赋值为下一个输入的record。同时会更新NR和FNR。 |
getline var |
把变量var赋值为文件file的下一个record。 |
command | getline [var] |
执行command并将输出作为getline或getline var的输入。 |
command |& getline [var] |
执行协同进程command并将输出作为getline或getline var的输入。(协同进程是gawk的扩展,command也可以是一个socket) |
next |
停止处理当前的record,并读取下一个record,然后重新从awk程序中第一个pattern开始处理新的record。如果当前已经达到输入数据中最后一个record,则开始执行END{ }程序块。 |
nextfile |
停止处理当前的文件,并从下一个文件中读取record,然后重新从awk程序中第一个pattern开始处理新的record。FILENAME和ARGIND参数会被更新,FNR被重置为1。如果当前已经达到输入数据中最后一个record,则开始执行END{ }程序块。 |
打印当前的record(根据ORS的值判断record是否输出完毕)。 |
|
print expr-list |
打印表达式列表。如果有多个表达式,每个表达式之间用OFS分隔。(根据ORS的值判断record是否输出完毕) |
print expr-list >file |
打印表达式列表到文件。如果有多个表达式,每个表达式之间用OFS分隔。(根据ORS的值判断record是否输出完毕) |
printf fmt, expr-list |
格式化的打印 |
printf fmt, expr-list >file |
格式化的打印到文件。例如: awk 'BEGIN{printf "%#x", 10 >"getnum.txt"}' |
system(cmd-line) |
执行命令cmd-line,并将exit status作为返回值。例如: system("uname –a") |
fflush([file]) |
清空已打开的输出文件或管道文件的所有缓存。如果不带file参数,则清空标准输出,如果file参数为null string,则所有打开的输出文件和管道的缓存都会被清空。 |
另外,print和printf也允许以下形式的输出重定向:
print ... >> file |
将输出追加到文件file |
print ... | command |
向管道写内容 |
print ... |& command |
向协同进程或socket发送数据 |
getline命令执行成功返回1,到达文件结尾返回0,出错返回-1。如果出错,则字符串ERRNO包含了出错信息。
注意,如果在打开一个双向(two-way)socket的时候失败,将会返回一个非致命的错误到调用者。
如果在一个循环中使用管道、协同进程或socket(向getline输出或从print/printf输入),你必须使用close()来创建新的command或socket的实例。AWK在管道、协同进程或socket返回EOF的时候不会自动关闭它们。AWK里的printf语句和sprintf()函数支持如下的标准格式转换符:
格式符 |
含义 |
%c |
一个ASCII字符。如果传入的参数是一个数值,则将其转换为字符并打印;否则,认为传入的参数是字符串,只打印该字符串的第一个字符。 |
%d, %i |
十进制数字(整数部分) |
%e, %E |
将一个浮点数以“[-]d.dddddde[+-]dd”的形式打印。%E使用大写的E。 |
%f, %F |
将一个浮点数以“[-]ddd.dddddd”的形式打印。使用%F(需要系统库支持),则以大写字母显示"not a number"和"infinity"这类的值。 |
%g, %G |
根据实际情况转换为%e或%f,选择其中最简短的格式。%G对应%E。 |
%o |
无符号的八进制整数。 |
%u |
无符号的十进制整数。 |
%s |
字符串。 |
%x, %X |
无符号的十六进制整数。 |
%% |
字符'%'的原意(不会取实参)。 |
在这些格式符中,'%'和控制字符之间可以放置如下的额外参数(和C语言中printf的格式符用法相同):
count$ |
位置说明符,指定对第count个参数进行格式化转换。例如:printf("%2$d\n", 23, 24);打印的值是24。 |
width |
指定格式化后的最短打印长度,如果不够长则填充空格,默认右对齐。 |
- |
左对齐,通常与width搭配使用。 |
0 |
上述不足width的话,使用前导0填充而不是空格。只对数值类型有效。 |
+ |
对数值添加正负号。只对数值类型有效。 |
# |
以另一种形式打印格式化的内容:例如%#o会在数值前面填0;%#x、%#X会在数值前面填0x或0X;对%#e、%#E、%#f和%#F,结果总会有小数点;%#g、%#G总会显示小数部分。 |
space |
对正数,前面填一个空格;对负数,前面填负号。 |
.prec |
对%e、%E、%f和%F,指明小数部分的最大位数;对%g、%G,指明有效数字的最大长度;对%d、%o、%i、%u、%x和%X,和面前的0width一样,指明最短打印长度,不足则前面补0;对%s,指明最长打印的字符数。 |
另外,printf和sprintf()支持动态的width和.prec:可以从参数列表中的值作为width或.prec的值。通过在count$前面加一个*号来达到这种效果。例如:
printf("%1$*2$s\n", "Bye bye!", 12);
这个例子中,1$仍然是位置说明符的作用,而*2$的意思是将第二个参数替换在这个位置,这个语句就变成了:
printf("%1$12s\n", "Bye bye!", 12);
在通过print、printf、getline进行I/O重定向时,gawk可识别一些特定的文件名,并且支持从gawk的父进程(通常是shell)中继承这些文件的文件描述符来进行文件操作。同时,这些文件也可以用作命令行中的数据文件名。这些文件为:
/dev/stdin 标准输入
/dev/stdout 标准输出
/dev/stderr 标准错误输出
/dev/fd/n 与文件描述符n关联的文件
另外,使用破折号“-”也可以引用标准输入,例如:
some_command | awk -f myprog.awk file1 - file2
这条命令中,awk会先读取file1,然后读取some_command的输出,然后读取file2。
下面三个特殊的文件名,可以与协同进程操作符“|&”配合使用,创建TCP/IP网络连接。
/inet/tcp/lport/rhost/rport 本地端口是lport,与远程主机rhost、远程端口rport的TCP/IP连接,如果端口为0,则让系统自己选择端口。
/inet/udp/lport/rhost/rport 同上,只是TCP改为UDP。
/inet/raw/lport/rhost/rport 未使用,供后续扩展。
下面这些文件名用来获取当前正在运行的gawk进程的信息:/dev/pid,/dev/ppid,/dev/pgrpid和/dev/user。目前已被弃用,改为使用PROCINFO获取进程信息。AWK提供以下一些内置的算术函数:
函数 |
作用 |
atan2(y, x) |
求y/x的反正切,单位是弧度。 |
cos(expr) |
求余弦,expr的单位是弧度。 |
exp(expr) |
求指数。 |
int(expr) |
截断expr保留整数部分。 |
log(expr) |
求自然对数。 |
rand() |
生成一个随机数N,0 ≤ N < 1。 |
sin(expr) |
求正弦,expr的单位是弧度。 |
sqrt(expr) |
求平方根。 |
srand([expr]) |
为随机数生成器指定一个新种子expr。如果不指定expr,就使用时间作为种子。函数的返回值是之前的种子。 |
Gawk提供以下内置的字符串函数:
函数 |
作用 |
asort(s [, d]) |
给数组s中的成员排序(按照gawk默认的升序排序方法),排序完成后,s的数组下标改为从1开始的整数序列。 如果指定第二个参数d,则排序的结果放在数组d中,原数组s不变化。 函数的返回值是原数组s的元素个数。 |
asorti(s [, d]) |
给数组s的下标排序,(按照gawk默认的升序排序方法),排序完成后,s的数组下标改为从1开始的整数序列,而原来的下标改为数组元素的值。原来的数组元素的值被丢弃。 如果不想丢弃原来的元素的值,可以指定第二个参数d,则排序的结果放在数组d中,原数组s不变化。 函数的返回值是原数组s的元素个数。 |
gensub(r, s, h [, t]) |
对原始字符串t,将匹配正则表达式r的子串替换为s。如果字符串h以’g’或’G’开头,则所有匹配都替换,否则只替换第一个匹配。函数的返回值即为执行替换后的字符串,也就是说,原始字符串t不会被修改。 如果不指定参数t,就从当前record中读取,即$0。 在s中,可以通过\1~\9来指代r中第n个圆括号中的匹配项,参考下面的例子1。\0或'&'则表示整个匹配的内容。 |
gsub(r, s [, t]) |
对原始字符串t,将匹配正则表达式r的子串全部替换为s。函数的返回值为匹配到的子串的个数,也就是说,原始字符串t会被修改。 如果不指定参数t,就从当前record中读取,即$0。 在s中,'&'则表示整个匹配的内容。如果要书写字符’&'的原意,要写作"\\&"。 |
index(s, t) |
返回字符串t在字符串s中第一次出现的位置。例如index(“abcdefg”, “def”)返回4,即index从1开始。如果没找到子串t,则返回0。 |
length([s]) |
返回字符串s的长度,如果没有参数s,则返回$0的长度。也可以传入一个数组,这时返回数组元素的个数。 |
match(s, r [, a]) |
在字符串s中匹配正则表达式r,匹配成功则返回子串的位置(index从1开始),并更新RSTART和RLENGTH,匹配失败则返回0; 如果有第三个参数a,则正则表达式r中每个圆括号的匹配内容会被依次赋值给数组a[1]~a[n],而整个的匹配内容则赋值给a[0]。同时,a[m, "start"]和a[m, "length"]两个数组下标成员的值为a[m]在s中的位置和字符串长度。 见下面的例子2。 注意,如果在调用match()之前数组a不为空,则会先清空a。 |
split(s, a [, r]) |
将字符串s按照正则表达式r作为分隔符来分割,分割成的每个field保存在数组a中,函数返回fields的数量。 如果没有r参数,则根据FS来分割。 注意,如果在调用split()之前数组a不为空,则会先清空a。 |
sprintf(fmt, expr-list) |
根据格式fmt打印表达式列表expr-list,最终的字符串作为返回值。例如: sprintf("name%d:%s, name%d:%s", 1, "George", 2, "Tim"); |
strtonum(str) |
将字符串转换为数值,可识别八进制(以0开头)和十六进制(以0x或0X开头)。例如strtonum(“34”),strtonum(34.50),strtonum(“017”)。 |
sub(r, s [, t]) |
同gsub,但只替换第一个匹配的子串。 |
substr(s, i [, n]) |
获取字符串s中,从第i个字符开始的n个字符形成的子串,该子串作为返回值。参数i从1开始。 |
tolower(str) |
将str中的大写字母都转换为小写字母后作为函数返回值。 |
toupper(str) |
将str中的小写字母都转换为大写字母后作为函数返回值。 |
注意,gawk3.1.5支持多字节的字符,这意味着index()/length()/substr()/match()都是针对字符起作用而不是字节。
例子1,匹配“me again”,然后将其中的me改为her:[root@ubuntu]awk_test:$ awk 'BEGIN{result=gensub("(me) (again)", "her \\2", "g", "tell me again, you go again"); print result}' awk_test.txt
tell her again, you go again
前面在sed命令中也讲到过这种用法,不过这里圆括号不需要加反斜杠转义,并且"\2"要写成"\\2"。
例子2,测试带第三个参数的match函数:[root@ubuntu]awk_test:$ awk 'BEGIN{sstr="name1:George, name2:Tim";pos=match(sstr, "name[1-9]:([a-zA-Z]*), name[1-9]:([a-zA-Z]*)", list); \
print pos; print RSTART" "RLENGTH; for (i=0; i<=2; i++) print list[i], list[i, "start"], list[i, "length"];}' awk_test.txt
1
1 23
name1:George, name2:Tim, 1 23
George 7 6
Tim 21 3
例子3,split函数测试:
[root@ubuntu]awk_test:$ awk 'BEGIN{sstr="name1:George, name2:Tim, name3:Jason";fcnt=split(sstr, list, "name[[:digit:]]+:"); print fcnt; \
for (i=0; i<=fcnt; i++) print list[i];}' awk_test.txt
由于AWK主要的用处就是处理包含时间戳信息的大型日志文件。因此AWK提供了以下时间函数来获取和转换时间戳:
mktime(datespec) 将datespec转换为相对于1970-01-01零点的秒数。参数datespec的格式为:“YYYY MM DD HH MM SS[ DST]”,其中夏令时标志DST是可选的。例如mktime("2017 2 6 22 54 00")即为2017年2月6日22时54分0秒,注意这个时间是算上时区的。举例来说,在东8区,mktime("1970 1 1 8 0 1")的返回值为1。
strftime([format [, timestamp[, utc-flag]]]) 将timestamp转换为format指定的格式,timestamp需为相对于1970-01-01零点的秒数。如果utc-flag不为0或null,则转换结果是UTC时间,否则是本地时间。如果不带timestamp参数,则使用当前时间;如果不带timestamp和format参数,则format默认与date命令结果的格式相同,format参数可用的格式请参考C语言中的strftime()函数,也可参考Effective AWKProgramming[2]中的说明。
systime() 将当前时间转换为相对于1970-01-01零点的秒数。Gawk3.1开始的版本,提供了下面的位运算函数:
函数 |
作用 |
and(v1, v2) |
按位与 |
compl(val) |
按位取反,同C语言中的'~'运算符 |
lshift(val, count) |
val左移count位,即val << count |
or(v1, v2) |
按位或 |
rshift(val, count) |
val右移count位(高位补符号位),即val >> count |
xor(v1, v2) |
按位异或 |
AWK中可以自定义函数,形式如下:
function name(parameter list) { statements }
或
func name(parameter list) { statements }
其中,name是函数名,前面添加关键字function或func。圆括号内是形参列表,后面大括号内书写函数的实现。
例如下面的例子:function add_INT(a, b)
{
return (a+b);
}
function add_ARRAY(array)
{
sum = 0;
for(i in array)
{
sum += array[i];
}
return sum;
}
BEGIN {
aint[0]=10; aint[1]=11; aint[2]=12;
print add_ARRAY(aint); print add_INT(12, 78);
}
注意,对于自定义函数,在调用函数时,函数名和左圆括号之间不能有空格(AWK的内置函数没有这个限制)。
在AWK中,所有的变量都是全局的,那么就可能出现一个函数中的局部变量和主程序的变量重名。如果想避免这种情况,可以在函数的参数列表里指明局部变量,方法是将局部变量写在形参后面,并且用多个空格与形参列表分开。
例如:function test(a, b, c)
{
c = 20;
sum = a + b;
}
BEGIN {
c = 10;
sum = 5;
test(11, 22);
print c" "sum;
}
这个例子中,有c和sum两个变量,在函数test中,会修改c和sum,但是由于指明了c在test()中是作为局部变量的,因此不会影响主程序中c的值。所以print打印出的结果是“10 33”。
实际上,参数列表里的形参都会被认为是局部的,用多个空格分开的做法只是为了代码的可读性。
当需要将一段通用代码制作成库函数时,自定义函数就派上了用场,可以配合--source选项来在命令行程序中引用库函数。
一些建议:在自定义函数内部,尽量避免定义外部可能使用的变量,例如“i”,“j”这样的变量,在外部程序中很可能用到,因此在函数内部就不要用这样的变量命名。建议在函数内变量命名时以“_”开头来避免冲突。另外,变量和函数的命名尽量体现它的作用和含义。最后,如果函数内定义了外部可以使用的全局变量,变量名可以第一个字母大写,如“Optind”以和局部变量区别(不全部大写是为了防止和AWK内置变量混淆)。
可以参考Effective AWK Programming [2]中第10章的部分库函数的实现和第11章的自定义函数举例来学习自定义函数的写法。pgawk可接收SIGUSR1和SIGHUP两个信号。SIGUSR1信号会使pgawk生成profile文件(如之前--profile所述),包含自定义函数的调用栈,之后pgawk程序继续运行。SIGHUP信号同样会产生profile文件,之后pgawk程序退出。
Gawk执行正确返回EXIT_SUCCESS,通常是0;执行失败返回EXIT_FAILURE,通常是1;如果发生严重错误,返回2,但有些系统上会返回EXIT_FAILURE。
如果exit语句指定了返回值,则gawk返回这个指定的值。[1] Gawk(1) manpage for GNU Awk 3.1.8
[2] Effective AWK Programming: http://www.gnu.org/software/gawk/manual/gawk.html