Linux三剑客之:awk使用教程

Linux三剑客之:awk使用教程

简介

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
-w443

如果你还是不明白什么是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

-w374

要输出它的第一列,可以这么写(命令行方式1)

awk '{print $1}' test.txt

-w400

当然也可以这样(命令行方式2)

cat test.txt | awk '{print $1}'

-w441

解释:
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打印第一列
-w527

不知道大家有没有发现,-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}'

-w555

如果我不想用空格分隔输出的列呢?比如我想用竖杠”|”来分隔输出的结果,可以这么写

tail -n 5 /etc/passwd | awk -F: '{print $1" | "$NF}'

-w577
注意:" | "就是分隔字符串,和传统的编程语言不同,awk的分隔字符串和变量之间,不需要用+号之类的连接符来连接,直接写就行。

如果print多个变量(即多列)时不用逗号也不用字符串会怎样?比如

tail -n 5 /etc/passwd | awk -F: '{print $1$NF}'
tail -n 5 /etc/passwd | awk -F: '{print $1 $NF}'

可以看到,输出的两列连在一起了,即使变量之间加了个空格,它也完全连在一起
-w556

分隔符

前面说过分隔符可以通过--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里用字符串拼接的方式一样的效果(点击图片可放大)
-w652

其它

打印倒数第二列,可以用总列数减1的方式

tail -n 5 /etc/passwd | awk -F: '{print $(NF-1)}'

-w558

还可以添加自己的字段(双引号引住的代表字符串)

tail -n 5 /etc/passwd | awk -F: '{print $1,"test"}'

-w575

换句话说,如果你把$1用双引号引住,那么它就是普通字符串了,比如这样,它会直接输出$1而不是第一个字段

tail -n 5 /etc/passwd | awk -F: '{print "$1"}'

-w537

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列
-w415

给每行开头加个行号再输出

awk '{print NR,$0}' test.txt

看,每行前面都多了一个行号
-w413


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

输出如下,相当于把两个文件合并了
-w494

但如果用FNR

awk '{print FNR, $0}' test.txt test2.txt

我们再来看看输出的行号,这次输出的行号,就是它们在各自文件里的行号了,这就是FNR与NR的区别
-w500


RS: Record Separator,输入记录(即行)分隔符(awk分割字符串时,是一行一行分割最后组合起来的),否则默认为\n

把行分隔符设置为空格,还是用前面的test.txt文件

awk -v RS=' ' '{print $0}' test.txt

设置空格为换行符后,可以看到第4行和第6行,本来有常规换行符的地方虽然显示上是换行了,但它还是和前面的归为一行(这里必须要打印行号才能看的出来,否则你可能以为它还是把\n当换行符了)
-w492


ORS:Output Record Separator,输出记录换行符。一般我们肯定会理所当然的认为,换行符肯定就是\n,但事实上,我非要用其它字符串(比如空格)当换行符也不是不可以,通过ORS指定即可。

指定空格为输出换行符

awk -v ORS=' ' '{print NR,$0}' test.txt

指定空格为换行符后,可以看到,它还是有三行(从行号1,2,3可以看出来),只不过行与行之间是用空格间隔的,而不是用传统的\n
-w494

至于最后的%,是因为我用的是zsh,zsh会用%指示最后无换行符\n,但实际还是会换行,这样比较美观,如果用bash,就不会输出%号,它会跟后面的当前用户连在一起,比较难看
-w522


FILENAME:当前文件名(如果同时处理两个文件,可以用文件名来区分数据属于哪个文件)。

有两个文件,一个是test.txt,内容如下

张三 男 22 北京
李四 男 23
小芳 女 18

test2.txt内容如下

张三|男|22|北京
李四|男|23
小芳|女|18

运行以下命令

awk '{print FILENAME,FNR,$0}' test.txt test2.txt

可以看到,每行都输出了它所属的文件名
-w558


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比较特殊吧
-w452

自定义变量

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

可以看到只输出了偶尔的行
-w478

上边的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开头的字符串,~表示能匹配上就返回真,!~表示不能匹配上则返回真,只有返回真,后面花括号里的语句都会执行)
-w536

不使用~!~符号,直接对整行进行匹配,如果2前面加了^就什么都不会输出,因为没有哪一行是以2开头的
-w449

两个特殊模式(BEGIN和END)

前面提到过,其实awk花括号里的内容,相当于有一个隐藏的循环,不断的一行一行的处理并输出。

但是,如果花括号用BEGINEND这两种模式定义了,则不会循环,它有特殊含义。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输出了表尾,而中间则是循环输出)
-w673

以上的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

输出结果如下
-w524

指定正则库

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

实验结果如下
-w647

注: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

输出如下
-w556

关于+号在%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

可以看到输出结果是一样的,但其原理其实是不同的,前面的命令里已经写了解释
-w571

示例

# *号放在前面,相当于字符串拼接,所以无所谓它是什么符号
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

输出如下
-w673

来个实用的,对/etc/passwd前5行进行格式化

tail -n 5 /etc/passwd | awk -F: 'BEGIN{printf "%-10s\t %s\n", "用户名", "用户id"} {printf "%-10s\t %s\n", $1,$3}'

输出如下图所示
-w674

%-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

-w551

花括号用于把多个动作括起来,表示一组组合动作

比如,一个花括号里有两个语句,语句之间用;分隔(这跟绝大多数编程语言相同),这样更能体现花括号是用于括住多个语句的,换句话说,我们写具体语句其实是在花括号里面写的

awk '{print $1; print $2}' test.txt

输出如下(可以看到,语句和语句之间的输出,是默认换行的)
-w493

而所谓的“动作”,其实就是编程语言的语句,前面我们一直都在用一个语句,那就是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

可以看到,效果跟用模式添加条件是一样的
-w506

根据我们写代码的经验,if语句里很多时候都不止有一条语句,当if里有多条语句时,像大多数编程语言一样,也是可以用花括号括起来的(其实只有一条语句也是可以括起来的,只不过也可以省略而已)

awk '{if(NR%2==0){print $1;print $2}}' test.txt

输出结果如下(注意,外部的花括号是用于括住里面多条语句,用于表示组合语句,而if的花括号,是用于括住if本身的语句块,不要跟外部的花括号混淆了)
-w554

if…else…的使用

tail -n 5 /etc/passwd | awk -F: '{if($3<500){print $1, "系统用户"}else{print $1, "普通用户"}}'

-w674

另外还有if…elseif….else,只不过多了个elseif,这跟大多数编程语言是一样的,就不再举例了!

for/while/do…while/continue/break

for循环(当然这里只是举例,不是说for循环一定得在BEGIN模式里使用)

awk 'BEGIN{for(i=1;i<=6;i++){print i}}'

输出如下
-w500


while循环

awk 'BEGIN{i=1;while(i<=5){print i;i++}}'

-w508


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语句块,再判断条件,所以它还是会输出一个)
-w552


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)
-w615

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

输出结果如下
-w528


前面说过,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模式里的语句,是会在循环结束后执行的
-w709

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]}'

其实上它会默认把不存在的这个元素初始化为空字符串
-w436


有创建就有删除,删除一个数组元素用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"]}'

可以看到,元素删除后,就没有了
-w689

当然也可以删除整个数组,不写键就是了,比如上边的例子就是这么删除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]}}'

输出结果如下
-w724


使用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]}}'

-w722

自动类型转换在数组中的应用

#数字相加
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的一种简写,其表现自然也跟数字相加一样
-w775


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

输出结果如下
-w687

获取请求次数排前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

输出如下
-w687


其实前面的统计,也可以用uniq命令实现(uniq是unique的意思,意思就是只取唯一的,其实就是去重,但是加了-c,就表示在行开头显示它有几个重复项,先sort排序再uniq去重,是因为uniq命令只能处理相邻地,而sort能把相同的行排在一起,而后面的sort是根据统计数字排序)

awk '{print $2}' ips.txt | sort | uniq -c | sort -nr -k1 | head -n 3

结果如下,只不过统计数字在前面
-w702


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

可以看到,它是区分大小写的,如果要把大小写都归为一类,则要转换大小写
-w715

真假值及其应用

在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/由于加了双斜线,这就表示查找了,所以只会打印出查找出来的行
-w452


打印奇数行和偶数行

#打印奇数行
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不断的在真假之间切换,于是出现只打印奇数行的效果。
而能出现奇数行效果,如果要打印偶数行,只需要在外边再套一个非,即可让真假切换倒过来,即第一次为假,后面反复切换。
-w482

实战

同时指定行列分割符

我们先捕获一段用于处理的内容,比如我想抓包分析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!=1NR>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脚本文件形式执行的注意事项

打赏

订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x

扫码在手机查看
iPhone请用自带相机扫
安卓用UC/QQ浏览器扫

Linux三剑客之:awk使用教程