awk命令篇

运维基础 2018-07-04

awk命令(基础篇)

首先awk命令是一个历史悠久的命令,最早于1977年由贝尔实验室开发,但是其功能异常强大,堪称文本处理神器。它支持用户自定义函数和动态正则表达式等先进功能,是linux/unix下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。awk有很多内建的功能,比如数组、函数等,这是它和C语言的相同之处,灵活性是awk最大的优势。

语法形式(基本)

awk '条件类型1{动作1} 条件类型2{动作2} 条件类型3{动作3}...' filename

awk命令类似于简单文本编程:对满足特定条件的文字,进行特定处理。

基本用法

我先取netstat -ano命令的返回结果的前几行创建一个新的文件,命名为netstat.txt,内容如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0     64 192.168.248.138:22          192.168.248.1:11403         ESTABLISHED on (0.24/0/0)
udp        0      0 127.0.0.1:789               0.0.0.0:*                   TIME_WAIT   off (0.00/0/0)
udp        0      0 0.0.0.0:44785               0.0.0.0:*                   FIN_WAIT1   off (0.00/0/0)
udp        0      0 0.0.0.0:631                 0.0.0.0:*                   FIN_WAIT2   off (0.00/0/0)

awk默认以空格和Tab键作为字段分隔符,以$1,$2...$n作为被分割的字段,比如

基本示例1:输出指定文本字段,使用print

awk '{print $4 "\t" $6}' netstat.txt

输出结果如下

Local-Address    State
0.0.0.0:22    LISTEN
127.0.0.1:631    LISTEN
0.0.0.0:3306    LISTEN
192.168.248.138:22    ESTABLISHED
127.0.0.1:789    TIME_WAIT
0.0.0.0:44785    FIN_WAIT1
0.0.0.0:631    FIN_WAIT2

这个时候注意两个问题:

1.想分开输出的字段可以使用制表符,但是要被双引号扩起。
2.使用了制表符之后发现显示还是无法很好对齐,这个时候我们可以和C语言一样,使用格式化字符串输出printf

格式化字符串格式

格式     描述
%d     十进制有符号整数
%u     十进制无符号整数
%f     浮点数
%s     字符串
%c     单个字符
%p     指针的值
%e     指数形式的浮点数
%x     %X 无符号以十六进制表示的整数
%o     无符号以八进制表示的整数
%g     自动选择合适的表示法

因此上面内容想要显示整齐,可使用格式化字符串进行输出

基本示例2:格式化字符串输出文本字段,使用printf

awk '{printf "%-22s %-8sn",$4,$6}' netstat.txt

输出结果如下:

Local-Address          State   
0.0.0.0:22             LISTEN  
127.0.0.1:631          LISTEN  
0.0.0.0:3306           LISTEN  
192.168.248.138:22     ESTABLISHED
127.0.0.1:789          TIME_WAIT
0.0.0.0:44785          FIN_WAIT1
0.0.0.0:631            FIN_WAIT2

然后对于/etc/passwd文件我想修改awk的默认分隔符该怎么办?

基本示例3:指定分隔符输出字段,使用内建变量FS

同样格式化输出一下

awk '{FS=":"}{printf "%-10s %-8sn",$1,$3}' /etc/passwd

输出结果如下:

root:x:0:0:root:/root:/bin/bash         
bin        1       
daemon     2       
adm        3       
lp         4       
sync       5       
shutdown   6       
halt       7       
mail       8     

还是两个问题:

1.使用了内建变量,内建变量是什么
2.第一行处理结果不符合预期要求

内建变量

内建变量是awk命令自带的一些非常使用的命令,能够指定任意分隔符,显示行号和字段数等等,具体内容如下:

$0          当前记录(这个变量中存放着整个行的内容)
$1~$n      当前记录的第n个字段,字段间由FS分隔
FS          输入字段分隔符 默认是空格或Tab
NF          当前记录中的字段个数,就是有多少列
NR          已经读出的记录数,就是行号,从1开始,如果有多个文件话,这个值也是不断累加中。
FNR      当前记录数,与NR不同的是,这个值会是各个文件自己的行号
RS          输入的记录分隔符, 默认为换行符
OFS      输出字段分隔符, 默认也是空格
ORS      输出的记录分隔符,默认为换行符
FILENAME 当前输入文件的名字

内建变量示例1:输出当前行是第几行数据,使用NR

awk '{print NR "t" $4 "\t" $6}' netstat.txt

输出结果如下:

1    Local-Address    State
2    0.0.0.0:22    LISTEN
3    127.0.0.1:631    LISTEN
4    0.0.0.0:3306    LISTEN
5    192.168.248.138:22    ESTABLISHED
6    127.0.0.1:789    TIME_WAIT
7    0.0.0.0:44785    FIN_WAIT1
8    0.0.0.0:631    FIN_WAIT2
9

内建变量示例2:指定输出分隔符,使用OFS

如上面的示例1,要输出3个变量,需要加两个t,那么要输出n个变量,就需要加n-1个t,这个就比较麻烦了,此时使用OFS指定输出变量的分隔符会非常方便

awk '{print NR,$4,$6} BEGIN{OFS="t"}' netstat.txt

BEGIN的作用后文会提到,输出结果如下:

1    Local-Address    State
2    0.0.0.0:22    LISTEN
3    127.0.0.1:631    LISTEN
4    0.0.0.0:3306    LISTEN
5    192.168.248.138:22    ESTABLISHED
6    127.0.0.1:789    TIME_WAIT
7    0.0.0.0:44785    FIN_WAIT1
8    0.0.0.0:631    FIN_WAIT2
9        

内建变量示例3:输出当前行的字段数,使用NF

awk '{print NF "t" $4 "\t" $6}' netstat.txt

输出结果如下:

7    Local-Address    State
8    0.0.0.0:22    LISTEN
8    127.0.0.1:631    LISTEN
8    0.0.0.0:3306    LISTEN
8    192.168.248.138:22    ESTABLISHED
8    127.0.0.1:789    TIME_WAIT
8    0.0.0.0:44785    FIN_WAIT1
8    0.0.0.0:631    FIN_WAIT2
0        

显示的是处理前每一行,awk命令根据分隔符得到的字段数

回到基本示例3的输出结果,发现第一行是原样输出的,并没有处理

原因:awk使用内建变量,默认从第二行开始生效

此时我们使用awk脚本,可以改变内建变量生效的时间

awk脚本(基础)

关于awk脚本,这个可以说是awk命令最为强大的部分了,awk同样支持条件,选择,循环等复杂操作。关于复杂的命令我们在awk进阶部分介绍,此处先看一下基本用法。

我们需要注意两个关键词BEGIN和END。

BEGIN{ 这里面放的是执行前的语句 }
END {这里面放的是处理完所有的行后要执行的语句 }

所以我们可以通过添加BEGIN的方式,让文本提前进行分隔处理,

awk 'BEGIN{FS=":"}{print $1 "\t" $3}' /etc/passwd

输出结果正常:


root    0
bin    1
daemon    2
adm    3
lp    4
sync    5
shutdown    6
halt    7
mail    8
uucp    10

本例添加开头输出"Hello world",结尾输出"This is result! Thank you for using awk."

awk 'BEGIN{FS=":";print "Hello world"}{print $1 "\t" $3} END{print "This is result! Thank you for using awk."}' /etc/passwd

输出结果正常:

Hello world
root    0
bin    1
daemon    2
adm    3
lp    4
sync    5
shutdown    6
halt    7
mail    8
uucp    10
This is result! Thank you for using awk.

除了使用内建变量FS指定分隔符,通用可以使用-F参数指定分隔符,因为没有使用内建变量,此时就不用BEGIN

上面命令等价于

awk -F ":" '{print $1 "\t" $3}' /etc/passwd

输出结果如下:

root    0
bin    1
daemon    2
adm    3
lp    4
sync    5
shutdown    6
halt    7
mail    8
uucp    10

解决了这个问题,我们继续想:我想输出/etc/passwd中第三个字段(uid)小于5的用户名和uid呢?

这个时候我们可以使用awk命令的逻辑运算功能解决这个问题

逻辑运算

常用逻辑运算符和描述如下:

运算符                        描述
= += -= *= /= %= ^= **=    赋值
?:                        C条件表达式
||                        逻辑或
&&                        逻辑与
~ ~!                    匹配正则表达式和不匹配正则表达式
< <= > >= != ==            关系运算符
空格    连接
+ -                        加,减
* / %                    乘,除与求余
+ - !                    一元加,减和逻辑非
^ ***                    求幂
++ --                    增加或减少,作为前缀或后缀
$                        字段引用
in                        数组成员

基本示例4:输出/etc/passwd中uid小于5的用户name和uid,使用关系运算符<

awk 'BEGIN{FS=":"}$3<5{print $1 "t" $3}' /etc/passwd

输出结果如下:

root    0
bin    1
daemon    2
adm    3
lp    4

逻辑运算示例1:输出netstat.txt中Send-Q字段为0且State字段为LISTEN的行,且保留表头部分

Send-Q字段和State字段使用&&表示关系且

保留表头使用或NR==1不使用前面的规则

awk '$3==0 && $6=="LISTEN" || NR==1 ' netstat.txt

输出结果如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)

逻辑运算示例2:输出满足特定行号要求的行,比如输出行号为偶数的行,且保留表头

awk 'NR%2==0 || NR==1{print NR,$4,$6}' netstat.txt

输出结果如下:

1  Local-Address  State
2  0.0.0.0:22     LISTEN
4  0.0.0.0:3306   LISTEN
6  127.0.0.1:789  TIME_WAIT
8  0.0.0.0:631    FIN_WAIT2

注意awk命令格式永远是条件在前,动作在后(满足某条件才执行某动作)

逻辑运算示例3:使用算术运算符++

awk 'BEGIN{a="1";print a++,++a;}'

输出结果是

1  3

我们使用算术运算符,注意一点

注意:所有用作算术运算符进行操作,操作数自动转为数值,所有非数值都变为0

具体看一下

awk 'BEGIN{a="b";print a++,++a;}'

输出结果:

0  2

因为非数值都会转换为0,所以a="b"一项,就变成了a=0,因此a++和++a就变成了0和2

逻辑运算示例4:输出netstat.txt中第六个字段state为LISTEN的行,保留表头,使用关系运算符进行查找==

awk '$6=="LISTEN" ||NR==1' netstat.txt

输出结果如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)

awk命令(进阶篇)

正则匹配模式

awk命令可以实现和grep命令一样的功能:对文本内容进行匹配,使用/ /,进入正则匹配模式,使用~表示模式开始

正则匹配示例1:找出netstat.txt中包含LISTEN的行,保留表头

awk '/LISTEN/||NR==1' netstat.txt

输出结果如下

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)

正则匹配示例2:输出netstat.txt中第六个字段state为LISTEN的行,保留表头

awk '$6 ~ /LISTEN/||NR==1' netstat.txt

输出结果如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)

似乎和使用关系运算符==查找没什么区别啊?

awk '$6=="LISTEN" ||NR==1' netstat.txt

其实如果查找文本是单个字符串的全匹配,那么确实区别不大

如果是其他匹配:包含匹配、多匹配、反选等等功能,正则匹配的优势一下就表现出来了

正则匹配示例3:输出netstat.txt中第六个字段state包含WAIT的行,保留表头,包含匹配问题,使用~ / /可以解决

属于包含匹配,此时使用==就无法解决这个问题了

awk '$6 ~ /WAIT/||NR==1' netstat.txt

输出结果如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
udp        0      0 127.0.0.1:789               0.0.0.0:*                   TIME_WAIT   off (0.00/0/0)
udp        0      0 0.0.0.0:44785               0.0.0.0:*                   FIN_WAIT1   off (0.00/0/0)
udp        0      0 0.0.0.0:631                 0.0.0.0:*                   FIN_WAIT2   off (0.00/0/0)

可以看到TIME_WAITFIN_WAIT1FIN_WAIT2的行都输出了

正则匹配示例4:输出netstat.txt中第六个字段state包含FIN或者TIME的行,保留表头,多匹配问题使用~ / | /可以解决

awk '$6 ~ /FIN|TIME/||NR==1' netstat.txt

输出结果如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
udp        0      0 127.0.0.1:789               0.0.0.0:*                   TIME_WAIT   off (0.00/0/0)
udp        0      0 0.0.0.0:44785               0.0.0.0:*                   FIN_WAIT1   off (0.00/0/0)
udp        0      0 0.0.0.0:631                 0.0.0.0:*                   FIN_WAIT2   off (0.00/0/0

使用或运算符|,可以看到包含FIN和TIME的选项都输出了

正则匹配示例5:输出netstat.txt中第六个字段state不包含FIN的行,保留表头,取反问题使用!~ / /可以解决

awk '$6 !~ /FIN/||NR==1' netstat.txt

输出结果如下:

 
Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0     64 192.168.248.138:22          192.168.248.1:11403         ESTABLISHED on (0.24/0/0)
udp        0      0 127.0.0.1:789               0.0.0.0:*                   TIME_WAIT   off (0.00/0/0)

可以看到使用了!~取反之后,包含FIN的行都不见了

正则匹配示例6:输出netstat.txt中第六个字段state以WAIT1结尾的行,保留表头

既然是正则模式,那么同样可以使用正则表达式的常用符号? * ^ $等等,所以这道题的解决方式就是

awk '$6 ~ /WAIT1$/||NR==1' netstat.txt

输出结果如下:

Proto Recv-Q Send-Q Local-Address               Foreign-Address             State       Timer
udp        0      0 0.0.0.0:44785               0.0.0.0:*                   FIN_WAIT1   off (0.00/0/0)

拆分文件

我们使用重定向功能>根据字段内容的不同,拆分文件的行,并将其保存为指定格式文件

拆分文件示例:将netstat.txt根据第六个字段state内容的不同将文件拆分,拆分文件保存为txt格式,表头不处理

awk 'NR!=1{print >$6".txt"}' netstat.txt

执行之后结果如下:

[root@localhost test]# ll
总用量 28
-rw-r--r-- 1 root root 102 2月   8 04:09 ESTABLISHED.txt
-rw-r--r-- 1 root root 103 2月   8 04:09 FIN_WAIT1.txt
-rw-r--r-- 1 root root 103 2月   8 04:09 FIN_WAIT2.txt
-rw-r--r-- 1 root root 309 2月   8 04:09 LISTEN.txt
-rw-r--r-- 1 root root 815 2月   8 04:08 netstat.txt
-rw-r--r-- 1 root root  94 2月   8 04:09 State.txt
-rw-r--r-- 1 root root 103 2月   8 04:09 TIME_WAIT.txt

可以看到,文件已经按照state内容的不同分成了好几个txt文件,任意打开一个看一下

[root@localhost test]# cat LISTEN.txt 
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:631               0.0.0.0:*                   LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN      off (0.00/0/0)

果然符合要求

awk脚本(进阶篇)

基本结构

awk 'BEGIN{ print "start" } pattern{ commands } END{ print "end" }' file

进阶脚本示例1:对输出文件的开头和结尾分别加上“output start”和“output end”

echo "Hello world"|awk 'BEGIN{print "output begin"}{print }END{print "output end"}'

输出结果如下:

output begin
Hello world
output end

此方法可以亦用于美化输出结果

脚本处理还支持条件、选择、循环等常见编程语言语句

进阶脚本示例2:给定一个变量test,如果test>90,就输出very good,

如果大于60小于90,输出pass,否则输出no pass

使用条件判断if-else语句

awk 'BEGIN{
test=100;
if(test>90){
  print "very good";
  }
  else if(test>60){
    print "pass";
  }
  else{
    print "no pass";
  }
}'

输出结果就是very good

进阶脚本示例3:使用awk语句输出1到100累加和,此时可以使用循环语句,此处以for循环为例

awk 'BEGIN{
total=0;
for(i=0;i<=100;i++){
  total+=i;
}
print total;
}'

输出结果为5050

for循环语句格式为

for(变量;条件;表达式)
{语句}

while循环和do-while循环与for循环类似,此处只给出两者语句格式,不再举例

while语句

while(表达式)
 {语句}

do-while语句

do
{语句} while(条件)

awk同样支持break和continue,效果和常规编程语言类似

语法形式进阶

awk [options] -f scriptfile var=value file(s)

之前提到awk命令最强大的地方在于脚本,我们同样可以编写awk格式的脚本,将其命名为xxx.awk,chmod赋予文件执行权限

开头以#!/bin/awk -f标识是awk脚本,然后以命令行方式,对某文件运用该awk脚本进行处理

进阶示例:成绩处理

我们有一个score.txt文件,每一行内容由一下内容组成

姓名  学号  数学成绩  英语成绩  计算机成绩

具体内容如下

Marry   2143 78 84 77
Jack    2321 66 78 45
Tom     2122 48 77 71
Mike    2537 87 97 95
Bob     2415 40 57 62

现在添加开头

NAME    NO.   MATH  ENGLISH  COMPUTER   TOTAL
---------------------------------------------

格式化输出成绩

每人成绩最后添加个人总分

结尾输出各科总分和平均分

使用awk脚本处理首先使用编写awk脚本,开头很好解决,数据读入很简单,只是应该如何处理读入的数据?

和常规编程思路类似:先定义3个变量累加每一行的数据(得各科总分),然后除以人数,可用NR统计(得各科平均分),个人总分在输出部分以三个变量相加的方式给出,最后脚本如下

#!/bin/awk -f
#运行前
BEGIN {
    math = 0
    english = 0
    computer = 0
 
    printf "NAME    NO.   MATH  ENGLISH  COMPUTER   TOTAL\n"
    printf "---------------------------------------------\n"
}
#运行中
{
    math+=$3
    english+=$4
    computer+=$5
    printf "%-6s %-6s %4d %8d %8d %8d\n", $1, $2, $3,$4,$5, $3+$4+$5
}
#运行后
END {
    printf "---------------------------------------------\n"
    printf "  TOTAL:%10d %8d %8d \n", math, english, computer
    printf "AVERAGE:%10.2f %8.2f %8.2f\n", math/NR, english/NR, computer/NR
}

保存为score.awk

使用chmod +x score.awk赋予执行权限

然后使用awk -f对score.txt文件执行脚本

awk -f score.awk score.txt

输出结果如下:

NAME    NO.   MATH  ENGLISH  COMPUTER   TOTAL
---------------------------------------------
Marry  2143     78       84       77      239
Jack   2321     66       78       45      189
Tom    2122     48       77       71      196
Mike   2537     87       97       95      279
Bob    2415     40       57       62      159
---------------------------------------------
  TOTAL:       319      393      350
AVERAGE:     63.80    78.60    70.00

和前面使用awk命令不会改变文件内容不同,对文件使用了awk脚本之后,文件的内容可能发生改变

也可以这样运行awk脚本

./score.awk score.txt

环境变量交互

使用-v参数和ENVIRON,使用ENVIRON的环境变量需要export

使用-v可以将命令行中的变量参与文本处理

环境变量示例1:给定上述score.txt,让每人的math成绩$3增加5

awk -v var=5 '$3+=var{print}' OFS="t" score.txt

输出结果如下:

Marry    2143    83    84    77    
Jack    2321    71    78    45    
Tom        2122    53    77    71    
Mike    2537    92    97    95    
Bob        2415    45    57    62    

注意两点:

1.变量名前不用加$
2.使用+=而不要使用+

环境变量示例2:给定上述score.txt,让每人的math成绩$3增加5,要求使用用户变量

其实就是添加一句定义用户变量

[root@localhost test]# x=5
[root@localhost test]# awk -v var=$x '$3+=var{print}' OFS="\t" score.txt 
Marry    2143    83    84    77    
Jack    2321    71    78    45    
Tom        2122    53    77    71    
Mike    2537    92    97    95    
Bob        2415    45    57    62    

使用用户变量的坏处就是一旦换一个bash环境或者关机重启了,就会失效,但是我们可以使用export将用户变量转换成环境变量

环境变量示例3:给定上述score.txt,让每人的math成绩$3增加5,要求使用环境变量

如果只使用环境变量,不用用户变量,那么不需要-v,而需要ENVIRON["环境变量名"]

[root@localhost test]# x=5
[root@localhost test]# export x
[root@localhost test]# awk '$3+=ENVIRON["x"]{print}' OFS="\t" score.txt 
Marry    2143    83    84    77    
Jack    2321    71    78    45    
Tom        2122    53    77    71    
Mike    2537    92    97    95    
Bob        2415    45    57    62    

使用组合技巧

组合示例1:从file文件中找出长度大于80的行

awk 'length>80' filename

组合示例2:按连接数查看客户端IP

netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr

组合示例3:打印99乘法表

seq 9 | sed 'H;g' | awk -v RS='' '{for(i=1;i<=NF;i++)printf("%dx%d=%d%s", i, NR, i*NR, i==NR?"n":"t")}'


本文由 古月蓝旻 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论