192. Linux中的命令之文本处理

sed

Linux sed 命令是利用脚本来处理文本文件,它可依照脚本的指令来处理、编辑文本文件。

Sed 主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等。

sed 会根据脚本命令来处理文本文件中的数据,这些命令要么从命令行中输入,要么存储在一个文本文件中,此命令执行数据的顺序如下:

  1. 每次 仅读取一行 内容;
  2. 根据提供的规则命令匹配并修改数据。注意,sed 默认不会直接修改源文件数据,而是会将数据复制到缓冲区中,修改也仅限于缓冲区中的数据;
  3. 将执行结果输出。

当一行数据匹配完成后,它会继续读取下一行数据,并重复这个过程,直到将文件中所有数据处理完毕。

语法

1
sed [-hnV][-e<script>][-f<script文件>][文本文件]

参数说明

参数 说明
-e<script>--expression=<script> 以选项中指定的script来处理输入的文本文件。
-f<script文件>--file=<script文件> 以选项中指定的script文件来处理输入的文本文件。
-h--help 显示帮助。
-n--quiet--silent 仅显示script处理后的结果。
默认情况下,sed 会在所有的脚本指定执行完毕后,会自动输出处理后的内容,而该选项会屏蔽启动输出,需使用 print 命令来完成输出。
--quiet 仅显示脚本处理后的结果
--silent 仅显示脚本处理后的结果
-r 支持扩展正则表达式
-V--version 显示版本信息。

操作说明

  • a :新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)
  • c :取代, c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行
  • d :删除,因为是删除,所以 d 后面通常不接任何内容
  • i :插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行)
  • p :打印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行
  • s :取代,可以直接进行取代的工作哩!通常这个 s 的动作可以搭配正规表示法!例如 1,20s/old/new/g

sed脚本命令

sed s 替换脚本命令

基本格式

1
[address]s/pattern/replacement/flags
  • address 表示指定要操作的具体行
  • pattern 指的是需要替换的内容
  • replacement 指的是要替换的新内容

常用的 flags 标记

flags 标记 功能
n 1~512 之间的数字,表示指定要替换的字符串出现第几次时才进行替换.
例如,一行中有 3 个 A,但用户只想替换第二个 A,这是就用到这个标记;
g 对数据中所有匹配到的内容进行替换,如果没有 g,则只会在第一次匹配成功时做替换操作。
例如,一行数据中有 3 个 A,则只会替换第一个 A;
p 会打印与替换命令中指定的模式匹配的行。此标记通常与 -n 选项一起使用。
w file 将缓冲区中的内容写到指定的 file 文件中;
& 用正则表达式匹配的内容进行替换;
\n 匹配第 n 个子串,该子串之前在 pattern 中用 () 指定。
\ 转义(转义替换部分包含:&、\ 等)。

注意

替换类似文件路径的字符串会比较麻烦,需要将路径中的正斜线进行转义,如:

1
sed 's/\/bin\/bash/\/bin\/csh/' /etc/passwd

sed d 替换脚本命令

基本格式

1
[address]d

sed a 和 i 脚本命令

  • a 命令表示在指定行的后面附加一行
  • i 命令表示在指定行的前面插入一行

基本格式

1
[address]a \新文本内容
1
[address]i \新文本内容

多行

如果你想将一个多行数据添加到数据流中,只需对要插入或附加的文本中的每一行末尾(除最后一行)添加反斜线即可

sed c 替换脚本命令

c 命令表示将指定行中的所有内容,替换成该选项后面的字符串。该命令的基本格式为:

1
[address]c\用于替换的新文本

sed y 转换脚本命令

y 转换命令是唯一可以处理单个字符的 sed 脚本命令,其基本格式如下:

1
[address]y/inchars/outchars/

转换命令会对 inchars 和 outchars 值进行一对一的映射,即 inchars 中的第一个字符会被转换为 outchars 中的第一个字符,第二个字符会被转换成 outchars 中的第二个字符…这个映射过程会一直持续到处理完指定字符。如果 inchars 和 outchars 的长度不同,则 sed 会产生一条错误消息。

sed p 打印脚本命令

p 命令表示搜索符号条件的行,并输出该行的内容,此命令的基本格式为:

1
[address]p

sed w 脚本命令

w 命令用来将文本中指定行的内容写入文件中,此命令的基本格式如下:

1
[address]w filename

这里的 filename 表示文件名,可以使用相对路径或绝对路径,但不管是哪种,运行 sed 命令的用户都必须有文件的写权限。

sed r 脚本命令

r 命令用于将一个独立文件的数据插入到当前数据流的指定位置,该命令的基本格式为:

1
[address]r filename

sed 命令会将 filename 文件中的内容插入到 address 指定行的后面

sed q 退出脚本命令

q 命令的作用是使 sed 命令在第一次匹配任务结束后,退出 sed 程序,不再进行对后续数据的处理。

sed 脚本命令的寻址方式

对各个脚本命令来说,address 用来表明该脚本命令作用到文本中的具体行

默认情况下,sed 命令会作用于文本数据的所有行。如果只想将命令作用于特定行或某些行,则必须写明 address 部分,表示的方法有以下 2 种:

  1. 以数字形式指定行区间;
  2. 用文本模式指定具体行区间。

以上两种形式都可以使用如下这 2 种格式,分别是:

1
[address]脚本命令

或者

1
2
3
address {
多个脚本命令
}

例如:

1
2
3
4
$ sed -n '/3/{
> p
> s/line/test/p
> }' data6.txt

以数字形式指定行区间

当使用数字方式的行寻址时,可以用行在文本流中的行位置来引用。sed 会将文本流中的第一行编号为 1,然后继续按顺序为接下来的行分配行号。

在脚本命令中,指定的地址可以是 单个行号,或是 用起始行号、逗号以及结尾行号指定的一定区间范围内的行

用文本模式指定行区间

sed 允许指定文本模式来过滤出命令要作用的行,格式如下:

1
/pattern/command

注意,必须用正斜线将要指定的 pattern 封起来,sed 会将该命令作用到包含指定文本模式的行上。

sed 允许在文本模式==使用正则表达式指明作用的具体行==

sed 多行命令

命令 说明
Next 命令(N) 将数据流中的下一行加进来创建一个多行组来处理。
Delete(D) 删除多行组中的一行。
Print(P) 打印多行组中的一行。

注意,以上命令的缩写,都为大写。

例子

在testfile文件的第四行后添加一行,并将结果输出到标准输出,在命令行提示符下输入如下命令:

1
sed -e 4a\newLine testfile

首先查看testfile中的内容如下:

1
2
3
4
5
6
#查看testfile 中的内容
$ cat testfile
HELLO LINUX!
Linux is a free unix-type opterating system.
This is a linux testfile!
Linux test

使用sed命令后,输出结果如下:

1
2
3
4
5
6
7
#使用sed 在第四行后添加新字符串
$ sed -e 4a\newline testfile
HELLO LINUX!
Linux is a free unix-type opterating system.
This is a linux testfile!
Linux test
newline

以行为单位的新增/删除

/etc/passwd 的内容列出并且列印行号,同时,请将第 2~5 行删除!

1
2
3
4
5
[root@www ~]# nl /etc/passwd | sed '2,5d'
1 root:x:0:0:root:/root:/bin/bash
6 sync:x:5:0:sync:/sbin:/bin/sync
7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
.....(后面省略).....

sed 的动作为 ‘2,5d‘ ,那个 d 就是删除!因为 2-5 行给他删除了,所以显示的数据就没有 2-5 了. 另外,注意一下,原本应该是要下达 sed -e 才对,没有 -e 也行啦!同时也要注意的是, sed 后面接的动作,请务必以 '' 两个单引号!

只要删除第 2 行

1
nl /etc/passwd | sed '2d'

要删除第 3 到最后一行

1
nl /etc/passwd | sed '3,$d'

在第二行后(亦即是加在第三行)加上『drink tea?』字样!

1
2
3
4
5
6
[root@www ~]# nl /etc/passwd | sed '2a drink tea'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
drink tea
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
.....(后面省略).....

那如果是要在第二行前

1
nl /etc/passwd | sed '2i drink tea' 

如果是要增加两行以上,在第二行后面加入两行字,例如 Drink tea or ….. 与 drink beer?

1
2
3
4
5
6
7
8
[root@www ~]# nl /etc/passwd | sed '2a Drink tea or ......\
> drink beer ?'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
Drink tea or ......
drink beer ?
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
.....(后面省略).....

每一行之间都必须要以反斜杠『 \ 』来进行新行的添加喔!所以,上面的例子中,我们可以发现在第一行的最后面就有 \ 存在。

以行为单位的替换与显示

将第2-5行的内容取代成为『No 2-5 number』呢?

1
2
3
4
5
[root@www ~]# nl /etc/passwd | sed '2,5c No 2-5 number'
1 root:x:0:0:root:/root:/bin/bash
No 2-5 number
6 sync:x:5:0:sync:/sbin:/bin/sync
.....(后面省略).....

透过这个方法我们就能够将数据整行取代了!

仅列出 /etc/passwd 文件内的第 5-7 行:

1
2
3
4
[root@www ~]# nl /etc/passwd | sed -n '5,7p'
5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
6 sync:x:5:0:sync:/sbin:/bin/sync
7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

可以透过这个 sed 的以行为单位的显示功能, 就能够将某一个文件内的某些行号选择出来显示。

数据的搜寻并显示

搜索 /etc/passwd有root关键字的行

1
2
3
4
5
6
7
8
9

nl /etc/passwd | sed '/root/p'
1 root:x:0:0:root:/root:/bin/bash
1 root:x:0:0:root:/root:/bin/bash
2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh
3 bin:x:2:2:bin:/bin:/bin/sh
4 sys:x:3:3:sys:/dev:/bin/sh
5 sync:x:4:65534:sync:/bin:/bin/sync
....下面忽略

如果root找到,除了输出所有行,还会输出匹配行。

使用-n的时候将只打印包含模板的行。

1
2
nl /etc/passwd | sed -n '/root/p'
1 root:x:0:0:root:/root:/bin/bash

数据的搜寻并删除

删除/etc/passwd所有包含root的行,其他行输出

1
2
3
4
5
nl /etc/passwd | sed  '/root/d'
2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh
3 bin:x:2:2:bin:/bin:/bin/sh
....下面忽略
#第一行的匹配root已经删除了

数据的搜寻并执行命令

搜索/etc/passwd,找到root对应的行,执行后面花括号中的一组命令,每个命令之间用分号分隔,这里把bash替换为blueshell,再输出这行:

1
2
nl /etc/passwd | sed -n '/root/{s/bash/blueshell/;p;q}'
1 root:x:0:0:root:/root:/bin/blueshell

最后的q是退出。

数据的搜寻并替换

除了整行的处理模式之外, sed 还可以用行为单位进行部分数据的搜寻并取代。基本上 sed 的搜寻与替代的与 vi 相当的类似!他有点像这样:

1
sed 's/要被取代的字串/新的字串/g'

先观察原始信息,利用 /sbin/ifconfig 查询 IP

1
2
3
4
5
6
[root@www ~]# /sbin/ifconfig eth0
eth0 Link encap:Ethernet HWaddr 00:90:CC:A6:34:84
inet addr:192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0
inet6 addr: fe80::290:ccff:fea6:3484/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
.....(以下省略).....

本机的ip是192.168.1.100。

将 IP 前面的部分予以删除

1
2
[root@www ~]# /sbin/ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g'
192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0

接下来则是删除后续的部分,亦即:192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0

将 IP 后面的部分予以删除

1
2
[root@www ~]# /sbin/ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g' | sed 's/Bcast.*$//g'
192.168.1.100

多点编辑

一条sed命令,删除/etc/passwd第三行到末尾的数据,并把bash替换为blueshell

1
2
3
nl /etc/passwd | sed -e '3,$d' -e 's/bash/blueshell/'
1 root:x:0:0:root:/root:/bin/blueshell
2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh

-e表示多点编辑,第一个编辑命令删除/etc/passwd第三行到末尾的数据,第二条命令搜索bash替换为blueshell。

直接修改文件内容(危险动作)

sed 可以直接修改文件的内容,不必使用管道命令或数据流重导向!不过,由於这个动作会直接修改到原始的文件,所以请你千万不要随便拿系统配置来测试!我们还是使用文件 regular_express.txt 文件来测试看看吧!

regular_express.txt 文件内容如下:

1
2
3
4
5
6
7
[root@www ~]# cat regular_express.txt 
runoob.
google.
taobao.
facebook.
zhihu-
weibo-

利用 sed 将 regular_express.txt 内每一行结尾若为 . 则换成 !

1
2
3
4
5
6
7
8
[root@www ~]# sed -i 's/\.$/\!/g' regular_express.txt
[root@www ~]# cat regular_express.txt
runoob!
google!
taobao!
facebook!
zhihu-
weibo-:q:q

利用 sed 直接在 regular_express.txt 最后一行加入 # This is a test:

1
2
3
4
5
6
7
8
9
[root@www ~]# sed -i '$a # This is a test' regular_express.txt
[root@www ~]# cat regular_express.txt
runoob!
google!
taobao!
facebook!
zhihu-
weibo-
# This is a test

由于 $ 代表的是最后一行,而 a 的动作是新增,因此该文件最后新增 # This is a test!

sed 的 -i 选项可以直接修改文件内容,这功能非常有帮助!举例来说,如果你有一个 100 万行的文件,你要在第 100 行加某些文字,此时使用 vim 可能会疯掉!因为文件太大了!这个时候就可以使用 sed ,通过 sed 直接修改/取代的功能,你甚至不需要使用 vim 去修订!

QA

Q: sed 's/\.$/\!/g' textsed -e s/\.$/\!/g text 区别

  • 前者是将text中每行行尾的.(如果有)换成!

    使用 -e 应当是这个命令: sed -e s/\\.$/\!/g text

  • 后者是将text中每行行尾的字符换成!

    原因: 在命令行中 \. 被转移成了 .

awk

和 sed 命令类似,awk 命令也是逐行扫描文件(从第 1 行到最后一行),寻找含有目标文本的行,如果匹配成功,则会在该行上执行用户想要的操作;反之,则不对行做任何处理。

基本格式

1
awk [选项] '脚本命令' 文件名

命令选项以及含义

选项 含义
-F fs 指定以 fs 作为输入行的分隔符,awk 命令默认分隔符为空格或制表符。
-f file 从脚本文件中读取 awk 脚本指令,以取代直接在命令行中输入指令。
-v var=val 在执行处理过程之前,设置一个变量 var,并给其设备初始值为 val
-mf nnn and -mr nnn 对nnn值设置内在限制
-mf选项限制分配给nnn的最大块数目;
-mr选项限制记录的最大数目。
这两个功能是Bell实验室版awk的扩展功能,在标准awk中不适用
-W compact or --compat, -W traditional or --traditional 在兼容模式下运行awk。所以gawk的行为和标准的awk完全一样,所有的awk扩展都被忽略。
-W copyleft or --copyleft, -W copyright or --copyright 打印简短的版权信息。
-W help or --help, 打印全部awk选项
-W usage or --usage 打印每个选项的简短说明
-W lint or --lint 打印不能向传统unix平台移植的结构的警告。
-W lint-old or --lint-old 打印关于不能向传统unix平台移植的结构的警告。
-W posix 打开兼容模式。但有以下限制,不识别:/x、函数关键字、func、换码序列以及当fs是一个空格时,将新行作为一个域分隔符;操作符****=不能代替^^=;fflush无效。
-W re-interval or --re-inerval 允许间隔正则表达式的使用,参考(grep中的Posix字符类),如括号表达式[[:alpha:]]
-W source program-text or --source program-text 使用program-text作为源代码,可与-f命令混用。
-W version or --version 打印bug报告信息的版本。

脚本命令

awk 的强大之处在于脚本命令,它由 2 部分组成,分别为匹配规则和执行命令,如下所示

1
'匹配规则{执行命令}'

匹配规则

和 sed 命令中的 address 部分作用相同:用来指定脚本命令可以作用到文本内容中的具体行,可以使用字符串(比如 /demo/,表示查看含有 demo 字符串的行)或者正则表达式指定

注意

  • 整个脚本命令是用单引号(’’)括起,而其中的执行命令部分需要用大括号({})括起来
  • 在 awk 程序执行时,如果没有指定执行命令,则默认会把匹配的行输出;如果不指定匹配规则,则默认匹配文本中所有的行

awk 使用数据字段变量

awk 的主要特性之一是其处理文本文件中数据的能力,它会自动给一行中的每个数据元素分配一个变量。

默认情况下,awk 会将如下变量分配给它在文本行中发现的数据字段:

变量 数据字段
$0 代表整个文本行;
$1 代表文本行中的第 1 个数据字段;
$2 代表文本行中的第 2 个数据字段;
$n 代表文本行中的第 n 个数据字段。

在 awk 中,默认的字段分隔符是任意的空白字符(例如空格或制表符)。 在文本行中,每个数据字段都是通过字段分隔符划分的。awk 在读取一行文本时,会用预定义的字段分隔符划分每个数据字段。

awk 脚本命令使用多个命令

方法1

awk 允许将多条命令组合成一个正常的程序。要在命令行上的程序脚本中使用多条命令,只要在命令之间放个分号即可,例如:

1
echo "My name is Rich" | awk '{$4="Christine"; print $0}'

第一条命令会给字段变量 $4 赋值。第二条命令会打印整个数据字段。

方法2

可以一次一行地输入程序脚本命令,比如说:

1
2
3
echo "My name is Rich" | awk '{
> $4="Christine"
> print $0}'

在你用了表示起始的单引号后,bash shell 会使用 > 来提示输入更多数据,我们可以每次在每行加一条命令,直到输入了结尾的单引号。

awk从文件中读取程序

跟 sed 一样,awk 允许将脚本命令存储到文件中,然后再在命令行中引用,比如:

1
awk -F : -f awk.sh /etc/passwd

注意,在程序文件中,也可以指定多条命令,只要一条命令放一行即可,之间不需要用分号

awk BEGIN关键字

awk 中还可以指定脚本命令的运行时机。默认情况下,awk 会从输入中读取一行文本,然后针对该行的数据执行程序脚本,但有时可能需要在处理数据前运行一些脚本命令,这就需要使用 BEGIN 关键字。

BEGIN 会强制 awk 在读取数据前执行该关键字后指定的脚本命令

例如:

1
2
awk 'BEGIN {print "The data3 File Contents:"}
> {print $0}' data3.txt

BEGIN 部分的脚本指令会在 awk 命令处理数据前运行,而真正用来处理数据的是第二段脚本命令。

awk END关键字

和 BEGIN 关键字相对应,END 关键字允许我们指定一些脚本命令,awk 会在读完数据后执行它们,例如:

1
2
3
awk 'BEGIN {print "The data3 File Contents:"}
> {print $0}
> END {print "End of File"}' data3.txt

当 awk 程序打印完文件内容后,才会执行 END 中的脚本命令。

awk 使用变量

在 awk 的脚本程序中,支持使用变量来存取值。awk 支持两种不同类型的变量:

  • 内建变量:awk 本身就创建好,用户可以直接拿来用的变量,这些变量用来存放处理数据文件中的某些字段和记录的信息。
  • 自定义变量:awk 支持用户自己创建变量。

内建变量

awk 程序使用内建变量来引用程序数据里的一些特殊功能

变量 功能
$0 代表整个文本行;
$1 代表文本行中的第 1 个数据字段;
$2 代表文本行中的第 2 个数据字段;
$n 代表文本行中的第 n 个数据字段。
FIELDWIDTHS 由空格分隔的一列数字,定义了每个数据字段的确切宽度。
一旦设定了 FIELDWIDTHS 变量的值,就不能再改变了,因此,这种方法并==不适用于变长的字段==
FNR 当前输入文档的记录编号,常在有多个输入文档时使用。
NR 输入流的当前记录编号。
FS 输入字段分隔符
RS 输入记录分隔符,默认为换行符 \n。
OFS 输出字段分隔符,默认为空格。
print 命令会自动将 OFS 变量的值放置在输出中的每个字段间
ORS 输出记录分隔符,默认为换行符 \n。
ARGC 命令行参数个数。
ARGIND 当前文件在 ARGC 中的位置。
ARGV 包含命令行参数的数组。
CONVFMT 数字的转换格式,默认值为 %.6g。
ENVIRON 当前 shell 环境变量及其值组成的关联数组。
ERRNO 当读取或关闭输入文件发生错误时的系统错误号。
FILENAME 当前输入文档的名称。
FNR ==当前数据文件==中的数据行数。
IGNORECASE 设成非 0 值时,忽略 awk 命令中出现的字符串的字符大小写。
NF 数据文件中的字段总数。
NR 已处理的输入记录==总数==。会持续计数直到处理完所有的数据文件
OFMT 数字的输出格式,默认值为 %.6g。
RLENGTH 由 match 函数所匹配的子字符串的长度。
TSTART 由 match 函数所匹配的子字符串的起始位置。

理解

  • 字段分隔符:作用于 awk 每次处理的一个单元
  • 记录分隔符:用于区分 awk 每次要处理的一个单元

自定义变量

awk 允许用户定义自己的变量在脚本程序中使用。awk 自定义变量名可以是任意数目的字母、数字和下划线,但不能以数字开头。更重要的是,awk 变量名区分大小写

也可以用 awk 命令行来给程序中的变量赋值,这允许我们在正常的代码之外赋值,即时改变变量的值,比如:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# awk '
\> BEGIN{
\> testing="This is a test"
\> print testing
\> testing=45
\> print testing
\> }'
This is a test
45

需要注意的是,使用命令行参数来定义变量值会有一个问题,即设置了变量后,这个值在代码的 BEGIN 部分不可用,如下所示:

1
2
3
4
5
6
7
8
[root@localhost ~]$ cat script2
BEGIN{print "The starting value is",n; FS=","}
{print $n}
[root@localhost ~]$ awk -f script2 n=3 data1
The starting value is
data13
data23
data33

解决这个问题,可以用 -v 命令行参数,它可以实现在 BEGIN 代码之前设定变量。在命令行上,-v 命令行参数必须放在脚本代码之前,如下所示:

1
2
3
4
5
[root@localhost ~]$ awk -v n=3 -f script2 data1
The starting value is 3
data13
data23
data33

awk使用数组

awk使用分支结构

awk使用循环结构

awk使用函数

grep

基本格式

1
grep [option] pattern file

常用参数

参数 含义
-a 在二进制文件中搜索
-b 显示匹配行距文件头部的偏移量
-c 只显示匹配的行数
-e 实现多个选项间的逻辑 or 关系
-E 支持扩展正则表达式
-f 从文件获取 PATTERN 匹配
-F 匹配固定字符串的内容
-h 搜索多文件时不显示文件名
-i 忽略关键词大小写
-l 只显示符合匹配条件的文件名
-n 显示所有匹配行及其行号
-o 显示匹配词距文件头部的偏移量
-q 静默执行模式
-r 递归搜索模式
-s 不显示没有匹配文本的错误信息
-v 显示不包含匹配文本的所有行,相当于[^] 反向匹配
-w 精准匹配整词
-x 精准匹配整行
-A <行数 x> 除了显示符合范本样式的那一列之外,并显示该行之后的 x 行内容。
-B <行数 x> 除了显示符合样式的那一行之外,并显示该行之前的 x 行内容
-C <行数 x> 除了显示符合样式的那一行之外,并显示该行之前后的 x 行内容

Linux中grep命令的实用示例

如果你查看man手册,你会看到grep工具的简短描述:“打印与模式匹配的行。”

但是,不要被这样一个简单的定义所误导:grep是Unix工具箱中最有用的工具之一,而且一旦您处理文本文件,就有无数次机会使用它。

最好有真实的例子来学习事物是如何工作的。因此,我将使用asciidector.js源代码树来说明一些grep功能。

你可以从GitHub下载源代码树,如果需要,甚至可以查看我在撰写本文时使用的相同变更集。这将确保获得与本文其余部分所述完全相同的结果:

1
2
3
git clone https://github.com/asciidoctor/asciidoctor.js
cd asciidoctor.js
git checkout v1.5.6-rc.1

基本用法:查找字符串中所有的匹配项

Asciidoctor.js支持Java平台的Nashorn JavaScript引擎。我不了解Nashorn,所以我可以借此机会通过探索引用JavaScript引擎的项目部分来了解更多关于它的信息。

首先,我检查了package.json文件中是否有与Nashorn相关的设置,这些设置描述了项目依赖关系:

1
2
$ grep nashorn package.json
"test": "node npm/test/builder.js && node npm/test/unsupported-features.js && node npm/test/jasmine-browser.js && node npm/test/jasmine-browser-min.js && node npm/test/jasmine-node.js && node npm/test/jasmine-webpack.js && npm run test:karmaBrowserify && npm run test:karmaRequirejs && node npm/test/nashorn.js",

是的,显然有一些 Nashorn-specific 测试。所以让我们再试一下。

在一个文件列表中不区分大小写的搜索

现在,我想仔细看看 ./npm/test/ 目录中的文件。

不区分大小写的搜索(-i选项)可能更好用,因为我需要同时查找对nashorn和nashorn的引用(或任何其他大小写字符的组合):

1
2
3
4
$ grep -i nashorn npm/test/*.js
npm/test/nashorn.js:const nashornModule = require('../module/nashorn');
npm/test/nashorn.js:log.task('Nashorn');
npm/test/nashorn.js:nashornModule.nashornRun('jdk1.8.0');

事实上,不区分大小写是很有用的,否则我将错过require('../module/nashorn')这一行。

查找所有不匹配的文件

另外,npm/test/ 目录中是否有一些非 Nashorm 特定的文件?为了回答这个问题,我们可以使用 grep 的“打印不匹配文件”选项(-L选项):

1
2
3
4
5
6
7
$ grep -iL nashorn npm/test/*
npm/test/builder.js
npm/test/jasmine-browser-min.js
npm/test/jasmine-browser.js
npm/test/jasmine-node.js
npm/test/jasmine-webpack.js
npm/test/unsupported-features.js

注意使用-L选项grep的输出是如何变为只显示文件名的。因此,上面的文件都不包含字符串“nashorn”(不管大小写)。这并不意味着它们与这项技术毫无关联,但至少,字母“n-a-s-h-o-r-n”并不存在。

在隐藏文件和子目录中查找

上面两个命令使用shell glob模式将要检查的文件列表传递给grep命令。但是,这有一些固有的限制:星号(*)将不匹配隐藏的文件。两者都不会匹配子目录中包含的文件(最终)。

解决方案是将grepfind命令结合起来,而不是依赖shell glob模式:

1
2
3
4
# This is not efficient as it will spawn a new grep process for each file
linux@handbook:~$ find npm/test/ -type f -exec grep -iL nashorn \{} \;
# This may have issues with filenames containing space-like characters
linux@handbook:~$ grep -iL nashorn $(find npm/test/ -type f)

正如我在上面的代码块中提到的,这些解决方案都有缺点。

关于包含空格字符的文件名,大家可以研究一下 grep -z 选项,它与find命令的 -print0 选项结合,可以解决这个问题。

然而,更好的解决方案是使用grep的“recursive”(-r)选项。使用该选项,你可以在命令行上指定搜索树的根目录(起始目录),而不是要检查的文件名的显式列表。

使用-r选项,grep将检查搜索目录中的所有文件,包括隐藏的文件,然后它将递归地遍历到所有子目录:

1
2
3
4
5
6
7
$ grep -irL nashorn npm/test/npm/
npm/test/builder.js
npm/test/jasmine-browser-min.js
npm/test/jasmine-browser.js
npm/test/jasmine-node.js
npm/test/jasmine-webpack.js
npm/test/unsupported-features.js

事实上,有了这个选项,我们可以尝试上一个例子,查找与Nashnorn不匹配的:

1
$ grep -irL nashorn npm/

大家可以自己尝试一下,看看结果是怎样的。

按文件名筛选文件(使用正则表达式)

所以,在这个项目中有一些关于Nashorn的测试,既然Nashorn是Java的,那么另一个问题是“项目中是否有一些Java源文件明确提到了Nashorn?”

根据你使用的grep版本,至少有两种解决方案可以回答这个问题。

第一个是使用grep查找包含模式“nashorn”的所有文件,然后将第一个命令的输出通过管道传输到第二个grep实例,过滤掉非java源文件:

1
2
3
4
5
6
7
$ grep -ir nashorn ./ | grep "^[^:]*\.java"
./spec/nashorn/AsciidoctorConvertWithNashorn.java:public class AsciidoctorConvertWithNashorn {
./spec/nashorn/AsciidoctorConvertWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn");
./spec/nashorn/AsciidoctorConvertWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/asciidoctor-convert.js"));
./spec/nashorn/BasicJavascriptWithNashorn.java:public class BasicJavascriptWithNashorn {
./spec/nashorn/BasicJavascriptWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn");
./spec/nashorn/BasicJavascriptWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/basic.js"));

这条命令的上半部分很好理解,那么后半部分的 “^[\^:]*\.java” 是什么意思呢?

除非指定-F选项,否则grep将假定搜索模式是正则表达式。这意味着,除了与逐字匹配的普通字符外,您还可以访问一组元字符来描述更复杂的模式。我上面使用的模式将只匹配:

  • ^ 表示文本的开始;
  • [^:]* 后跟除冒号以外的任何字符序列;
  • \. 后跟一个点(点在regex中有特殊的含义,所以我必须用反斜杠来保护它,以表示我想要一个文字匹配);
  • java 接着是四个字母“java”。

实际上,由于grep将使用冒号将文件名与上下文分开,因此我只保留文件名部分中包含.java的行。值得一提的是,它还将匹配.javascript文件名。这个大家可以自己尝试一下。

使用grep按文件名筛选文件

正则表达式功能非常强大。然而,在这种特殊情况下,这似乎是杀伤力过大。更不用说上面的解决方案了,我们花时间检查所有文件以搜索“nashorn”模式—大多数结果都被管道的第二步丢弃。

如果您使用的是grep的GNU版本(如果您使用的是Linux,则很可能是这样),那么您还有另一个带有--include选项的解决方案。这指示grep只搜索名称与给定glob模式匹配的文件:

1
2
3
4
5
6
7
$ grep -ir nashorn ./ --include='*.java'
./spec/nashorn/AsciidoctorConvertWithNashorn.java:public class AsciidoctorConvertWithNashorn {
./spec/nashorn/AsciidoctorConvertWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn");
./spec/nashorn/AsciidoctorConvertWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/asciidoctor-convert.js"));
./spec/nashorn/BasicJavascriptWithNashorn.java:public class BasicJavascriptWithNashorn {
./spec/nashorn/BasicJavascriptWithNashorn.java: ScriptEngine engine = engineManager.getEngineByName("nashorn");
./spec/nashorn/BasicJavascriptWithNashorn.java: engine.eval(new FileReader("./spec/nashorn/basic.js"));

查找单词

Asciidector.js项目的有趣之处在于它是一个多语言项目。asciidector的核心代码是用Ruby编写的,因此,为了在JavaScript世界中可用,它必须使用Opal(一种Ruby-to-JavaScript源代码到源代码的编译器)进行“传输”。另一项我以前不知道的技术。

因此,在研究了Nashorn的特性之后,我需要更好地理解opal api。作为这个任务的第一步,我在项目的JavaScript文件中搜索了所有提到的Opal全局对象。它可能出现在(Opal=)、成员访问(Opal.)中,甚至可能出现在其他上下文中。正则表达式就可以了。然而,grep再一次有了一些更轻量级的解决方案来解决这个常见的用例。使用-w选项,它将只匹配单词,即前面和后面都是非单词字符的模式。非单词字符是行首、行尾或既非字母、数字也非下划线的任何字符:

1
2
$ grep -irw --include='*.js' Opal .
...

为输出着色

我将上一个命令的输出粘贴在这里,因为有许多匹配项。当输出像那样密集时,您可能希望添加一点颜色以增加可读性。如果系统默认情况下尚未配置此功能,则可以使用GNU--color选项激活该功能:

1
2
$ grep -irw --color=auto --include='*.js' Opal .
...

您应该获得与以前相同的长结果,但是这次搜索字符串应该以彩色显示。

计算匹配行或匹配文件

我曾两次提到,前面命令的输出非常长。到底多长呢?

1
2
$ grep -irw --include='*.js' Opal . | wc -l
86

这意味着我们在所有检查过的文件中总共有86行匹配的行。但是,有多少文件是匹配的呢?使用-l选项可以限制grep输出匹配文件,而不是显示匹配行。因此,简单的更改将显示匹配的文件数:

1
2
$ grep -irwl --include='*.js' Opal . | wc -l
20

如果这让您想起 -L 选项,那就不足为奇了,因为它比较常见,所以使用小写/大写来区分互补选项 -l 显示匹配的文件名 -L 显示不匹配的文件名。作为另外一个例子,你可以查看手册中的 -h/-H 选项。

让我们结束题外话,回到刚才的结果:86条匹配行,20个匹配的文件。但是,匹配文件中的匹配行是如何分布的?我们可以知道,使用grep的-c选项将计算每个已检查文件(包括零匹配的文件)的匹配行数:

1
2
$ grep -irwc --include='*.js' Opal .
...

通常,这还需要做一些事后处理工作。因为它按照检查文件的顺序显示其结果,并且还包括不匹配的文件—这通常是我们不感兴趣的。后者很容易解决:

1
$ grep -irwc --include='*.js' Opal . | grep -v ':0$'

关于排序,可以在管道末尾添加 sort 命令:

1
$ grep -irwc --include='*.js' Opal . | grep -v ':0$' | sort -t: -k2n

大家可以自己查看关于 sort 的手册,来看一下上面的结果。

寻找两个匹配集之间的差异

在前几个命令中,我搜索过单词“Opal”。但是,如果我在同一个文件集中搜索所有出现的字符串“Opal”,我会得到大约20个以上的答案:

1
$ grep -irw --include='*.js' Opal . | wc -l86$ grep -ir --include='*.js' Opal . | wc -l105

找出这两组之间的差异将是有趣的。那么,包含四个字母“opal”的行是什么,但这四个字母不构成一个完整的单词呢?

回答这个问题没那么容易。因为同一行既可以包含Opal这个词,也可以包含包含这四个字母的更大的词。但作为第一个近似值,您可以使用该管道:

1
2
3
4
5
$ grep -ir --include='*.js' Opal . | grep -ivw Opal
./npm/examples.js: const opalBuilder = OpalBuilder.create();
./npm/examples.js: opalBuilder.appendPaths('build/asciidoctor/lib');
./npm/examples.js: opalBuilder.appendPaths('lib');
...

结语

当然,仅仅通过发出几个grep命令,您将无法理解项目组织,更不用说代码体系结构了!

然而,我发现在探索新的代码库时,识别基准和起点的命令是不可避免的。

因此,希望能帮助您理解grep命令的强大功能,并将其添加到工具箱中。

ripgrep

ripgrep(rg) 是开源社区正在进行的 RIIR(re-write in Rust)工作的一个优秀成果。它旨在成为经典 grep 命令的高级替代工具。

它的语法如下:

1
rg <pattern> [files/directories]

使用 ripgrep,可以不提供待搜索的文件名。如果没有提供文件名,那么就会搜索所有的文件。如果你不知道搜索的关键词在哪个文件中,那这种情况下是非常有用的。

当然,我们也可以使用 grep 搜索所有的文件,但是 ripgrep 不需要提供额外的参数。

什么是 ripgrep

ripgrep 是一个递归正则表达式模式匹配工具,它考虑了 gitignore。如果你的 gitignore 中有排除的文件或目录,那么 ripgrep 将会忽略它们,从而加快搜索的执行时间。

ripgrep 几个比较突出的特点如下:

  • 在目录中递归搜索;
  • 输出中不同颜色高亮显示;
  • 支持多种编码格式,比如 UTF-8,SHIFT_JIS等;
  • 可以在压缩文件的zip文件中搜索;
  • 默认情况下会忽略隐藏文件
  • 另外也会忽略 gitignore 文件中的过滤设置。

你可以将其视同为 grep,但 ripgrep 搜索的是文件和文件内容,而不是 grep 所处理的原始字节流

安装 ripgrep

大多数 Linux 系统中都预装了 grep,但是 ripgrep 并没有这样的特权,所有我们需要手动安装它。

ripgrep 在所有主流 Linux 发行版的存储库中都可用,所以我们可以使用包管理器来安装。

如果你是 Arch Linux 用户,可以使用如下命令安装:

1
pacman -S ripgrep

Gentoo 用户使用如下命令安装 ripgrep:

1
emerge sys-apps/ripgrep

Fedoras 或者 Red Hat 使用如下命令:

1
sudo dnf install ripgrep

openSUSE(15.1及更新版本)用户使用如下命令:

1
sudo zypper install ripgrep

Debian Buster(v10)或更高版本的用户,可使用 apt;Ubuntu Cosmic Cutlefish(18.10)或更高级版本也可以使用发行版的官方存储库:

1
sudo apt install ripgrep

使用 ripgrep 命令

如果你熟悉 grep 命令,就会发现 ripgrep 与其工作原理类似。它接受一个字符串和文件名作为参数,运行时会搜索文件,并显示输入字符串与文件内容匹配的位置。

基本搜索

如下例子,我们在 Cargo.html 中搜索单词 description:

1
2
3
$ rg description Cargo.toml
3:description = "A more intuitive version of du"
53:extended-description = """\

ripgrep 将在指定的文件中搜索,结果将显示匹配的文本和行号

如果搜索的是多个文件(如果不指定任何文件,它将搜索所有文件),那么ripgrep在搜索结果中还会显示文件名:

或者,可以使用 --file 选项,其中包含要搜索的关键词(表达式)。当你要搜索一组关键词时,可以将其放在一个文件中,然后使用 --file 选项指定:

前后文搜索

有时候,有匹配的前后文是很好的显示方式,特别是在代码库中搜索时。使用前后文搜索,可以使用 -C 或者 --context 选项,该选项接受一个数值,并显示匹配值的前一行和后一行:

有时,我们只希望看到上面的几行,包括匹配的行;还有时候,我们只需要下面的行,包括匹配的行。使用选项 -A,或者 --after-context,后跟一个数值,将显示每个匹配行后的几行:

至于显示匹配行前面的几行,可以使用 -B 或者 --before-context,再提供一个数值(即行数):

列选项

关于 ripgrep 提供的列,有几个选项。

如果你使用的是 vim,可以使用 --column 选项,这样将在结果中显示匹配文本在哪一列,以”行:列“的方式显示:

与列相关的另一个选项是 -M--max-columns,它取最大列数的值。如果匹配行的列超过最大值,它会告诉你某一特定行在输出到终端时被忽略:

其他选项

除了上文中提到的,ripgrep 中还有其他几个选项。比如:可以使用 -s--case-sensitive 选项来区分大小写:

如果不想区分大小写,可以使用 -i--ignore-case

另外,如果你要搜索的目标文件特别大,可以启用多线程进行搜索。使用 -j--threads 选项,后跟一个数值:

1
$ rg -j 4 TODO

在搜索中要排除某个关键词或表达式,可以使用 -v--invert-match 选项:

ripgrep 可以实现在压缩文件(如果压缩文件是文本文件)中进行搜索,使用 -z--search-zip 选项。其通常与 -a 选项一起使用,-a 选项会将二进制文件也当作文本文件。

ripgrep 是一个非常好用的工具,虽然它暗指要替代 grep,但实际上并不会取代 grep,因为它们的搜索目标是不同的。我们可以在日常工作中按需求来使用。