详解著名的awk oneliner
偶然在网上看到有人注释过的《awk oneliner》,而且注释的人声称"全部弄懂这些就已经是awk高手了",而我也一直想学下awk又懒得花时间,然后就觉得这个东西一定很适合我。为了督促自己看完这个系列,决定整个给翻译一遍。说实话看了 第一篇之后觉得awk的语句也不是那么天书了。原作者是Peteris Krumins,一个很酷的家伙,你可以在titter上follow他(Peteris Krumins)。
- 原文请见:http://www.catonmat.net/blog/awk-one-liners-explained-part-one/
- 中文译者: http://roylez.heroku.com/
Part 1:空行、行号和计算
-
将每行后面都添加一个空行
awk '1;{ print ""}'
这是怎么意思呢?一个单行awk命令,其实也是一个用awk语言写的程序,每个awk程序,都是由一系列的"匹配模式 { 执行动作 }"语句所组成的。在这个例子里面,有两个语句,"1"
和"{print ""}"
。
在每个"匹配模式——执行动作"语句中,模式和动作都是可以被省略的。如果匹配模式被省略,那么预定的动作将会对输入文件的每一行执行。如果动作被省略,那么就默认会执行{print }。所以,这个单行awk语句等同于下面的语句
awk '1 {print } {print ""}'
动作只有在匹配模式的值为真的时候才会执行。因为"1"永远为真,所以,这个例子也可以写成下面的形式
awk '{print } {print ""}'
awk中每条print语句后都默认会输出一个 ORS变量 (Output Record Separator,即输出行分隔符,默认为换行符)。
第一个不带参数的print语句,等同于print \(0
,其中\)0
是代表整行内容的变量。第二个print语句什么也不输出,但是由于print语句后都会被自动加上ORS变量,这句的作用就是输出一个新行。于是每行后面加空行的目的就达到了。
-
添加空行的另一种方法
awk 'BEGIN { ORS="\n\n" };1′
BEGIN是一个特殊的模式,后面所接的内容,会在文件被读入前执行。这里,对ORS变量进行了重新定义,将一个换行符改成了两个。后面的"1",同样等价于{print },这样就达到了在每行后用新的ORS添加空行的目的。
-
在每个非空的行后面添加空行
awk 'NF {print $0 "\n"}'
这个语句里面用到了一个新的变量, NF (number of fields),即本行被分割成的字段的数目。
例如,"this is a test",会被awk分割成4个词语,NF的值就为4。当遇到空行,分割后的字段数为0,NF为0,后面的匹配动作就不会被执行。这条语句,可以理解成 "如果这一行可以分割成任意大于0的部分,那么输出当前行以及一个换行符"。
-
在每行后添加两个空行
awk '1; {print "\n"}'
这一语句与前面的很相似。"1"可以理解为{print },所以整个句子可以改写为
awk '{print ; print "\n"}'
它首先输出当前行,然后再输出一个换行符以及一个结束print语句的ORS,也就是另外一个换行符。
-
为每个文件的内容添加行号
awk '{ print FNR "\t" $0 }'
这个awk程序在每行的内容前添加了一个变量FNR的输出,并用一个制表符进行分隔。 FNR(File Number of Row) 这个变量记录了当前行在当前文件中的行数。在处理下一个文件时,这个变量会被重置为0。
-
为所有文件的所有行统一添加行号
awk '{print NR "\t" $0}'
这一句与上一例基本一样,除了使用的行号变量是 NR(Number of Row) ,这个变量不会在处理新文件的时候被重置。所以说,如果你有2个文件,一个10行一个12行,那这个变量会从1一直变到22。
-
用更漂亮的样式添加行号
awk '{printf("%5d : %s\n", FNR, $0)}'
这个例子用了printf函数来自定义输出样式,它所接受的参数与标准C语言的printf函数基本一致。需要注意的是,printf后不会被自动添加ORS,所以你需要自己指定换行。这个语句指定了行号会右对齐,然后是一个空格和冒号,接着是当前行的内容。
-
为文件中的非空行添加行号
awk 'NF { $0=++a " :" $0}; {print }'
Awk的变量都是自动定义的:你第一次用到某个变量的时候它就自动被定义了。这个语句在每次遇到一个非空行的时候先把一个变量a加1,然后把a的数值添加到行首,然后输出当前行的内容。
-
计算文件行数(模拟 wc -l)
awk 'END {print NR}'
END是另外一个不会被检验是否为真的模式,后面的动作会在整个文件被读完后进行。这里是输出最终的行号,即文件的总行数。
-
对每行求和
awk '{s=0;for (i=0;i<NF;i++) s=s+$i; print s}'
Awk有些类似C语言的语法,比如这里的for (;;;){ … }
循环。这句命令会让程序遍历所有NF个字段,并把字段的总和存在变量s中,最后输出s的数值并处理下一行。
-
对所有行所有字段求和
awk '{for (i=0;i<NF;i++) s=s+$i; END {print s+0}'
这个例子与上一个基本一致,除了输出的是所有行所有字段的和。由于变量会被自动定义,s只需要定义一次,故而不需要把s定义成0。另外需要注意的是,它输出{print s+0}而非{print s},这是因为如果文件为空,s不会被定义就不会有任何输出了,输出s+0可以保证在这种情况下也会输出更有意义的0。
-
将所有字段替换为其绝对值
awk '{ for (i = 1; i <= NF; i++) if ($i < 0) $i = -$i; print }'
这条语句用了C语言的另外两个特性,一个是if (…) {…}
结构,另外就是省略了大括号。它检查对每一行,检查每个字段的值是否小于0,如果值小于0,则将其改为正数。字段名可以间接地用变量的形式引用,如i=5;$i='hello'
会将第5个字段的内容置为hello。
下面的是将这条语句完整的写出来的形式。print语句会在行中所有字段被改为正数后执行。
awk '{ for (i = 1; i <= NF; i++) { if ($i < 0) { $i = -$i; } } print }'
-
计算文件中的总字段(单词)数
awk '{total=total+NF};END {print total+0}'
这个命令匹配所有的行,并不断的把行中的字段数累加到变量total。执行完成上述动作后,输出total的数值。
-
输出含有单词Beth的行的数目
awk '/Beth/ {n++}; END {print n+0}'
这个例子含有两个语句。第一句找出匹配/Beth/的行,并对变量n进行累加。在/../
之间的内容为正则表达式,/Beth/匹配所有含有"Beth"的单词(它不仅匹配Beth,同样也匹配Bethe)。第二句在文件处理完成后输出n的数值。这里用n+0是为了让n为0的情况下输出0而不是一个空行。
-
寻找第一个字段为数字且最大的行
awk '$1 > max { max=$1; maxline=$0 }; END { print max, maxline }'
这个例子用变量max记录第一个字段的最大值,并把第一个字段最大的行的内容存在变量maxline中。在循环终止后,输出max和maxline的内容。注意:如果在数字都为负数的情况下,这个例子就不能用了,下面的是修改过的版本
awk 'NR == 1 { max = $1; maxline = $0; next; } $1 > max { max=$1; maxline=$0 }; END { print max, maxline }'
-
在每一行前添加输出该行的字段数
awk '{print NF ":" $0}'
这个例子仅仅是在逐行输出字段数NF,一个冒号,以及该行的内容。
-
输出每行的最后一个字段
awk '{print $NF}'
awk里面的字段可以用变量的形式引用。这一句输出第NF个字段的内容,而NF就是该行的字段数。
-
打印最后一行的最后一个字段
awk '{ field = $NF };END {print field}'
这个例子用field记录最后一个字段的内容,并在循环后输出field的内容。
这里是一个更好的版本。它更常用、更简洁也更高效:
awk 'END {print $NF}'
-
输出所有字段数大于4的行
awk 'NF > 4′
这个例子省略了要执行的动作。如前所述,省略动作等价于{print}
。
-
输出所有最后一个字段大于4的行
awk '$NF > 4′
这个例子用$NF
引用最后一个字段,如果它的数值大于4,那么就输出。
Part 2:文本替换
果然人偷懒的潜力是无限的,第一篇完成后半个多月,现在终于又开始翻译第二篇。不管怎么说,没太监掉已经证明我人品还算可以了。
-
将Windows/dos格式的换行(CRLF)转成Unix格式(LF)
awk '{ sub(/\r$/,""); print }'
这条语句使用了sub(regex,repl,[string])
函数。此函数将匹配regex的string替换成repl,如果没有提供string参数,则\(0
将会被默认使用。\)0的含义在上一篇已经介绍过,代表整行。
这句话其实是将\r删除,然后print语句在行后自动添加一个ORS,也就是默认的\n
。
-
将Unix格式的换行(LF)换成Windows/dos格式(CRLF)
awk '{ sub(/$/,"\r"); print }'
这句话同样使用了sub函数,它将长度为0的行结束符$替换成\r,也就是CR。然后print语句在后面添加一个ORS,使每行最后以CRLF结束。
-
在Windows下,将Unix格式的换行换成Windows/dos格式的换行符
awk 1
这条语句不是在所有情况下都可以用,要视使用的awk版本是不是能识别Unix格式的换行而定。如果能,那么它会读出每个句子,然后输出并用CRLF结束。1其实就是{ print }的简写形式。
-
删除行首的空格和制表符
awk '{ sub(/^[ \t]+/,""); print }'
和前面的例子相似,sub函数将[ \t]+用空字符串替换,达到删除行首空格的目的。
-
删除行首和行末的空格
awk '{ gsub(/^[ \t]+|[ \t]+$/,""); print }'
这条语句使用了一个新函数 gsub ,它和sub的区别在于它会替换句子里面的所有匹配。如果仅仅是要删除字段间的空格,你可以这样
awk '{ $1=$1; print }'
这是条很取巧的语句,看起来是什么也没作。awk会在给字段重新赋值的时候对$0重新进行架构,用OFS也就是单个空格分隔所有字段,这样以来所有的多余的空格就消失了。
-
在每行首加5个空格
awk '{ sub(/^/," "); print }'
同上,sub将行首符替换为5个空格,达到了在行首添加空格的目的。
-
让内容在79个字符宽的页面上右对齐
awk '{ printf "%79s\n", $0 }'
同上一篇,又使用了printf函数。
-
让内容在79个字符宽的页面上居中对齐
awk '{ l=length(); s=int((79-l)/2); printf "%"(s+l)"s\n", $0 }'
第一句用length函数计算当前行内容的长度,第二句计算行首应该添加多少空格,第三句让内容在s+l宽度靠右对齐。
-
把foo替换成bar
awk '{ sub(/foo/,"bar"); print }'
同上,sub函数将每行中第一个foo换成了bar。但是,如果想要把每行中所有的foo都替换成bar,你需要
awk '{ gsub(/foo/,"bar"); print }'
另外一种方法就是使用gensub函数
awk '{ $0=gensub(/foo/,"bar",4); print }'
这条语句的不同在于它只是将每行第四次出现的foo替换成bar,它的原型是gensub(regex,s,h[,t])
,它将字符串t中第h个regex替换成s,如果没有提供t参数,那么默认t就是$0。gensub不是一个标准函数,你只有在GNU Awk或者netBSD带的awk里面才能用到它。
-
在含有baz的行里面,把foo替换成bar
awk '/baz/ { sub(/foo/,"bar") }; {print}'
(真的没什么好说的)
-
在不含baz的行里,把foo替换成bar
awk '!/baz/ { gsub(/foo/, "bar") }; { print }'
跟上一句的差别是用 ! 让搜到baz的返回为假。
-
把scarlet或者ruby或者puce替换成red
awk '{ gsub(/scarlet|ruby|puce/,"red"); print }'
(再懒一次)
-
让文本首位倒置,模仿tac
awk '{ a[i++] = $0} END { for (j=i-1; j>=0;) print a[j--] }'
首先,把每一行的内容放到数组a里面。在最后,让变量j从a的最大编号变到0,从后望前逐行输出a里面的内容。
-
把以\结束的行同下一行相接
awk '/\\$/ { sub(/\\$/,""); getline t; print $0 t; next }; 1′
首先查找以\结束的行,并删除\。然后getline函数获取下一行的内容,输出当前行(去掉了\)和下一行的相接后的内容。并用next跳过后面的1避免重复输出。如果当前行行末没有\,会直接执行1输出。
-
将所有用户名排序并输出
awk -F ":" '{ print $1 | "sort" }' /etc/passwd
这里首先用-F指定了分隔符为冒号,然后用|指定输出的内容逐行pipe给外部程序sort。(这写法真是奇怪)。
-
将前两个字段倒序输出
awk '{ print $2, $1 }' file
没什么好说的。
-
将每行里面的前两个字段交换位置
awk '{ temp = $1; $1 = $2; $2 = temp; print }'
因为要输出整行,只好重新给\(1
和\)2
赋值,用个临时变量做中转。
-
删除每行的第二个字段
awk '{ $2 = ""; print }'
就是把第二个字段赋值为空字符串
-
把每行的所有字段倒序输出
awk '{ for (i=NF; i>0; i–) printf("%s ", $i); printf ("\n") }'
没什么好说的
-
删除连续的重复行
awk 'a != $0; { a = $0 }'
前一句省略了action,选择输出整行内容与a不一样的;后一句省略了pattern,把当前行的内容赋值给a。a在这个例子中的左右是记录上一行的内容。
-
删除非连续的重复行
awk '!a[$0]++'
这一句真是很有ee的风范!把当前行的内容作为数组a的索引,如果a里面已经有了$0的记录,即遇到了重复,pattern的值为0,不输出。
-
把每5行用逗号相连
awk 'ORS=NR%5?",":"\n"'
这里在每行输出前,对ORS重新进行定义,如果行号能被5整除则为\n,否则为逗号。(也很天书的一句)
Part 3:选择性输出特定行
-
输出文件的前10行(模拟 head -n 10 )
awk ' NR < 11 '
如前所述,这里省略了动作,即为打印输出。匹配模式是变量NR需要小于11,NR即为当前的行号。这个写法很简单,但是有一个问题,在NR大于10的时候,awk其实还是对每行进行了判断,如果文件很大,比如说有上万行,浪费的时间是无法忽略的。
所以,更好的写法是
awk '1; NR = 10 { exit }'
第一句对当前行进行输出。第二句判断是不是已经到了第10行,如果是则退出。
-
输出文件的第一行(模拟 head -n 1 )
awk 'NR > 1 { exit }; 1′
这个例子与前一个很相似,中心思想就是第二行就退出。
-
输出文件的最后两行(模拟 tail -n 2 )
awk '{ y=x "\n" $0; x=$0}; END { print y }'
的确,这一句看起来确实有些别扭。第一句总是把一个在当前行前面再加上变量x的内容赋值给y,然后用x记录当前行内容。这样的效果是y的内容始终是上一行加上当前行的内容。在最后,输出y的内容。如果仔细看的话,不难发现这个写法是很不高效的,因为它不停的进行赋值和字符串连接,只为了找到最后一行!所以,如果你想要输出文件的最后两行,tail -n 2是最好的选择。
-
输出文件的最后一行(模拟 tail -n 1 )
awk 'END { print }'
句法方面没什么好说的,print省略参数即是等价于print $0。但是这个语句可能不能被非GNU awk的某些awk版本正常执行,如果为了兼容,下面的写法是最安全的:
awk '{ rec = $0 }; END { print rec }'
-
输出只匹配某些模式的行(模拟 grep )
awk '/regex/'
似乎没什么好说的了。
-
输出不匹配某些模式的行(模拟 grep -v )
awk '!/regex/'
匹配模式前加"!"就是否定判断结果。
-
输出匹配模式的行的上一行,而非当前行
awk '/regex/ { print x }; { x = $0 }'
变量x总是用来记录上一行的内容,如果模式匹配了当前行,则输出x的内容。
-
输出匹配模式的下一行
awk '/regex/ { getline; print }'
这里使用了getline函数取得下一行的内容并输出。getline的作用是将\(0
的内容置为下一行的内容,并同时更新NR,NF,FNR变量。如果匹配的是最后一行,getline会出错,\)0不会被更新,最后一行会被打印。
-
输出匹配AA或者BB或者CC的行
awk '/AA|BB|CC/'
没什么好说的,正则表达式。如果有看不懂的朋友,请自行学习正则表达式。
-
输出长过65个字符的行
awk 'length > 64′
length([str])返回字符串的长度,如果参数省略,即是以$0作为参数,括号也可以省略了。
-
输出短于65个字符的行
awk 'length < 65′
和上例基本一样。
-
输出从匹配行到最后一样的内容
awk '/regex/,0′
这里使用了"pattern1,pattern2"的形式来指定一个匹配的范围,其中pattern2这里为0,也就是false,所以一直会匹配到文件结束。
-
从第8行输出到第12行
awk 'NR==8,NR==12′
同上例,这也是个范围匹配。
-
输出第52行
awk 'NR==52′
如果想要少执行些不必要的循环,就这样写:
awk 'NR==52 {print;exit}'
-
输出两次正则表达式匹配之间的行
awk '/regex1/,/regex2/'
-
删除所有的空行
awk NF
NF为真即是非空行。另外一种写法是用正则表达式:
awk '/./'
这个很类似grep .的思路,但是是不如awk NF好的,因为"."也是可以匹配空格和TAB的。
Part 4:定义字符串和数组
-
创建一个固定长度的字符串
awk 'BEGIN { while (a++<513) s=s "x"; print s }'
这段程序用BEGIN这个特殊的匹配模式让后面的代码在awk试图读入任何东西前就执行。在里面是一个被执行了513次的循环,每次循环中"x"都被添加到变量s的最后。循环结束后,s的内容被输出。因为这段代码只有这一句,所以awk在执行完BEGIN模式语句后就退出了。
这段循环代码不仅仅可用在BEGIN中,你可以在awk的任何代码段里面使用,包括END。
很不幸这段代码不是最有效率的,这是一个线性的解决方案。10 Awk Tips, Tricks and Pitfalls的作者 waldner 有一个非线性的解决方案
function rep(str, num, remain, result) { if (num < 2) { remain = (num == 1) } else { remain = (num % 2 == 1) result = rep(str, (num – remain) / 2) } return result result (remain ? str : "") }
我看不懂,被这awk oneliner的作者蒙了,没成高手„T_T
这个函数可以这样用
awk 'BEGIN { s = rep("x", 513) }'
-
在某个位置插入指定长度的字符串
gawk –re-interval 'BEGIN{ while(a++<49) s=s "x" }; { sub(/^.{6}/,"&" s) }; 1′
这段代码只能在gawk下使用,因为它用到了interval expression,即这里的{6},作用是让前一个字符.匹配多次。.{6}便可以匹配6个任意字符。gawk使用interval expression需要用到参数-re-interval。
同前一例一样,首先在BEGIN段里面,一个叫做s的49个字符长的字符串被建立了。接下来是对每一行,进行替换,&这里代表的是匹配的字符串部分,所以sub的结果是将每一行第7个字符开始的内容替换成了s。然后是逐行输出。
如果不是gawk,需要这样写
awk 'BEGIN{ while(a++<49) s=s "x" }; { sub(/^……/,"&" s) }; 1′
- 利用字符串建立数组
这里数组这个翻译,其实并不能十分正确的表达Array的含义。鉴于大部分时候大家都是这么叫的,这里也用数组代表Array。
split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", month, " ")
很简单,数组month被初始化了。month[1]的内容是Jan,month[2]是Feb。
-
建立用字符串做编号的数组(类似Ruby的Hash,Python的dict)
for (i=1; i<=12; i++) mdigit[month[i]] = i
很明了,所以不说了,用到了上面的month数组。
-
输出第5个字段为abc123的行
awk '$5 == "abc123″'
没什么好说的,等价于
awk '$5 == "abc123″ {print $0}'
-
输出第5个字段不为abc123的行
awk '$5 != "abc123″'
也等价于
awk '$5 != "abc123″ {print $0}' # 或 awk '!($5 == "abc123″)'
-
输出第7字段匹配某个正则表达式的行
awk '$7 ~ /^[a-f]/'
这里用了~来进行正则表达式的匹配哦。如果你要不匹配的行,可以
awk '$7 !~ /^[a-f]/'