Linux三剑客之awk命令

一、awk简介

awk其名称得自于它的创始人 Alfred Aho 、Peter Weinberger 和 Brian Kernighan 姓氏的首个字母。实际上 AWK 的确拥有自己的语言: AWK 程序设计语言 , 三位创建者已将它正式定义为“样式扫描和处理语言”。它允许您创建简短的程序,这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表,还有无数其他的功能。

awk是行处理器,相比较屏幕处理的优点,在处理庞大文件时不会出现内存溢出或是处理缓慢的问题,通常用来格式化文本信息。awk处理过程是依次对每一行进行处理,然后输出。

awk逐行处理文本,按照指定的分隔符,将行分割为多个字段,如果没有指定分隔符,默认以空格为分隔符,每个字段按照顺序,发呢别对应到awk的内置变量中,比如,分割完后的第一个字段为 $1 ,第二个字段为 $2 ,依此内推,用 $0 表示当前处理的整行。
$0 表示显示整行 , $NF 表示当前行分割后的最后一列( $0 $NF 均为内置变量)。
注意, $NF NF 要表达的意思是不一样的,对于awk来说, $NF 表示最后一个字段, NF 表示当前行被分隔符切开以后,一共有几个字段。

也就是说,假如一行文本被空格分成了7段,那么 NF 的值就是7, $NF 的值就是 $7 , 而 $7 表示当前行的第7个字段,也就是最后一列,那么每行的倒数第二列可以写为 $(NF-1)

二、awk基本语法

awk [options] 'Pattern{Action}' file
options:选项参数
Pattern:模式参数
Action:行为参数
file:文件参数

先不适用选项、模式,用一个最简单的action来认识awk。

大家都知道df命令可以用来显示文件系统的磁盘使用情况统计。
由上面简介可知, $5 为分割后的第5个字段,即磁盘使用率。
使用|管道符将df的标准输出作为awk的标准输入,于是这条命令将磁盘的使用率给列出来了。

我们也可以一次输出多列,使用逗号隔开要输出的多个列,如下,一次性输出第一列和第二列

上述写法表示,在开始处理test文件中的文本之前,先执行打印动作,输出的内容为"aaa","bbb".

也就是说,上述示例中,虽然指定了test文件作为输入源,但是在开始处理test文本之前,需要先执行BEGIN模式指定的"打印"操作

既然还没有开始逐行处理test文件中的文本,那么是不是根本就不需要指定test文件呢,我们来试试。

经过实验发现,还真是,我们并没有给定任何输入来源,awk就直接输出信息了,因为,BEGIN模式表示,在处理指定的文本之前,需要先执行BEGIN模式中指定的动作,而上述示例没有给定任何输入源,但是awk还是会先执行BEGIN模式指定的"打印"动作,打印完成后,发现并没有文本可以处理,于是就只完成了"打印 aaa bbb"的操作。

这个时候,如果我们想要awk先执行BEGIN模式指定的动作,再根据执我们自定义的动作去操作文本,该怎么办呢?示例如下

上图中,蓝色标注的部分表示BEGIN模式指定的动作,这部分动作需要在处理指定的文本之前执行,所以,上图中先打印出了"aaa bbb",当BEGIN模式对应的动作完成后,在使用后面的动作处理对应的文本,即打印test文件中的第一列与第二列,这样解释应该比较清楚了吧。

看完上述示例,似乎更加容易理解BEGIN模式是什么意思了,BEGIN模式的作用就是,在开始逐行处理文本之前,先执行BEGIN模式所指定的动作。以此类推,END模式的作用就一目了然了,举例如下。

输入分隔符,英文原文为field separator,此处简称为FS。
输入分割符,默认是空白字符(即空格),awk默认以空白字符为分隔符对每一行进行分割。

输出分割符,英文原文为output field separator,此处简称为OFS。
awk将每行分割后,输出在屏幕上的时候,以什么字符作为分隔符,awk默认的输出分割符也是空格。

4.1输入分隔符

输入分隔符比较容易理解,当awk逐行处理文本的时候,以输入分隔符为准,将文本切成多个片段,默认使用空格,但是,如果一段文字中没有空格,我们可以指定以特定的文字或符号作为输入分割符,比如下图中的例子,我们指定使用"#"作为输入分隔符。

其实不管是通过-F选项,还是通过FS这个内置变量,目的都是设置指定的输入分隔符,达到的效果是相同的,下面会单独对awk的变量进行总结,如果你不理解这些变量,没有关系,后面自然会明白。

而此处,我们使用了awk中的一个选项,就是-F,还记得我们之前总结的awk的使用语法吗。

我们说过,awk的语法如下

awk [options] 'Pattern{Action}' file

而-F,就是options的一种,用于指定输入分隔符。

-v也是options的一种,用于设置变量的值。

再结合之前的文章,我们已经将options 、pattern 、action都简单的应用了一遍,好了,我们已经"会用"awk了。

4.2输出分隔符

那么什么是输出分隔符呢?聪明的你应该已经发现了,当awk为我们输出每一列的时候,会使用空格隔开每一列,其实,这个空格,就是awk的默认的输出分隔符,下图中红线标注的空格部分,就是awk的默认的输出分隔符。

细心如你一定发现了,上图中的示例在语法上的区别就是,一个有"逗号",一个没有"逗号"。

awk '{print $1 $2}' 表示每行分割后,将第一列(第一个字段)和第二列(第二个字段)连接在一起输出。

awk '{print $1,$2}' 表示每行分割后,将第一列(第一个字段)和第二列(第二个字段)以输出分隔符隔开后显示。

五、awk变量

在使用到"输入分隔符"和"输出分隔符"的时候,我们都提到了一个名词:"变量"。
对于awk来说"变量"又分为"内置变量" 和 "自定义变量" , "输入分隔符FS"和"输出分隔符OFS"都属于内置变量。
内置变量就是awk预定义好的、内置在awk内部的变量,而自定义变量就是用户定义的变量。

5.1awk常用的内置变量以及其作用

FS:输入字段分隔符, 默认为空白字符
OFS:输出字段分隔符, 默认为空白字符
RS:输入记录分隔符(输入换行符), 指定输入时的换行符
ORS:输出记录分隔符(输出换行符),输出时用指定符号代替换行符
NF:number of Field,当前行的字段的个数(即当前行被分割成了几列),字段数量
NR:行号,当前处理的文本行的行号。
FNR:各文件分别计数的行号
FILENAME:当前文件名
ARGC:命令行参数的个数
ARGV:数组,保存的是命令行所给定的各参数

上面描述到的"输入字段分隔符FS和输出字段分隔符OFS在之前的文章中已经解释过了,字段数量NF也大致说了。

RS、ORS、NR、FNR、FILENAME、ARGC、ARGV这些术语对于我们来说是新接触的,但是触类旁通,RS其实与FS类似,ORS与OFS类似,FS是字段输入分隔符,RS是行输入分隔符,OFS是字段输出分隔符,ORS是行输出分隔符,它们的原理都很相似。

5.2内置变量NR

如下图所示,test1文件中一共有两行文本,使用空格隔开,第1行有4列,第2行有5列

好了,现在每一行的开头都有行号了,简单吧。

细心如你一定注意到了一个细节,就是在打印 $0 , $1 , $2 这些内置变量的时候,都有使用到" $ "符号,但是在调用 NR , NF 这些内置变量的时候,就没有使用" $ ",如果你有点不习惯,那么可能是因为你已经习惯了使用bash的语法去使用变量,在bash中,我们在引用变量时,都会使用 $ 符进行引用,但是在awk中,只有在引用 $0 $1 等内置变量的值的时候才会用到" $ ",引用其他变量时,不管是内置变量,还是自定义变量,都不使用" $ ",而是直接使用变量名。

5.3内置变量FNR

当我们使用awk同时处理多个文件,并且使用NR显示行号的时候,效果如下图。

如上图所示,我们先使用了默认的"回车换行"作为"行分隔符"输出了test1文本,这时显示文本一共有2行。

而后来,我们又指定了使用"空格"作为"行分隔符"输出test1文本,这时显示文本一共有8行。

看到了吗?当我们指定使用空格作为"行分隔符"时,在awk解析文本时,每当遇到空格,awk就认为遇到的空格是换行符,于是awk就将文本换行了,而此时人类理解的"回车换行",对于awk来说并不是所谓的换行符,所以才会出现上图中第4行的现象,即使从人类的角度去看是两行文本,但是在awk的世界观里,它就是一行。

如果你还是没有理解,那么我们换个方式描述,再来啰嗦一遍。

默认情况下,awk使用"回车换行"作为"行分隔符(换行符)",此时,人类的世界观与awk的世界观是一致的,因为我们和awk都认为,遇到回车换行,就表示当前行结束,开始新的一行。

而当我们指定了特定的"行分隔符"时,比如空格,那么当awk遇到空格时,就认为当前行结束了,新的一行开始了,此时,awk的世界观与人类的世界观已经不同,人类仍然认为"回车换行"才是新的一行,awk却认为"回车换行"并不是新的一行的开始,所以,从上图中返回的信息中,我们可以看到,人类所以为的"两行",共用了一个行号,awk认为它们就是第4行。

这就是输入行分隔符的使用方法。同理,我们来看看"输出行分隔符",理解输出行分隔符之前,请做好心理准备,最好不要以正常的思维去理解换行,才能比较容易的学明白输出行分隔符。

5.5内置变量ORS

在理解"输出行分隔符"ORS之前,请先理解刚才描述的"输入行分隔符"RS,否则理解起来可能比较困难。

默认情况下,awk将人类眼中的"回车换行",当做"输出行分隔符",此时,awk的"世界观"与人类的"世界观"是相同的。

现在,我们改变一下awk的想法,我们让awk认为,"+++"才是真正的输出行分隔符,示例如下图

看懂了吗,我们再啰嗦的解释一遍,在没有指定输出行分隔符之前,awk跟人类的逻辑思维是一样一样的,当人类想要换行的时候,就会"另起一行"(回车换行),awk也是一样的,当它在输出文字的时候,如果想要换行,就会"另起一行"(回车换行), 可是,如果我们指定了"输出行分隔符"为"+++",那么,当awk在输出文字的时候,如果想要换行,就会"另起一行"(+++),所以,对于awk来说,它完成了"另起一行"的动作,只不过,它所认为的"另起一行"的动作就是输出"+++",而不再是原来的输出" 回车换行",所以,从人类看到的"表象上",awk并没有换行,那是因为我们还是以"回车换行"作为换行的标准,而awk已经变了,它认为,"+++"就是换行的标准。

这次明白了吧,真的明白了吗?

我们把刚才学到的"输入换行符"和"输出换行符"同时使用,看看是什么效果,示例如下。

如果你能明白awk为什么会将test1的文本输出成上图中的模样,那么你已经彻底理解了RS与ORS两个内置变量。

如果你又懵逼了,那么,从RS内置变量开始,再看一遍吧。

5.6内置变量FILENAME

FILENAME这个内置变量,从字面上,就能看出是什么意思,没错,就是显示文件名,演示效果如下。

上图中,我们先使用BEGIN模式,输出一个字符串"aaa",然后,传入两个文件的文件名作为参数,我们发现,BEGIN模式正常执行了打印操作,输出了"aaa"字符串 ,我们使用同样的命令,同样使用BEGIN模式,只不过,这次不只打印"aaa",还打印ARGV这个数组中的第二个元素的值。

我说已经说过,ARGV内置变量表示的是一个数组,既然是数组,就需要用上图中的下标的方式,引用对应元素的值,因为数组的索引都是从0开始的,所以,ARGV[1]表示引用ARGV数组中的第二个元素的值,从返回结果可以看出,ARGV[1]对应的值为test1,同理,我们又使用第三条命令,多打印了一个ARGV[2]的值,发现ARGV[2]对应的值为test2,这个时候,你明白ARGV内置变量的含义了吗,说白了,ARGV内置变量表示的是:所有参数组成的数组。那么细心的你一定会问了,ARGV[0]对应的是哪个参数呢,我们来打印一下。

我擦,第一个参数竟然是awk这个命令本身??太神奇了,有没有很出乎意料···

好吧,awk就是这么规定的,'pattern{ action }'并不被看做是参数,awk被看做为参数。

好了,说明了ARGV变量以后,再说ARGC变量的作用,就容易多了。

在刚才的例子中,应该有三个参数,awk、test1、test2,这三个参数作为数组的元素存放于ARGV中,现在,而ARGC则表示参数的数量,也可以理解为ARGV数组的长度。示例如下

5.8自定义变量

好了,内置变量解释完了,现在我们来看看自定义变量,自定义变量,顾名思义,就是用户定义的变量,有两种方法可以自定义变量。

方法一:-v varname=value 变量名区分字符大小写。

方法二:在program中直接定义。

我们来看一些小例子,即可明白上述两种方法。

通过方法一自定义变量。

没错,printf动作与printf命令一样,都不会输出换行符,默认会将文本输出在一行里面。

聪明如你一定想到了,既然printf动作的用法与printf命令一样,那么,printf动作有没有printf命令中所谓的"格式替换符"呢?

必须有啊,"格式替换符"是什么我们就不再赘述了,因为在 printf命令详解 中已经详细的解释过它,那么我们来使用"格式替换符"来指定一下$1的格式,示例如下。

好了,这就是awk中printf动作在使用时的一些注意点。

我们来总结一下,在awk中使用printf动作时,需要注意以下3点。

1)使用printf动作输出的文本不会换行,如果需要换行,可以在对应的"格式替换符"后加入"\n"进行转义。

2)使用printf动作时,"指定的格式" 与 "被格式化的文本" 之间,需要用"逗号"隔开。

3)使用printf动作时,"格式"中的"格式替换符"必须与 "被格式化的文本" 一一对应。

好了,我们来看一些小示例,练练手。

我们可以利用格式替换符对文本中的每一列进行格式化,示例如下。

七、awk模式

"模式"这个词听上去文绉绉的,不是特别容易理解,那么我们换一种说法,我们把"模式"换成"条件",可能更容易理解,那么"条件"是什么意思呢?我们知道,awk是逐行处理文本的,也就是说,awk会先处理完当前行,再处理下一行,如果我们不指定任何"条件",awk会一行一行的处理文本中的每一行,如果我们指定了"条件",只有满足"条件"的行才会被处理,不满足"条件"的行就不会被处理。这样说是不是比刚才好理解一点了呢?这其实就是awk中的"模式"。

再啰嗦一遍,当awk进行逐行处理的时候,会把pattern(模式)作为条件,判断将要被处理的行是否满足条件,是否能跟"模式"进行匹配,如果匹配,则处理,如果不匹配,则不进行处理。

看个小例子,就能秒懂,前提是建立在之前知识的基础之上。

如下图所示,test2文件中有3行文本,第一行有4列,第二行有5列,第三行只有2列。而下图的awk命令中,就使用到了一个简单的模式。

上图中,我们使用了一个简单的"模式",换句话说,我们使用了一个简单的"条件",这个条件就是,如果被处理的行正好有5列字段,那么被处理的行则满足"条件",满足条件的行会执行相应的动作,而动作就是{print $0},即打印当前行,换句话说,就是只打印满足条件的行,条件就是这一行文本有5列(NF是内置变量,表示当前行的字段数量,如果你忘了,那么请你重新看一遍之前的文章),而上例中,只有第二行有5列,所以,只有第二行能与我们指定的"模式"相匹配,最终也就只输出了第二行。

这就是所谓的"模式",其实很简单,对吧。聪明如你,应该已经能够举一反三了,举例如下。

没错,"模式"怎样写,取决于我们想要给出什么样的限制条件。

细心如你一定发现了,上图中使用的"模式"都有一个共同点,就是上述"模式"中,都使用到了关系表达式(关系操作符),比如 ==,比如<=,比如>,当经过关系运算得出的结果为"真"时,则满足条件(表示与指定的模式匹配),满足条件,就会执行相应的动作,而上例中使用到的运算符都是常见的关系运算符,我们就不解释了,那么awk都支持哪些关系运算符呢?我们来总结一下。

7.1关系运算模式

上图中的命令1指定了"模式",而且这种"模式"是"关系表达式模式",如果当前行的字段数量等于5,模式被匹配,对应的行被打印。

上图中的命令2貌似没有使用任何"模式",所以,每一行都执行了指定的动作,即每一行都被输出了,其实,这种没有被指定任何"模式"的情况,也是一种"模式",我们称这种情况为"空模式","空模式"会匹配文本中的每一行,所以,每一行都满足"条件",所以,每一行都会执行相应的动作。

现在,我们不仅懂得了什么是awk的"模式(Pattern)",而且还掌握了两种"模式",空模式和关系运算模式。

不对,我们似乎遗忘了什么 ,我们还用过BEGIN模式和END模式,我们来回顾一下吧。

BEGIN模式,表示在开始处理文本之前,需要执行的操作。

END模式,表示将所有行都处理完毕后,需要执行的操作。

还记得我们在第一篇awk博文中使用到的例子吗,温故知新,回过头看,会有新发现

上图中的示例用到了BEGIN模式,空模式,END模式。

7.2正则模式

我们知道,在Linux中,/etc/passwd文件中存放了用户信息,那么假设 ,我们想要从/etc/passwd文件中找出"用户名以zsy开头"的用户,我们该怎么办呢?

没错,我们可以使用grep命令,配合正则表达式,找出对应的信息,示例如下。

注:如果你还不了解grep命令和正则表达式,请参考博客中的文章,此处不再赘述。

聪明如你一定看出来了,不管是使用grep命令,还是使用awk命令,都使用了相同的正则表达式"^zsy"

唯一的区别就是,在grep命令中,直接使用了正则表达式,而在awk命令中,正则表达式被放入了两个斜线中。

这样说可能不容易理解,看图说话似乎更加容易理解。

猛然一看,上例似乎非常复杂,但是如果你已经掌握了前文中的知识,那么你一定能够看明白,上例中蓝线标注的部分使用了BEGIN模式,并且格式化输出了一行文本作为"表头",上例中红线标注的部分使用了正则模式,并且格式化输出了/etc/passwd文件中的第一列与第三列(用户名字段与用户ID字段),上例中,只使用了awk一条命令就完成了如下多项工作。

1、从/etc/passwd文件中找出符合条件的行(用户名以zsy开头的用户)。
2、找出符合条件的文本行以后,以":"作为分隔符,将文本行分段。
3、取出我们需要的字段,格式化输出。
4、结合BEGIN模式,输出一个格式化以后的文本,提高可读性。

因为我们在处理文本时,往往需要用到正则表达式,所以,awk的正则模式应该会经常用到。

但是需要注意,在使用正则模式时,如果正则中包含"/",则需要进行转义,这样说可能不容易理解,我们来看个例子。

仍然使用/etc/passwd进行测试,我们知道,/etc/passwd中保存了用户信息,其中每行的最后一个字段为用户使用的登录shell,假设,我们想要从passwd文件中找出使用/bin/bash作为登录shell的用户,我们该怎么办呢?

没错,我们可以使用grep命令,配合正则表达式完成我们的需求,示例如下。

如上图所示,经过转义后,awk命令即可正常的匹配到符合正则条件的行,并执行了相应的动作。

除此之外,还要注意以下两点

1、当在awk命令中使用正则模式时,使用到的正则用法属于"扩展正则表达式"(如果不理解,请参考博客中的"正则表达式"系列文章)。

2、当使用 {x,y} 这种次数匹配的正则表达式时,需要配合--posix选项或者--re-interval选项。

上例中,正则模式中的正则表达式为"he{2,3}y",此表达式表示"hey"中的字母e最少需要连续出现2次,最多只能连续出现3次,才能被正则表达式匹配到,但是正如上图所示,没有使用--posix选项或者--re-interval选项时,awk无法根据正则表达式对文本进行处理,因为上例的正则中包含类似"{x,y}"这样的次数匹配字符,所以,在使用正则模式时,如果对应的正则表达式中包含类似"{x,y}"这样的次数匹配字符,则需要使用--posix选项或者--re-interval选项。

好了,正则模式我们已经说明白了,赶快动手试试吧。

7.3行范围模式

现在聊聊行范围模式。

其实,只要理解了正则模式,再理解行范围模式,就容易多了。

在介绍行范围模式之前,先来思考一个小问题,有一个文本文件,文件内容如下。

如上图所示,Lee这个名字出现了两次,第一次出现是在第2行,Kevin这个名字也出现了两次,第一次出现是在第5行。

假设我想从上述文本中找出,从Lee第一次出现的行,到Kevin第一次出现的行之间的所有行,我该怎么办呢?

使用awk的行范围模式,即可完成上述要求,示例如下。

上图中第一种语法是正则模式的语法,表示被正则表达式匹配到的行,将会执行对应的动作。

上图中第二种语法是行范围模式的语法,它表示,从被正则1匹配到的行开始,到被正则2匹配到的行结束,之间的所有行都会执行对应的动作,所以,这种模式被称为行范围模式,因为它对应的是一个范围以内的所有行,但是需要注意的是,在行范围模式中,不管是正则1,还是正则2,都以第一次匹配到的行为准,就像上述示例中,即使Lee在第2行与第3行中都出现了,但是由于正则1先匹配到第2行中的lee,所以,最终打印出的内容从第2行开始,即使Kevin在第5行与第7行中都出现了,但是由于Kevin第一次出现在第5行,所以最终打印出的内容到第5行结束,也就是说,最终打印出了第2行到第5行以内的所有行。

但是,你可能会有这样的需求,你不想依靠正则表达式去匹配行的特征,你只是想单纯的打印出从X行到Y行之间的所有行。

比如,我们有一个文本文件,这个文件中一共有7行文本,你想要打印出从第3行到第6行之间的所有行,该怎么做呢?

其实,使用之前学习到的"关系运算符模式",即可满足我们的需求,示例如下。

上图中,NR为awk的内置变量,表示行号,"NR>=3 && NR<=6"表示行号大于等于3,并且行号小于等于6时,执行对应的动作,而对应的动作就是打印整行,所以,上述命令表示打印出文本中从第3行到第6行之间的所有行。

比如,我想要从如下文本中找出,网卡1的IP地址在192.168.0.0/16网段内的主机,该怎么办呢?

上述示例中, $2 为awk的内置变量,表示文本中的第2列," $2 ~/正则/"表示文本中的第2列如果与正则匹配,则执行对应的动作,对应的动作为" {print $1,$2} ",表示打印文本中的第1列与第2列,上例中的正则表达式我就不再赘述了,如果你还不太了解正则表达式,可以参考博客中的正则系列文章。

是不是很简单?我想你应该明白了。

到目前为止,我们已经认识了awk的模式,模式可以总结为如下5种。
1、空模式
2、关系运算模式
3、正则模式
4、行范围模式
5、BEGIN/END模式

八、awk动作

上面介绍过了awk的选项、模式以及动作,这里详细总结一下动作action。

8.1 if else

红线标注为第一部分:最外侧的括号,即"{ }"。

蓝线标注为第二部分:"print $0"

在之前的示例中,我们一直把上图中的两个部分当做一个整理去理解,但是现在,我们要把它们分开去理解。

其实,这两个部分都可以被称之为"动作",只不过它们是不同"类型"的动作而已。

"print"属于"输出语句"类型的动作,顾名思义,"输出语句"类型的动作的作用就是输出、打印信息,没错,"print"与"printf"都属于"输出语句"类型的动作。

"{ }"其实也可以被称之为"动作",只不过,"{ }"属于"组合语句"类型的动作,顾名思义,"组合语句"类型的动作的作用就是将多个代码组合成代码块。

这样说可能不容易理解,我们来看个小示例,就容易理解了,示例如下。

也就是说,我们可以这样理解,上图中一共有4个"动作",两对大括号,两个print,但是上图中,每个大括号中只有一个动作,而我们说过,"组合语句"的作用是将多个代码或多个动作组合成代码块,组合后的代码块被当做一个整体,那么,我们能不能把上图中的两个print动作组合成一个整体呢?

必须能啊,示例如下。

好了,我想你应该明白了,除了print这种"输出语句"能够被称之为动作以外,像"{ }"这种"组合语句"也能被称之为动作,只不过它们的类型不同,功能也不同。

那么,除了"输出语句"与"组合语句"以外,还有其他种类的动作吗?

必须的,我们现在就来认识另一种动作,它就是"控制语句"。

不过,"控制语句"又有很多种,不过不用怕,我们慢慢来,一个一个聊,先来认识一种简单的"控制语句",它就是"条件判断"。

如果你有过任何一种编程语言的开发经验,你都会非常容易理解"条件判断",条件判断无非就是条件成立,则执行对应的代码,条件不成立,则不执行对应的命令,没错,在编程语言中,通常使用如下语法结构进行条件判断,也就是编程语法中的 if 条件判断语句。

在awk中,我们同样可以使用if这种语法进行条件判断,只不过,上例中的语法结构是由"多行"组成,而在命令行中使用awk时,我们可以将上例中的"多行"语句写在"一行"中,示例如下。

上图中红线标注的部分即为"条件判断"类型的语法,我们把红线标注的部分单独取出来,来描述一下。

"if(NR == 1)"中的NR为awk的内置变量,NR为行号之意,所以,"if(NR == 1)"表示行号为1时,条件成立。

"if(NR == 1){ print $0 }"表示行号为1是满足条件,条件满足时,打印整行,换句话说就是只打印第一行。

你可能会纠结,为什么最外侧还需要有一层大括号呢?如下图所示。

上例表示,如果行号为1,则满足条件,就会执行if对应的大括号中的所有代码,而大括号中,有两个print动作,当条件成立时,这两个print动作都会被执行,当条件不成立时,这两个动作都不会执行。

上例中,"if"对应的大括号中有多条语句,所以"if"语法中的大括号不能省略,但是,如果"if"对应的大括号中只有一条命令,那么"if"对应的大括号则可以省略,示例如下。

没错,上图中的用法为awk的"模式"的用法,而我们今天所介绍的用法为awk的"动作"的用法,虽然两者在语法上有所区别,但是达到的目的相同的。

编程语言中,除了"if"之外,还有"if...else..."或者"if...else if...else"这样的语法,awk中也有这样的用法。

我们知道,/etc/passwd文件中的第3列存放了用户的ID,在centos6中,用户ID小于500的用户都属于系统用户,用户ID大于500的用户都属于普通用户。

所以,我们可以以500为分界线,根据用户ID判断用户是属于系统用户还是普通用户,centos7中以1000为分界线,此处用于示例的系统为centos6,所以以500作为分界线。

我们可以通过一条awk命令,判断出/etc/passwd文件中的哪些用户属于系统用户,哪些用户属于普通用户,示例如下。

上图中,就用到了"if...else..."语法,如上图所示,$3对应了passwd文件中的第三列,即用户ID,如果用户ID小于500,则输出$1,即passwd文件中的第一列,也就是用户名,并且输出"系统用户"字样,否则,则执行else中的命令,即打印用户名并输出"普通用户"字样,但是上例中,为了方便演示,我们并没有对输出的文本进行格式化,你也可以结合之前的知识,进行格式化。

好了,再来看一个"if...else if...else"这样的例子,其他它们都差不多,示例如下:

上例中,我们使用了"关系表达式"模式,同时,在动作中,使用了"if...else if...else"这样的"控制语句",只要前文中的知识都掌握了,那么看懂上述示例,应该是没有任何问题的。

8.2 循环控制语句

因为我们还没有介绍过数组,所以此处只演示上述语法中的格式1的用法。

上例中,我们使用了BEGIN模式,BEGIN模式对应的动作中,包含了for循环语句,看到这里,是不是感觉与其他语言中的for循环完全没有区别嘛?只不过,上例中的for循环语句都写在了一行中而已。

再来看看while循环的具体使用,为了方便演示,仍然使用BEGIN模式,示例如下。

正如上图所示,无论是否满足while中的条件,都会先执行一遍do对应的代码。

那么,说到循环,就不能不说说与循环有关的跳出语句。

没错,与其他编程语言中一样,在awk中,同样可以使用break与continue跳出循环。

continue的作用:跳出"当前"循环

break的作用:跳出"整个"循环

示例如下,先看看continue的示例

如上图所示,break结束的更加彻底,当使用break时,整个循环都将被结束,循环中的动作将不会再被执行。

continue与break同样可以用于while循环与do...while循环,此处就不再赘述了。

当然,如果你经常编写过shell脚本,你可能会问,awk中有类似exit的语句吗?必须有啊,在shell中,exit命令表示退出当前脚本,在awk中,它的含义也是类似的,它表示不再执行awk命令,相当于退出了当前的awk命令,示例如下。

如上图所示,上图中第一条命令中,执行了多个动作(多条语句),上图中的第二条命令中,也执行了多个动作,但是当在awk中执行了exit语句以后,之后的所有动作都不会再被执行,相当于退出了整个awk命令。

其实,这样描述exit的作用并不准确,因为,当在awk中使用了END模式时,exit的作用并不是退出整个awk命令,而是直接执行END模式中的动作,示例如下。

如上图所示,当awk中使用了END模式时,如果执行了exit语句,那么exit语句之后的所有动作都将不会再被执行,END模式中的动作除外。

换句话说就是,当执行了exit语句后,如果使用了END模式,将直接执行END模式中的动作,其他动作将不会被执行,如果没有使用END模式,当执行了exit语句后,将直接退出整个awk命令。

在awk中,除了能够使用"exit命令"结束"整个awk",还能够使用"next命令"结束"当前行",什么意思呢?我们慢慢聊。

在前文中,我们提到过,awk是逐行对文本进行处理的,也就是说,awk会处理完当前行,再继续处理下一行,那么,当awk需要处理某一行文本的时候,我们能不能够告诉awk :"不用处理这一行了,直接从下一行开始处理就行了"。

没错,使用next命令即可让awk直接从下一行开始处理,换句话说就是,next命令可以促使awk不对当前行执行对应的动作,而是直接处理下一行,示例如下。

其实,next与continue有些类似,只是,continue是针对"循环"而言的,continue的作用是结束"本次循环",而next是针对"逐行处理"而言的,next的作用是结束"对当前行的处理",从而直接处理"下一行",其实,awk的"逐行处理"也可以理解成为一种"循环",因为awk一直在"循环"处理着"每一行",不是吗?

前文中提及过,awk其实可以算作一门脚本语言,因为它包含了一个脚本语言的各种语法结构,比如条件判断语句,比如循环语句,那么,awk中能否使用"数组"呢?必须能啊,今天我们就来聊聊awk中的数组。

如果你有过任何一种编程语言的使用经验,那么你一定知道,我们可以通过数组的下标(或者称索引),引用数组中的元素,其他语言中,数组的下标通常由0开始,也就是说,如果想要引用数组中的第1个元素,则需要引用对应的下标"[0]",awk中的数组也是通过引用下标的方法,获取数组中的元素的,但是在awk中,数组元素的下标默认从1开始,但是为了兼容你的使用习惯,我们也可以从0开始设置下标,此处不用纠结,到后面自然会明白,我们先来看一个最简单的示例。

在其他语言中,你可能会习惯性的先"声明"一个数组,在awk中,则不用这样,直接为数组中的元素赋值即可,示例如下。

如上图所示,为了方便示例,上例中使用了BEGIN模式,在BEGIN模式中,存在一个名为"葫芦娃"(拼音)的数组,我们在这个数组中放置了3个元素,第1个元素为"大娃",第2个元素为"二娃",第3个元素为"三娃",如果我们想要引用数组中第二个元素的值,只要引用下标为1的元素即可,正如上图所示,我们使用下标"[1]",获得了huluwa这个数组中第二个元素的值,即"二娃"。

当然,如果你想要看到更多的"葫芦娃",可以在数组里面放置更多的元素。

如上图所示,上例数组中的第5个元素的值被设置为了"空字符串",当我们打印数组中的第5个元素的值时,打印出的值就是"空"(注:"空格"不为"空")。

为什么要举这个例子呢?之所以举这个例子,是因为在awk中,元素的值可以设置为"空",在awk中,将元素的值设置为"空字符串"是合法的。

既然在awk中,元素的值可以为"空",那么我们就不能再根据元素的值是否为"空"去判断元素是否存在了,所以,在awk中,如果你使用如下方法判断数组中的元素是否存在,是不合理的,如下图所示。

其实,awk中的数组本来就是"关联数组",之所以先用以数字作为下标的数组举例,是为了让读者能够更好的过度,不过,以数字作为数组下标的数组在某些场景中有一定的优势,但是它本质上也是关联数组,awk默认会把"数字"下标转换为"字符串",所以,本质上它还是一个使用字符串作为下标的关联数组。

使用delete可以删除数组中的元素,如下所示

注意,在这种语法中,for循环中的变量"i"表示的是元素的下标,而并非表示元素的值,所以,如果想要输出元素的值,则需要使用"print 数组名[变量]"

细心如你,一定发现了一个小问题,当数组中的下标为"字符串"时,元素值输出的顺序与元素在数组中的顺序不同,这是因为awk中的数组本质上是关联数组,所以默认打印出的元素是无序的。

那么你可能会提问了,既然之前说过,数字下标最终也会被转换成 "字符串",本质上也是关联数组,既然都属于关联数组,那么为什么第一种for循环语法能够按照顺序输出数组中的元素值呢?

这就是以数字作为下标的优势,因为第一种for循环语法中的变量"i"为数字,由于for循环的原因,"i"是按照顺序递增的,当"i"的值与下标的值相同时,我们即可按照下标的顺序,输出对应元素的值,换句话说就是,我们是通过下标的顺序,输出对应元素值的顺序,也就是键值定位。但是,即使数组元素的下标为数字,如果使用第二种for循环语法,也不能够按照顺序输出,示例如下。

上例又印证我们之前所说的,awk中的数组本质上就是关联数组。

我想,经过上述对比,你应该已经明白了。

前文中,我们都是手动的为数组中的元素赋值,那么我们能不能将指定的文本分割,然后将分割后的字段自动赋值到数组的元素中呢?答案是必须的,但是如果我们想要实现这样的效果,需要借助于split函数,而我们还没有介绍过函数,所以此处就先跳过了,不过需要提前说明的是,通过split函数生成的数组的下标默认是从1开始的,这就是为什么之前说,awk中数组的下标默认是从1开始的了。

在实际的工作中,我们往往会使用数组,统计某些字符出现的次数,比如,我们想要统计日志中每个IP地址出现了多少次,我们就可以利用数组去统计。

但是,统计的时候需要配合一些特殊用法,别着急,我们慢慢聊。

在awk中,我们可以进行数值运算,示例如下

当然,看懂上图中的命令,需要掌握前文中的知识,同时需要理解今天所介绍的知识。

上图中,我们使用了一个空模式,一个END模式。

空模式中,我们随便创建了一个数组,并且将IP地址作为引用元素的下标,进行了引用,所以,当执行到第一行时,我们引用的是count["192.168.1.1"]

很明显,这个元素并不存在,所以,当第一行被空模式中的动作处理完毕后,count["192.168.1.1"]的值已经被赋值为1了。

由于END模式中的动作会最后执行,所以我们先不考虑END模式。

这时,空模式中的动作继续处理下一行,而下一行的IP地址为192.168.1.2

所以,count["192.168.1.2"]第一次参与运算的过程与上述过程同理。

其他IP地址第一次参与运算的过程与上述过程同理。

直到再次遇到相同的IP地址时,使用同样一个IP地址作为下标的元素将会再次被自加,每次遇到相同的IP地址,对应元素的值都会加1。

直到处理完所有行,开始执行END模式中的动作。

而END模式中,我们打印出了count数组中的所有元素的下标,以及元素对应的值。

此刻,count数组中的下标即为IP地址,元素的值即为对应IP地址出现的次数。

最终,我们统计出了每个IP地址出现的次数。

其实,我们就是利用了之前所演示的一个知识点:

我们对一个不存在的元素进行自加运算后,这个元素的值就变成了自加运算的次数

上述过程可能比较绕,如果你之前没有接触过awk,一遍看不懂是很正常的,自己按照上述过程动手做几遍,细细品味一番,相信你会搞明白的。

如果你以后再想统计文本中某类文本出现的"次数",就可以使用上述套路了,活学活用以后,你会发现上述套路特别好使。

比如,如果我们想要统计如下文本中每个人名出现的次数,我们则可以使用如下命令。

关于awk中数组的用法,就先总结到这里,这些知识已经能够满足我的日常使用了,但是这些并不是数组的全部,如果你想要更加深入的了解数组,可以参考官方手册的数组部分,链接如下。
http://www.gnu.org/software/gawk/manual/gawk.html#Arrays

十、awk函数

10.1 算数函数

最常用的算数函数有rand函数、srand函数、int函数。

可以使用rand函数生成随机数,但是使用rand函数时,需要配合srand函数,否则rand函数返回的值将一直不变,示例如下。

细心如你一定已经发现了,当使用gsub函数时,gsub会替换指定范围内的所有符合条件的字符。

而使用sub函数则不同,当使用sub函数时,sub函数只会替换指定范围内第一次匹配到的符合条件的字符。

我们可以把gsub函数的作用理解为指定范围内的全局替换。

可以把sub函数的作用理解为指定范围内的单次替换,只替换第一次匹配到的字符。

这就是sub函数与gsub函数的为唯一的不同之处。

我们可以通过length函数,获取到指定字符串的长度,示例如下

上图中,我们使用index函数,在每一行中咋找字符串"Lee",如果Lee存在于当前行,则返回字符串Lee位于当前行的位置,如果Lee不存在于当前行,则返回0,表示当前行并不存在Lee,如上图所示,第二行中包含Lee,而且Lee位于第二行的第7个字符的位置,所以返回数字7。

在前文中,我们在总结数组时,提到过一个函数,借助这个函数可以动态的生成数组,而不用手动的设置数组中每个元素的值,没错,这个函数就是split函数。通过split函数,我们可以将指定的字符串按照指定的分割符切割,将切割后的每一段赋值到数组的元素中,从而动态的创建数组,示例如下。

我们先使用了split函数生成了数组,并且将split的返回值保存在变量arrlen中,然后利用for循环中变量的递增,顺序的输出了数组中的对应下标以及元素值,如果你不明白为什么,请参考前文。

10.3其他函数

我们还能够通过asort函数根据元素的值进行排序,但是,经过asort函数排序过后的数组的下标将会被重置,示例如下

理解完asort 函数,我们来认识一下asorti 函数,仔细看,是 asort 与 asorti

使用asort 函数可以根据元素的值进行排序,而使用asorti 函数可以根据元素的下标进行排序。

当元素的下标为字符串时,我们可以使用asorti 函数,根据下标的字母顺序进行排序,当元素的下标为数字时,我们就没有必要使用函数排序了,直接使用for循环即可排序,所以,此刻我们只考虑数组的下标为字符串时,怎样通过asorti 函数根据下标对数组进行排序。

当数组的下标为字符串时,asorti 函数会根据原数组中的下标的字母顺序进行排序,并且将排序后的下标放置到一个新的数组中,并且asorti函数会返回新的数组的长度,示例如下

如上图所示,asorti 函数根据数组t的下标排序后,创建了一个新的数组newt,newt中元素的值即为t数组下标的值,上例中,我们使用len变量保存了asorti函数的返回值,并且输出了最后排序后的新数组。

那么,聪明如你,一定想到了,既然我们已经将t数组的下标排序输出了,那么我们一定可以根据排序后的下标再次输出对应的元素值,从而达到根据数组下标排序后,输出原数组元素的目的,示例如下。

没错,上述过程,其实就是新数组负责排序老数组的下标,并将排序后的下标作为新数组的元素,而我们输出新数组元素的同时,又将新数组的元素值作为老数组下标,从而输出了老数组中的元素值,这句话好绕,不过我觉得你应该明白了。

十一、拾遗之”三元运算”与”打印奇偶行”

11.1三元运算

在centos6中,我们可以判断用户的UID是否小于500,如果用户的UID大于500,则用户为普通用户,如果用户的UID小于500,则用户为系统用户。

所以,我们可以通过awk的 "if...else结构",判断用户的UID范围,从而判断出用户属于哪种用户类型,示例如下

正如上图所示,我们使用"if...else"结构,对usertype变量进行了赋值,如果用户的UID小于500,则对usertype变量赋值为"系统用户",否则则赋值usertype变量为"普通用户",最后打印出用户名所在的列与usertype变量的值。

其实,我们可以使用三元运算,替换上例中的"if...else"结构语句,示例如下

正如上图所示,红线标注部分则使用了三元运算的语法,代替了之前"if...else"的语法,而三元运算的语法如下:

条件 ? 结果1 : 结果2

上述语法表示,如果条件成立,则返回结果1,如果条件不成立,则返回结果2。

而上例中,"$3<500"就是上述语法中的"条件","系统用户"就是上述语法中"?"后面的"结果1","普通用户"就是上述语法中":"后面的"结果2" ,同时,在上例中我们使用usertype变量接收了三元运算后的返回值,所以,当条件成立时,usertype变量被赋值为"系统用户",当条件不成立时,usertype变量被赋值为"普通用户"。

是不是很方便?其实,三元运算还有另外一种使用方式,示例如下

我们通过上述命令,统计出了,系统用户有42个,普通用户有7个,上图中红线标注的用法可以理解为三元运算的另一种语法。如下

表达式1 ? 表达式2 : 表达式3

上述语法表示,如果表达式1为真,则执行表达式2,如果表达式1为假,则执行表达式3

而上例中,"$3<500"即为表达式1,"a++"即为表达式2,"b++"即为表达式3

也就是说,当每遇到一个UID小于500的用户,我就对变量a加1,否则我就对变量b加1,从而算出了系统用户与普通用户的数量,最后再END模式中输出了变量a与变量b的值。

是不是很容易理解?你一定已经明白了。

打印奇偶行

如果我们想要使用awk打印文本中的奇数行或者偶数行,则是非常简单的。

我们先来看看怎样使用awk打印奇数行或偶数行,然后再结合示例解释原理,所以看不懂没关系,后面会有解释。

正如上图所示,test12文件中有11行文本,我们可以使用非常简洁的awk命令,打印出了奇数行或者偶数行。

但是如果我们想要彻底搞明白原理,则需要搞明白如下两个知识点(后面会有更详细的解释)

1、在awk中,如果省略了模式对应的动作,当前行满足模式时,默认动作为打印整行,即{print $0}。

2、在awk中,0或者空字符串表示"假",非0值或者非空字符串表示"真"

上述两个知识点是什么意思呢?我们慢慢聊。

在之前介绍awk模式的文章中提及过,模式可以理解为条件,如果当前行能与模式匹配,则会执行对应的动作。示例如下

上图中的两个命令均使用到了模式

第一个命令表示如果当前行中包含字符"1",则执行对应的动作,而对应的动作就是打印整行。

第二个命令表示如果test12文本中文本行的第二列的值如果大于10,则执行对应的动作,而对应的动作就是打印整行。

那么,如果我们将上例中awk命令中的动作都省略,会出现什么情况呢?我们来试试。

我们发现,当使用了模式时,如果省略了对应的动作,会默认的输出整行。

也就是说,当使用了模式时,如果省略了模式对应的动作,默认动作为"{print $0}"

当然,"空模式"与"BEGIN/END模式"除外。

这就是第1个知识点的含义,我想你应该明白了,那么我们来聊聊第2个知识点。

在awk中,0或者空字符串表示"假",非0值或者非空字符串表示"真",什么意思呢?我们还是可以从模式说起,"模式"可以理解为"条件",当条件成立,则为真,当条件不成立,则为假,所以,当模式为真时,则会执行对应的动作,当模式为假时,则不会执行对应的动作。

那么,我们能不能直接把模式替换为"真"或者"假"呢?我们来试试。

上例中,命令1使用了"空模式",也就是说,每一行都满足模式,每一行经过"空模式"匹配以后结果都是"真",所以每一行都会执行对应的动作。

命令2中,原来"模式的位置"被替换为了数字"1",我们可以把数字"1"理解成一种模式匹配后的结果,而1是非零值,刚才说过,在awk中非零值表示真,所以,"1"表示"真", 换句话说就是模式的匹配结果为真,模式成立则会执行对应的动作,而命令2中,对应的动作为打印整行。

命令3 与 命令2 同理,在命令3中, 数字"2"为非零值,表示真,可以理解为:模式的匹配结果为真,则会执行对应的动作,聪明如你一定想到了,数值"2"可以换做任何非0值或者非空字符串。

命令4中,数字"2"为非零值,表示模式为真,而之前说过,当使用模式时,可以省略动作,当使用模式并省略动作时,默认动作为打印整行,所以,命令4表示打印所有行,因为每一行的模式都为真。

命令5与命令6同理,在awk中,数字"0"与空字符串表示假,当模式为假时,不会执行对应的动作,而当存在模式并省略动作时,默认动作为打印整行,但是由于模式为假,所以对应的动作并未执行。

其实,我们还能对真与假进行取反,非真即为假,非假即为真,示例如下。

当awk开始处理第一行时,变量 i 被初始化,变量 i 在被初始化时,值为"空",而awk中,数字0或者"空字符串"表示假,所以可以认为模式为假,但是 i 直接取反了,对假取反后的值为真,将取反后的值又赋值给了变量i,此刻,变量i的值为真,所以当awk处理第一行文本时,变量i的值被赋值为真,模式成立则需要执行对应的动作,而上例中又省略了动作,所以默认动作为"{print $0}",所以,第一行被整行打印了。

当第一行文本处理完毕后,awk开始处理第二行文本,此时,i 为真,但是取反后,i 为假,所以第二行没有被输出,依次类推,最终只打印了奇数行。

为了能够更加直观的看到上述过程,我们将i的值打印出来,通过如下动作,能够打印出处理每一行时,i 对应的值。