AWK命令和SED命令

4,186 阅读6分钟

加班的日子也要抓紧时间学习哇🤩

为什么要用

对于我们后端人员来说,经常需要去服务器查找日志信息,排查详细错误信息或者监控服务器,强大如grep已经可以满足绝大部分需求,但是awk和sed这两个强大的命令工具也很好用,下面记录一下这两个工具如何使用。


AWK

引自百科:

AWK是一个优良的文本处理工具,Linux及Unix环境中现有的功能最强大的数据处理引擎之一。这种编程及数据操作语言(其名称得自于它的创始人阿尔佛雷德·艾侯、彼得·温伯格和布莱恩·柯林汉姓氏的首个字母)的最大功能取决于一个人所拥有的知识。awk经过改进生成的新的版本nawk,gawk,现在默认linux系统下日常使用的是gawk,用命令可以查看正在应用的awk的来源(ls -l /bin/awk )


开始使用

先查询一份网络数据,使用重定向保存下来:

$ netstat -ano > netstat.txt
$ awk '{print}' netstat.txt
Proto Recv-Q Send-Q Local Address           Foreign Address         State       Timer
tcp        0      0 0.0.0.0:8009            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:18090           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:41999           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:42801           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:42869           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:10050           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:8005          0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0.0.0.0:10050        192.168.120.1:7569      TIME_WAIT   timewait (27.08/0/0)

简单使用,输出第1列和第4列:

  • 其中单引号中的被大括号括起来的就是awk的语句,注意,其只能被单引号包含。
  • 其中的1....n表示第几列。($0表示整个行)
$ awk '{print $1,  $4}' netstat.txt
Proto Local
tcp 0.0.0.0:8009
tcp 0.0.0.0:18090
tcp 0.0.0.0:41999
tcp 0.0.0.0:8080
tcp 0.0.0.0:42801
tcp 0.0.0.0:42869
tcp 0.0.0.0:22
tcp 0.0.0.0:10050
tcp 127.0.0.1:8005

awk可以格式化输出,用过C和Java等语系的应该都挺熟悉:

$ awk '{printf "%-8s %-8s %-8s %-18s %-22s %-15s\n",$1,$2,$3,$4,$5,$6}' netstat.txt
Proto    Recv-Q   Send-Q   Local              Address                Foreign
tcp      0        0        0.0.0.0:8009       0.0.0.0:*              LISTEN
tcp      0        0        0.0.0.0:18090      0.0.0.0:*              LISTEN
tcp      0        0        0.0.0.0:41999      0.0.0.0:*              LISTEN
tcp      0        0        0.0.0.0:8080       0.0.0.0:*              LISTEN

过滤记录

可以对其中某个或某些字段进行判断,使用比较判断符即可(通过与或非进行连接)

比较判断符:==、!=、>=、>=、>、< 连接符:|| 、&&、 !

例如筛选出第三列大于0的数据行:

$ awk '$3 > 0 {print }' netstat.txt
Proto Recv-Q Send-Q Local Address           Foreign Address         State       Timer
tcp        0     48 10.96.0.33:22           0.0.0.0:42280       ESTABLISHED on (0.20/0/0)

如果需要打印行号的话,可以使用内建变量NR:

$ awk '$3 > 0 {print NR, $0 }' netstat.txt
1 Proto Recv-Q Send-Q Local Address           Foreign Address         State       Timer
23 tcp        0         0.0.0.0:22            0.0.0.0:42280     ESTABLISHED on (0.20/0/0)

内建变量

内建变量相当于该程序的内置变量,可以直接拿来使用:

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

例如有些文件的分隔符不是空格,而是其他,可以自定义分隔符:

$ awk 'BEGIN{FS=":"} {print $1, $3, $6}' /etc/passwd
nobody -2 /var/empty
root 0 /var/root

上面命令等价于:(-F的意思就是制定分隔符)

$ awk -F: '{print $1, $3, $6}' /etc/passwd

注意:如果要制定多个分隔符,可以使用:

awk -F '[;:]'

自定义输出字段的分隔符(例如制表符\t):

$ awk 'BEGIN{FS=":"} {print $1, $3, $6}' OFS="\t" /etc/passwd
nobody	-2	/var/empty
root	0	/var/root
daemon	1	/var/root

字符串匹配

可以跟grep一样,匹配相关字符:

$ awk '/LISTEN|WAIT/' netstat.txt
tcp        0      0 0.0.0.0:8009            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:18090           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:41999           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:42801           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:42869           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:10050           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:8005          0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:10050           0.0.0.0:7569      TIME_WAIT   timewait (27.08/0/0)

还可以精确匹配某个字段:

$ awk '$6 ~/LISTEN|CLOSE/ {print $0}' netstat.txt
tcp        0      0 0.0.0.0:8009            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:18090           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:41999           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:42801           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:42869           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:10050           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 127.0.0.1:8005          0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        1      0 0.0.0.0:44690           0.0.0.0:80            CLOSE_WAIT  off (0.00/0/0)
tcp6       0      0 :::27777                :::*                    LISTEN      off (0.00/0/0)
tcp6       0      0 :::10050                :::*                    LISTEN      off (0.00/0/0)

其中:~表示模式开始,两个/中间是模式,相当于进行正则表达式的匹配(同样道理,在正则表达式前使用!可以进行取反操作,这里就不展示了)


拆分文件

指定某列为分类符,使用重定向就可以导出到不同的文件中,例如下列语句通过第六列进行分割:

$ awk 'NR!=1 {print > $6}' netstat.txt
$ ls
ESTABLISHED          TIME_WAIT            netstat.txt          test.txt     CLOSE_WAIT           LISTEN               off

可以查看各自文件,发现已经是分类后的结果:

$ cat ESTABLISHED
tcp        0      0 0.0.0.0:38440        0.0.0.0:20880       ESTABLISHED keepalive (5806.94/0/0)
tcp        0      0 0.0.0.0:58814        0.0.0.0:20880        ESTABLISHED keepalive (5872.47/0/0)
tcp        0      0 0.0.0.0:49998        0.0.0.0:80        ESTABLISHED off (0.00/0/0)
tcp        0      0.0.0.03:22            0.0.0.02:42280    ESTABLISHED on (0.20/0/0)
tcp        0      0 0.0.0.0:56146        0.0.0.0:20892    ESTABLISHED keepalive (2661.21/0/0)

同样,可以指定某些列进行输出,也可以使用复杂的表达式(例如if-else-if语句,awk是个脚本解释器)

$ awk 'NR!=1{if($6 ~ /TIME|ESTABLISHED/) print > "1.txt";
else if($6 ~ /LISTEN/) print > "2.txt";
else print > "3.txt" }' netstat.txt
$ ls *.txt
1.txt       2.txt       3.txt       netstat.txt

注意,if-else语句要在同一个花括号{}里~


统计

下面语句是用来统计以.txt为后缀的文件总大小:

$ ll *.txt | awk '{sum+=$5} END {print sum}'
769.9

同样,可以在统计时使用数据,分开统计不同项目的总数

用来统计网络状态

$ awk 'NR!=1{a[$6]+=1} END {for (i in a) print i ", " a[i];}' netstat.txt
LISTEN, 11
CLOSE_WAIT, 1
TIME_WAIT, 26
off, 7
ESTABLISHED, 10

用来统计每个用户的进程占了多少内存:(通过ps -aux查看,第六列表示占用的内存)

$ ps -aux | awk 'NR!=1{a[$1]+=$6} END {for (i in a) print i ", " a[i]"kb";}'
apache, 156952kb
dbus, 1236kb
polkitd, 8252kb
named, 104220kb
libstor+, 148kb
mysql, 106304kb
root, 325464kb

awk脚本

有两个关键字需要注意BEGIN和END

  • BEGIN{这里放的是,执行前的语句}
  • END{这里放的是,执行后的语句}
  • {这里放的是处理每一行时要执行的语句}

例如下面例子,简单统计行数:(只是简单介绍一下脚本如何写,更多定制化的可以小伙伴去探索~)

$ vim cal.awk
#! /bin/awk -f
# 运行前
BEGIN {
    lineNumber = 0
    print "开始执行"
}
# 运行中
{
    lineNumber += 1
    printf "当前行号 %s, 数据为 %s \n", lineNumber, $0

}
# 运行后
END {
    print "结束"
}

执行脚本(也可以使用./cal.awk netstat.txt)

$ awk -f cal.awk netstat.txt
awk -f cal.awk netstat.txt
开始执行
当前行号 1, 数据为 Proto Recv-Q Send-Q Local Address           Foreign Address         State       Timer
当前行号 2, 数据为 tcp        0      0 0.0.0.0:8009            0.0.0.0:*               LISTEN      off (0.00/0/0)
当前行号 3, 数据为 tcp        0      0 0.0.0.0:18090           0.0.0.0:*               LISTEN      off (0.00/0/0)
当前行号 4, 数据为 tcp        0      0 0.0.0.0:41999           0.0.0.0:*               LISTEN      off (0.00/0/0)
当前行号 5, 数据为 tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      off (0.00/0/0)
当前行号 6, 数据为 tcp        0      0 0.0.0.0:42801           0.0.0.0:*               LISTEN      off (0.00/0/0)
当前行号 7, 数据为 tcp        0      0 0.0.0.0:42869           0.0.0.0:*               LISTEN      off (0.00/0/0)
结束

结合环境变量

通过使用-v参数和ENVIRON,与环境变量打交道:

$ echo $x
0
$ echo ${test}
LISTEN
$ awk -v val=${x} '$2==val || $6==ENVIRON["test"] { print $1, $2, $3}' netstat.txt
tcp 0 0
tcp 0 0
tcp 0 0
tcp 0 0
tcp 0 0

当然,在使用环境变量的时候,记得要export该变量


SED命令

全称是:Stream EDitor,功能月awk类似,差别在于,sed比awk简单一点,常用于字符替换。匹配符可以使用正则表达式进行匹配,所以想要用更强大的功能,需要去了解一下正则表达式~


使用S命令替换

使用以下文本进行测试

$ cat self.txt
First line
    My name is John
Second line
    I like milk
Third line
    I like basketball
Four line
    I live in china

S命令介绍:

s表示替换命令,/I/表示匹配I, /John/表示将前面的I替换成Json,/g表示进行所有行上所有字符的匹配(注意,匹配时区分大小写)

$ sed "s/I/John/g" self.txt
First line
    My name is John
Second line
    John like milk
Third line
    John like basketball
Four line
    John live in china

这样只是对于输出流的字符进行替换,原文本的数据不会改变,有两种方法可以修改输出流的数据:

  • 使用重定向 >
$ sed "s/I/John/g" self.txt > other.txt
  • 使用-i参数
$ sed -i "s/I/John/g" self.txt

可以在每一行最前面加数据:(例如将文本开头加个#字符)

$ sed "s/^/#/g" self.txt
sed "s/^/#/g" self.txt
#First line
#    My name is John
#Second line
#    I like milk
#Third line
#    I like basketball
#Four line
#    I live in china

可以在行末尾加一些数据:(例如加分割符或者结束符)

$ sed "s/$/---/g" self.txt
First line---
    My name is John---
Second line---
    I like milk---
Third line---
    I like basketball---
Four line---
    I live in china---

使用正则表达式去掉html中的tags:

Html文本为:

<b>This is</b><p> a test file</p><span style="text-decoration: underline;"> @John hahah</span>

替换脚本:

# 熟悉正则表达式的小伙伴应该不陌生
# 使用[^>]表示除了>之外的字符,*号表示任意个字符
$ sed "s/<[^>]*>//g" html.txt
This is a test file @John hahah

可以指定替换特定行号的文本,多行间使用逗号(,)进行连接:(例如下面只替换第5到第8行的数据)

$ sed "5,8s/I/John/g" self.txt
First line
    My name is John
Second line
    I like milk             #这行的I没有被替换
Third line
    John like basketball     #替换了
Four line
    John live in china       #替换了

测试文本:

$ cat 1.txt
This is first line.
This is second line.
This is third line.
This is four line.

指定只替换每一行第一个出现的字符:

$ sed "s/s/S/1" 1.txt
ThiS is first line.
ThiS is second line.
ThiS is third line.
ThiS is four line.

指定替换每一行第二个及第二个之后的字符:

$ sed "s/s/S/2g" 1.txt
This iS firSt line.
This iS Second line.
This iS third line.
This iS four line.

多个匹配

如果需要在一次命令中,匹配多个模式,可以写多个匹配表达式,通过分号;进行分割:(例如下面将1,2行的This替换成That,3到最后一行的is替换成are)

$ sed '1,2s/This/That/g; 3,$s/is/are/g' 1.txt
That is first line.
That is second line.
Thare are third line.   #没想到将This的is也替换成are了=-=
Thare are four line.    #可以修改sed '1,2s/This/That/g; 3,$s/is/are/2' 1.txt, 指定替换第二个字符is

可以使用&,用来当做被匹配的变量,然后在匹配的变量前后加一些内容:

$ sed 's/line/front [&] end/g' 1.txt
This is first front [line] end.
This is second front [line] end.
This is third front [line] end.
This is four front [line] end.

圆括号匹配

圆括号括起来的正则表达式所匹配的字符串可以当成变量来使用,其中,\1表示第一个匹配,\2表示第二个匹配,以此类推)

$sed 's/^This is \([A-Za-z]*\) line.$/\1/g' 1.txt
first
second
third
four

正则表达式博大精深,每次都是学到用时方恨少!


N命令

这条命令会将下一行的内容送入缓存区,两行并成一行进行匹配: (例如下面的命令,两行合成一行后,两行之间的换行符\n被替换成空格,所以输出结果,原偶数行与奇数行合并)

$ sed 'N;s/\n/ /g' self.txt
First line     My name is John
Second line     I like milk
Third line     I like basketball
Four line     I live in china

a命令和i命令

a:表示append,在某行后面进行追加:

$ sed '$ a --- the end ---' 1.txt
This is first line.
This is second line.
This is third line.
This is four line.
--- the end ---

i:表示insert,在某行之前进行插入:

$ sed '1 i --- the start ---' 1.txt
--- the start ---
This is first line.
This is second line.
This is third line.
This is four line.

可以进行匹配,然后在匹配行后面进行追加或者之前插入数据:

$ sed '/line/a  I find a line' 1.txt
This is first line.
I find a line
This is second line.
I find a line
This is third line.
I find a line
This is four line.
I find a line

c命令

C命令:替换命令,将匹配到的行替换掉

单行替换:

$ sed '2 c You have been replaced' 1.txt
This is first line.
You have been replaced
This is third line.
This is four line.

多行替换

$ sed '2,$ c You have been replaced' 1.txt
This is first line.
You have been replaced

或者匹配行进行替换:

$ sed '/This is/c You have been replaced' 1.txt
You have been replaced
You have been replaced
You have been replaced
You have been replaced

d命令

d命令:delete,删除 跟上面的替换c命令很类似

单行删除:

$ sed '2 d' 1.txt
This is first line.
This is third line.
This is four line.

多行替换

$ sed '2,$ d' 1.txt
This is first line.

或者匹配行进行替换:

$ sed '/This is/d' 1.txt
# 都木有了=-=

p命令

p命令:print,打印命令,与grep类似

# 匹配到third这一行,但是重复输出了
$ sed '/third/p' 1.txt
This is first line.
This is second line.
This is third line.
This is third line.
This is four line.
# 使用n参数进行过滤
$ sed -n '/third/p' 1.txt
This is third line.
# 多个模式匹配(两个模式中间的行次也会被匹配出来)
$ sed -n '/first/, /third/p' 1.txt
This is first line.
This is second line.
This is third line.
# 指定某一行到匹配行进行打印
$ sed -n '2,/four/p' 1.txt
This is second line.
This is third line.
This is four line.

命令打包

多个命令可以使用分号进行分开,使用大括号括起来作为嵌套命令:

# 从一二行中,匹配到This字符,然后整行进行删除
$ sed '1,2 {/This/d}' 1.txt
This is third line.
This is four line.
# 从一二行中,匹配到This字符,然后再匹配first字符,最后整行进行删除
$ sed '1,2 {/This/{/first/d}}' 1.txt
This is second line.
This is third line.
This is four line.
# 多命令用分号;分开
# 例如删掉包含first的行次,并且给每一行的开头加上#号
$ sed '1,$ {/first/d;s/^/#/g}' 1.txt
#This is second line.
#This is third line.
#This is four line.

总结

这两个文档工具如grep一样好用,而且感觉awk的功能更加强大,建议大家去看耗子叔的酷壳博客,干货满满~


参考资料

耗子大神的网站值得一看~

  1. AWK 简明教程
  2. SED 简明教程
  3. 正则表达式工具网站
  4. 正则表达式学习文档