Linux三剑客之:awk使用教程
Table of Contents
简介
Linux三剑客
grep 、sed、awk被称为Linux中的”三剑客”,它们的使用场景,大概是:
grep
适合单纯的查找或匹配文本(Globally search for a Regular Expression and Print matching lines);sed
适合编辑匹配到的文本(sed是stream editor的缩写,所以它其实就是个编辑器,只不过不用打开手动去编辑,而是写好命令去编辑);awk
适合格式化文本,对文本进行较复杂格式处理。
本文主要讲awk的使用,但毕竟不是参考手册,所以不可能把所有的功能全部列出来,具体可以参考官方文档,比如内置函数。
awk简介
awk其实是一门编程语言,它支持条件判断、数组、循环等功能。所以,我们也可以把awk理解成一个脚本语言解释器,就好像python、php等一样。
但是大多数时候,awk只表现为一个命令,你可以用这个命令来对文本进行格式化处理。
awk名称的由来
awk这三个字母,其实是它的三个开发者的姓(Last Name)的首字母,这三个人分别是:Alfred Vaino Aho(加拿大人)、Peter Jay Weinberger(美国人)和Brian Wilson Kernighan(加拿大人),注意,老外的姓是在后面的。他们一开始都是在贝尔实验室工作的,其中Brian Wilson Kernighan还是Unix系统开发者之一。
awk诞生于贝尔实验室,最初是在unix上实现的,所以,我们现在在Linux中所使用的awk其实是GNU awk,简称为gawk,awk还有一个版本,New awk,简称为nawk,但是Linux中用的都是gawk!
可以看到,awk
其实是指向gawk
的
如果你还是不明白什么是gawk,这么说吧:当时有人看不惯Unix什么都要钱买,大家都买不起用不起,于是发起GNU计划把Unix上的东西都重写一遍,在兼容unix的同时又开源免费,比如Linux就相当于把Uinx系统重写了一遍(虽然最开始Linux其实并不属于GNU,但它是开源的,Linux能被大家选用,一方面是它本身符合当时大家的需求,另一方面也是因为GNU计划的Hurd系统难产,最终被Linux代替),但GNU计划不止有系统,还有很多系统里配套的程序,而gawk就是其中一个程序,就是重写Unix的awk的(就是不知道它的代码,只知道它的功能,从头开始开发,在完全兼容原版的同时,还扩展及修复它的一些功能)。
关于GNU
有人可能不知道GNU是什么意思,这里大概说一下,GNU是“GNU is Not Unix”的缩写(这是一种把名字放到定义中的递归缩写)。
1964年左右,受到软硬件专利的刺激,麻省理工的黑客(不同于日常理解的靠搞破坏而获利的“黑客”)自由软件精神逐渐萌芽并发展,他们谴责专利软硬件在道德层面的罪恶,并试图打破软硬件专利对人类智慧结晶的封锁,从此,不断有UNIX某些软件的替代品出现。
随后Richard M. Stallman于1984年开创GNU计划,取代UNIX的工作取得良好的进展,GNU工具逐渐取代了UNIX专有程序,其BASH、GCC、GDB、Emacs等软件也已经足够成熟。GNU计划以GNU Hurd为整个GNU操作系统的核心,然而,GNU操作系统的核心Hurd直至1991年仍不可使用。
而在1991年,当年的计算机业余爱好者Linus Torvalds(如今为世界顶级计算机科学家),通过对教学用的Minix操作系统的研究扩展,独立发表了开源的Linux内核。当时Linus Torvalds已经成功将GNU的工具链GCC等核心软件运行于Linux内核之上,从1992年开始,Linux受到广泛关注,大量使用Linux内核以及GNU软件的整套操作系统开始出现,并且发展壮大。GNU计划为Linux等新内核的产生及发展创造了合适的土壤,而Linux等新内核弥补了GNU计划的内核Hurd发展迟缓的缺憾。
awk的几种运行方式
1.命令行方式1
awk [options] '[Pattern]{Action}' /path/to/file1 [/path/to/file2]
2.命令行方式2
用管道符把前面的输出交给awk处理
cat /path/to/file | awk [options] '[Pattern]{Action}'
注意:前面的输出未必是用cat
,只要是输出就行,比如ls -l
。
3.脚本方式1
保存以下内容到test.awk
,并使用chmod +x test.awk
添加可执行权限
#!/usr/bin/awk -f
# 使用BEGIN指定字符来设定"FS"内置变量,不能用"-F"来指定了
BEGIN { FS=":" }
# 这里可以写命令行方式引号内的语句,即正则+命名
{
print $1
}
使用方式
./test.awk /path/to/file
4.脚本方式2
把以下脚本保存到test.awk
中,不用赋可执行权限
#!/usr/bin/awk
# 使用 BEGIN 指定字符来设定 FS 内置变量,不能用-F指定了
BEGIN { FS=":" }
# 这里可以写命令行方式引号内的语句,即正则+命名
{
print $1
}
使用方式(相当于把脚本方式1的#!/usr/bin/awk -f
中的-f
拿到外面了)
awk -f /path/to/test.awk /path/to/file
基础使用
第一个例子
Linux命令中约定俗成的规则是,方括号表示非必须(即可以没有它),所以前面的命令行方式1的简单版本是
awk '{Action}' file
比如有个文件test.txt
的内容是这样的
张三 男 22
李四 男 23
小芳 女 18
把整个文件输出(相当于cat test.txt
)
awk '{print}' test.txt
要输出它的第一列,可以这么写(命令行方式1)
awk '{print $1}' test.txt
当然也可以这样(命令行方式2)
cat test.txt | awk '{print $1}'
解释:
awk默认会以空格为分隔符,把文本切成一列一列的,$0
是当前行所有列,$1
是当前行第一列,$2
是当前行第二列,以此类推,$NF
为当前行最后一列,NF是awk的一个内部变量,是Number of Fields的缩写,意思是当前行字段的数量(比如一共有3列,则NF=3,如果你不加$
,即print NF
,那结果就是3,如果加了$
就表示打印第三列)。
所以,第一个例子里的这句
awk '{print}' test.txt
其实也可以写成这样(表示循环打印所有行)
awk '{print $0}' test.txt
逐行处理:
之所以说“当前行”,是因为awk处理文本是一行一行处理的,虽然打印$0
,最后它会列出整个文本(而不是一行),你可以认为awk默认有一个隐藏的for循环或者while循环,会循环把所有行都打印出来,但它本质是按行处理的,所以才说“当前行”。
使用-F
指定列分隔符
前面说过,awk默认会以空格为分隔符!那如果文本不是以空格为分隔符呢(比如/etc/password
是用:
分隔的)?
答案是可以用--field-separator=
指定(当然我们一般用它的简写-F
),我们打印passwd
文件的第一列看看
tail -n 5 /etc/passwd | awk -F: '{print $1}'
用tail
获取/etc/passwd
的最后5行,再通过管道符|
交给awk处理,awk
用-F
指定:
为分隔符,用print $1
打印第一列
不知道大家有没有发现,-F
是个选项,:
是选项指定的值,为什么选项和值是挨着的?只要用过Linux命令的童鞋,应该都知道,命令的选项和值之间,一般都是要空格隔开的呀。
比如应该写成这样
tail -n 5 /etc/passwd | awk -F : '{print $1}'
而且由于这是对字符串处理,分隔符:
本身也是字符串,为什么不用引号括起来,比如这样
tail -n 5 /etc/passwd | awk -F ':' '{print $1}'
其实以上的写法都对,而且我认为加空格和引号的方式是最标准的写法,但是这样会比较麻烦,分隔符直接挨着-F
的写法是最方便也最常用的,这个应该是awk特殊支持的吧。
注意:分隔符未必是固定字符串,也可以是正则表达式,只不过写的时候不用双斜线括起来,也是直接用引号就行!
awk指定参数的两种方式
- 1.通过选项的方式指定(比如前面的
-F
); - 2.通过内置变量来指定(比如
-F
对应的内置变量是FS
),其实选项的方式,最终还是会被赋值给内置变量的。
比如,以下两句中的-F:
和-v FS=':'
都是指定分隔符为:
tail -n 5 /etc/passwd | awk -F: '{print $1}'
tail -n 5 /etc/passwd | awk -v FS=':' '{print $1}'
只不过,一个通过选项指定,一个通过给内置变量赋值来指定,因为是在命令行里,所以要用-v
来说明这是对内置变量赋值(v是variable,变量),如果是在代码块里(花括号里),或者用脚本方式写(其实脚本方式也是写在花括号里),就不用-v
来指定了,后面会说到。
输出多列
当然也可以同时打印多列,用逗号隔开即可,比如打印第一列和最后一列
tail -n 5 /etc/passwd | awk -F: '{print $1,$NF}'
如果我不想用空格分隔输出的列呢?比如我想用竖杠”|”来分隔输出的结果,可以这么写
tail -n 5 /etc/passwd | awk -F: '{print $1" | "$NF}'
注意:" | "
就是分隔字符串,和传统的编程语言不同,awk的分隔字符串和变量之间,不需要用+
号之类的连接符来连接,直接写就行。
如果print多个变量(即多列)时不用逗号也不用字符串会怎样?比如
tail -n 5 /etc/passwd | awk -F: '{print $1$NF}'
tail -n 5 /etc/passwd | awk -F: '{print $1 $NF}'
可以看到,输出的两列连在一起了,即使变量之间加了个空格,它也完全连在一起
分隔符
前面说过分隔符可以通过--field-separator=
或它的简写-F
来指定,其实这只是输入分隔符,另外还有输出分隔符,只不过输出分隔符无法用选项的方式指定,只有内置变量,我猜是因为它没有输入分隔符常用。
输入分隔符的内置变量是FS(Field Separator),输出分隔符的内置变量是OFS(Output Field Separator)。
输入分隔符的作用我们已经知道了,就是告诉awk,要根据什么字符串来分割文本。
那输出分隔符的作用呢?与输入分隔符类似,是告诉awk,输出结果的各列之间,要用什么字符串来分隔(像前面我们没有指定过,都是默认用一个空格来分隔)。
其实前面输出多列里已经用过|
作为输出结果的列分隔符,但还可以用内置变量的方式来指定
tail -n 5 /etc/passwd | awk -F: -v OFS=" | " '{print $1,$NF}'
可以看到,用指定输出分隔符的方式,可以达到手动在print里用字符串拼接的方式一样的效果(点击图片可放大)
其它
打印倒数第二列,可以用总列数减1的方式
tail -n 5 /etc/passwd | awk -F: '{print $(NF-1)}'
还可以添加自己的字段(双引号引住的代表字符串)
tail -n 5 /etc/passwd | awk -F: '{print $1,"test"}'
换句话说,如果你把$1
用双引号引住,那么它就是普通字符串了,比如这样,它会直接输出$1
而不是第一个字段
tail -n 5 /etc/passwd | awk -F: '{print "$1"}'
awk的变量
在awk简介里就说过,awk其实是一个脚本语言,既然是语言,那肯定就有它最基本的东西,那就是变量。前面就提到过NF、FS、OFS等这些内置变量,这里统一说一下。
awk的内置变量
这里列出了大部分,少数不常用的可以自己用man awk
命令查看。
- FS:Field Separator,输入字段分隔符,默认为一个空格;
- OFS:Output Field Separator,输出字段分隔符,默认为一个空格;
- RS:Record Separator,输入记录(即行)分隔符(awk分割字符串时,是一行一行分割最后组合起来的),否则默认为
\n
; - ORS:Output Record Separator,输出记录换行符。一般我们肯定会理所当然的认为,换行符肯定就是
\n
,但事实上,我非要用其它字符串(比如空格)当换行符也不是不可以,通过ORS指定即可; - NF:Number of Fields,当前行的字段的个数(即当前行被分割成了几列);
- NR:Number of Records,当前行的行号(awk是逐行处理文本的);
- FNR:Number of Record of File,各文件分别计数的行号(说“各文件”,是因为awk可以指定处理多个文件,空格隔开),也就是说,FNR只有在同时处理两个文件的时候,才会体现它与NR的区别,在处理一个文件的时候,它与NR没区别;
- FILENAME:当前文件名(如果同时处理两个文件,可以用文件名来区分数据属于哪个文件);
- ARGC:Arguments Count,命令行参数的个数;
- ARGV:Arguments Variables,数组,保存的是命令行所给定的各参数(命令行后面空格隔开的都属于参数);
- OFMT:Output ForMaT,数字的输出格式,默认是
%.6g
;
注:Record是“记录”的意思,在这里是指“一条记录”,其实就是一行,所以我认为把Record理解成Row,更好理解,反正都是R开头的。
NR: 当前行的行号
前面举的例子,每行的列数都是相同的,现在来个不同的,比如有test.txt
内容如下
张三 男 22 北京
李四 男 23
小芳 女 18
用awk分别打印每行的列数
awk '{print NR,NF}' test.txt
可以看到,第1行有4列,第2行有3列,第3行也是3列
给每行开头加个行号再输出
awk '{print NR,$0}' test.txt
看,每行前面都多了一个行号
FNR:Number of Record of File,各文件分别计数的行号(说“各文件”,是因为awk可以指定处理多个文件,空格隔开),也就是说,FNR只有在同时处理两个文件的时候,才会体现它与NR的区别,在处理一个文件的时候,它与NR没区别。
比如有两个文件,test.txt
内容如下
张三 男 22 北京
李四 男 23
小芳 女 18
test2.txt
内容如下
张三|男|22|北京
李四|男|23
小芳|女|18
我们还是给像前面NR一样,加行号再输出,只不过这次是同时处理两个文件
awk '{print NR, $0}' test.txt test2.txt
输出如下,相当于把两个文件合并了
但如果用FNR
awk '{print FNR, $0}' test.txt test2.txt
我们再来看看输出的行号,这次输出的行号,就是它们在各自文件里的行号了,这就是FNR与NR的区别
RS: Record Separator,输入记录(即行)分隔符(awk分割字符串时,是一行一行分割最后组合起来的),否则默认为\n
。
把行分隔符设置为空格,还是用前面的test.txt
文件
awk -v RS=' ' '{print $0}' test.txt
设置空格为换行符后,可以看到第4行和第6行,本来有常规换行符的地方虽然显示上是换行了,但它还是和前面的归为一行(这里必须要打印行号才能看的出来,否则你可能以为它还是把\n
当换行符了)
ORS:Output Record Separator,输出记录换行符。一般我们肯定会理所当然的认为,换行符肯定就是\n
,但事实上,我非要用其它字符串(比如空格)当换行符也不是不可以,通过ORS指定即可。
指定空格为输出换行符
awk -v ORS=' ' '{print NR,$0}' test.txt
指定空格为换行符后,可以看到,它还是有三行(从行号1,2,3可以看出来),只不过行与行之间是用空格间隔的,而不是用传统的\n
至于最后的%
,是因为我用的是zsh,zsh会用%
指示最后无换行符\n
,但实际还是会换行,这样比较美观,如果用bash,就不会输出%
号,它会跟后面的当前用户连在一起,比较难看
FILENAME:当前文件名(如果同时处理两个文件,可以用文件名来区分数据属于哪个文件)。
有两个文件,一个是test.txt
,内容如下
张三 男 22 北京
李四 男 23
小芳 女 18
test2.txt
内容如下
张三|男|22|北京
李四|男|23
小芳|女|18
运行以下命令
awk '{print FILENAME,FNR,$0}' test.txt test2.txt
可以看到,每行都输出了它所属的文件名
ARGC:Arguments Count,命令行参数的个数;
ARGV:Arguments Variables,数组,保存的是命令行所给定的各参数(命令行后面空格隔开的都属于参数);
其实这两个参数,在很多编程语言里都有,比如python、php等等。
打印ARGV和ARGC(awk没有直接打印数组的方法,只能逐个打印),而且这两个变量只能在BEGIN模式里打印,因为BEGIN表示所有语句运行之前的操作(见两种特殊的模式)
#打印ARGV第一个元素(下标为0)
awk 'BEGIN{print ARGV[0]}' aa bb
#打印ARGV第二个元素(下标为1)
awk 'BEGIN{print ARGV[1]}' aa bb
#打印ARGV第三个元素(下标为2)
awk 'BEGIN{print ARGV[2]}' aa bb
#打印ARGC
awk 'BEGIN{print ARGC}' aa bb
只能说,awk比较奇怪,第一个元素(即ARGV[0]),对于其它语言来说,第一个元素一般都是命令本身后面的那一个,比如在这里按道理应该是BEGIN{print ARGV[0]}
,只能说awk比较特殊吧
自定义变量
在awk指定参数的两种方式中用过这句,意思是把内置变量FS(输出分隔符变量)的值设置了:
tail -n 5 /etc/passwd | awk -v FS=':' '{print $1}'
其实自定义也可以用这种方式,只要你写的变量名不是内置的,那就相当于你自定义了,比如
#只能在BEGIN里打印,因为没有输入的文件,如果去掉BEGIN,则打印不出来,而且会挂起,要用ctrl+c结束
awk -v test='aaa' 'BEGIN{print test}'
也可以直接在BEGIN模式里定义,不要要用分号隔开(其实花括号里写的就是程序,而很多编程语言都是用分号来表示一句的结尾的)
awk 'BEGIN{test2="bbb";print test2}'
awk的模式(Pattern)
模式简介
模式(Pattern),就是花括号前面的那一串字符串!回忆一下前面说过的命令行方式1,Pattern由于有方括号(表示该参数可忽略),所以前面我们一直都忽略了它,没有使用这个参数
awk [options] '[Pattern]{Action}' /path/to/file1 [/path/to/file2]
Pattern的意思是“模式”,你理解为“条件”更容易理解,在第一个例子的最后说过,awk处理文本是逐行处理的,我们前面的例子都是每行都输出了,那是因为我们一直没有加条件,如果添加条件了,那只有符合条件的行才会被处理并输出。
模式的两种类型
- 1.数学及逻辑运算符,包括:
<
、<=
、==
、>
、>=
、!=
、&&
、||
、!
(我只想到这么多,如果有漏的,请评论指出); - 2.正则表达式匹配,只有两个:
~
(正则匹配返回true)、!~
(正则不匹配返回true)。
其实正则表达式的~
和!~
这两个符号并不是必须的,当不使用这两个符号时,表示对整行进行匹配,否则可以对指定列进行匹配,另外正则表达式必须写在两个斜杠里(/这是正则表达式/
),有正则表达式使用经验的童鞋应该对这个比较了解。
另外,正则表达式具体怎么写,不属于本文范围,因为正则还挺复杂,是一个专门的知识,如果你不会,要专门去学。
特别注意:当模式为非正则时(即没有双斜杠括起来时),它的值一定是布尔值,也就是只有“真”和“假”两种情况(比如你用什么大于号小于号,最终它运算出来的也是布尔值)。但是awk并没有true和false这两个布尔值,只有0和1(1以上的数字也属于真,包括小数也可以,我猜是会自动转换)。
test.txt
内容如下
张三 男 22 北京
李四 男 23
小芳 女 18
小红 女 19
小玉 女 20
小强 男 20
只打印偶数行(NR%2==0
就是模式,只有符合这个条件,后面的print才会打印)
awk 'NR%2==0 {print NR, $0}' test.txt
可以看到只输出了偶尔的行
上边的awk语句等效于有一个while循环不断的循环每一行,当if条件成立时,即执行if条件里面的动作,在这里是print,只不过,这个if条件还支持正则而已(注意以下代码并非可以直接运行的代码,只是帮助理解的等效代码)
while(!END){
if(NR%2==0){
print NR, $0;
}
}
打印第三列是2开头的行和第三列非2开头的行(正则的使用)
#打印第三列以2开头的行(“~”符号的使用)
awk '$3~/^2/{print $NR, $0}' test.txt
#打印第三列不以2开头的行(“!~”符号的使用)
awk '$3!~/^2/ {print NR, $0}' test.txt
如下图,第一次是未加条件,第二次加了正则条件$3~/^2/
($3
第三列,/^2/
正则表达式,表示匹配2开头的字符串,~
表示能匹配上就返回真,!~
表示不能匹配上则返回真,只有返回真,后面花括号里的语句都会执行)
不使用~
或!~
符号,直接对整行进行匹配,如果2前面加了^
就什么都不会输出,因为没有哪一行是以2开头的
两个特殊模式(BEGIN和END)
前面提到过,其实awk花括号里的内容,相当于有一个隐藏的循环,不断的一行一行的处理并输出。
但是,如果花括号用BEGIN
和END
这两种模式定义了,则不会循环,它有特殊含义。BEGIN
定义的模式,故名思义,只会在循环开始的时候执行一次,而END
自然就是循环结束后,再执行。
test.txt
内容如下
张三 男 22 北京
李四 男 23
小芳 女 18
小红 女 19
小玉 女 20
小强 男 20
执行以下awk语句
awk -v OFS='|' 'BEGIN{print "姓名","性别","年龄","地址","\n------------"} {print $1,$2,$3,$4} END{print "-----------"}' test.txt
打印出结果(可以看到,BEGIN相当于在循环前输出了表头,END输出了表尾,而中间则是循环输出)
以上的awk语句,相当于这样
#BEGIN
print "姓名","性别","年龄","地址","\n------------"
#Action
while(!END){
{print $1,$2,$3,$4};
}
#END
print "-----------"
BEGIN还可以用来定义变量,比如定义输入分隔符变量和输出分隔符变量,这样就不用在参数里定义了(事实上如果你用脚本的方式写,是必须用BEGIN模式来定义的,因为你没法传参数)
tail -n 5 /etc/passwd | awk 'BEGIN{FS=":"; OFS="-------"} {print NR,$1,$6,$7}'
正则行范围模式
其实awk的模式里,是可以同时写两个正则的,以逗号隔开。正则行范围模式,就是从第一个正则匹配的第一行开始(包含该行)到第二个正则匹配到的第一行结束(包含该行),之间的所有行都会输出。
test.txt
文件内容如下
张三 男 22 北京
李四 男 23
小芳 女 18
小红 女 19
小玉 女 20
小强 男 20
打印匹配/李四/
和/小玉/
这两行之间的所有行(包含这两行)
awk '/李四/,/小玉/ {print NR, $0}' test.txt
输出结果如下
指定正则库
awk默认使用的正则是扩展正则,如果要使用POSIX标准正则,需要使用--posix
来指定,扩展正则用--re-interval
指定(如果不指定默认就是它)。
有google.txt
文件,内容如下
google
gooogle
goooogle
gooooogle
goooooogle
据说用{2,4}
这种匹配次数的表达式时,需要指定要用POSIX标准的正则还是用扩展正则,否则不会出来结果,但实验显示,不指定也能出来,也许是版本的问题吧
awk --re-interval '/go{2,4}gle/{print NR, $0}' google.txt
awk --posix '/go{2,4}gle/{print NR, $0}' google.txt
实验结果如下
注:POSIX是Portable Operating System Interface of UNIX的缩写,POSIX标准定义了操作系统应该为应用程序提供的接口标准(个人认为可以理解成:POSIX标准的正则是以前最开始的时候开始的,比较老,而扩展的正则则是后来在原来的基础上增加或修改了功能,但大部分都是一样的)。
awk格式化输出命令printf
其实,printf应该几乎在所有语言里都有吧,shell脚本也有printf,只要在某一种语言里用过它的就不会陌生,它的主要原理,就是通过占位符的方式来格式化输出。
test.txt
文件内容如下
张一三 男 22
李四 男 23
小芳 女 18
小红 女 19
小玉 女 20
小强 男 20
sprintf函数中,%
开头的是点位符,后面那个字母表示点位符的类型,s就是string,说明这是一个字符串(其实类型后面再说),有几个点位符,后面就要有几个参数对应它
awk '{printf "%s %s %s\n",$1,$2,$3}' test.txt
#加个5表示输出宽度为5,不足空格补,超过了会直接显示所有,不会截取
awk '{printf "%5s %s %s\n",$1,$2,$3}' test.txt
#前面再加个-号,表示左对齐(注意-号表示左对齐,但+号并不代表右对齐,因为不用-号默认就是右对齐,+号用于输出正号,因为数字里不能写正号,否则会被认为是字符串)
awk '{printf "%-5s %s %s\n",$1,$2,$3}' test.txt
输出如下
关于+号在%d
前面和里面的区别
# +号在%d里面(之间),表示数字显示正号,换个其它符号是不可以的(除了-号表示左对齐外)
awk '{printf "%-5s %s %+d\n",$1,$2,$3}' test.txt
# +号在%d前面,表示一个字符串,它只是拼接了后面的数字,换个字符串(不是+号)也可以
awk '{printf "%-5s %s +%d\n",$1,$2,$3}' test.txt
可以看到输出结果是一样的,但其原理其实是不同的,前面的命令里已经写了解释
示例
# *号放在前面,相当于字符串拼接,所以无所谓它是什么符号
awk '{printf "%5s %s *%d\n",$1,$2,$3}' test.txt
# 会报错,因为%d之间是不可能用+、-号以外的其它符号的
awk '{printf "%5s %s %*d\n",$1,$2,$3}' test.txt
# -号表示左对齐,不表示负号
awk '{printf "%5s %s %-d\n",$1,$2,$3}' test.txt
# 表示负数要把负号放在真正的值里
awk '{printf "%5s %s %d\n",$1,$2,-$3}' test.txt
输出如下
来个实用的,对/etc/passwd
前5行进行格式化
tail -n 5 /etc/passwd | awk -F: 'BEGIN{printf "%-10s\t %s\n", "用户名", "用户id"} {printf "%-10s\t %s\n", $1,$3}'
输出如下图所示
%-10s
,%
开头表示占位符,s
表示以字符串形式输出,-
表示左对齐(不写默认右对齐),10表示显示宽度,不够补空格,超出会原样显示出来,不会截取。
awk的动作(Action)
组合动作(花括号)
在两个特殊模式(BEGIN和END)里,我们用过这个命令
awk -v OFS='|' 'BEGIN{print "姓名","性别","年龄","地址","\n------------"} {print $1,$2,$3,$4} END{print "-----------"}' test.txt
虽然它有点长,但我们还是可以看出来,单引号里,是可以有多个花括号的,并且不一定要BEGINT和END模式才能这么写。只不过BEGIN和END模式的动作比较特殊,一个是在所有动作前执行一次,一个是在所有动作后执行一次,而其它动作,还是像前面说的一样,是“自带隐藏循环”的。
test.txt
内容如下
张一三 男 22
李四 男 23
小芳 女 18
小红 女 19
小玉 女 20
小强 男 20
比如,连续三个花括号,分别打印第一列,第二列,第三列
awk '{print $1}{print $2}{print $3}' test.txt
花括号用于把多个动作括起来,表示一组组合动作。
比如,一个花括号里有两个语句,语句之间用;
分隔(这跟绝大多数编程语言相同),这样更能体现花括号是用于括住多个语句的,换句话说,我们写具体语句其实是在花括号里面写的
awk '{print $1; print $2}' test.txt
输出如下(可以看到,语句和语句之间的输出,是默认换行的)
而所谓的“动作”,其实就是编程语言的语句,前面我们一直都在用一个语句,那就是print
,后来还介绍了一个printf
。
其实,awk作为一门编程语言,我们很容易想到,它肯定有一个编程语言基本的一些控制语句,比如if, if…else…, while, do…while, for,三元运算符等等,而有循环就必须要有停止循环的语句,continue, break。事实上,它确实都有这些语句!
if/if…else…
前面我们输出过偶数行,是这么输出的,是在模式里添加条件的
awk 'NR%2==0 {print NR, $0}' test.txt
现在我们知道了还有if语句,所以还可以这么写
awk '{if(NR%2==0) print NR, $0}' test.txt
可以看到,效果跟用模式添加条件是一样的
根据我们写代码的经验,if语句里很多时候都不止有一条语句,当if里有多条语句时,像大多数编程语言一样,也是可以用花括号括起来的(其实只有一条语句也是可以括起来的,只不过也可以省略而已)
awk '{if(NR%2==0){print $1;print $2}}' test.txt
输出结果如下(注意,外部的花括号是用于括住里面多条语句,用于表示组合语句,而if的花括号,是用于括住if本身的语句块,不要跟外部的花括号混淆了)
if…else…的使用
tail -n 5 /etc/passwd | awk -F: '{if($3<500){print $1, "系统用户"}else{print $1, "普通用户"}}'
另外还有if…elseif….else,只不过多了个elseif,这跟大多数编程语言是一样的,就不再举例了!
for/while/do…while/continue/break
for循环(当然这里只是举例,不是说for循环一定得在BEGIN模式里使用)
awk 'BEGIN{for(i=1;i<=6;i++){print i}}'
输出如下
while循环
awk 'BEGIN{i=1;while(i<=5){print i;i++}}'
do…while循环
awk 'BEGIN{i=1;do{print "test";i++}while(i<1)}'
awk 'BEGIN{i=1;do{print "test";i++}while(i<5)}'
输出结果如下(注意第一条,虽然i=1,条件是i<1时输出,但由于do…while的特性是先执行一次do语句块,再判断条件,所以它还是会输出一个)
continue是跳过本次循环,继续下次循环,break是直接跳出整个循环(结束循环)
awk 'BEGIN{for(i=0;i<6;i++){print i}}'
awk 'BEGIN{for(i=0;i<6;i++){if(i==3)continue; print i}}'
awk 'BEGIN{for(i=0;i<6;i++){if(i==3)break; print i}}'
continue和break区别如下(这里只是用for来演示,与其它编程语言一样,只要是循环都能用ccontinue和break来进行跳过本次循环和跳出循环的操作,比如while, do…while)
next与exit
前面我说过很多次,除了BEGIN和END模式,其它的花括号语句块,其实都是有一个“隐藏的while循环”的,因为awk是逐行处理文本的。
而next和exit,相当于是这个“隐藏while循环”的continue和break,不知道你理解这是什么意思没?
在while循环里要跳过当前循环继续循环,我们用的是continue,而如果这个“隐藏while循环”要跳过当前行,继续循环呢?没错,就是用next。
使用next跳过一行继续循环
awk '{if(NR==2){next} print NR,$0}' test.txt
输出结果如下
前面说过,exit作用相当于“隐藏while循环”的break,而循环结束了,循环外的语句还是会执行,而END模式,就是在循环外的
awk 'BEGIN{print 1; print 2; print 3}'
awk 'BEGIN{print 1; exit; print 2; print 3}'
awk 'BEGIN{print 1; exit; print 2; print 3} END{print "This is end."}'
输出结果如下,可以看到,END模式里的语句,是会在循环结束后执行的
awk数组与for…in循环
awk数组
awk只有关联数组(associative array),关联数组的意思是,数组的键是字符串,即使你写的是数字,内部也把这个数字认为是字符串。
awk创建一个数组,需要逐个键赋值
awk 'BEGIN{websites["google"]="www.google.com";websites["baidu"]="www.baidu.com";websites["bing"]="www.bing.com"; print websites["google"]}'
打印一个不存在的数组元素,不会报错
awk 'BEGIN{print testArr[0]}'
其实上它会默认把不存在的这个元素初始化为空字符串
有创建就有删除,删除一个数组元素用delete
命令
awk 'BEGIN{websites["google"]="www.google.com";websites["baidu"]="www.baidu.com";websites["bing"]="www.bing.com"; print "before delete: "websites["google"];delete websites["google"];print "after delete: "websites["google"]}'
可以看到,元素删除后,就没有了
当然也可以删除整个数组,不写键就是了,比如上边的例子就是这么删除delete websites;
for…in循环
for…in的使用(因为数组的键不是数字,所以无法用for/while/do…while等数字增加的方式来遍历)
awk 'BEGIN{websites["google"]="www.google.com";websites["baidu"]="www.baidu.com";websites["bing"]="www.bing.com"; for(key in websites){print websites[key]}}'
输出结果如下
使用length()
函数统计数组元素字数(事实上我也是用count()
,len()
,length()
逐个测试试出来的)
awk 'BEGIN{websites[0]="www.google.com";websites[1]="www.baidu.com";websites[2]="www.bing.com"; total=length(websites);for(i=0;i<total;i++){print websites[i]}}'
#当键是数字字符串时,也可以用for循环遍历(虽然此时键本身上已经是字符串,但看上去它是有类型自动转换的,这也是弱类型语言的特点)
awk 'BEGIN{websites["0"]="www.google.com";websites["1"]="www.baidu.com";websites["2"]="www.bing.com"; total=length(websites);for(i=0;i<total;i++){print websites[i]}}'
自动类型转换在数组中的应用
#数字相加
awk 'BEGIN{a=1;a=a+1;print a}'
#字符串与数字相加
awk 'BEGIN{a="a string";a=a+1;print a}'
#自增也一样会自动转换,因为a++的本质是a=a+1的简写
awk 'BEGIN{a="a string";a++;print a}'
#把a变量换成数组的元素,其表现也是一样的,因为此时数组的元素也是一个变量
awk 'BEGIN{testArr["ele1"]="a string";testArr["ele1"]++;print testArr["ele1"]}'
可以看到,字符串与数字相加,字符串会被转成数字0,而自增因为其本质上是加1的一种简写,其表现自然也跟数字相加一样
ips.txt
文件内容如下
www.google.com 192.168.1.1
www.google.com 192.168.1.2
www.google.com 192.168.1.2
www.google.com 192.168.1.1
www.google.com 192.168.1.3
www.google.com 192.168.1.3
www.google.com 192.168.1.2
www.google.com 192.168.1.4
www.google.com 192.168.1.2
www.google.com 192.168.1.3
统计每个ip访问www.google.com的次数(把第2列,即ip列作为数组的键进行自增统计)
awk 'BEGIN{print" ip "," count"} {ipCount[$2]++} END{for(key in ipCount){print key, "\t"ipCount[key]}}' ips.txt
输出结果如下
获取请求次数排前3位的ip(sort的-k2
表示按第二个字段排序,-n
表示按数字排序-r
表示倒序排序,head -n 3
表示只取前三条数据)
awk '{ipCount[$2]++} END{for(key in ipCount){print key, "\t"ipCount[key]}}' ips.txt | sort -nr -k2 | head -n 3
输出如下
其实前面的统计,也可以用uniq
命令实现(uniq
是unique的意思,意思就是只取唯一的,其实就是去重,但是加了-c
,就表示在行开头显示它有几个重复项,先sort排序再uniq去重,是因为uniq命令只能处理相邻地,而sort能把相同的行排在一起,而后面的sort是根据统计数字排序)
awk '{print $2}' ips.txt | sort | uniq -c | sort -nr -k1 | head -n 3
结果如下,只不过统计数字在前面
有text.txt
文件,内容如下
awk sss awk
aaa awk bbb
AWK ccc AWK
统计“awk”出现的次数
awk '{for(i=1;i<NF;i++){count[$i]++}} END{for(j in count) print j, count[j]}' text.txt
# 用tolower()函数转小写,另有toupper转大写
awk '{for(i=1;i<NF;i++){$i=tolower($i);count[$i]++}} END{for(j in count) print j, count[j]}' text.txt
可以看到,它是区分大小写的,如果要把大小写都归为一类,则要转换大小写
真假值及其应用
在awk中,0为假,1及1以上的数(包括小数)都为真。另外,前面说过,字符串作为真假值时,会转换为0,所以字符串相当于假。
awk '0 {print $0}' text.txt
awk '1 {print $0}' text.txt
awk '2 {print $0}' text.txt
awk '2.1 {print $0}' text.txt
awk 'awk {print $0}' text.txt
awk '/awk/ {print $0}' text.txt
#当有模式时,动作(Action)可以忽略不写,不写默认为{print $0},即打印全部列
awk '1' text.txt
如下所示,0表示假,不打印,1表示真,打印,awk是字符串,自动转换成0,所以不打印,而/awk/
由于加了双斜线,这就表示查找了,所以只会打印出查找出来的行
打印奇数行和偶数行
#打印奇数行
awk 'i=!i {print NR,$0}' test.txt
#打印偶数行
awk '!(i=!i) {print NR,$0}' test.txt
i=!i
,前面说过,未定义的变量默认是空字符串,所以!i
就相当于对空字符串取非,而空字符串会默认转成0,再加个非,就是1,也就是说,第一个循环(前面多数说过隐藏循环),i=1,为真,所以会打印,但是第二个循环的时候,因为i之前是1,再加个非就变成0,所以第二个循环不会打印,以此类推,i不断的在真假之间切换,于是出现只打印奇数行的效果。
而能出现奇数行效果,如果要打印偶数行,只需要在外边再套一个非,即可让真假切换倒过来,即第一次为假,后面反复切换。
实战
同时指定行列分割符
我们先捕获一段用于处理的内容,比如我想抓包分析TCP协议,使用tcpdump
命令来捕获一个TCP连接的包,这里我们捕获一个本机的redis连接的包(redis客户端连接服务器,传输层也是用的TCP协议)。
先运行以下tcpdump命令,让它处理监听状态,捕获的包最后会输出到当前文件夹下的tcpdump.txt
文件中(所以你必须知道“当前文件夹”是在哪里pwd
命令可以看)
tcpdump -vvvne -i lo0 port 6378 -S > tcpdump.txt
然后终端另开一个Tab,运行以下命令连接本地redis服务器,执行后就会前面命令的“当前文件夹中”生成一个tcpdump.txt文件,然后需要在前面那个命令那儿按ctrl+C
终止监听
redis-cli -h 127.0.0.1 -p 6379
以下内容是为tcpdump监听redis连接时捕获的包内容(你可以把它保存到tcpdump.txt
文件中,以方便后续的测试)
12:53:29.622282 AF IPv4 (2), length 68: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [S], cksum 0xfe34 (incorrect -> 0x72ef), seq 1792162841, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 4108181674 ecr 0,sackOK,eol], length 0
12:53:29.622359 AF IPv4 (2), length 68: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [S.], cksum 0xfe34 (incorrect -> 0x578d), seq 1832868919, ack 1792162842, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 3256389569 ecr 4108181674,sackOK,eol], length 0
12:53:29.622369 AF IPv4 (2), length 56: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [.], cksum 0xfe28 (incorrect -> 0xb896), seq 1792162842, ack 1832868920, win 6379, options [nop,nop,TS val 4108181674 ecr 3256389569], length 0
12:53:29.622376 AF IPv4 (2), length 56: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [.], cksum 0xfe28 (incorrect -> 0xb896), seq 1832868920, ack 1792162842, win 6379, options [nop,nop,TS val 3256389569 ecr 4108181674], length 0
12:53:29.622612 AF IPv4 (2), length 73: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 69, bad cksum 0 (->3cb1)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [P.], cksum 0xfe39 (incorrect -> 0x3009), seq 1792162842:1792162859, ack 1832868920, win 6379, options [nop,nop,TS val 4108181674 ecr 3256389569], length 17
12:53:29.622641 AF IPv4 (2), length 56: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [.], cksum 0xfe28 (incorrect -> 0xb885), seq 1832868920, ack 1792162859, win 6379, options [nop,nop,TS val 3256389569 ecr 4108181674], length 0
12:53:29.662595 AF IPv4 (2), length 90: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 86, bad cksum 0 (->3ca0)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [P.], cksum 0xfe4a (incorrect -> 0xe855), seq 1832868920:1832868954, ack 1792162859, win 6379, options [nop,nop,TS val 3256389609 ecr 4108181674], length 34
12:53:29.662630 AF IPv4 (2), length 56: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [.], cksum 0xfe28 (incorrect -> 0xb813), seq 1792162859, ack 1832868954, win 6379, options [nop,nop,TS val 4108181714 ecr 3256389609], length 0
我们分析以上文件内容,发现它其实是每两行是一个数据包,只不过一行显示太长,它分两行显示,所以我们需要以每两行为1行,这样就不能以默认的\n
为换行符,观察发现每两行为一行,其实都是以12:53:29.622282
这种时间格式开头,所以我们可以用这个时间格式正则来作为“行分割符”(Row Seperator,变量为它的两个首字母,即RS)。
另外列分割符也不是空格,而是逗号(,
),所以我们要分别设置行分割符RS和列分割符FS。
假设需要第11列和12列内容,我们可以这样写
awk -v RS='[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}' -F, '{print $11,$12}' tcpdump.txt
注意:数字正则只能用[0-9]
表示,不能用\d
,否则无法匹配,只会解析第一行。另外英文句点(.
)不需要反斜杠转义,当然你写了转义也能用。
以上awk命令输出结果如下
seq 1792162841 win 65535
seq 1832868919 ack 1792162842
seq 1792162842 ack 1832868920
seq 1832868920 ack 1792162842
seq 1792162842:1792162859 ack 1832868920
seq 1832868920 ack 1792162859
seq 1832868920:1832868954 ack 1792162859
seq 1792162859 ack 1832868954
你会发现第一行是空行,那是因为我们设置了12:53:29.622282
这个时间格式为行分隔符,分割符都是指“到哪儿开始分割”,所以行分割符意思就是“遇到你指定的字符,它就会新起一行,包含这个指定的字符”,而12:53:29.622282
前面并没有内容,遇到它就新起一行,它前面那行自然就变成空行了。
要去掉空行,只需要不输出第一行,行号变量用NR(Number of Row)来表示,我们可以让NR!=1
或NR>1
,这样输出就没有第一行
awk -v RS='[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}' -F, 'NR!=1{print $11,$12}' tcpdump.txt
awk -v RS='[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}' -F, 'NR>1{print $11,$12}' tcpdump.txt
某列只取其中一部分
接着前面同时指定行列分割符的例子,如果我还要第9列的数据(注意,如果你点博客页面中的“copy”按钮复制的,你需要按一次退格删除一下自动回车,否则第一行还是会是空行,因为“copy”按钮会自动加一个回车)
awk -v RS='[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}' -F, 'NR>1{print $9,$11,$12}' tcpdump.txt
输出结果如下,你会发现它竟然输出两行,原因是我们设置了另外的RS(行分割符),所以\n
就变成普通字符了(虽然它在显示的时候是换行,但它对awk来说已经不是换行符了),所以就显示成这样
bad cksum 0 (->3cb6)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [S] seq 1792162841 win 65535
bad cksum 0 (->3cb6)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [S.] seq 1832868919 ack 1792162842
bad cksum 0 (->3cc2)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [.] seq 1792162842 ack 1832868920
bad cksum 0 (->3cc2)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [.] seq 1832868920 ack 1792162842
bad cksum 0 (->3cb1)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [P.] seq 1792162842:1792162859 ack 1832868920
bad cksum 0 (->3cc2)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [.] seq 1832868920 ack 1792162859
bad cksum 0 (->3ca0)!)
127.0.0.1.6378 > 127.0.0.1.63111: Flags [P.] seq 1832868920:1832868954 ack 1792162859
bad cksum 0 (->3cc2)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [.] seq 1792162859 ack 1832868954
我们把第9列的数据单独拿出一行来,如下所示,其实我只需要冒号后面的Flags [S]
,其它部分不要,这相当于我只需要一列中的一部分,而不是全部。
bad cksum 0 (->3cb6)!)
127.0.0.1.63111 > 127.0.0.1.6378: Flags [S]
我们可以用split()
函数来分割一列中的一部分(按冒号分隔),如下所示
awk -v RS='[0-9]{2}\:[0-9]{2}\:[0-9]{2}\.[0-9]{6}' -F, 'NR>1{split($9, subfield,": "); print subfield[2],$11,$12}' tcpdump.txt
解释:在print所在的花括号里(print的前面)写一个split函数,用分号与print分隔,表示这是两个不同的语句,然后split有三个参数,依次为:分割哪一列、分割后的存储变量、按什么符号分割,分割后,我们在print里打印subfield[2]
即是冒号后面的部分。
注意:如果你点博客页面中的“copy”按钮复制的,你需要按一次退格删除一下自动回车,否则第一行还是会是空行,因为“copy”按钮会自动加一个回车。
输出结果如下所示
Flags [S] seq 1792162841 win 65535
Flags [S.] seq 1832868919 ack 1792162842
Flags [.] seq 1792162842 ack 1832868920
Flags [.] seq 1832868920 ack 1792162842
Flags [P.] seq 1792162842:1792162859 ack 1832868920
Flags [.] seq 1832868920 ack 1792162859
Flags [P.] seq 1832868920:1832868954 ack 1792162859
Flags [.] seq 1792162859 ack 1832868954
如果我用\n
作为分割符,可以打印出第9列的客户端和服务器端ip和端口
awk -v RS='[0-9]{2}\:[0-9]{2}\:[0-9]{2}\.[0-9]{6}' -F, 'NR>1{split($9, subfield,"\n "); print NR-1,subfield[2],$11,$12}' tcpdump.txt
NR是Number of record,其实就是行号,减1是因为第一行是空行被我排除掉了,而我希望第2行从1开始显示,所以就减了1
输出结果如下
1 127.0.0.1.63111 > 127.0.0.1.6378: Flags [S] seq 1792162841 win 65535
2 127.0.0.1.6378 > 127.0.0.1.63111: Flags [S.] seq 1832868919 ack 1792162842
3 127.0.0.1.63111 > 127.0.0.1.6378: Flags [.] seq 1792162842 ack 1832868920
4 127.0.0.1.6378 > 127.0.0.1.63111: Flags [.] seq 1832868920 ack 1792162842
5 127.0.0.1.63111 > 127.0.0.1.6378: Flags [P.] seq 1792162842:1792162859 ack 1832868920
6 127.0.0.1.6378 > 127.0.0.1.63111: Flags [.] seq 1832868920 ack 1792162859
7 127.0.0.1.6378 > 127.0.0.1.63111: Flags [P.] seq 1832868920:1832868954 ack 1792162859
8 127.0.0.1.63111 > 127.0.0.1.6378: Flags [.] seq 1792162859 ack 1832868954
参考
awk 系列:如何使用 awk 语言编写脚本
AWK命令总结之从放弃到入门(通俗易懂,快进来看)
Linux awk+uniq+sort 统计文件中某字符串出现次数并排序
awk执行的三种方式,以及awk以shell脚本文件形式执行的注意事项