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 COMMANDroot 1 0.1 3652 1916 Ss Jan07 0:03 /sbin/initroot 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 --daemonuser 860 0.0 3584 908 S Jan07 0:01 /sbin/udevd --daemonuser 1137 0.0 4520 776 S Jan07 0:00 smbd -Fuser 1550 0.1 4521 1816 Ss Jan07 0:15 nmbd -DOutput 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 –fBEGIN{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<'/''>" }'AWK选项
-f program-file 执行文件中的程序,前面已讲过。Program-file可以有多个,通常用来将通用的代码或函数做成库,以实现代码复用。
环境变量AWKPATH用来指定-f的搜索路径,如果不指定AWKPATH,则默认搜索“.:/usr/local/share/awk”,可以通过修改AWKPATH或ENVIRON["AWKPATH"]来修改搜索路径,每个路径之间用冒号隔开(.或::都可以表示当前路径)。如果-f选项后面跟的是包含“/”的文件名,就不会去额外搜索路径了。
-v var=value 变量赋值,在BEGIN之前进行。例如,定义变量name,并赋值为“jason”:[root@ubuntu]awk_test:$ awk -v name=jason 'BEGIN{printf("name=%s/n", name)}'name=jason注意变量的引用不需要加“$”,实际上AWK的语法很多跟ANSI C的语法类似。“$”在AWK中是用来引用field的,后面会讲到。
-F fs 使用fs作为分隔符(默认是空格)例如,打印/etc/passwd文件中的用户名一列:[root@ubuntu]awk_test:$ cat /etc/passwd | awk -F ':' '{print $1}'rootdaemonbinsyssyncgames --compat 或 --traditional 使用原生awk,不会识别gawk的扩展。例如,原生awk允许在while/for/do循环外面使用continue和break语句,这会被认为和next语句意思相同。Gawk添加--traditional则可以使用这一特性。 --dump-variables[=file] 输出排好序的AWK内置的全局变量到文件(若不指定文件,则默认为awkvars.out)这个选项可以查看当前可用的内置全局变量。在编程的时候要注意,不要定义和这些内置变量重名的变量。
--non-decimal-data 识别输入流中的十六进制和八进制例如,将下面文件中的数字相加求和:[root@ubuntu]awk_test:$ cat number.txt 0x120x3201210[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!”,一方面这个选项可能破坏旧的程序,另一方面这个选项可能在以后被摒弃。
--profile[=prof_file] 生成awk命令的profile文件这个选项以优雅的格式将awk命令保存到文件,如果不指定文件名,则默认为awkprof.out。[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执行命令,则还会显示每条语句以及每个函数的调用次数。
--re-interval 在正则表达式中支持间隔表达式传统的awk不支持间隔表达式,必须加上--re-interval选项或者--posix选项才能使用。
-e program-text 或 --source program-text program-text为awk的program源码,这个选项允许将文件中的源码和命令行中的源码混合使用。在需要引用自定义的库函数时就可以使用该选项,例如:awk -f func_test.awk --source 'BEGIN{printadd_INT(1,2)}' awk_test.txt
这个例子中,func_test.awk里面定义了函数add_INT(),这里将-f指定的文件程序和--source指定的命令行程序混合在了一起。
-E file 或 --exec file 意义和-f选项相同,不过有两点区别:命令行中的其他选项都直接传给awk,而awk先处理其他选项和参数,最后才处理--exec选项;另外,不允许“var=value”形式的变量赋值。这个选项应该在#!开头的脚本里使用,例如:
#! /usr/local/bin/gawk -E awk program here …这个选项可以防止向脚本里传递参数,因为所有的参数都先被awk识别并处理了。
--include source-file--load ext这两个选项都是针对引用函数库的,也可以在文件中使用@include和@load来引用库文件。不过在我所用系统的gawk不支持这两个参数以及相应的AWKLIBPATH环境变量,我就不介绍了。我们就使用-f来引用库文件吧,只是-f的文件里面可以是任何程序内容,并不是只针对库文件而设计的。
-- 标记选项的结束这告诉awk选项部分已经结束,可以用来传递以“-”开头的参数,而不被误认为是选项。
AWK的变量、Records和Fields
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.txtRecords
一个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 COMMANDroot 1 0.1 3652 1916 Ss 0:03 /sbin/initroot 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 --daemonuser 860 0.0 3584 908 S 0:01 /sbin/udevd --daemonuser 1137 0.0 4520 776 S 0:00 smbd -Fuser 1550 0.1 4521 1816 Ss 0:15 nmbd -D看效果就知道是什么意思了。注意“Jan07”本身没有打印出来。
如果RS被赋值为单个字符,则这个字符就是records的分隔符;如果被赋值为多个字符,那RS实际上是一个正则表达式。
如果RS被赋值为空,则以空行作为record的分隔符。fields
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 || || PIDro||ot || || 1ro||ot || || 2ro||ot || || 3ro||ot || || 26us||er || || 495us||er || || 860us||er || ||1137us||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 TIMEjson 1 0.1 3652 1916 Ss Jan07 0:03json 2 0.0 0 0 S Jan07 0:00json 3 0.0 0 0 S Jan07 0:02json 26 0.0 0 0 S Jan07 0:40json 495 0.1 3588 1092 Ss Jan07 0:00 /sbin/udevdjson 860 0.0 3584 908 S Jan07 0:01 /sbin/udevdjson 1137 0.0 4520 776 S Jan07 0:00 smbdjson 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 da b c d这条命令通过$2=$2,让awk重新划分fields,由于FS模式是空格,这时就会先把前导空格和尾部空格剥掉。
数组
AWK中的数组是关联数组(键值对的形式),数组下标放在方括号“[ ]”内,数组下标可以是数值或字符串。可以用(expr, expr ...)来模拟多维数组的下标。例如: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 /regular expression/ relational expression pattern && pattern pattern || pattern pattern ? pattern : pattern (pattern) ! pattern pattern1, pattern2BEGIN和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一个relationalexpression可以使用后面将要讲到的任何运算符,这种模式通常用来测试一个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:]] |
/< | 匹配一个单词开头的空串,例如//<away/匹配‘away’但不匹配‘stowaway’ |
/> | 匹配一个单词结尾的空串 |
/w | 匹配Word-constituent的字符(包括字母、数字和下划线),同‘[[:alnum:]_]’ |
/W | 匹配非word-constituent的字符,同‘[^[:alnum:]_]’ |
/` | 匹配一个buffer(字符串)开头的空串,同^ |
/' | 匹配一个buffer结尾的空串,同$ |
上表中红色标记的字符用于连接多个正则表达式,我们称之为“正则表达式运算符”或“元字符”。
在字符串常量中有效的转义字符,在正则表达式中同样有效。
正则表达式通常用两个斜杠“/ /”包裹起来作为pattern使用,我们称之为正则表达式常量。
在中括号表达式中引用‘/’、‘] ’、‘-’或‘^’,需要加反斜杠,例如“[d/]]”匹配字符'd'或字符']'。
正则表达式的匹配遵循最左开始最长的匹配的原则,例如下面表达式的结果是"<A>bcd":
echo aaaabcd | awk '{ sub(/a+/, "<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()或中括号表达式,来只对某一些规则忽略大小写。动作(actions)
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这两种形式。 |
控制语句列举如下:
if (condition) statement [ elsestatement ] while (condition) statement do statement while (condition) for (initialization; condition; increment) statement for (var in array) statement switch (expression) {case value or regular expression: case-bodydefault: default-body} break continue delete array[index] delete array exit [ expression ] { statements }这些控制语句和ANSI C中类似语句的语法相同。Switch的每个case分支通常要添加break语句以保证唯一匹配。
输入输出语句列举如下:
I/O statement | 解释 |
close(file [, how]) | 关闭文件、管道或协同进程。参数“how”只有在关闭协同进程的双向管道其中一端时才会用到,是字符串类型,可取"to"或"from"。 |
getline | 把$0赋值为下一个输入的record。同时会更新NF、NR和FNR。 |
getline <file | 把$0赋值为文件file的下一个record。同时会更新NF。 |
getline var | 把变量var赋值为下一个输入的record。同时会更新NR和FNR。 |
getline var <file | 把变量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("%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.txt11 23name1:George, name2:Tim, 1 23George 7 6Tim 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 MMSS[ DST]”,其中夏令时标志DST是可选的。例如mktime("2017 2 6 22 54 00")即为2017年2月6日22时54分0秒,注意这个时间是算上时区的。举例来说,在东8区,mktime("19701 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
新闻热点
疑难解答