patsplit()函数的功能与FPAT预定义变量的功能相同。
至此我们学习了3种划分字段的方式:
根据字段分隔符(预定义变量FS和选项-F)划分字段;
根据字段的宽度(预定义变量FIELDWIDTHS)划分字段;
根据字段的模式(预定义变量FPAT)划分字段。
这三种方式只能选择一种,它们相互之间是冲突的。
检查字段分隔的方式
数组变量PROCINFO["FS"]存储了字段分隔的三种方式,其值分别是FS、FIELDWIDTHS和FPAT。
~]# cat test.awk
BEGIN {
if(PROCINFO["FS"]=="FS"){
print "FS"
} else if(PROCINFO["FS"]=="FPAT") {
print "FPAT"
} else {
print "FIELDWIDTHS"
~]# awk -f test.awk
~]# awk -v FS=":" -f test.awk
~]# awk -v FIELDWIDTHS="3" -f test.awk
~]# awk -v FPAT="[[:alpha:]]+" -f test.awk
字段与记录的重建
预定义变量FS的含义我们已经很了解。有一个十分类似的预定义变量叫做OFS(Output FS),它表示当$0(记录)重新计算(可以理解为重建)的时候使用OFS的值作为输出字段的分隔符。接下来我们来看几个重新计算的情况。
1、当修改$0的时候,将使用FS(假定我们就使用FS不使用其他划分字段的方式)重新计算各个字段以及NF值。
awk 'BEGIN{FS=":"}{$0="a:b:c";print NF;for(i=1;i<=NF;i++){print $i}}' a.txt
2、当修改具体的字段的时候,使用OFS重建记录。注意,哪怕是自我赋值也属于字段的修改。
awk 'BEGIN{OFS="-"}{$1=0;print $0}' a.txt
awk 'BEGIN{OFS="-"}{$1=$1;print $0}' a.txt
3、为不存在的字段赋值,将新增字段并为不存在的字段(若有)赋空字符串,使用OFS重建记录。
awk 'BEGIN{OFS="-"}{$(NF+3)=5;print $0}' a.txt
4、增加NF,使用空字符串为新记录赋值;减少NF,截断多余记录。均会使用OFS重建记录。
# awk 'BEGIN{OFS="-"}{NF+=3;print $0}' a.txt
# awk 'BEGIN{OFS="-"}{NF-=3;print $0}' a.txt
awk读取记录以后将数据原原本本存放于$0当中,只要不会发生上述使用OFS重建记录的事情,即便指定了OFS也无妨。
# awk 'BEGIN{OFS="-"}{print $0}' a.txt
OFS的默认值是1个空格。因此即便没指定具体的值也会使用单个空格重建记录。
awk '{$1=$1;print $0}' a.txt
一般我们会先设置OFS的值再重建记录。所以将其放入BEGIN中。如果先重建再设置OFS,那么第一行会按照默认OFS重建,后续行才按照新OFS值重建。
awk '{$1=$1;OFS="-";print $0}' a.txt
awk '{$1=$1;OFS="-";$1=$1;print $0}' a.txt
awk 'BEGIN{OFS="-"}{$1=$1;print $0}' a.txt
awk '{$1=$1;print $0}' OFS="-" a.txt
这里如果看不懂的朋友,等后面学习了awk工作流程和变量以后就会明白awk执行的顺序了。
根据这个特性我们可以压缩连续的多个空格。
# echo " a b c d " | awk '{$1=$1;print $0}'
# echo " a b c d " | awk 'BEGIN{OFS="-"}{$1=$1;print $0}'
1、根据行号(NR或者FNR)筛选记录。
awk 'NR==2{print $0}' a.txt
awk 'NR>2{print}' a.txt
awk 'NR<2' a.txt
awk 'NR>=2' a.txt
awk 'NR<=2' a.txt
此前已经说过,省略{action}即表示{print}等价于{print $0}。
2、根据正则表达式筛选记录。
正则匹配,默认使用$0来匹配,可以省略$0。
awk '/qq.com/' a.txt
awk '$0~/qq.com/' a.txt
匹配不包含@的记录,即整条记录均由非@字符构成。
awk '/^[^@]+$/' a.txt
awk支持取反,使用取反更易理解。
awk '!/@/' a.txt
3、根据字段筛选记录。
# awk '$4>24' a.txt
ID name gender age email phone
1 Bob male 28 abc@qq.com 18023394012
7 Jerry female 25 exdsa@189.com 18785234906
10 Bruce female 27 bcbd@139.com 13942943905
第一条记录的$4是age,age是字符串,24是数字,其在进行比较时会有内部转换机制,将24识别为字符串,字符串比较根据ASCII编码(maybe)按字符一一比较,字符a大于字符2。如果我们期望不筛选出age那条,可以将其+0从而转换成数字。字符串+0等于数字0。
# awk '($4+0)>24' a.txt
1 Bob male 28 abc@qq.com 18023394012
7 Jerry female 25 exdsa@189.com 18785234906
10 Bruce female 27 bcbd@139.com 13942943905
awk '$5~/qq.com/' a.txt
4、组合筛选。
使用逻辑与和逻辑或运算符组合多个条件。
awk 'NR>=2&&NR<=4' a.txt
awk '($4+0)>=20||$3=="male"' a.txt
5、按照范围筛选(flip-flop)。
awk 'NR==2,NR==4' a.txt
awk '$2=="Kevin",$5~/qq.com/' a.txt
字段的筛选即print $X(X表示具体的字段)没什么好说的,因此讲字段的处理。
# awk 'NR>1{$4+=4;print $0}' a.txt
1 Bob male 32 abc@qq.com 18023394012
2 Alice female 28 def@gmail.com 18084925203
... ...
处理字段目前只接触到赋值,修改了字段值会导致使用OFS重建$0。
要想使得输出结果恢复重建前的效果,可以结合外部命令,例如该示例中的column。
# awk 'NR>1{$4+=4;print $0}' a.txt | column -t
1 Bob male 32 abc@qq.com 18023394012
2 Alice female 28 def@gmail.com 18084925203
... ...
或者在后续学会了字符串处理函数以后来实现。基本思路是取得$0重建前的$4的前后部分保留,然后修改$4的值,最后再将三部分组合。
awk 'NR>1{$6=$6"*";print $0}' a.txt
awk 'NR>1{$6=$6"*";print $0}' a.txt | column -t
数据筛选示例
该示例要求我们从ifconfig的输出结果中取得ipv4地址(不包含环回地址lo),该示例同时也是常见的运维面试题。
# ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255
inet6 fe80::7a4:5a06:46b4:9ce5 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:46:79:46 txqueuelen 1000 (Ethernet)
RX packets 3151 bytes 258273 (252.2 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1606 bytes 166414 (162.5 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 72 bytes 8088 (7.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 72 bytes 8088 (7.8 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
virbr0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
ether 52:54:00:a6:3d:cf txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
有3种思路来取得地址。
思路一:ipv4地址位于包含“inet ”(注意有空格)的记录,因此筛选出该记录。同时我们要过滤掉换回地址,因此$2不以127打头的记录。将2个条件使用逻辑与连接。
# ifconfig | awk '/inet /&&!($2~/^127/)'
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255
inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
思路二:ifconfig输出信息中包含了3段信息,每段表示1张网卡并以空行作为记录分隔符。因此结合我们在讲解RS时提到的,这里我们以段划分记录。记录不包含lo,同时我们取得ip地址所在的字段(手工数一下可知是第6字段)。
# ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'
192.168.152.100
192.168.122.1
思路三:基于思路二,假设ip地址所在的字段数比较靠后,那么我们就需要数好几个字段才可以数到ipv4地址,我们来看一下下面这个输出结果。
# ifconfig | awk 'BEGIN{RS=""}!/lo/{print "---"$0"---"}'
---ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255
inet6 fe80::7a4:5a06:46b4:9ce5 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:46:79:46 txqueuelen 1000 (Ethernet)
RX packets 3997 bytes 330636 (322.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2063 bytes 225016 (219.7 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0---
---virbr0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
ether 52:54:00:a6:3d:cf txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0---
按段划分并筛选记录以后,ipv4地址,从我们的视觉上来看,可以理解为每条记录的第2“行”。不过我们这里因为已经将整个网卡的信息(多行)理解为了1条记录(行)了,因此我们要将原本的第二行识别为第2个字段,即修改FS的值。
# ifconfig | awk 'BEGIN{RS=""}!/lo/{FS="\n";print $2}'
flags=4163<UP,BROADCAST,RUNNING,MULTICAST>
inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
输出结果中第一行并不是我们所期望的字段信息。这是因为根据RS=""读取记录以后会赋值$0,并按照FS的值(没有另外指定因此使用FS=" ")赋值$1...$NF各个位置参数。赋值完毕以后才执行main代码块赋值FS="\n",此时第一条记录的各位置参数已经确定好了。因此从第二条记录开始,$2才是我们所想要的信息。
我们只要将FS设置在BEGIN中即可,这也是为什么大多数情况下如果要修改默认的FS和RS都在BEGIN中设置。
# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{print $2}'
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255
inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
接下来我们将每条记录的$2赋值给记录$0本身,设置FS并取第2个字段的信息。按照下面的命令明显无法取正确。
ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";print $2}'
# 第一条空信息
# 第二条空信息,注意这两条空信息所取的字段是不同的。
原因此前我们也说了,修改$0($0=$2)会重新划分各字段,而FS=" "在修改$0之后出现,因此第一条记录依然是按照FS="\n"划分字段。
【第一条空信息】的$0是:inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255,根据FS,因此它也会是$1,因此$2为空。
想让【第一条空信息】取值正确的话,就要重新设置$0。
# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2}'
192.168.152.100
# 第二条空信息
【第二条空信息】取值错误的原因是从第二条记录开始,FS的值就一直是main中的" ",我们需要在main的结尾再将其设置回BEGIN中的值。
# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2;FS="\n"}'
192.168.152.100
192.168.122.1
在我们实际使用当中使用思路一和思路二取ipv4地址即可,思路三只是利于我们理解awk的工作原理,看不懂的同学多看看上面的【字段与记录的重建】。