grep, sed & awk

参考:man grep, man sed, man awk, Regular-Expressions.info, One-line scripts for sed, awk guide, awk in bash-scripts, Handy one-line scripts for awk, sed: by Bruce Barnett



grep

grep 是 Linux 中一个用来对文本内容进行搜索和匹配的命令行工具,是 “Global Regular Expression Print” 的简写,顾名思义,grep 与 “Regular Expression” 是密不可分的,学习 grep ,关键就是掌握正则表达式的用法。

grep是按行搜索的,且区分大小写,其格式为:

$ grep ‘需要匹配的字符串’ 文件名

下面我们就用这条命令来练习正则表达式。


正则表达式

正则表达式使用一系列特殊字符构建了一套规则,用于匹配字符串,这些特殊字符称为元字符。

. (一个点): 匹配除换行符之外的任意单个字符

$ grep 'r..t' /etc/passwd

r..t可以匹配root, reet, r t等,但不能匹配rat, rot等。

*: 匹配前一个字符0次或任意多次

$ grep 'r*t' /etc/passwd

输出含有t, rt, rrt等字符串的行。

.*”是一个很常用的组合,表示匹配任意次“.”,也就是匹配任意长度的任意字符。

$ grep 'r.*t' /etc/passwd

r.*t表示“字母r+任意长度任意字符+字母t”。

{n,m}: 匹配前一个字符的至少n次,最多m次

$ grep 'ro\{2\}t' /etc/passwd

匹配字母’o’两次。注意:花括号{}需要使用转义符。

$ grep 'ro\{2,\}t' /etc/passwd

匹配字母’o’至少两次。

$ grep 'ro\{,2\}t' /etc/passwd

匹配字母’o’最多两次。

^$: 匹配行首和行尾字符串

$ grep '^root.*h$' /etc/passwd

匹配以’root’开头,’h’结尾的行。^$匹配空行。

[ ]: 匹配括号内出现的任意单个字符

[ABCD]匹配A、B、C、D任一字母;[A-Z]匹配任意一个大写字母;[^A-D]匹配一个不是大写字母的字符(^[ ]内时表示取反)。

$ grep '^1[358][0-9]{9}' contact.txt

这条命令可以匹配手机号码。

\: 转义符,用于对特殊符号的“转义”

$ grep '022[\ \-][1-9][0-9]{7}' contact.txt

匹配类似”022 87401033”、”022-87401033”这样的电话号码。

注意:”!”在正则表达式中不是特殊符号,不需要转义。

\<\>: 界定单词的左边界和右边界

\<hello匹配以“hello”开头的单词,hello\>匹配以“hello”结尾的单词,\<hello\>可以精确匹配“hello”。

上面列举的这些元字符通用性好,易读易写,配合grep的语法足够满足一般的应用。此外,还有匹配换行符的\n,匹配回车符的\r,匹配制表符的\t,匹配换页符的\f,匹配任意空白字符的\s以及匹配任意非空白字符的\S等等,用法都比较简单。

[TOP]


grep 选项

$ grep [选项] 表达式 [FILE...]
选项:  
--color: 对匹配的字符串标记颜色显示
-i: 不区分大小写
-v: 只显示没有被匹配到的行
-n: 显示相应的行号
-c: 统计含有匹配字符串的总行数
-r, -R: 递归所有文件夹中的每一个文件
-l: 显示含有匹配字符串的文件名
-L: 显示没有匹配字符串的文件名
-H: 显示匹配字符串及相应的文件名
-h: 不显示字符串对应的文件名
-A N: 显示匹配字符串所在行及后N行
-B N: 显示匹配字符串所在行及前N行
-C N: 显示匹配字符串所在行及前N行和后N行
-E: 使用扩展的正则表达式


[TOP]


扩展的正则表达式

上文我们介绍的是基本的正则表达式,下面我们再简单了解一下扩展的正则表达式。

?: 匹配前一个字符0次或1次
+: 匹配前一个字符1次以上
|: “或”
( ): 字符串组,常配合|使用


$ grep -E ‘(nologin|false)$’ /etc/passwd

这是显示以”nologin”或”false”结尾的行。

[TOP]


关于引号

单引号是完全的引用,其中的内容不会做任何处理,完全保持原样输出给grep;双引号是部分的引用,其中的命令、变量会被解析、替换,再与其它内容一并输出给grep;不加引号时,如果字符串中含有空格,输出会不完整,因此最好还是根据需要选用单引号或双引号。

[TOP]


关于汉字匹配

需要匹配汉字时,有时候会涉及到编码问题,如“中文”,查到其utf-8编码和gb2312编码分别是E4B8ADE69687和D6D0CEC4,匹配命令可写作:

grep -rnP "\xE4\xB8\xAD\xE6\x96\x87|\xD6\xD0\xCE\xC4" file

关于汉字编码的知识,请看这里

[TOP]


sed

grep一样,sed也是按行处理文件;不同的是,sed命令具有编辑功能,是一种“流编辑器”(stream editor),因此,除了使用正则表达式匹配目标字符,还需要明确编辑类型,并指定编辑位置,这些功能使得sedgrep命令的格式更复杂一些。

对文本的编辑动作,包括替换、插入、删除等,下面我们通过“替换”动作来了解sed命令的操作方法。


s: 文本替换

$ sed 's/root/ROOT/' /etc/passwd

这条命令是将/etc/passwd中的“root”替换为“ROOT”,其中s是“替换”命令符,/是分隔符。

$ sed '1,5s/root/ROOT/' /etc/passwd

这是将第2行到第5行中的“root”替换为“ROOT”。需要指出的是,sed默认只替换一行中的第一个匹配项,如果要替换该行所有匹配项,需要在命令后加上g

$ sed '1,5s/root/ROOT/g'  /etc/passwd

如果只替换一行的第二个匹配项,应该这样写:

$ sed '1,5s/root/ROOT/2'  /etc/passwd

如果替换一行中第二个和以后的匹配项,应该这样写:

$ sed '1,5s/root/ROOT/2g'  /etc/passwd

如果替换一行中第一个和第三个匹配项,可以这样写:

$ sed '1,5s/root/ROOT/1;1,5s/root/ROOT/2'  /etc/passwd

也可以这样写:

$ sed -e '1,5s/root/ROOT/1' -e '1,5s/root/ROOT/2'  /etc/passwd

这里我们使用了一个选项-e,表示允许表达式有多个执行动作,动作之间用”;”分开。注意:第二个动作附加的是数字2而不是3!这表明多个动作是相继进行的,而不是同时进行的,因此,动作的顺序会影响最终的输出结果

sed处理后文件后会全部输出到屏幕,这样不方便查看处理结果,有时候我们更需要sed只输出经过处理的行,这时应该使用选项-n

上面几条命令中,sed仅将处理过的内容输出到屏幕,而不会影响文件本身,如果希望该结果覆盖原文件,应该使用选项-i

[TOP]


sed 选项

$ sed [选项] 表达式 [FILE...]
选项:  
-e: 允许表达式有多个动作,动作之间以”;”分开
-n: 只显示经过sed处理过的行
-i: sed输出结果保存到原文件
-r: 使用扩展的正则表达式


也可以先把动作编写成命令文件,然后调用该文件执行动作,此时需使用选项-f

$ sed [选项] -f [scriptFILE] [FILE...]


[TOP]


sed 常用动作

和上文的替换文本动作相比,下面这些动作就比较容易理解了。

ia: 添加行

$ sed '1 i This is my passwd file.'  /etc/passwd

i: insert,在匹配项所在行之前添加新行。

$ sed '$ a This is my passwd file.'  /etc/passwd

a: append,在匹配项所在行之后添加新行。这里的$表示最后一行。

$ sed '/false/a Attention!\Attention!\Attention!' /etc/passwd

在含有匹配字符“false’的行之后添加新行。

c: 替换行

$ sed '/false/c Attention 3 plese!\
Attention 2 plese!\
Attention 1 plese!' /etc/passwd

替换多行时,每行末尾应添加\表示命令尚未结束,上面的i命令和a命令中也有类似的用法。

d: 删除行

$ sed '/false/d' /etc/passwd

p: 打印

$ sed -n '1,/false/p' /etc/passwd

表示从第一行开始打印到匹配”false”成功的那一行。ps:p命令常与-n选项配合使用。

$ sed -n '/nobody/,/false/p' /etc/passwd

表示从匹配”nobody”成功的行开始打印,到匹配”false”成功的行时结束打印。

[TOP]


位置参数

指定行

$ sed '1 i This is my passwd file.'  /etc/passwd

指定行范围

$ sed '1,5s/root/ROOT/' /etc/passwd

正则匹配行

$ sed '/false/d' /etc/passwd
$ sed '/root/s/root/ROOT/2'  /etc/passwd

可使用正则表达式指定匹配项所在的行。注意:末尾的数字2表示对该行第2个匹配项执行s动作。

正则匹配行范围

$ sed -n '/nobody/,/false/p' /etc/passwd

从含有”nobody”的行开始打印,到含有”false”的行时结束打印。

[TOP]


其它常用项

&: 引用

$ sed 's/root/[&]/g' /etc/passwd

把”root”替换成”[root]”,&引用被匹配的字符串。

( ): 多项引用

$ sed 's/\(^.*\):x:.*no\(.*\)$/\1:never\2/g' /etc/passwd

( )配合使用\1\2…可实现多个项目的引用。注意:( )需要使用转义符。

!: 不执行

$ sed '1!s/root/[&]/g' /etc/passwd

不执行!后的动作。

q: 退出

$ sed '/false/q' /etc/passwd

发现含有”false”的行时,退出sed

=: 标记行号

$ sed '=' /etc/passwd | sed 'N;s/\n/\t/'

给文件加上行号。这里用到了一个新动作的命令N,要理解其动作方法,需要了解sed命令中“Pattern Space”和“Hold Space”的概念。

[TOP]


缓存空间

有时候我们需要合并两行文本,比如,把第1行和第2行合并成1行,而sed只能处理1行文本,怎么办?sed引入了一个能够暂存数据的区域,这个数据缓存区被称为Hold Space (辅缓存区,保持空间);相对的,正在处理数据的区域就被称为Pattern Space (主缓存区,模式空间)了。通常使用的都是主缓存区,而涉及到数据交换的时候,就需要用到辅缓存区了。

引入辅缓存区使sed的功能得到极大的拓展,常用的相关动作命令有:

n: 复制下一行到pattern
N: 追加下一行到pattern,以\n分隔
d: 清空pattern
D: 清空pattern中第一个\n之前的内容
p: 打印pattern
P: 打印pattern中第一个\n之前的内容
g: 复制hold到pattern
G: 追加hold到pattern,以\n分隔
h: 复制pattern到hold
H: 追加pattern到hold,以\n分隔
x: 交换pattern和hold


注:n-next, d-delete, p-print, g-get, h-hold, x-exchange。

$ sed '=' /etc/passwd | sed 'N;s/\n/\t/'

在这条命令中,管道符左侧的命令为文本加上了行号,但是行号是自成一行,因此,我们使用N命令,首先将当前行的下一行追加到主缓存区,然后把换行符\n替换为制表符\t,这样就实现了删除奇数行换行符的功能。

如果需要删除所有换行符,使用sed 'N;s/\n/\t/g'并不能实现预期的效果,简单的,可以使用tr

$ sed '=' /etc/passwd | tr -t '\n' '\t'

标签

如果使用sed实现删除所有换行符的功能,就需要引入标签跳转功能进行流程控制:

$ sed '=' /etc/passwd | sed ':label;N;s/\n/\t/;b label'

这是一个循环结构,:label是一个标签,b label指示命令跳转到”label”标签,”标签”可以任意命名,且与b之间的空格也不是必需的。注意:标签后面不要有空格

除了b跳转,还有t跳转和T跳转(bt分别是branchtest的缩写),如:

$ sed 's/false/FALSE/;t label;s/nologin/nologin:[NotPeople]/;b;:label;s/$/:[ALERT]/' /etc/passwd

如果’s/false/FALSE/’替换成功,则跳转到:label,执行s/$/:[ALERT]/;否则,则按顺序执行s/nologin/nologin:[NotPeople]/,然后就会被无标签的b跳转指示跳过之后的所有指令,完成这一行的处理动作。

注意:bt之后如果没有指定的标签,则会跳过此后的所有指令,开始下一行的处理任务。

$ sed ':again;$!N;s/\n/\t/;tagain' text.txt

这条命令也可以实现删除所有换行符的功能,这里需要使用t跳转,如果将tagain换成bagain就会陷入死循环。这是因为,t跳转是有条件的,即之前最后一个s///执行成功时跳转,而b则是无条件的,并不受s///执行结果的影响。与此类似,T也是有条件跳转,但与t相反,即当s///执行不成功时才跳转。

如果某一行以”-“开头,则将其合并到上一行

$ sed -e :again -e '$!N;s/\n-//;tagain' -e 'P;D' text.txt

[TOP]


awk 基础

cut

grepsed都是按行处理文本的,如果我们要读取文件的某一列,应该怎么做呢?最简单的方法是用cut

$ cut -f1,5-7 -d':' /etc/passwd

这条命令指定“:”为分隔符,打印出第1,5,6,7列。cut不能指定空格和Tab作为分隔符,如df -h的输出结果,就无法使用cut进行截取,这时,就需要使用强大的awk了。

AWK是一种编程语言,具有完整的变量赋值、运算与判断、函数调用、流程控制、输入输出等等功能,awk也是与grepsed并列的一个文本处理工具。

[TOP]


awk 记录

awk认为文件都是格式化的,每一行称为一个记录,一个记录由分隔符分成多个字段,$1$2…分别表示左起第一个字段、第二个字段…,$0表示全部字段,也就是当前记录、当前行。

awk常用的内建变量有:

$0: 当前记录,即当前行的全部字段及其分隔符
$n: 当前记录的第n个字段
FS: 字段分隔符,默认是空格和Tab
NF: 当前行中字段的个数,即列的个数
NR: 当前行数,如果是多个文件,这是一个累加值
FNR: 当前行数,仅指在当前单个文件中的行数
RS: 记录的分隔符,默认是换行符
OFS: 输出时,字段的分隔符,默认是空格
ORS: 输出时,行的分隔符,默认是换行符
FILENAME: 当前文件名


[TOP]


打印字段

$ df -h | awk '{print $1}'

打印出df -h的第一列。

awk命令是这样的:

$ awk [选项] '条件1 {动作1} 条件2 {动作2} ...' [FILE]

首先是选项,然后是命令语句,命令语句用单引号扩起来,一个条件加一个动作,动作用花括号扩起来。上边的命令中,简单地打印指定的列,所以没有条件参数;awk可以通过管道符接收前一个命令的输出结果。

$ ps aux | awk '$1 !~ /ro*t/ || NR==1 {print NR,$1,$11}' OFS="\t"
$ ps aux | awk '$1 !~ /ro*t/ || NR==1 {printf("%d\t%s\t%s\n", NR,$1,$11)}'

这两条命令的输出结果是一样的,printawk的内置函数,而printf是Linux的标准格式化输出函数,%d%s标记了变量的类型分别是十进制整数和字符串,常用的还有%c-单个字符,%o-八进制整数,%x-十六进制整数,%f-浮点数等。$1 !~ /ro*t/ || NR==1是执行条件,$1 !~ /ro*t/ 是正则匹配,与后边的NR==1进行“或”运算。这条命令用到了两个运算符:!~||

[TOP]


指定分隔符

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

也可以指定多个分隔符:

$ awk -F '[;:]'

还可以这样指定分隔符:

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

这是把文件的第一列提取出来。BEGINawk的一个特殊的条件表达式,表示在编辑语句{print $1 "\t" $3}开始执行之前,先执行FS=":"来指定分隔符。类似的还有END,表示在所有编辑语句完成之后执行。

为什么要使用BEGIN?如果把BEGIN去掉,可以发现输出结果中的第一行没有按照预期完成截取,这是因为,如果不进行流程控制,awk各个动作是同时执行的。

[TOP]


重定向

$ ps aux | awk 'NR!=1 {print > $1}'

这是把指定的行通过重定向输出到新建文件。

$ ps aux | awk '{print $1,$11}' > USERCOMMAND

这是把指定的列通过重定向输出到新建文件。

[TOP]


运算符

数学: +,-,*,/,%(余数),^(指数)
赋值: =,+=,-=,*=,/=,%=,^=
逻辑: &&,||,!
关系: >,>=,<,<=,==,!=,~(匹配),!~
条件: 判断条件 ? value1:value2


运算优先级:

$(字段标记)
^
!
*,/,%
+,-
>,>=,<,<=,==,!=
~,!~
&&
||
?(条件运算符)
=,+=,-=,*=,/=,%=,^=


$ ps aux | awk 'NR!=1{a[$1]+=$6;} END { for(i in a) print i ", " a[i]"KB";}'

这条命令中用到了数组的赋值运算,还用到了for循环,awk的功能强大,正是因为它具有完整的变量和流程控制,除此之外,awk还有非常丰富的内置函数,进一步学习这方面的知识,请点击这里

[TOP]


awk 脚本

$ awk [选项] -f [scriptFILE] [FILE...]

awk可以这样调用编写好的命令文件,也像shell脚本一样直接调用,awk脚本文件的格式为:

#!/bin/awk -f
#声明脚本调用方式,注意-f后有一个空格
BEGIN{
#这一行的"{"要紧跟在BEGIN之后,不能换行
#BEGIN用来完成初始工作
}
{
#这里是脚本的主体部分
}
END{
#这一行的"{"要紧跟在END之后,不能换行
#END用来完成变量的恢复工作
}

保存为script.awk,然后加上可执行权限:

$ chmod +x script.awk

这样就可以直接执行脚本了:

$ ./script.awk [FILE]

[TOP]