概述
本文深入介绍基础的 Linux 进程管理技术。您将学习如何:
- 创建简单的正则表达式
- 使用正则表达式搜索文件和文件系统
- 使用正则表达式和 sed
本文帮助您准备 Linux Professional Institute's Junior Level Administration (LPIC-1) 考试的 103 主题下的 103.7 考核目标。该考核目标的权值为 2。
先决条件
为了从本文获得最大的收益,您应该具备基础的 Linux 知识,并且具有一个能够正常运行的 Linux 系统,以便练习本文讨论的命令。不同版本的程序输出的结果的格式可能不同,因此您的结果可能与本文图片和清单所示的结果有所不同。
设置示例
们将使用文章 中建立的文件练习命令。如果您没有学习该文或者没有保存得到的文件,您可以在名为 lpi103-7 的子目录中新建一个子目录,并创建必要的文件。打开文本窗口,使用主目录作为当前目录。然后,将清单 1 中的内容复制到窗口运行命令,这将创建 lpi103-7 子目录以及您将使用的文件。
清单 1. 创建示例文件
mkdir -p lpi103-7 && cd lpi103-7 && {
echo -e "1 apple\n2 pear\n3 banana" > text1
echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
echo "This is a sentence. " !#:* !#:1->text3
split -l 2 text1
split -b 17 text2 y;
cp text1 text1.bkp
mkdir -p backup
cp text1 backup/text1.bkp.2
}
您的窗口应类似于清单 2,当前目录现在是 lpi103-7 目录中新建的目录。
清单 2. 创建示例文件——输出
ian@attic4:~$ mkdir -p lpi103-7 && cd lpi103-7 && {
> echo -e "1 apple\n2 pear\n3 banana" > text1
> echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
> echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y;
> cp text1 text1.bkp
> mkdir -p backup
> cp text1 backup/text1.bkp.2
> }
ian@attic4:~/lpi103-7$
正则表达式
正则表达式在计算机语言理论中有很长的历史。大部分计算机学科的学生都知道,可以使用正则表达式表示的语言与有限时序机(finite automata)可以接受的语言一样。本文中的正则表达式所代表的含义更为复杂,与您在计算机科学课堂上学到的内容可能不同,虽然传承是一样的。
正则表达式
(也称为 “regex” 或 “regexp”)是描述文本字符串的一种方式或者一种
模式,程序可以根据任何文本字符串
匹配 该模式,以提供强大的搜索功能。grep(正则表达式处理程序的缩写)是 Linux 或 UNIX® 程序员或管理员的标准装备,他们可以在文件搜索或命令输出中使用正则表达式。我们介绍了 sed(流编辑器的缩写),这是使用正则表达式在文件或文本流中查找和替换文本的另一个标准工具。本文将帮助您更好地理解 grep
和
sed 使用的正则表达式。使用正则表达式的另一个程序是 awk。
结合本系列文章中的其他部分您会发现,整本书都是以正则表达式和计算机语言理论为基础的。根据您对正则表达式的了解,您可能发现正则表达式语法与中讨论的通配符语法有类似之处。但这种相似之处只是表面现象。
基本的构建块
大部分 Linux 系统中的 GNU 程序可以使用两种常规表达式语法:basic
和
extended。使用 GNU grep,功能上没有不同之处。本文将介绍基本的语法,以及它和扩展语法之间的不同之处。
正则表达式通过元字符
加强的
字符 和
操作符 构建。大部分字符与自身匹配,大部分元字符必须使用反斜杠(\)进行转义。基本的操作包括:
- 连接
- 连接两个正则表达式创建一个更长的正则表达式。例如,正则表达式 a 匹配字符串 abcdcba 两次,正则表达式 b 也是一样。但是,ab 将只匹配 abcdcba,而 ba 将只匹配 abcdcba。
- 重复
- Kleene * 或重复操作符将匹配 0 次或多次前一个正则表达式。因此像 a*b 之类的表达式将匹配以任何以 a 开头以 b 结尾的字符串,包括 b 本身。Kleene * 不用转义,因此希望匹配字面值星号(*)的表达式必须让星号转义。这里使用的 * 与通配符中使用的 * 不同,通配符中的 * 号匹配任何字符串。
- 交替
- 交替操作符(|)匹配前置或后置表达式。它必须匹配前一个或后一个表达式之一。在基本语法中它必须转义。例如,表达式 a*\|b*c 匹配由任何数量的 a 或 b 组成(但不是同时)且以一个 c 结尾的字符串。同样,单个字符 c 也是匹配的。
尽量不要引用正则表达式以避免 shell 膨胀。
搜索文件和文件系统
我们将之前的示例中创建的使用文本文件。研究清单 3 中的示例。注意 grep 使用一个正则表达式作为参数,还有 0 个或多个要搜索的文件。如果没有给定文件,grep 将搜索 stdin,这让它成为一个可以在管道中使用的过滤器。如果没有匹配任何行,则 grep 没有输出,尽管可以测试它的退出代码。
清单 3. 简单的正则表达式
ian@attic4:~/lpi103-7$ grep p text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep pea text1
2 pear
ian@attic4:~/lpi103-7$ grep "p*" text1
1 apple
2 pear
3 banana
ian@attic4:~/lpi103-7$ grep "pp*" text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "x" text1; echo $?
1
ian@attic4:~/lpi103-7$ grep "x*" text1; echo $?
1 apple
2 pear
3 banana
0
ian@attic4:~/lpi103-7$ cat text1 | grep "l\|n"
1 apple
3 banana
ian@attic4:~/lpi103-7$ echo -e "find an \ns* here" | grep "s\*"
s* here
从上例中可以看出,有时候会得到出乎意料的结果,尤其是使用重复的时候。您可能预期 p* 或者 pp* 能够匹配几个带 p 的字符串,但是 p*
和
x* 能匹配文件的所有行,因为 * 操作符匹配 0 次或多次前一个正则表达式。
有两个示例演示了从 grep 退出的代码。如果找到匹配,则返回值 0。如果发生错误,比如要搜索的文件不存在,则返回大于 1 的值(GNU grep 总是返回 2)。
快捷键
现在可以使用 grep
和
基本的正则表达式构建块了,以下是一些方便的快捷键。
- +
- + 操作符类似于 * 操作符,但是它匹配一次或多次前一个正则表达式。基本表达式中它必须转义。
- ?
- ? 表示前一个表达式是可选的,因此它表示匹配 0 次或多次。这与通配符中使用的 ? 不同。
- .
- .(句点)是表示任何字符的元字符。最常使用的方式是 .*,该表达式匹配包含任何字符(或没有字符)的任意长度的字符串。不用说您就明白,这一般在一个较长的表达式中使用。比较句点与通配符中使用的 ?,.* 与通配符中使用的 *。
清单 4. 更多正则表达式
ian@attic4:~/lpi103-7$ grep "pp\+" text1 # at least two p's
1 apple
ian@attic4:~/lpi103-7$ grep "pl\?e" text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "pl\?e" text1 # pe with optional l between
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "p.*r" text1 # p, some string then r
2 pear
ian@attic4:~/lpi103-7$ grep "a.." text1 # a followed by two other letters
1 apple
3 banana
匹配一行的开始或结束
^(脱字符号)匹配一行的开始,$(美元符号)匹配行的结束。^..b 匹配行开始处任何后跟 b 的两个字符,而 ar$ 匹配任何以 ar 结束的行。正则表达式 ^$ 匹配空行。
更复杂的表达式
到目前为止,我们已经学习了用于单个字符的重复。如果希望搜索一个或多个多字符字符串,比如 banan 中 an 出现了两次,那么可以使用圆括号,在基本语法中必须转义。类似地,您可能希望搜索一些字符,但又不想使用 . 这么通用或者交替这么啰嗦的表达式。那么,您可以使用方括号([])将交替情况括起来,常规语法需要转义。方括号中的表达式构成了一个字符类。使用方括号还可以减少转义特殊字符(比如 .
和
*)的需求,例外情况见后文。
清单 5. 圆括号和字符类
ian@attic4:~/lpi103-7$ grep "\(an\)\+" text1 # find at least 1 an
3 banana
ian@attic4:~/lpi103-7$ grep "an\(an\)\+" text1 # find at least 2 an's
3 banana
ian@attic4:~/lpi103-7$ grep "[3p]" text1 # find p or 3
1 apple
2 pear
3 banana
ian@attic4:~/lpi103-7$ echo -e "find an\ns* here\nsomewhere." | grep "s[.*]"
s* here
ian@attic4:~/lpi103-7$ echo -e "find an\n * in position 2." | grep ".[.*]"
* in position 2.
字符类还有几个有趣的可能性。
- 范围表达式(Range expression)
- 范围表达式是使用 -(连字符)分隔的双字符,比如数字里面的 0-9,十六进制里的 0-9a-fA-F。注意,范围与语言环境有关。
- 署名类(Named class)
- 有些署名类可以为通常使用的类提供便捷。署名类以 [: 开始,以 :] 结束,可以在括号表达式中使用。示例如下:
- [:alnum:]
- 字母数字字符
- [:blank:]
- 空格和制表符
- [:digit:]
- 数字 0 到 9(等效于 0-9)
- [:upper:]
和
[:lower:] - 分别为大写字母和小写字母。
- ^(求反)
- 在字符类 [ 后的第一个字符使用时,^(脱字符号)对剩余字符求反,因此只有类中不存在该字符时(前导^除外)才能匹配。
了解了以上特殊含义后我们知道,如果希望匹配一个字符类中的字面值 -(连字符),那么您必须将其放在第一个或最后一个。如果想匹配字面值^(脱字字符),那么它不能是第一个字符。] 在非第一个位置时表示结束类。
字符类中,正则表达式和通配符是类似的,但使用的否定符号不同(^
和
!)。清单 6 展示了一些字符类示例。
清单 6. 更多字符类
ian@attic4:~/lpi103-7$ # Match on range 3 through 7
ian@attic4:~/lpi103-7$ echo -e "123\n456\n789\n0" | grep "[3-7]"
123
456
789
ian@attic4:~/lpi103-7$ # Find digit followed by no n or r till end of line
ian@attic4:~/lpi103-7$ grep "[[:digit:]][^nr]*$" text1
1 apple
ian@attic4:~/lpi103-7$ # Find a digit, n, or z followed by no n or r till end of line
ian@attic4:~/lpi103-7$ grep "[[:digit:]nz][^nr]*$" text1
1 apple
3 banana
最后一个示例让您感到奇怪吗? 在这种情况下,第一个括号表达式匹配字符串中的任何数字、 n 或 z,至少 n 后面没有另一个 n 或 r,因此字符串结尾处的 na 匹配该正则表达式。
哪些内容匹配?
如果您能够区分高亮显示,比如用颜色、粗体或下划线,那么您可以设置 GREP_COLORS 环境变量来高亮显示匹配内容。默认设置使用粗体红色高亮显示匹配内容,如图 1 所示。您会看到整个输出的第一行都是匹配的,但是第二行只匹配最后两个字符。
图 1. 使用颜色区分 grep 匹配内容
如果您是正则表达式新手,或者不确定 grep 为什么返回某一行,那么这项技术可以帮您。
扩展的正则表达式
扩展的正则表达式语法是 GNU 扩展。我们在基本语法中使用时,它不需要转义一些字符,包括圆括号、'?'、'+'、'|'和 '{'。但缺点在于,如果您在正则表达式中将它们作为字符解释,那么必须进行转义。您可以使用 -E(或者 grep 的 --extended-regexp 选项)表示您正在使用扩展的正则表达式语法。此外,egrep 命令也可以帮助您实现这一点。清单 7 展示了本节上文中使用的示例,以及 egrep 使用的相应扩展表达式。
清单 7. 扩展的正则表达式
ian@attic4:~/lpi103-7$ # Find b followed by one or more an's and then an a
ian@attic4:~/lpi103-7$ grep "b\(an\)\+a" text1
3 banana
ian@attic4:~/lpi103-7$ egrep "b(an)+a" text1
3 banana
在文件中查找内容
现在您了解了基本的命令,让我们使用 grep
和
find 在文件系统中查找内容。示例相对比较简单;中创建的文件或者您在 lpi103-7 目录及其子目录中创建的文件。如果使用本系列之前的文章中创建的文件,您将有一些额外的文件,因此将看到一些额外的结果。
首先,grep 可以一次搜索多个文件。如果添加 -n 选项,它将告诉您匹配的行号。如果只想知道匹配多少行,可以使用 -c 选项,如果只想获得匹配的文件列表,可以使用 -l 选项。清单 8 展示了一些示例。
清单 8. 搜索多个文件
ian@attic4:~/lpi103-7$ grep plum *
text2:9 plum
yaa:9 plum
ian@attic4:~/lpi103-7$ grep -n banana text[1-4]
text1:3:3 banana
text2:2:3 banana
ian@attic4:~/lpi103-7$ grep -c banana text[1-4]
text1:1
text2:1
text3:0
ian@attic4:~/lpi103-7$ grep -l pear *
text1
text1.bkp
xaa
查看清单 8 中的 -c 选项,您会看到一行 text3:0。 您经常需要知道某个内容在文件中出现了多少次,但是不用知道没有出现该内容的文件。grep 命令有一个 -v 选项,它表示只显示不匹配的行输出。因此,我们可以使用正则表达式 :0$ 查找以逗号和 0 结尾的行。
下一个示例是使用 find 查找当前目录及其子目录中的所有常规文件,然后使用 xargs 将文件列表传递到 grep,以确定每个文件中出现 banana 的次数。最后,通过再一次调用 grep 筛选该输出,这一次使用 -v 选项查找所有不以 :0 结尾的行,只用告诉我们包含字符串 banana 的文件计数。
清单 9. 查找至少包含一次 banana 的文件
ian@attic4:~/lpi103-7$ find . -type f -print0| xargs -0 grep -c banana| grep -v ":0$"
./backup/text1.bkp.2:1
./text2:1
./text1:1
./yaa:1
./xab:1
./text1.bkp:1
正则表达式和 sed
如果您需要查找某内容,那么只需要使用 grep。如果需要从匹配行中提取搜索字符串,或者相关字符串,那么需要进一步操作,您可以选择使用 sed。让我们解释一下它的工作方式。首先回忆我们的两个示例文件,text1
和
text2,其中包含了一个数字,后跟空格,再加一个水果的名称,而 text3 包含重复的语句。我们在清单 10 中再看一次它的内容。
清单 10. text1、text2 和
text3 的内容
ian@attic4:~/lpi103-7$ cat text[1-3]
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
This is a sentence. This is a sentence. This is a sentence.
首先,我们将使用 grep
和
sed 提取以一个或多个数字开头,且后跟空白字符(空格或制表符)的行。一般情况下,sed 在一个周期结束时打印出每个行,因此我们使用 sed 的 -n 选项禁止输出,然后使用 sed 中的 p 命令只打印匹配我们正则表达式的行。要确认我们对这两个工具使用的正则表达式相同,我们将其赋予一个变量。
清单 11. 搜索 grep 和
sed
ian@attic4:~/lpi103-7$ grep "$oursearch" text[1-3]
text1:1 apple
text1:2 pear
text1:3 banana
text2:9 plum
text2:3 banana
text2:10 apple
ian@attic4:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/p"
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
注意,grep 在搜索到多个文件时将显示文件名称。因为我们使用 cat 提供 sed 的输出,所以 sed 无法知道源文件名。但是,匹配行是相同的,正如我们期望的那样。
现在假设我们只需要找到的行中的第二个字。在本例中是水果的名称,但是我们需要查询 HTTP URL 或者文件名等等其他内容。例如,删除我们试图匹配的字符串就足够了,如清单 12 所示。
清单 12. 使用 sed 删除前导数字
ian@attic4:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/s/$oursearch//p"
apple
pear
banana
plum
banana
apple
对于最后一个示例,假设我们的行在水果名称之后还有些内容。我们添加了一行 “lemon pie”,查看如何只提取 lemon。我们将对输出排序,放弃非唯一的值,因此我们得到一个找到的水果列表,每个水果只出现一次。
清单 13 展示了两种实现同一个任务的方式。首先,我们剔除了前导数字以及后面的空格,然后剔除第一个空格或选项卡之后的所有内容,并打印剩下的内容。在第二个示例中,我们引入了圆括号将整个行分为 3 个部分,数字和后面的空格、第二个字以及其他内容。我们使用 s 命令将整个行替换为第二个字,然后打印结果。您可以尝试变化一下方式,忽略第三部分,\(.*\),看看是否能解释发生了什么。
清单 13. 获取水果名
ian@attic4:~/lpi103-7$ echo "7 lemon pie" | cat - text[1-3] |
> sed -ne "/$oursearch/s/\($oursearch\)\([^[:blank:]]*\)\(.*\)/\2/p" |
> sort | uniq
apple
banana
lemon
pear
有些旧版本的 sed 不支持扩展的正则表达式。如果您的 sed 版本不支持扩展的 regexps,请使用 -r 选项告诉 sed 您使用的是扩展语法。清单 14 展示了要对 oursearch 变量和 sed 命令进行哪些更改才能让扩展的正则表达式完成清单 13 中基本正则表达式完成的任务。
清单 14. 使用扩展的正则表达式和 sed
ian@attic4:~/lpi103-7$ echo "7 lemon pie" | cat - text[1-3] |
> sed -nre "/$oursearchx/s/($oursearchx)([^[:blank:]]*)(.*)/\2/p" |
> sort | uniq
apple
banana
lemon
pear
plum
本文介绍了您可以使用正则表达式以及 grep
和
sed 对 Linux 命令行执行的操作,这还只是冰山的一角。使用手册了解更多有关这些高价值工具的信息。