192. Linux中的命令之文本处理
sed
Linux sed 命令是利用脚本来处理文本文件,它可依照脚本的指令来处理、编辑文本文件。
Sed 主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等。
sed 会根据脚本命令来处理文本文件中的数据,这些命令要么从命令行中输入,要么存储在一个文本文件中,此命令执行数据的顺序如下:
- 每次 仅读取一行 内容;
- 根据提供的规则命令匹配并修改数据。注意,sed 默认不会直接修改源文件数据,而是会将数据复制到缓冲区中,修改也仅限于缓冲区中的数据;
- 将执行结果输出。
当一行数据匹配完成后,它会继续读取下一行数据,并重复这个过程,直到将文件中所有数据处理完毕。
语法
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 种:
- 以数字形式指定行区间;
- 用文本模式指定具体行区间。
以上两种形式都可以使用如下这 2 种格式,分别是:
1 | [address]脚本命令 |
或者
1 | address { |
例如:
1 | $ sed -n '/3/{ |
以数字形式指定行区间
当使用数字方式的行寻址时,可以用行在文本流中的行位置来引用。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 | #查看testfile 中的内容 |
使用sed命令后,输出结果如下:
1 | #使用sed 在第四行后添加新字符串 |
以行为单位的新增/删除
将 /etc/passwd 的内容列出并且列印行号,同时,请将第 2~5 行删除!
1 | [root@www ~]# nl /etc/passwd | sed '2,5d' |
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 | [root@www ~]# nl /etc/passwd | sed '2a drink tea' |
那如果是要在第二行前
1 | nl /etc/passwd | sed '2i drink tea' |
如果是要增加两行以上,在第二行后面加入两行字,例如 Drink tea or ….. 与 drink beer?
1 | [root@www ~]# nl /etc/passwd | sed '2a Drink tea or ......\ |
每一行之间都必须要以反斜杠『 \ 』来进行新行的添加喔!所以,上面的例子中,我们可以发现在第一行的最后面就有 \ 存在。
以行为单位的替换与显示
将第2-5行的内容取代成为『No 2-5 number』呢?
1 | [root@www ~]# nl /etc/passwd | sed '2,5c No 2-5 number' |
透过这个方法我们就能够将数据整行取代了!
仅列出 /etc/passwd 文件内的第 5-7 行:
1 | [root@www ~]# nl /etc/passwd | sed -n '5,7p' |
可以透过这个 sed 的以行为单位的显示功能, 就能够将某一个文件内的某些行号选择出来显示。
数据的搜寻并显示
搜索 /etc/passwd有root关键字的行
1 |
|
如果root找到,除了输出所有行,还会输出匹配行。
使用-n的时候将只打印包含模板的行。
1 | nl /etc/passwd | sed -n '/root/p' |
数据的搜寻并删除
删除/etc/passwd所有包含root的行,其他行输出
1 | nl /etc/passwd | sed '/root/d' |
数据的搜寻并执行命令
搜索/etc/passwd,找到root对应的行,执行后面花括号中的一组命令,每个命令之间用分号分隔,这里把bash替换为blueshell,再输出这行:
1 | nl /etc/passwd | sed -n '/root/{s/bash/blueshell/;p;q}' |
最后的q是退出。
数据的搜寻并替换
除了整行的处理模式之外, sed 还可以用行为单位进行部分数据的搜寻并取代。基本上 sed 的搜寻与替代的与 vi 相当的类似!他有点像这样:
1 | sed 's/要被取代的字串/新的字串/g' |
先观察原始信息,利用 /sbin/ifconfig 查询 IP
1 | [root@www ~]# /sbin/ifconfig eth0 |
本机的ip是192.168.1.100。
将 IP 前面的部分予以删除
1 | [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
将 IP 后面的部分予以删除
1 | [root@www ~]# /sbin/ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g' | sed 's/Bcast.*$//g' |
多点编辑
一条sed命令,删除/etc/passwd第三行到末尾的数据,并把bash替换为blueshell
1 | nl /etc/passwd | sed -e '3,$d' -e 's/bash/blueshell/' |
-e表示多点编辑,第一个编辑命令删除/etc/passwd第三行到末尾的数据,第二条命令搜索bash替换为blueshell。
直接修改文件内容(危险动作)
sed 可以直接修改文件的内容,不必使用管道命令或数据流重导向!不过,由於这个动作会直接修改到原始的文件,所以请你千万不要随便拿系统配置来测试!我们还是使用文件 regular_express.txt 文件来测试看看吧!
regular_express.txt 文件内容如下:
1 | [root@www ~]# cat regular_express.txt |
利用 sed 将 regular_express.txt 内每一行结尾若为 . 则换成 !
1 | [root@www ~]# sed -i 's/\.$/\!/g' regular_express.txt |
利用 sed 直接在 regular_express.txt 最后一行加入 # This is a test:
1 | [root@www ~]# sed -i '$a # This is a test' regular_express.txt |
由于 $ 代表的是最后一行,而 a 的动作是新增,因此该文件最后新增 # This is a test!
sed 的 -i 选项可以直接修改文件内容,这功能非常有帮助!举例来说,如果你有一个 100 万行的文件,你要在第 100 行加某些文字,此时使用 vim 可能会疯掉!因为文件太大了!这个时候就可以使用 sed ,通过 sed 直接修改/取代的功能,你甚至不需要使用 vim 去修订!
QA
Q: sed 's/\.$/\!/g' text和 sed -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 | echo "My name is Rich" | awk '{ |
在你用了表示起始的单引号后,bash shell 会使用 > 来提示输入更多数据,我们可以每次在每行加一条命令,直到输入了结尾的单引号。
awk从文件中读取程序
跟 sed 一样,awk 允许将脚本命令存储到文件中,然后再在命令行中引用,比如:
1 | awk -F : -f awk.sh /etc/passwd |
注意,在程序文件中,也可以指定多条命令,只要一条命令放一行即可,之间不需要用分号。
awk BEGIN关键字
awk 中还可以指定脚本命令的运行时机。默认情况下,awk 会从输入中读取一行文本,然后针对该行的数据执行程序脚本,但有时可能需要在处理数据前运行一些脚本命令,这就需要使用 BEGIN 关键字。
BEGIN 会强制 awk 在读取数据前执行该关键字后指定的脚本命令
例如:
1 | awk 'BEGIN {print "The data3 File Contents:"} |
BEGIN 部分的脚本指令会在 awk 命令处理数据前运行,而真正用来处理数据的是第二段脚本命令。
awk END关键字
和 BEGIN 关键字相对应,END 关键字允许我们指定一些脚本命令,awk 会在读完数据后执行它们,例如:
1 | awk 'BEGIN {print "The data3 File Contents:"} |
当 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 | [root@localhost ~]# awk ' |
需要注意的是,使用命令行参数来定义变量值会有一个问题,即设置了变量后,这个值在代码的 BEGIN 部分不可用,如下所示:
1 | [root@localhost ~]$ cat script2 |
解决这个问题,可以用 -v 命令行参数,它可以实现在 BEGIN 代码之前设定变量。在命令行上,-v 命令行参数必须放在脚本代码之前,如下所示:
1 | [root@localhost ~]$ awk -v n=3 -f script2 data1 |
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 | git clone https://github.com/asciidoctor/asciidoctor.js |
基本用法:查找字符串中所有的匹配项
Asciidoctor.js支持Java平台的Nashorn JavaScript引擎。我不了解Nashorn,所以我可以借此机会通过探索引用JavaScript引擎的项目部分来了解更多关于它的信息。
首先,我检查了package.json文件中是否有与Nashorn相关的设置,这些设置描述了项目依赖关系:
1 | $ grep nashorn package.json |
是的,显然有一些 Nashorn-specific 测试。所以让我们再试一下。
在一个文件列表中不区分大小写的搜索
现在,我想仔细看看 ./npm/test/ 目录中的文件。
不区分大小写的搜索(-i选项)可能更好用,因为我需要同时查找对nashorn和nashorn的引用(或任何其他大小写字符的组合):
1 | $ grep -i nashorn npm/test/*.js |
事实上,不区分大小写是很有用的,否则我将错过require('../module/nashorn')这一行。
查找所有不匹配的文件
另外,npm/test/ 目录中是否有一些非 Nashorm 特定的文件?为了回答这个问题,我们可以使用 grep 的“打印不匹配文件”选项(-L选项):
1 | $ grep -iL nashorn npm/test/* |
注意使用-L选项grep的输出是如何变为只显示文件名的。因此,上面的文件都不包含字符串“nashorn”(不管大小写)。这并不意味着它们与这项技术毫无关联,但至少,字母“n-a-s-h-o-r-n”并不存在。
在隐藏文件和子目录中查找
上面两个命令使用shell glob模式将要检查的文件列表传递给grep命令。但是,这有一些固有的限制:星号(*)将不匹配隐藏的文件。两者都不会匹配子目录中包含的文件(最终)。
解决方案是将grep与find命令结合起来,而不是依赖shell glob模式:
1 | # This is not efficient as it will spawn a new grep process for each file |
正如我在上面的代码块中提到的,这些解决方案都有缺点。
关于包含空格字符的文件名,大家可以研究一下 grep -z 选项,它与find命令的 -print0 选项结合,可以解决这个问题。
然而,更好的解决方案是使用grep的“recursive”(-r)选项。使用该选项,你可以在命令行上指定搜索树的根目录(起始目录),而不是要检查的文件名的显式列表。
使用-r选项,grep将检查搜索目录中的所有文件,包括隐藏的文件,然后它将递归地遍历到所有子目录:
1 | $ grep -irL nashorn npm/test/npm/ |
事实上,有了这个选项,我们可以尝试上一个例子,查找与Nashnorn不匹配的:
1 | $ grep -irL nashorn npm/ |
大家可以自己尝试一下,看看结果是怎样的。
按文件名筛选文件(使用正则表达式)
所以,在这个项目中有一些关于Nashorn的测试,既然Nashorn是Java的,那么另一个问题是“项目中是否有一些Java源文件明确提到了Nashorn?”
根据你使用的grep版本,至少有两种解决方案可以回答这个问题。
第一个是使用grep查找包含模式“nashorn”的所有文件,然后将第一个命令的输出通过管道传输到第二个grep实例,过滤掉非java源文件:
1 | $ grep -ir nashorn ./ | grep "^[^:]*\.java" |
这条命令的上半部分很好理解,那么后半部分的 “^[\^:]*\.java” 是什么意思呢?
除非指定-F选项,否则grep将假定搜索模式是正则表达式。这意味着,除了与逐字匹配的普通字符外,您还可以访问一组元字符来描述更复杂的模式。我上面使用的模式将只匹配:
^表示文本的开始;[^:]*后跟除冒号以外的任何字符序列;\.后跟一个点(点在regex中有特殊的含义,所以我必须用反斜杠来保护它,以表示我想要一个文字匹配);java接着是四个字母“java”。
实际上,由于grep将使用冒号将文件名与上下文分开,因此我只保留文件名部分中包含.java的行。值得一提的是,它还将匹配.javascript文件名。这个大家可以自己尝试一下。
使用grep按文件名筛选文件
正则表达式功能非常强大。然而,在这种特殊情况下,这似乎是杀伤力过大。更不用说上面的解决方案了,我们花时间检查所有文件以搜索“nashorn”模式—大多数结果都被管道的第二步丢弃。
如果您使用的是grep的GNU版本(如果您使用的是Linux,则很可能是这样),那么您还有另一个带有--include选项的解决方案。这指示grep只搜索名称与给定glob模式匹配的文件:
1 | $ grep -ir nashorn ./ --include='*.java' |
查找单词
Asciidector.js项目的有趣之处在于它是一个多语言项目。asciidector的核心代码是用Ruby编写的,因此,为了在JavaScript世界中可用,它必须使用Opal(一种Ruby-to-JavaScript源代码到源代码的编译器)进行“传输”。另一项我以前不知道的技术。
因此,在研究了Nashorn的特性之后,我需要更好地理解opal api。作为这个任务的第一步,我在项目的JavaScript文件中搜索了所有提到的Opal全局对象。它可能出现在(Opal=)、成员访问(Opal.)中,甚至可能出现在其他上下文中。正则表达式就可以了。然而,grep再一次有了一些更轻量级的解决方案来解决这个常见的用例。使用-w选项,它将只匹配单词,即前面和后面都是非单词字符的模式。非单词字符是行首、行尾或既非字母、数字也非下划线的任何字符:
1 | $ grep -irw --include='*.js' Opal . |
为输出着色
我将上一个命令的输出粘贴在这里,因为有许多匹配项。当输出像那样密集时,您可能希望添加一点颜色以增加可读性。如果系统默认情况下尚未配置此功能,则可以使用GNU--color选项激活该功能:
1 | $ grep -irw --color=auto --include='*.js' Opal . |
您应该获得与以前相同的长结果,但是这次搜索字符串应该以彩色显示。
计算匹配行或匹配文件
我曾两次提到,前面命令的输出非常长。到底多长呢?
1 | $ grep -irw --include='*.js' Opal . | wc -l |
这意味着我们在所有检查过的文件中总共有86行匹配的行。但是,有多少文件是匹配的呢?使用-l选项可以限制grep输出匹配文件,而不是显示匹配行。因此,简单的更改将显示匹配的文件数:
1 | $ grep -irwl --include='*.js' Opal . | wc -l |
如果这让您想起 -L 选项,那就不足为奇了,因为它比较常见,所以使用小写/大写来区分互补选项 -l 显示匹配的文件名 -L 显示不匹配的文件名。作为另外一个例子,你可以查看手册中的 -h/-H 选项。
让我们结束题外话,回到刚才的结果:86条匹配行,20个匹配的文件。但是,匹配文件中的匹配行是如何分布的?我们可以知道,使用grep的-c选项将计算每个已检查文件(包括零匹配的文件)的匹配行数:
1 | $ 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 | $ grep -ir --include='*.js' Opal . | grep -ivw Opal |
结语
当然,仅仅通过发出几个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 | $ rg description Cargo.toml |
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,因为它们的搜索目标是不同的。我们可以在日常工作中按需求来使用。