一、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。
上图用了一个命令把testd文件中的内容打印了出来。
下面来操作一个类似的场景。
大家都知道df命令可以用来显示文件系统的磁盘使用情况统计。
由上面简介可知,$5
为分割后的第5个字段,即磁盘使用率。
使用|管道符将df的标准输出作为awk的标准输入,于是这条命令将磁盘的使用率给列出来了。
我们也可以一次输出多列,使用逗号隔开要输出的多个列,如下,一次性输出第一列和第二列
同理,也可以一次性输出多个指定的列,如下图
我们发现,第一行并没有第5列,所以并没有输出任何文本,而第二行有第五列,所以输出了。
除了输出文本中的列,我们还能够添加自己的字段,将自己的字段与文件中的列结合起来,如下做法,都是可以的。
从上述实验中可以看出,awk可以灵活的将我们指定的字符与每一列进行拼接,或者把指定的字符当做一个新列插入到原来的列中,也就是awk格式化文本能力的体现。
但是要注意,$1
这种内置变量的外侧不能加入双引号,否则$1
会被当做文本输出,示例如下
我们也可以输出整行,比如,如下两种写法都表示输出整行。
三、akw特殊模式
akw包含两种特殊的模式:BEGIN 和 END。
BEGIN 模式指定了处理文本之前需要执行的操作,END 模式指定了处理完所有行之后所需要执行的操作。
从BEGIN模式开始,示例如下
上述写法表示,在开始处理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模式的作用就一目了然了,举例如下。
聪明如你一定明白了,END模式就是在处理完所有的指定的文本之后,需要指定的动作。
那么,我们可以结合BEGIN模式和END模式一起使用。示例如下
上述示例中返回的结果有没有很像一张"报表",有"表头" 、"表内容"、 "表尾"。
四、awk分隔符
输入分隔符,英文原文为field separator,此处简称为FS。
输入分割符,默认是空白字符(即空格),awk默认以空白字符为分隔符对每一行进行分割。
输出分割符,英文原文为output field separator,此处简称为OFS。
awk将每行分割后,输出在屏幕上的时候,以什么字符作为分隔符,awk默认的输出分割符也是空格。
4.1输入分隔符
输入分隔符比较容易理解,当awk逐行处理文本的时候,以输入分隔符为准,将文本切成多个片段,默认使用空格,但是,如果一段文字中没有空格,我们可以指定以特定的文字或符号作为输入分割符,比如下图中的例子,我们指定使用"#"作为输入分隔符。
上图中,我们使用了-F 选项,指定了使用#号作为输入分隔符,于是,awk将每一行都通过#号为我们分割了。
除了使用 -F 选项指定输入分隔符,还能够通过设置内部变量的方式,指定awk的输入分隔符,awk内置变量FS可以用于指定输入分隔符,但是在使用变量时,需要使用-v选项,用于指定对应的变量,比如 -v FS='#',如下图:
其实不管是通过-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的内置变量OFS来设定awk的输出分隔符,当然,使用变量的时候要配合使用-v选项,示例如下
现在,我们可以同时指定输入分隔符和输出分割符了,示例如下
我们刚才解释了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列
而内置变量NR表示每一行的行号,内置变量NF表示每一行中一共有几列,那么,也就是说,我们可以通过下例中的方法,得到test1文本中,每一行的行号以及每一行对应的列的数量。
或者,利用NR内置变量,先打印出行号,再打印出整行的内容,相当于为test1中的每一行都添加了行号以后再进行输出,示例如下。
好了,现在每一行的开头都有行号了,简单吧。
细心如你一定注意到了一个细节,就是在打印 $0
, $1
, $2
这些内置变量的时候,都有使用到"$
"符号,但是在调用 NR , NF 这些内置变量的时候,就没有使用"$
",如果你有点不习惯,那么可能是因为你已经习惯了使用bash的语法去使用变量,在bash中,我们在引用变量时,都会使用$
符进行引用,但是在awk中,只有在引用$0
、$1
等内置变量的值的时候才会用到"$
",引用其他变量时,不管是内置变量,还是自定义变量,都不使用"$
",而是直接使用变量名。
5.3内置变量FNR
当我们使用awk同时处理多个文件,并且使用NR显示行号的时候,效果如下图。
从返回结果可以看出,awk处理多个文件的时候,如果使用NR显示行号,那么,多个文件的所有行会按照顺序进行排序。
可是,如果我们想要分别显示两个文件的行号,该怎么办呢,这个时候就会用到内置变量FNR,效果如下。
FNR内置变量的作用就是当awk处理多个文件时,分别对每个文件的行数进行计数。
5.4内置变量RS
RS是输入行分隔符,如果不指定,默认的"行分隔符"就是我们所理解的"回车换行"。
假设,我们不想以默认的"回车换行"作为"行分隔符",而是想使用空格作为所谓的行分隔符,也就是说,我们想让awk认为,每遇到一个空格,就换行,换句话说,我们想让awk以为每次遇到一个空格就是新的一行。那么我们该怎么做呢?示例如下。
如上图所示,我们先使用了默认的"回车换行"作为"行分隔符"输出了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这个内置变量,从字面上,就能看出是什么意思,没错,就是显示文件名,演示效果如下。
5.7内置变量ARGC与ARGV
ARGC内置变量表示命令行参数的个数,什么意思呢?我们先不解释ARGC,先看看ARGV是什么。
别眼花了。
一个是ARGC,
一个是ARGV,
先说说ARGV。
ARGV内置变量表示的是一个数组,这个数组中保存的是命令行所给定的参数。这样解释还是很模糊,不容易理解,我们来看看示例。
上图中,我们先使用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中直接定义。
我们来看一些小例子,即可明白上述两种方法。
通过方法一自定义变量。
这种方式,与设置内置变量的值的方法是一样的。
使用方法二自定义变量,直接在program中定义即可,但是注意,变量定义与动作之间需要用分号";"隔开。
当然,我们也可以一次性定义多个变量
第一种方法虽然看上去比较麻烦,但是这种方法也有自己的优势
当我们需要在awk中引用shell中的变量的时候,则可以通过方法一间接的引用。举例如下
六、awk格式化
利用awk中的printf动作,即可对文本进行格式化输出,printf动作的用法与printf命令的用法非常相似,只是有略微的不同而已,不过,我们还是从最简单的示例开始看起,首先对比一下print动作与printf动作的区别,示例如下
没错,printf动作与printf命令一样,都不会输出换行符,默认会将文本输出在一行里面。
聪明如你一定想到了,既然printf动作的用法与printf命令一样,那么,printf动作有没有printf命令中所谓的"格式替换符"呢?
必须有啊,"格式替换符"是什么我们就不再赘述了,因为在printf命令详解中已经详细的解释过它,那么我们来使用"格式替换符"来指定一下$1的格式,示例如下。
如果只看上图中红线标注的部分,你肯定会认为,这就是printf命令的用法,只是printf动作与printf命令在语法上唯一的不同点就是,在使用printf动作时,指定的"格式"与列$1
之间需要用"逗号"隔开,而使用printf命令时,指定的格式与传入的文本不需要使用"逗号"隔开,如下图所示
其实,它们还有一些其他的不同之处,我们在使用printf命令时,当指定的格式中只有一个"格式替换符",但是传入了多个参数时,那么这多个参数可以重复的使用这一个格式替换符,示例如下
但是在awk中,我们则不能这样使用,在awk中,格式替换符的数量必须与传入的参数的数量相同,换句话说,格式替换符必须与需要格式化的参数一一对应,示例如下。
好了,这就是awk中printf动作在使用时的一些注意点。
我们来总结一下,在awk中使用printf动作时,需要注意以下3点。
1)使用printf动作输出的文本不会换行,如果需要换行,可以在对应的"格式替换符"后加入"\n"进行转义。
2)使用printf动作时,"指定的格式" 与 "被格式化的文本" 之间,需要用"逗号"隔开。
3)使用printf动作时,"格式"中的"格式替换符"必须与 "被格式化的文本" 一一对应。
好了,我们来看一些小示例,练练手。
我们可以利用格式替换符对文本中的每一列进行格式化,示例如下。
我们可以利用awk的内置变量FS,指定输入字段分隔符,然后再利用printf动作,进行格式化,示例如下。
上例完美的体现了awk的格式化能力,因为awk本身负责文本切割,printf动作负责格式化文本,双剑合璧了。
继续扩展一下,可以利用awk的begin模式,结合printf动作,输出一个像样的表格,下图中用到的"修饰符"此处不再赘述,如果不明白,参考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命令,能否完成上述需求呢?
答案是肯定的,那么我们一起来看看,使用awk命令,怎样从/etc/passwd文件中找出用户名以zsy开头的用户,示例如下
聪明如你一定看出来了,不管是使用grep命令,还是使用awk命令,都使用了相同的正则表达式"^zsy"
唯一的区别就是,在grep命令中,直接使用了正则表达式,而在awk命令中,正则表达式被放入了两个斜线中。
这样说可能不容易理解,看图说话似乎更加容易理解。
上图中,awk命令在使用正则表达式时,将正则表达式放入了"/ /"中。
其实,这就是我们今天要介绍的"正则模式",在使用"正则模式"时,文本行如果能够被正则表达式匹配到,就会执行对应的动作,如果没有被正则匹配到,则不会执行对应的动作,而上例中,对应的动作就是{print $0},也就是打印整行,所以,上例中的grep命令与awk命令所实现的效果是完全相同的,那么你可能会问,既然效果完全相同,为什么还要使用awk呢?似乎grep更加简单一些,没错,上例中,grep是更加简单一些,但是不要忘了,awk有自己的优势,就是格式化能力,那么,我们换一个场景,可能使用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命令,配合正则表达式完成我们的需求,示例如下。
如上图所示,使用"/bin/bash"作为登录shell的用户都被我们找了出来。
同理,我们使用awk命令,同样能够实现与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
如上图所示,红线标注部分就是awk命令中的"动作"。
其实,我们可以把上述"动作"分解,拆开成两部分去理解,如下图所示。
上图中,我们将动作拆分成了两个部分。
红线标注为第一部分:最外侧的括号,即"{ }"。
蓝线标注为第二部分:"print $0"
在之前的示例中,我们一直把上图中的两个部分当做一个整理去理解,但是现在,我们要把它们分开去理解。
其实,这两个部分都可以被称之为"动作",只不过它们是不同"类型"的动作而已。
"print"属于"输出语句"类型的动作,顾名思义,"输出语句"类型的动作的作用就是输出、打印信息,没错,"print"与"printf"都属于"输出语句"类型的动作。
"{ }"其实也可以被称之为"动作",只不过,"{ }"属于"组合语句"类型的动作,顾名思义,"组合语句"类型的动作的作用就是将多个代码组合成代码块。
这样说可能不容易理解,我们来看个小示例,就容易理解了,示例如下。
如上图所示,我们使用了两个大括号"{ }",它们属于"组合语句"类型的动,它们分别将两个print括住,表示这两个print动作分别作为两个独立的个体,如下图所示。
也就是说,我们可以这样理解,上图中一共有4个"动作",两对大括号,两个print,但是上图中,每个大括号中只有一个动作,而我们说过,"组合语句"的作用是将多个代码或多个动作组合成代码块,组合后的代码块被当做一个整体,那么,我们能不能把上图中的两个print动作组合成一个整体呢?
必须能啊,示例如下。
awk从放弃到入门(7):awk动作总结之一如上图所示,我们只使用了一个大括号,将两个print动作组合成了一个整体,但是细心如你一定发现来了,当我们把多个动作(多段代码)组合成一个代码块的时候,每段动作(每段代码)之间需要用分号";"隔开,如下图所示。
好了,我想你应该明白了,除了print这种"输出语句"能够被称之为动作以外,像"{ }"这种"组合语句"也能被称之为动作,只不过它们的类型不同,功能也不同。
那么,除了"输出语句"与"组合语句"以外,还有其他种类的动作吗?
必须的,我们现在就来认识另一种动作,它就是"控制语句"。
不过,"控制语句"又有很多种,不过不用怕,我们慢慢来,一个一个聊,先来认识一种简单的"控制语句",它就是"条件判断"。
如果你有过任何一种编程语言的开发经验,你都会非常容易理解"条件判断",条件判断无非就是条件成立,则执行对应的代码,条件不成立,则不执行对应的命令,没错,在编程语言中,通常使用如下语法结构进行条件判断,也就是编程语法中的 if 条件判断语句。
在awk中,我们同样可以使用if这种语法进行条件判断,只不过,上例中的语法结构是由"多行"组成,而在命令行中使用awk时,我们可以将上例中的"多行"语句写在"一行"中,示例如下。
上图中红线标注的部分即为"条件判断"类型的语法,我们把红线标注的部分单独取出来,来描述一下。
"if(NR == 1)"中的NR为awk的内置变量,NR为行号之意,所以,"if(NR == 1)"表示行号为1时,条件成立。
"if(NR == 1){ print $0 }"表示行号为1是满足条件,条件满足时,打印整行,换句话说就是只打印第一行。
你可能会纠结,为什么最外侧还需要有一层大括号呢?如下图所示。
告诉你原因,原因就是·····
没有为什么,就是要这样写,否则会报错。
如果你非要一个理由,那么我们可以这样理解,所有动作的最外侧必须用"{ }"括起。
你可能还是会纠结,if语句的语法结构中也包含大括号啊,那么它属于"组合语句"吗?如下图所示
虽然它的语法结构中也包含大括号,但是我们仍然把if语句称之为"控制语句",或者我们可以这样理解,"控制语句"中包含"组合语句"。
if语句中的大括号中,也可以执行多个动作,把多个代码段当做一个整体,也就是说,如果if所对应的条件成立,则执行if的大括号中的所有命令,示例如下。
上例表示,如果行号为1,则满足条件,就会执行if对应的大括号中的所有代码,而大括号中,有两个print动作,当条件成立时,这两个print动作都会被执行,当条件不成立时,这两个动作都不会执行。
上例中,"if"对应的大括号中有多条语句,所以"if"语法中的大括号不能省略,但是,如果"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...while的示例如下,它与while循环的不同之处在于,while循环只有当满足条件时才会执行对应语句,而do...while循环则是无论是否满足条件,都会先执行一遍do对应的代码,然后再判断是否满足while中对应的条件,满足条件,则执行do对应的代码,如果不满足条件,则不再执行do对应的代码。
正如上图所示,无论是否满足while中的条件,都会先执行一遍do对应的代码。
那么,说到循环,就不能不说说与循环有关的跳出语句。
没错,与其他编程语言中一样,在awk中,同样可以使用break与continue跳出循环。
continue的作用:跳出"当前"循环
break的作用:跳出"整个"循环
示例如下,先看看continue的示例
由于在for循环中添加了条件判断,所以当 i 的值为 3 时,跳过了"当前本次"循环,没有执行当前本次循环需要执行的动作,所以上例中,数字"3"并没有被打印出来。
当然,如果你想"结束"的更加"彻底",可以使用break结束循环,示例如下。
如上图所示,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这个数组中第二个元素的值,即"二娃"。
当然,如果你想要看到更多的"葫芦娃",可以在数组里面放置更多的元素。
如上图所示,由于命令太长,可读性可能会降低,为了在编写时提高命令的可读性,我们可以使用Linux命令行的"换行符"进行换行,Linux中,命令行的换行符为反斜杠"",上述命令换行后,如下。
可以看到,目前葫芦娃数组中已经存在6个葫芦娃了,我们可以获取到我们想要的葫芦娃,换句话说,我们可以通过数组的下标,获取到任何一个元素的值。
我们知道,在动画中,六娃的超能力是"隐身",所以六娃也叫"隐身娃",那么,我们就把上述数组中的第5个元素的值设置为"空字符串"吧,用空字符串表示六娃已经"隐身"了,示例如下。
如上图所示,上例数组中的第5个元素的值被设置为了"空字符串",当我们打印数组中的第5个元素的值时,打印出的值就是"空"(注:"空格"不为"空")。
为什么要举这个例子呢?之所以举这个例子,是因为在awk中,元素的值可以设置为"空",在awk中,将元素的值设置为"空字符串"是合法的。
既然在awk中,元素的值可以为"空",那么我们就不能再根据元素的值是否为"空"去判断元素是否存在了,所以,在awk中,如果你使用如下方法判断数组中的元素是否存在,是不合理的,如下图所示。
正如上图所示,第6个元素明明已经存在,但是通过上述方法判断元素是否存在时,仍然显示对应的元素不存在。
其实,使用上述方法判断元素是否存在之所以不合理,除了上述原因,还有另外一个原因,就是当一个元素不存在于数组时,如果我们直接引用这个不存在的元素,awk会自动创建这个元素,并且默认为这个元素赋值为"空字符串",示例如下。
如上图所示,数组中并没有第7个元素,但是当我们输出第7个元素时,输出了"空",所以,出于此原因,在awk中使用之前的方法判断元素是否为空也是不合理的,因为当我们引用一个不存在于数组中的元素时,这个元素其实已经被赋值为"空字符串"了,如下图所示。
那么,在awk中,应该怎样判断元素是否存在呢?我们可以使用如下语法。
如上图所示,我们可以使用语法 "if(下标 in 数组名)" ,从而判断数组中是否存在对应的元素。
当然,我们还可以使用 "!" 对条件进行取反,如下图所示。
在awk中,数组的下标不仅可以为"数字",还可以为"任意字符串",如果你使用过shell中的数组,你可以把awk的数组比作bash中的"关联数组",示例如下
其实,awk中的数组本来就是"关联数组",之所以先用以数字作为下标的数组举例,是为了让读者能够更好的过度,不过,以数字作为数组下标的数组在某些场景中有一定的优势,但是它本质上也是关联数组,awk默认会把"数字"下标转换为"字符串",所以,本质上它还是一个使用字符串作为下标的关联数组。
使用delete可以删除数组中的元素,如下所示
也可以使用delete删除整个数组,如下所示
到目前为止,我们已经介绍了怎样为数组中的元素赋值、怎样输出数组中的某个元素、以及怎样删除数组中的元素,那么现在,我们来聊聊在awk中怎样输出数组中的所有元素,在awk中,如果想要输出数组中的所有元素,则需要借助for循环语句。
你一定看出来了,我们利用了for循环中的变量"i"与数组中的下标都是"数字"的这一特性,按照顺序输出了数组中的元素值。
那么,当数组中的元素的下标为"无规律的字符串"时,我们该怎么办呢?这时可以使用for循环的第二种语法,示例如下。
注意,在这种语法中,for循环中的变量"i"表示的是元素的下标,而并非表示元素的值,所以,如果想要输出元素的值,则需要使用"print 数组名[变量]"
细心如你,一定发现了一个小问题,当数组中的下标为"字符串"时,元素值输出的顺序与元素在数组中的顺序不同,这是因为awk中的数组本质上是关联数组,所以默认打印出的元素是无序的。
那么你可能会提问了,既然之前说过,数字下标最终也会被转换成 "字符串",本质上也是关联数组,既然都属于关联数组,那么为什么第一种for循环语法能够按照顺序输出数组中的元素值呢?
这就是以数字作为下标的优势,因为第一种for循环语法中的变量"i"为数字,由于for循环的原因,"i"是按照顺序递增的,当"i"的值与下标的值相同时,我们即可按照下标的顺序,输出对应元素的值,换句话说就是,我们是通过下标的顺序,输出对应元素值的顺序,也就是键值定位。但是,即使数组元素的下标为数字,如果使用第二种for循环语法,也不能够按照顺序输出,示例如下。
上例又印证我们之前所说的,awk中的数组本质上就是关联数组。
我想,经过上述对比,你应该已经明白了。
前文中,我们都是手动的为数组中的元素赋值,那么我们能不能将指定的文本分割,然后将分割后的字段自动赋值到数组的元素中呢?答案是必须的,但是如果我们想要实现这样的效果,需要借助于split函数,而我们还没有介绍过函数,所以此处就先跳过了,不过需要提前说明的是,通过split函数生成的数组的下标默认是从1开始的,这就是为什么之前说,awk中数组的下标默认是从1开始的了。
在实际的工作中,我们往往会使用数组,统计某些字符出现的次数,比如,我们想要统计日志中每个IP地址出现了多少次,我们就可以利用数组去统计。
但是,统计的时候需要配合一些特殊用法,别着急,我们慢慢聊。
在awk中,我们可以进行数值运算,示例如下
我们将变量a的值设置为1,进行加法计算,每次自加后,再次打印变量a的值,都会加1
这并不难理解,因为上例中,a的值本来就是一个数字。
那么,如果变量a的值是一个字符串,我们能否对变量a进行自加运算呢?我们来试试。
如上图所示,在awk中,当变量a的值为字符串时,竟然也可以进行加法运算,从上例可以看出,awk中,如果字符串参与运算,字符串将被当做数字0进行运算。
那么"空字符串"呢?当空字符串参与运算时,也会被当做数字0吗?我们来试试。
看样子,我们猜的不错,空字符串在参与运算时,也会被当做数字0
之前说过,当我们直接引用一个数组中不存在的元素时,awk会自动创建这个元素,并且为其赋值为"空字符串"。
所以,如果我们引用一个不存在元素,并对其进行自加运算,那么会出现什么效果呢?我们来试一试
如上图所示,当引用了一个不存在的元素时,元素被赋值为空字符串,当对这个元素进行自加运算时,元素的值就变成了1,因为,空字符串在参与运算时,被当做0使用了,所以,综上所述,我们对一个不存在的元素进行自加运算后,这个元素的值就变成了自加运算的次数,自加x次,元素的值就被赋值为x,自加y次,元素的值就被赋值为y,示例如下。
利用这一点,我们就可以统计文本中某些字符出现的次数,比如IP地址,示例如下。
当然,看懂上图中的命令,需要掌握前文中的知识,同时需要理解今天所介绍的知识。
上图中,我们使用了一个空模式,一个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函数返回的值将一直不变,示例如下。
可以看到,如果单纯的使用rand函数,生成的值是不变的,可以配合srand函数,生成一个大于0小于1的随机数,示例如下
可以看到,上图中生成的随机数都是小于1的小数,如果我们想要生成整数随机数,可以将上述生成的随机数乘以100,然后截取整数部分,使用int函数可以截取整数部分的值,示例如下
经过上述处理以后,可以得到一个小于100的随机整数。
10.2字符串函数
我们可以使用gsub函数或sub函数替换某些文本,先来 看看gsub函数怎样使用。
如果我们想要将如下文本中的第一列中的小写字母"l"都替换成大写字母"L",则可以使用gsub函数,示例如下。
如上图所示,我们使用gsub函数,将小写字母"l"替换成大写字母"L",但是替换的范围只限于"1"换成"0",示例如下。
看完上述示例,我想你应该已经明白了gsub函数的作用,没错,gsub函数会在指定范围内查找指定的字符,并将其替换为指定的字符串。
其实,我们还可以根据正则表达式,替换字符串,示例如下。
好了,经过上述示例,你应该已经明白gsub的用法了。
那么sub函数与gsub函数有什么不同呢?我们来对比一下。
细心如你一定已经发现了,当使用gsub函数时,gsub会替换指定范围内的所有符合条件的字符。
而使用sub函数则不同,当使用sub函数时,sub函数只会替换指定范围内第一次匹配到的符合条件的字符。
我们可以把gsub函数的作用理解为指定范围内的全局替换。
可以把sub函数的作用理解为指定范围内的单次替换,只替换第一次匹配到的字符。
这就是sub函数与gsub函数的为唯一的不同之处。
我们可以通过length函数,获取到指定字符串的长度,示例如下
如上图所示,我们输出了文本中每个单词的长度,其实,length函数可以省略传入的参数,即不指定任何字符换,当省略参数时,默认使用"$0"作为参数,示例如下。
正如上图所示,我们使用length函数,获取到了文本中每一行的长度。
我们可以使用index函数,获取到指定字符位于整个字符串中的位置,示例如下
上图中,我们使用index函数,在每一行中咋找字符串"Lee",如果Lee存在于当前行,则返回字符串Lee位于当前行的位置,如果Lee不存在于当前行,则返回0,表示当前行并不存在Lee,如上图所示,第二行中包含Lee,而且Lee位于第二行的第7个字符的位置,所以返回数字7。
在前文中,我们在总结数组时,提到过一个函数,借助这个函数可以动态的生成数组,而不用手动的设置数组中每个元素的值,没错,这个函数就是split函数。通过split函数,我们可以将指定的字符串按照指定的分割符切割,将切割后的每一段赋值到数组的元素中,从而动态的创建数组,示例如下。
如上图所示,我们通过split函数,将字符串ts切割了,以":"作为分割符,将分割后的字符串保存到了名为huluwa的数组中,当我们输出数组中的元素时,每个元素的值为分割后的字符,其实,split函数也有对应的返回值,其返回值就是分割以后的数组长度,示例如下。
注意,被split函数分割后的数组的元素下标从1开始,不像其他语言中的数组下标是从0开始的,而且数组中元素输出的顺序可能与字符串中字符的顺序不同,原因在前文中已经说过了,如果我们想要按照顺序输出数组中的元素,可以使用如下方法。
我们先使用了split函数生成了数组,并且将split的返回值保存在变量arrlen中,然后利用for循环中变量的递增,顺序的输出了数组中的对应下标以及元素值,如果你不明白为什么,请参考前文。
10.3其他函数
我们还能够通过asort函数根据元素的值进行排序,但是,经过asort函数排序过后的数组的下标将会被重置,示例如下
如上图所示,数组中元素的值均为数字,但是下标为自定义的字符串,通过asort函数对数组排序后,再次输出数组中的元素时,已经按照元素的值的大小进行了排序,但是,数组的下标也被重置为了纯数字,其实,asort还有一种用法,就是在对原数组元素值排序的同时,创建一个新的数组,将排序后的元素放置在新数组中,这样能够保持原数组不做任何改变,我们只要打印新数组中的元素值,即可输出排序后的元素值,示例如下。
其实,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 赋值为1,当 i=1 以后,i为非零值,表示为真,我们可以认为这是一种模式匹配后的结果,当模式为真时,同时省略了对应动作时,默认动作为打印整行,所以上例会输出test3中的所有行。
理解完上述示例以后,我们再回过头来,看看之前打印奇数行的示例,你可能就会明白了。
当awk开始处理第一行时,变量 i 被初始化,变量 i 在被初始化时,值为"空",而awk中,数字0或者"空字符串"表示假,所以可以认为模式为假,但是 i 直接取反了,对假取反后的值为真,将取反后的值又赋值给了变量i,此刻,变量i的值为真,所以当awk处理第一行文本时,变量i的值被赋值为真,模式成立则需要执行对应的动作,而上例中又省略了动作,所以默认动作为"{print $0}",所以,第一行被整行打印了。
当第一行文本处理完毕后,awk开始处理第二行文本,此时,i 为真,但是取反后,i 为假,所以第二行没有被输出,依次类推,最终只打印了奇数行。
为了能够更加直观的看到上述过程,我们将i的值打印出来,通过如下动作,能够打印出处理每一行时,i 对应的值。
转载文章
awk从放弃到入门
网友评论