本章介绍正则表达式,以及我们可以用来利用它们的能力的主要命令。我们将首先研究正则表达式背后的理论,然后深入研究使用grep
和sed
正则表达式的实际例子。
我们还将解释 globbing,以及如何在命令行上使用它。
本章将介绍以下命令:grep
、set
、egrep
和sed
。
本章将涵盖以下主题:
- 什么是正则表达式?
- 通配符
- 使用带有
egrep
和sed
的正则表达式
本章所有脚本均可在 GitHub:https://GitHub . com/tam mert/learn-Linux-shell-scripting/tree/master/chapter _ 10上找到。除此之外,Ubuntu 虚拟机仍然是我们测试和运行本章脚本的方式。
你可能以前听过正则表达式或者正则表达式这个术语。对许多人来说,正则表达式看起来非常复杂,经常是从互联网或教科书的某个地方提取的,而没有完全掌握它的功能。
虽然这对于完成设定的任务来说很好,但是比一般的系统管理员更好地理解正则表达式确实可以让您在创建脚本和使用终端方面脱颖而出。
一个定制良好的正则表达式确实可以帮助您保持脚本的简短、简单和对未来变化的鲁棒性。
本质上,正则表达式是一段文本,用作其他文本的搜索模式。正则表达式可以很容易地说,例如,我想选择所有包含五个字符长的单词的行,或者寻找所有以.log
结尾的文件。
一个例子可能有助于你的理解。首先,我们需要一个可以用来探索正则表达式的命令。在 Linux 中使用正则表达式最著名的命令是grep
。
grep
是首字母缩略词,意思是globalreeexpressionp*rint*。如你所见,这似乎是一个很好的解释这个概念的候选人!
我们将深入如下:
reader@ubuntu:~/scripts/chapter_10$ vim grep-file.txt
reader@ubuntu:~/scripts/chapter_10$ cat grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'cool' grep-file.txt
Regular expressions are pretty cool
reader@ubuntu:~/scripts/chapter_10$ cat grep-file.txt | grep 'USA'
but in the USA they use color (and realize)!
首先,我们先来探讨一下grep
的基本功能,然后再来讨论正则表达式。grep
做的真的很简单,如man grep
: 打印符合图案的线条。
在前面的例子中,我们创建了一个包含一些句子的文件。其中一些以大写字母开头;它们的结局大多不同;他们用了一些相似的词,但并不完全相同。这些和更多的特征将在进一步的例子中使用。
首先,我们使用grep
匹配一个单词(默认情况下搜索区分大小写),并打印出来。grep
有两种工作模式:
grep <pattern> <file>
grep <pattern>
(需要以管道形式输入,或|
)
第一种操作模式允许您指定一个文件名,如果需要打印的行与您指定的模式匹配,您可以从中指定要打印的行。grep 'cool' grep-file.txt
命令就是一个例子。
还有另一种使用grep
的方式:在溪流中。一条小溪是运送到你的终点站的东西,但是可以在移动中改变。在这种情况下,文件的cat
通常会将所有行打印到您的终端。
但是,使用管道符号(|
)我们将cat
的输出重定向到grep
;在这种情况下,我们只需要指定要匹配的模式。任何不匹配的行将被丢弃,并且不会显示在您的终端中。
如你所见,这个的完整语法是cat grep-file.txt | grep 'USA'
。
Piping is a form of redirection that we will further discuss in Chapter 12, Using Pipes and Redirection in Scripts. For now, keep in mind that by using the pipe, the output of cat
is used as input for grep
, in the same manner as the filename is used as input. While discussing grep
, we will (for now) use the method explained first, which does not use redirection.
因为酷和美国这两个词只出现在一行,所以grep
的两个实例都只打印了那一行。但是如果一个单词出现在多行中,所有的单词都会按照grep
遇到它们的顺序打印出来(通常是从上到下):
reader@ubuntu:~/scripts/chapter_10$ grep 'use' grep-file.txt
We can use this regular file for testing grep.
but in the USA they use color (and realize)!
通过grep
,可以指定我们希望搜索不区分大小写,而不是默认的区分大小写方法。例如,这是在日志文件中查找错误的好方法。有些程序使用错误这个词,有些程序使用错误,我们甚至偶尔会遇到错误。所有这些结果都可以通过向grep
提供-i
标志来返回:
reader@ubuntu:~/scripts/chapter_10$ grep 'regular' grep-file.txt
We can use this regular file for testing grep.
reader@ubuntu:~/scripts/chapter_10$ grep -i 'regular' grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
通过提供-i
,我们现在看到“常规*”和“常规”*都已经匹配,并且它们的行已经被打印。
默认情况下,正则表达式被认为是贪婪的。这似乎是一个描述技术概念的奇怪术语,但它确实非常适合。为了说明为什么正则表达式被认为是贪婪的,请看这个例子:
reader@ubuntu:~/scripts/chapter_10$ grep 'in' grep-file.txt
We can use this regular file for testing grep.
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
reader@ubuntu:~/scripts/chapter_10$ grep 'the' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
如您所见,grep
默认情况下不会查找完整的单词。它会查看文件中的字符,如果一个字符串与搜索匹配(不管它们之前或之后是什么),就会打印该行。
在第一个例子中,in
既匹配中的正常单词**,又测试** g 中的**,在第二个例子中,两行都有两个匹配项,都是的和的 y。**
如果您只想返回整个单词,请确保在您的grep
搜索模式中包含空格:
reader@ubuntu:~/scripts/chapter_10$ grep ' in ' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
reader@ubuntu:~/scripts/chapter_10$ grep ' the ' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
如您所见,现在对' in '
的搜索没有返回带有单词测试的行,因为在中的字符串没有被空格包围。
A regular expression is just a definition of a particular search pattern, which is implemented differently by individual scripting/programming languages. The regular expressions we are using with Bash are different from those in Perl or Java, for example. While in some languages, greediness can be tuned or even turned off, regular expressions under grep
and sed
are always greedy. This is not really an issue, just something to consider when defining your search patterns.
我们现在知道如何搜索整个单词,即使我们还不完全确定大写和小写。
我们还看到(大多数)Linux 应用下的正则表达式是贪婪的,所以我们需要确保通过指定空白和字符锚来正确处理这个问题,我们将很快解释这一点。
在这两种情况下,我们都知道自己在寻找什么。但是,如果我们并不真正知道我们在寻找什么,或者也许只是它的一部分呢?这个困境的答案是字符匹配。
在正则表达式中,有两个字符可以用来替代其他字符:
.
(点)匹配任何一个字符(除了换行符)*
(星号)匹配字符之前的任意重复次数(甚至零个实例)
一个例子将有助于理解这一点:
reader@ubuntu:~/scripts/chapter_10$ vim character-class.txt
reader@ubuntu:~/scripts/chapter_10$ cat character-class.txt
eee
e2e
e e
aaa
a2a
a a
aabb
reader@ubuntu:~/scripts/chapter_10$ grep 'e.e' character-class.txt
eee
e2e
e e
reader@ubuntu:~/scripts/chapter_10$ grep 'aaa*' character-class.txt
aaa
aabb
reader@ubuntu:~/scripts/chapter_10$ grep 'aab*' character-class.txt
aaa
aabb
那里发生了很多事情,其中一些可能感觉非常反直觉。我们将一个接一个地讨论它们,并详细介绍正在发生的事情:
reader@ubuntu:~/scripts/chapter_10$ grep 'e.e' character-class.txt
eee
e2e
e e
在这个例子中,我们用点来代替任何字符。如我们所见,这包括字母(e e e)和数字(e 2 e)。但是,它也匹配最后一行两个 es 之间的空格字符。
这里还有一个例子:
reader@ubuntu:~/scripts/chapter_10$ grep 'aaa*' character-class.txt
aaa
aabb
当我们使用*
替换时,我们寻找的是前面字符的零个或多个实例。在搜索模式aaa*
中,这意味着以下字符串有效:
aa
aaa
aaaa
aaaaa
...等等。而第一个结果之后的一切应该都很清楚了,为什么aa
也匹配aaa*
?因为零在*零甚至更多!*那样的话,如果最后一个a
是零,我们只剩下aa
了。
在最后一个例子中也发生了同样的事情:
reader@ubuntu:~/scripts/chapter_10$ grep 'aab*' character-class.txt
aaa
aabb
图案aab*
匹配 aa a 内的 aa,因为b*
可以为零,这使得图案最终成为aa
。当然也匹配一个或多个 bs ( aabb
完全匹配)。
当您对要查找的内容只有一个大概的了解时,这些通配符非常有用。然而,有时你会对自己的需求有更具体的想法。
在这种情况下,我们可以使用括号[...】,将我们的替换缩小到某个字符集。下面的例子应该能让你很好地理解如何使用它:
reader@ubuntu:~/scripts/chapter_10$ grep 'f.r' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'f[ao]r' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'f[abcdefghijklmnopqrstuvwxyz]r' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'f[az]r' grep-file.txt
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'f[a-z]r' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'f[a-k]r' grep-file.txt
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep 'f[k-q]r' grep-file.txt
We can use this regular file for testing grep
首先,我们演示使用.
(点)替换任何字符。在这种情况下,模式 f.r 匹配的和 far 。
接下来,我们使用f[ao]r
中的括号符号来表示我们将接受f
和r
之间的单个字符,这在ao
的字符集中。不出所料,这将再次返回的远和**。**
如果用f[az]r
模式做这个,只能搭配远和 fzr 。由于字符串fzr
不在我们的文本文件中(显然一个字也没有),我们只看到打印了远的行。
接下来,假设你想匹配一个字母,但不是一个数字。如果像第一个例子一样使用.
(点)进行搜索,将返回字母和数字。因此,您还会得到,例如, f2r 作为匹配项(应该在文件中,而不是在文件中)。
如果使用括号符号,可以使用以下符号:f[abcdefghijklmnopqrstuvwxyz]r
。匹配任何字母 a-z,在f
和r
之间。然而,在键盘上打出来并不好(相信我)。
幸运的是,POSIX 正则表达式的创建者为此引入了一个简写:[a-z]
,如前面的例子所示。我们也可以使用字母表的子集,如图所示:f[a-k]r
。由于字母 o 不在 a 和 k 之间,因此与在上不匹配。
最后一个例子表明,这是一个强大且实用的模式:
reader@ubuntu:~/scripts/chapter_10$ grep reali[sz]e grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
希望这一切都有意义。在继续在线锚之前,我们将通过组合符号更进一步。
在前面的例子中,您可以看到我们可以使用括号符号来处理美国英语和英国英语之间的一些差异。然而,这仅在拼写差异为单个字母时有效,就像意识到一样。
就颜色而言,我们需要处理一个额外的字母。这听起来像是零或更多的情况,不是吗?
reader@ubuntu:~/scripts/chapter_10$ grep 'colo[u]*r' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
通过使用模式colo[u]*r
,我们正在搜索一行,该行包含以 colo 开头的单词,可以包含也可以不包含任意数量的 u s,并以 r 结尾。由于color
和colour
对于该图案都是可以接受的,所以两行都被打印。
您可能会尝试使用零或更多符号的点字符*
。但是,仔细看看在这种情况下会发生什么:
reader@ubuntu:~/scripts/chapter_10$ grep 'colo.*r' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
同样,两条线是匹配的。但是,由于第二行包含另一个更远的 r ,字符串color (and r
匹配,以及colour
和color
。
这是正则表达式模式对于我们的目的来说过于贪婪的典型例子。虽然我们不能说它不那么贪婪,但grep
中有一个选项,让我们只寻找匹配的单个单词。
符号-w
计算空格和行尾/开头,只找到整个单词。它是这样使用的:
reader@ubuntu:~/scripts/chapter_10$ grep -w 'colo.*r' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
现在只匹配colour
和color
两个字。之前,我们在单词周围放了空格来促进这种行为,但是由于单词colour
在行尾,所以后面没有空格。
自己尝试一下,看看为什么封闭colo.*r
搜索模式不能使用空白,但是可以使用-w
选项。
Some implementations of regular expressions have the {3}
notation, to supplement the *
notation. In this notation, you can specify exactly how often a pattern should be present. The search pattern [a-z]{3}
would match all lowercase strings of exactly three characters. In Linux, this can only be done with extended regular expressions, which we will see later in this chapter.
我们已经简单提到了线锚。根据我们到目前为止给出的解释,我们只能在一行中搜索单词;我们还不能对设定期望,这些词在中的位置。为此,我们使用线锚。
在正则表达式中,^
(插入符号)字符表示一行的开始,而$
(美元)表示一行的结束。我们可以在搜索模式中使用这些,例如,在以下场景中:
- 查找单词 error,但只能在一行的开头:
^error
- 寻找以点结束的线:
\.$
- 寻找空行:
^$
第一个用法,在一行的开头找一些东西,应该很清楚。下面的例子使用grep -i
(记住,这允许我们不区分大小写地搜索),展示了我们如何使用它来按行位置过滤:
reader@ubuntu:~/scripts/chapter_10$ grep -i 'regular' grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
reader@ubuntu:~/scripts/chapter_10$ grep -i '^regular' grep-file.txt
Regular expressions are pretty cool
在第一个搜索模式regular
中,我们被返回两行。这并不意外,因为两行都包含单词常规(尽管大小写不同)。
现在,为了选择以单词开始的行,我们使用插入符号^
形成模式^regular
。这仅返回单词位于该行第一个位置的行。(注意,如果我们没有选择在grep
上包含-i
,我们可以使用[Rr]egular
来代替。)
下一个例子,我们寻找以点结束的线,有点复杂。大家记得,正则表达式中的点被认为是一个特殊的字符;它是任何其他角色的替代品。如果我们正常使用,我们会看到文件返回中的所有行(因为所有行都以任何一个字符结束)。
为了在文本中实际搜索一个点,我们需要通过在它前面加一个反斜杠来转义这个点;这告诉正则表达式引擎不要将点解释为特殊字符,而是搜索它:
reader@ubuntu:~/scripts/chapter_10$ grep '.$' grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep '\.$' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
由于\
用于转义特殊字符,您可能会遇到在文本中寻找反斜杠的情况。在这种情况下,您可以使用反斜杠来转义反斜杠的特殊功能!在这种情况下,您的图案将是\\
,与\
弦相匹配。
在这个例子中,我们遇到了另一个问题。到目前为止,我们一直用单引号引用所有模式。然而,这并不总是需要的!比如grep cool grep-file.txt
和grep 'cool' grep-file.txt
一样好用。
那么,我们为什么要这么做?提示:试试前面的例子,用虚线结尾,不带引号。然后记住,Bash 中的一个美元字符也被用来表示变量。如果引用的话,$
不会被 Bash 展开,Bash 会返回有问题的结果。
我们将在第 16 章、 Bash 参数替换和扩展中讨论 Bash 扩展。
最后,我们提出了^$
模式。这会搜索一个行的开头,然后直接搜索一个行的结尾。只有一种情况会出现这种情况:空行。
为了说明你为什么想要找到空行,让我们来看看一个新的grep
标志:-v
。这个标志是--invert-match
的简写,它应该给出一个关于它实际做什么的好线索:它不是打印匹配的行,而是打印不匹配的行。
使用grep -v '^$' <file name>
,可以打印一个没有空行的文件。尝试一下随机配置文件:
reader@ubuntu:/etc$ cat /etc/ssh/ssh_config
# This is the ssh client system-wide configuration file. See
# ssh_config(5) for more information. This file provides defaults for
# users, and the values can be changed in per-user configuration files
# or on the command line.
# Configuration data is parsed as follows:
<SNIPPED>
reader@ubuntu:/etc$ grep -v '^$' /etc/ssh/ssh_config
# This is the ssh client system-wide configuration file. See
# ssh_config(5) for more information. This file provides defaults for
# users, and the values can be changed in per-user configuration files
# or on the command line.
# Configuration data is parsed as follows:
<SNIPPED>
可以看到,/etc/ssh/ssh_config
文件以空行开始。然后,在注释块之间,还有另一个空行。通过使用grep -v '^$'
,这些空行被删除。虽然这是一个很好的练习,但这并没有给我们省下那么多台词。
然而,有一种搜索模式被广泛使用并且非常强大:从配置文件中过滤掉注释。这个操作让我们快速了解实际配置了什么,并省略了所有注释(注释有其自身的优点,但当您只想查看配置了哪些选项时,可能会造成阻碍)。
为此,我们将行首插入符号与一个 hashtag 组合在一起,hashtag 表示一个注释:
reader@ubuntu:/etc$ grep -v '^#' /etc/ssh/ssh_config
Host *
SendEnv LANG LC_*
HashKnownHosts yes
GSSAPIAuthentication yes
这仍然会打印所有空行,但不再打印注释。在这个特殊的文件中,在 51 行中,只有 4 行包含实际的配置指令!所有其他行要么为空,要么包含注释。很酷,对吧?
With grep
, it is also possible to use multiple patterns at the same time. By using this, you can combine the filtering of empty lines and comment lines for a condensed, quick overview of configuration options. Multiple patterns are defined using the -e
option. The full command in this case is grep -v -e '^$' -e '^#' /etc/ssh/ssh_config
. Try it!
我们现在已经看到了许多如何使用正则表达式的例子。虽然大多数事情都很直观,但我们也看到,如果我们想同时过滤大写和小写字符串,我们要么必须为grep
指定-i
选项,要么将搜索模式从[a-z]
更改为[a-zA-z]
。对于数字,我们需要使用[0-9]
。
有些人可能觉得这很好,但其他人可能不同意。在这种情况下,可以使用另一种符号:[[:pattern:]]
。
下一个示例使用了新的双括号符号和旧的单括号符号:
reader@ubuntu:~/scripts/chapter_10$ grep [[:digit:]] character-class.txt
e2e
a2a
reader@ubuntu:~/scripts/chapter_10$ grep [0-9] character-class.txt
e2e
a2a
正如你所看到的,这两种模式会产生相同的线条:带有数字的线条。大写字符也可以做到这一点:
reader@ubuntu:~/scripts/chapter_10$ grep [[:upper:]] grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ grep [A-Z] grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
说到底,你喜欢用哪种符号是一个问题。不过,对于双括号符号有一点要说:它更接近于其他脚本/编程语言的实现。例如,大多数正则表达式实现使用\w
(单词)选择字母,使用\d
(数字)搜索数字。在\w
的情况下,大写变体直观上是\W
。
为了方便起见,这里有一个表,其中包含了最常见的 POSIX 双括号字符类:
| 符号 | 描述 | 单括号等效 |
| [[:alnum:]]
| 匹配小写和大写字母或数字 | [a-z A-Z 0-9] |
| [[:alpha:]]
| 匹配小写和大写字母 | [a-z A-Z] |
| [[:digit:]]
| 匹配数字 | [0-9] |
| [[:lower:]]
| 匹配小写字母 | [a-z] |
| [[:upper:]]
| 匹配大写字母 | [阿-兹] |
| [[:blank:]]
| 匹配空格和制表符 | [ \t] |
We prefer to use the double bracket notation, as it maps better to other regular expression implementations. Feel free to use either in your scripting! However, as always: make sure you choose one, and stick with it; not following a standard results in sloppy scripts that are confusing to readers. The rest of the examples in this book will use the double bracket notation.
我们现在已经掌握了正则表达式的基本知识。Linux 上还有一个和正则表达式密切相关的主题: globbing 。即使你可能没有意识到,你已经在这本书里看到了全球化的例子。
更好的是,实际上很有可能你已经在实践中使用了全球模式。如果,在命令行上工作时,你曾经使用过通配符*
,那么你已经全局化了!
简单地说,glob 模式描述了在文件路径操作中注入通配符。所以,当你做cp * /tmp/
的时候,你复制所有的文件(不是目录!)在当前工作目录到/tmp/
目录。
*
扩展到工作目录内的所有常规文件,然后全部复制到/tmp/
中。
这里有一个简单的例子:
reader@ubuntu:~/scripts/chapter_10$ ls -l
total 8
-rw-rw-r-- 1 reader reader 29 Oct 14 10:29 character-class.txt
-rw-rw-r-- 1 reader reader 219 Oct 8 19:22 grep-file.txt
reader@ubuntu:~/scripts/chapter_10$ cp * /tmp/
reader@ubuntu:~/scripts/chapter_10$ ls -l /tmp/
total 20
-rw-rw-r-- 1 reader reader 29 Oct 14 16:35 character-class.txt
-rw-rw-r-- 1 reader reader 219 Oct 14 16:35 grep-file.txt
<SNIPPED>
我们没有同时执行cp grep-file.txt /tmp/
和cp character-class.txt /tmp/
,而是使用*
来选择它们。相同的球形图案可用于rm
:
reader@ubuntu:/tmp$ ls -l
total 16
-rw-rw-r-- 1 reader reader 29 Oct 14 16:37 character-class.txt
-rw-rw-r-- 1 reader reader 219 Oct 14 16:37 grep-file.txt
drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350...
drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350...
reader@ubuntu:/tmp$ rm *
rm: cannot remove 'systemd-private-c34c8acb350...': Is a directory
rm: cannot remove 'systemd-private-c34c8acb350...': Is a directory
reader@ubuntu:/tmp$ ls -l
total 8
drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350...
drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350...
默认情况下,rm
只删除文件,不删除目录(从上例的错误中可以看到)。如第六章、文件操作所述,增加一个-r
也会递归删除目录*。*
同样,一定要考虑这有多具有破坏性:在没有警告的情况下,您可以删除当前树位置中的每个文件(当然,如果您有权限)。前面的例子展示了*
glob 模式有多强大:它扩展到它能找到的每个文件,不管是什么类型。
如上所述,glob 命令实现了类似于正则表达式的效果。尽管有一些不同。例如,正则表达式中的*
字符代表前面字符出现零次或多次的*。对于 globbing,它是任何和所有字符的通配符,更类似于正则表达式的.*
符号。*
与正则表达式一样,glob 模式可以由普通字符和特殊字符组合而成。看一个例子,其中ls
与不同的参数/全局模式一起使用:
reader@ubuntu:~/scripts/chapter_09$ ls -l
total 68
-rw-rw-r-- 1 reader reader 682 Oct 2 18:31 empty-file.sh
-rw-rw-r-- 1 reader reader 1183 Oct 1 19:06 file-create.sh
-rw-rw-r-- 1 reader reader 467 Sep 29 19:43 functional-check.sh
<SNIPPED>
reader@ubuntu:~/scripts/chapter_09$ ls -l *
-rw-rw-r-- 1 reader reader 682 Oct 2 18:31 empty-file.sh
-rw-rw-r-- 1 reader reader 1183 Oct 1 19:06 file-create.sh
-rw-rw-r-- 1 reader reader 467 Sep 29 19:43 functional-check.sh
<SNIPPED>
reader@ubuntu:~/scripts/chapter_09$ ls -l if-then-exit.sh
-rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh
reader@ubuntu:~/scripts/chapter_09$ ls -l if-*.sh
-rw-rw-r-- 1 reader reader 448 Sep 30 20:10 if-then-else-proper.sh
-rw-rw-r-- 1 reader reader 422 Sep 30 19:56 if-then-else.sh
-rw-rw-r-- 1 reader reader 535 Sep 30 19:44 if-then-exit-rc-improved.sh
-rw-rw-r-- 1 reader reader 556 Sep 30 19:18 if-then-exit-rc.sh
-rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh
在前一章的scripts
目录中,我们首先运行一个普通的ls -l
。如您所知,这会打印目录中的所有文件。现在,如果我们使用ls -l *
,我们会得到完全相同的结果。看起来,在没有参数的情况下,ls
将为我们注入一个通配符 glob。
接下来,我们使用ls
的替代模式,这是我们呈现文件名作为参数的地方。在这种情况下,因为文件名对于每个目录都是唯一的,所以我们只看到返回的一行。
但是,如果我们想要所有以 if-
开头的脚本(以.sh
结尾)呢?我们使用if-*.sh
的球状模式。在这个模式中,*
通配符被扩展为匹配,正如man glob
所说,任何字符串,包括空字符串。
Globbing 在 Linux 中非常普遍。如果您正在处理一个处理文件的命令(在下,一切都是文件原则下,是大多数命令),很有可能您可以使用 globbing。为了让您对此有个印象,请考虑以下示例:
reader@ubuntu:~/scripts/chapter_10$ cat *
eee
e2e
e e
aaa
a2a
a a
aabb
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
结合通配符 glob 模式的cat
命令打印当前工作目录中所有文件的内容。在这种情况下,由于所有文件都是 ASCII 文本,这并不是一个真正的问题。如您所见,文件是一个接一个打印的;两者之间连一条空线都没有。
如果您cat
一个二进制文件,您的屏幕将看起来像这样:
reader@ubuntu:~/scripts/chapter_10$ cat /bin/chvt
@H!@8 @@@�888�� �� � H 88 8 �TTTDDP�td\\\llQ�tdR�td�� � /lib64/ld-linux-x86-64.so.2GNUGNU��H������)�!�@`��a*�K��9���X' Q��/9'~���C J
最坏的情况是,二进制文件包含某个字符序列,该序列会对您的 Bash shell 进行临时更改,这将使其不可用(是的,这种情况在我们身上发生过很多次)。这里的教训应该很简单:世纪之交要小心!
到目前为止,我们看到的其他可以处理全局模式的命令包括chmod
、chown
、mv
、tar
、grep
等等。也许现在最有趣的是grep
。我们在单个文件上使用了带有grep
的正则表达式,但是我们也可以使用 glob 来选择文件。
让我们来看看grep
和 globbing 最可笑的例子:在一切中找到任何东西。
reader@ubuntu:~/scripts/chapter_10$ grep .* *
grep: ..: Is a directory
character-class.txt:eee
character-class.txt:e2e
character-class.txt:e e
character-class.txt:aaa
character-class.txt:a2a
character-class.txt:a a
character-class.txt:aabb
grep-file.txt:We can use this regular file for testing grep.
grep-file.txt:Regular expressions are pretty cool
grep-file.txt:Did you ever realise that in the UK they say colour,
grep-file.txt:but in the USA they use color (and realize)!
grep-file.txt:Also, New Zealand is pretty far away.
这里,我们使用正则表达式.*
搜索模式(任意,零次或更多次)和*
的 glob 模式(任意文件)。如您所料,这应该匹配每个文件中的每一行。
当我们以这种方式使用grep
时,它与早期的cat *
具有几乎相同的功能。然而,当grep
用于多个文件时,输出包括文件名(因此您知道该行在哪里找到)。
Make a note: a globbing pattern is always related to files, whereas a regular expression is used inside the files, on the actual content. Since the syntax is similar, you will probably not be too confused about this, but if you ever run into a situation where your pattern is not working as you'd expect, it would be good to take a moment and consider whether you're globbing or regexing!
基本的 globbing 主要是用通配符完成的,有时还结合了文件名的一部分。然而,正如正则表达式允许我们替换单个字符一样,globs 也是如此。
正则表达式通过点来实现这一点;在 globbing 模式中,使用问号:
reader@ubuntu:~/scripts/chapter_09$ ls -l if-then-*
-rw-rw-r-- 1 reader reader 448 Sep 30 20:10 if-then-else-proper.sh
-rw-rw-r-- 1 reader reader 422 Sep 30 19:56 if-then-else.sh
-rw-rw-r-- 1 reader reader 535 Sep 30 19:44 if-then-exit-rc-improved.sh
-rw-rw-r-- 1 reader reader 556 Sep 30 19:18 if-then-exit-rc.sh
-rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh
reader@ubuntu:~/scripts/chapter_09$ ls -l if-then-e???.sh
-rw-rw-r-- 1 reader reader 422 Sep 30 19:56 if-then-else.sh
-rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh
球状模式if-then-e???.sh
现在应该自己说话了。当出现?
时,任何字符(字母、数字、特殊字符)都是有效的替代。
在前面的例子中,所有三个问号都用字母代替。正如您可能已经推断的那样,正则表达式.
字符的功能与 globbing 模式?
字符相同:它只对一个字符有效。
最后,我们用于正则表达式的单括号符号也可以用在 globbing 中。一个简单的例子展示了我们如何使用cat
:
reader@ubuntu:/tmp$ echo ping > ping # Write the word ping to the file ping.
reader@ubuntu:/tmp$ echo pong > pong # Write the word pong to the file pong.
reader@ubuntu:/tmp$ ls -l
total 16
-rw-rw-r-- 1 reader reader 5 Oct 14 17:17 ping
-rw-rw-r-- 1 reader reader 5 Oct 14 17:17 pong
reader@ubuntu:/tmp$ cat p[io]ng
ping
pong
reader@ubuntu:/tmp$ cat p[a-z]ng
ping
pong
尽管全球化很强大,但这也是它变得危险的原因。出于这个原因,你可能想采取激烈的措施,关闭 globbing。虽然这是可能的,但我们还没有在实践中看到。然而,对于一些工作或脚本来说,关闭 globbing 可能是一个很好的保障。
使用set
命令,如手册页所述,可以更改 Shell 选项的值。在这种情况下,使用-f
将关闭 globbing,当我们尝试重复前面的例子时可以看到:
reader@ubuntu:/tmp$ cat p?ng
ping
pong
reader@ubuntu:/tmp$ set -f
reader@ubuntu:/tmp$ cat p?ng
cat: 'p?ng': No such file or directory
reader@ubuntu:/tmp$ set +f
reader@ubuntu:/tmp$ cat p?ng
ping
pong
选项通过在前面加一个减号(-
)来关闭,通过在前面加一个加号(+
)来打开。您可能还记得,这不是您第一次使用此功能。当我们调试 Bash 脚本时,我们不是从bash
开始的,而是从bash -x
开始的。
在这种情况下,Bash 子 Shell 在调用脚本之前执行一个set -x
命令。如果您在当前终端中使用set -x
,您的命令将如下所示:
reader@ubuntu:/tmp$ cat p?ng
ping
pong
reader@ubuntu:/tmp$ set -x
reader@ubuntu:/tmp$ cat p?ng
+ cat ping pong
ping
pong
reader@ubuntu:/tmp$ set +x
+ set +x
reader@ubuntu:/tmp$ cat p?ng
ping
pong
注意,我们现在可以看到球化模式是如何解析的:从cat p?ng
到cat ping pong
。试着记住这个功能;如果你曾经因为不知道为什么一个剧本不能达到你的目的而感到毛骨悚然,一个简单的set -x
可能会让一切变得不同!如果没有,你可以通过set +x
恢复正常行为,如示例所示。
set
has many interesting flags that can make your life easier. To see an overview of the capabilities of set
in your Bash version, use the help set
command. Because set
is a shell builtin (which you can verify with type set
), looking for a man page with man set
does not work, unfortunately.
我们现在已经讨论了正则表达式和 globbing。正如我们所见,它们非常相似,但仍有需要注意的差异。在我们的正则表达式示例中,还有一点关于 globbing,我们已经看到了如何使用grep
。
在这一部分,我们将介绍另一个命令,当与正则表达式结合时非常方便:sed
(不要与set
混淆)。我们将从grep
的一些高级用途开始。
我们已经讨论了grep
改变其默认行为的几个流行选项:--ignore-case
(-i
)--invert-match
(-v
)和--word-regexp
( -w
)。提醒一下,他们是这样做的:
-i
允许我们不区分大小写地搜索-v
只打印不匹配的行,而不是匹配的行-w
仅匹配由空格和/或行锚和/或标点符号包围的完整单词
我们还想与您分享另外三种选择。第一个新选项--only-matching
( -o
)只打印匹配的单词。如果您的搜索模式不包含任何正则表达式,这可能是一个非常无聊的选项,正如您在这个示例中看到的:
reader@ubuntu:~/scripts/chapter_10$ grep -o 'cool' grep-file.txt
cool
它完全如你所料:它打印了你要找的单词。然而,除非你只是想确认这一点,否则它可能没那么有趣。
现在,如果我们在使用更有趣的搜索模式(包含正则表达式)时做同样的事情,这个选项更有意义:
reader@ubuntu:~/scripts/chapter_10$ grep -o 'f.r' grep-file.txt
for
far
在这(简化!)例如,您实际上获得了新的信息:属于您的搜索模式的任何单词现在都会被打印出来。虽然对于如此小的文件中如此短的单词来说,这可能看起来并不令人印象深刻,但是想象一下在一个更大的文件上更复杂的搜索模式!
这就引出了另一点:grep
是快。由于采用了 Boyer-Moore 算法,grep
即使在非常大的文件(100 MB+)中也可以非常快速地进行搜索。
第二个额外选项--count
( -c
)不返回任何行。但是,它返回一个位数:搜索模式匹配的行数。一个很好的例子就是查看包安装的日志文件:
reader@ubuntu:/var/log$ grep 'status installed' dpkg.log
2018-04-26 19:07:29 status installed base-passwd:amd64 3.5.44
2018-04-26 19:07:29 status installed base-files:amd64 10.1ubuntu2
2018-04-26 19:07:30 status installed dpkg:amd64 1.19.0.5ubuntu2
<SNIPPED>
2018-06-30 17:59:37 status installed linux-headers-4.15.0-23:all 4.15.0-23.25
2018-06-30 17:59:37 status installed iucode-tool:amd64 2.3.1-1
2018-06-30 17:59:37 status installed man-db:amd64 2.8.3-2
<SNIPPED>
2018-07-01 09:31:15 status installed distro-info-data:all 0.37ubuntu0.1
2018-07-01 09:31:17 status installed libcurl3-gnutls:amd64 7.58.0-2ubuntu3.1
2018-07-01 09:31:17 status installed libc-bin:amd64 2.27-3ubuntu1
在这里的常规grep
中,我们看到显示哪个包是在哪个日期安装的日志行。但是如果我们只是想知道在某个日期安装了多少个软件包呢?--count
去救援!
reader@ubuntu:/var/log$ grep 'status installed' dpkg.log | grep '2018-08-26'
2018-08-26 11:16:16 status installed base-files:amd64 10.1ubuntu2.2
2018-08-26 11:16:16 status installed install-info:amd64 6.5.0.dfsg.1-2
2018-08-26 11:16:16 status installed plymouth-theme-ubuntu-text:amd64 0.9.3-1ubuntu7
<SNIPPED>
reader@ubuntu:/var/log$ grep 'status installed' dpkg.log | grep -c '2018-08-26'
40
我们分两个阶段执行这个grep
操作。第一个grep 'status installed'
过滤掉所有与成功安装相关的线路,跳过中间步骤,如打开和半配置。
我们使用管道后面的替代形式grep
(我们将在第 12 章、在脚本中使用管道和重定向 来将另一个搜索模式与已经过滤的数据进行匹配。这第二个grep '2018-08-26'
过滤日期。
现在,如果没有-c
选项,我们会看到 40 行。如果我们对包装感到好奇,这可能是一个不错的选择,但除此之外,只打印数量比手工计算行数要好。
Alternatively, we could have written this as a single grep search pattern, using regular expressions. Try it yourself: grep '2018-08-26 .* status installed' dpkg.log
(be sure to replace the date with some day on which you have run updates/installations).
最后一个选项非常有趣,特别是对于脚本来说,就是--quiet
( -q
)选项。想象一种情况,您想知道某个搜索模式是否存在于文件中。如果找到搜索模式,则删除该文件。如果没有找到搜索模式,您将把它添加到文件中。
如你所知,你可以用一个很好的if-then-else
构造来完成。但是,如果您使用普通的grep
,当您运行脚本时,您将看到终端中打印的文本。
这并不是什么大问题,但是一旦你的脚本变得足够大和复杂,屏幕上的大量输出会让脚本难以使用。对此,我们有--quiet
选项。看看这个示例脚本,看看您将如何做到这一点:
reader@ubuntu:~/scripts/chapter_10$ vim grep-then-else.sh
reader@ubuntu:~/scripts/chapter_10$ cat grep-then-else.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-16
# Description: Use grep exit status to make decisions about file manipulation.
# Usage: ./grep-then-else.sh
#####################################
FILE_NAME=/tmp/grep-then-else.txt
# Touch the file; creates it if it does not exist.
touch ${FILE_NAME}
# Check the file for the keyword.
grep -q 'keyword' ${FILE_NAME}
grep_rc=$?
# If the file contains the keyword, remove the file. Otherwise, write
# the keyword to the file.
if [[ ${grep_rc} -eq 0 ]]; then
rm ${FILE_NAME}
else
echo 'keyword' >> ${FILE_NAME}
fi
reader@ubuntu:~/scripts/chapter_10$ bash -x grep-then-else.sh
+ FILE_NAME=/tmp/grep-then-else.txt
+ touch /tmp/grep-then-else.txt
+ grep --quiet keyword /tmp/grep-then-else.txt
+ grep_rc='1'
+ [[ '1' -eq 0 ]]
+ echo keyword
reader@ubuntu:~/scripts/chapter_10$ bash -x grep-then-else.sh
+ FILE_NAME=/tmp/grep-then-else.txt
+ touch /tmp/grep-then-else.txt
+ grep -q keyword /tmp/grep-then-else.txt
+ grep_rc=0
+ [[ 0 -eq 0 ]]
+ rm /tmp/grep-then-else.txt
如你所见,诀窍在于退出状态。如果grep
找到一个或多个匹配的搜索模式,给出退出代码 0。如果grep
没有找到任何东西,这个返回码将是 1。
您可以在命令行上看到这一点:
reader@ubuntu:/var/log$ grep -q 'glgjegeg' dpkg.log
reader@ubuntu:/var/log$ echo $?
1
reader@ubuntu:/var/log$ grep -q 'installed' dpkg.log
reader@ubuntu:/var/log$ echo $?
0
在grep-then-else.sh
中,我们抑制grep
的所有输出。尽管如此,我们仍然可以实现我们想要的:脚本的每次运行都在然后和否则条件之间变化,正如我们的bash -x
调试输出清楚地显示的那样。
没有--quiet
,脚本的非调试输出如下:
reader@ubuntu:/tmp$ bash grep-then-else.sh
reader@ubuntu:/tmp$ bash grep-then-else.sh
keyword
reader@ubuntu:/tmp$ bash grep-then-else.sh
reader@ubuntu:/tmp$ bash grep-then-else.sh
keyword
它并没有给剧本增加什么,是吗?更好的是,很多命令都有一个--quiet
、-q
或者等效选项。
编写脚本时,请始终考虑命令的输出是否相关。如果不是,您可以使用退出状态,这几乎总是有助于更干净的输出体验。
到目前为止,我们已经看到grep
与改变其行为的各种选项一起使用。还有最后一个重要选项,我们想和大家分享:--extended-regexp
( -E
)。正如man grep
页面所述,这意味着将 PATTERN 解释为扩展的正则表达式。
与 Linux 中的默认正则表达式相反,扩展正则表达式的搜索模式更接近于其他脚本/编程语言中的正则表达式(如果您已经有这方面的经验的话)。
具体来说,当使用扩展正则表达式而不是默认正则表达式时,可以使用以下构造:
| ? | 将前一个字符重复 0 次或更多次 |
| + | 将前一个字符重复一次或多次 |
| {n} | 精确匹配前一个字符n 次 |
| {n,m} | 匹配前一个字符在 n 和 m 次之间的重复 |
| {,n} | 匹配前一个字符的重复次数 n 次或更少 |
| {n,} | 匹配前一个字符 n 次或更多次 |
| (xx|yy) | 交替字符,允许我们在搜索模式中找到 xx 或 yy(非常适合包含多个字符的模式,否则,[xy]
符号就足够了) |
As you might have seen, the man page for grep
contains a dedicated section on regular expressions and search patterns, which you may find very convenient as a quick reference.
现在,在我们开始使用新的 ERE 搜索模式之前,我们将看看一个新的命令:egrep
。如果你试图找出它的作用,你可能会从一个which egrep
开始,这将导致/bin/egrep
。这可能会让你认为它是一个独立于grep
的二进制文件,你现在已经用了很多了。
然而,最终,egrep
不过是一个小小的包装脚本:
reader@ubuntu:~/scripts/chapter_10$ cat /bin/egrep
#!/bin/sh
exec grep -E "$@"
如你所见,这只是一个 shell 脚本,但没有习惯的.sh
扩展。它使用exec
命令用新的工艺图像替换当前工艺图像。
您可能还记得,通常情况下,命令是在当前环境的分叉中执行的。在这种情况下,由于我们使用这个脚本来将(因此它被称为包装脚本的原因)包装为egrep
,所以替换它而不是再次分叉它是有意义的。
"$@"
构造也是新的:它是一个数组(如果你不熟悉这个术语,可以考虑一个有序的参数列表)。在这种情况下,它基本上将egrep
收到的所有参数传递到grep -E
中。
因此,如果完整的命令是egrep -w [[:digit:]] grep-file.txt
,它将被包装并最终作为grep -E -w [[:digit:]] grep-file.txt
执行到位。
在实践中,使用egrep
还是grep -E
并不重要。我们更喜欢使用egrep
,所以我们可以确定我们正在处理扩展的正则表达式(因为在实践中,在我们的经验中,扩展的功能经常被使用)。然而,对于简单的搜索模式,不需要 ere。
我们建议您找到自己的系统来决定何时使用每个系统。
现在来看一些扩展正则表达式搜索模式功能的例子:
reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:lower:]]{5}' grep-file.txt
but in the USA they use color (and realize)!
reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:lower:]]{7}' grep-file.txt
We can use this regular file for testing grep.
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:alpha:]]{7}' grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
第一个命令egrep -w [[:lower:]]{5} grep-file.txt
用小写字母向我们显示了所有正好五个字符长的单词。不要忘记这里我们需要-w
选项,因为否则,一行中的任何五个字母也匹配,忽略单词边界(在本例中,中的 prett 也匹配 y)。结果只有一个五个字母的单词:颜色。
接下来,我们对七个字母的单词进行同样的操作。我们现在得到了更多的结果。然而,因为我们只使用小写字母,所以我们遗漏了两个同样是七个字母长的单词:常规和新西兰。我们用[[:alpha:]]
代替[[:lower:]]
来解决这个问题。(我们也可以使用-i
选项使所有内容都不区分大小写— egrep -iw [[:lower:]]{7} grep-file.txt
)。
虽然这在功能上是可以接受的,但请考虑一下。在这种情况下,您将搜索不区分大小写的由 7 个小写字母组成的单词。那真的没有任何意义。在这种情况下,我们总是选择逻辑而不是功能,在这种情况下,这意味着将[[:lower:]]
更改为[[:alpha:]]
,而不是使用-i
选项。
因此,我们知道如何搜索特定长度的单词(或行,如果省略-w
选项)。我们现在寻找比最小或最大长度更长或更短的单词怎么样?
这里有一个例子:
reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:lower:]]{5,}' grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:alpha:]]{,3}' grep-file.txt
We can use this regular file for testing grep.
Regular expressions are pretty cool
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ egrep '.{40,}' grep-file.txt
We can use this regular file for testing grep.
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
这个例子演示了边界语法。第一个命令egrep -w '[[:lower:]]{5,}' grep-file.txt
寻找五个或更多字母的小写单词。如果您将这些结果与前面的例子进行比较,在前面的例子中,我们正在寻找正好五个字母长的单词,您现在可以看到更长的单词也是匹配的。
接下来,我们反转边界条件:我们只希望匹配三个字母或更少的单词。我们看到所有两个和三个字母的单词都匹配(而且,因为我们从[[:lower:]]
切换到[[:alpha:]]
,所以行首的 UK 和大写字母也匹配)。
在最后一个例子egrep '.{40,}' grep-file.txt
中,我们去掉了-w
,所以我们是整行匹配。我们匹配任何字符(如点所示),我们希望一行中至少有 40 个字符(如{40,}
所示)。在这种情况下,五行中只有三行匹配(因为另外两行更短)。
Quoting is very important for search patterns. If you do not use quotes in your pattern, especially when using special characters, such as { and }, you will need to escape them with a backslash. This can and will lead to confusing situations, where you're staring at the screen wondering why on earth your search pattern is not working, or even throwing errors. Just remember: if you single-quote the search pattern at all times, you will have a much better chance of avoiding these frustrating situations.
我们要展示的扩展正则表达式的最后一个概念是交替。这使用管道语法(不要与用于重定向的管道混淆,这将在第 12 章、在脚本中使用管道和重定向中进一步讨论)来传达匹配 xxx 或 yyy 的含义。
一个例子应该说明这一点:
reader@ubuntu:~/scripts/chapter_10$ egrep 'f(a|o)r' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ egrep 'f[ao]r' grep-file.txt
We can use this regular file for testing grep.
Also, New Zealand is pretty far away.
reader@ubuntu:~/scripts/chapter_10$ egrep '(USA|UK)' grep-file.txt
Did you ever realise that in the UK they say colour,
but in the USA they use color (and realize)!
在单个字母不同的情况下,我们可以选择是使用扩展交替语法,还是前面讨论的括号语法。我们建议使用最简单的语法来实现这个目标,在这个例子中,就是括号语法。
然而,一旦我们寻找一个以上字符差异的模式,使用括号语法就变得极其复杂。在这种情况下,扩展交替语法清晰简洁,特别是因为|
或||
在大多数脚本/编程逻辑中代表一个OR
构造。对于这个例子,这就像说:我想找到包含单词 USA 或单词 UK 的行。
因为这个语法很好地符合语义视图,所以它感觉很直观,也是可以理解的,这是我们应该在脚本中努力实现的!
既然我们现在已经完全熟悉了正则表达式、搜索模式和(扩展的)grep
,那么是时候转向 GNU/Linux 领域中最强大的工具之一了:sed
。这个术语是 T2 的《T3》流《T4》编辑版《T5》的简称,它所做的正是隐含的意思:编辑流。
在这种情况下,一个流可以是很多东西,但一般来说,它是文本。该文本可以在一个文件中找到,但也可以从另一个进程(如cat grep-file.txt | sed ...
)流式传输到*。在该示例中,cat
命令的输出(等于grep-file.txt
的内容)用作sed
命令的输入。*
在我们的示例中,我们将同时考虑就地文件编辑和流编辑。
我们将首先使用sed
查看实际的流编辑。流编辑允许我们做非常酷的事情:例如,我们可以改变文本中的一些单词。我们也可以删除我们不关心的某些行(例如,不包含单词 ERROR 的所有行)。
我们将从一个简单的例子开始,搜索并替换一行中的一个单词:
reader@ubuntu:~/scripts/chapter_10$ echo "What a wicked sentence"
What a wicked sentence
reader@ubuntu:~/scripts/chapter_10$ echo "What a wicked sentence" | sed 's/wicked/stupid/'
What a stupid sentence
就这样,sed
把我的肯定句变成了什么...不太积极。sed
使用的模式(用sed
术语来说,这只是一个脚本)是s/wicked/stupid/
。s
代表搜索-替换,第二个单词替换脚本的第一个单词。
观察对搜索词有多个匹配的多行会发生什么:
reader@ubuntu:~/scripts/chapter_10$ vim search.txt
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much wood would a woodchuck chuck
if a woodchuck could chuck wood?
reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed 's/wood/stone/'
How much stone would a woodchuck chuck
if a stonechuck could chuck wood?
从这个例子中,我们可以学到两件事:
- 默认情况下,
sed
只替换每行每个单词的第一个实例*。* sed
不仅全词匹配,部分词也不匹配。
如果我们想替换每一行中的所有实例,该怎么办?这叫做全局搜索-替换,语法只是略有不同:
reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed 's/wood/stone/g'
How much stone would a stonechuck chuck
if a stonechuck could chuck stone?
通过在sed
脚本的末尾添加一个g
,我们现在正在全局替换所有实例,而不仅仅是每行的第一个实例。
另一种可能是,您只想在第一行搜索替换。您可以在通过sed
发送之前使用head -1
仅选择该行,但这意味着您需要在之后追加其他行。
我们可以通过将行号放在sed
脚本前面来选择要编辑的行,如下所示:
reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed '1s/wood/stone/'
How much stone would a woodchuck chuck
if a woodchuck could chuck wood?
reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed '1s/wood/stone/g'
How much stone would a stonechuck chuck
if a woodchuck could chuck wood?
reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed '1,2s/wood/stone/g'
How much stone would a stonechuck chuck
if a stonechuck could chuck stone?
第一个脚本'1s/wood/stone/'
,指示sed
将第一行木的第一个实例替换为石。下一个脚本'1s/wood/stone/g'
,告诉sed
将木的所有实例替换为石,但只在第一行。最后一个脚本'1,2s/wood/stone/g'
,让sed
替换木之间所有行的所有实例(包括!)1
和2
。
虽然不是大不了在我们发送到sed
之前给cat
一个文件,但幸运的是,我们真的不需要这么做。sed
的用法如下:sed [OPTION] {script-only-if-no-other-script} [input-file]
。正如你在最后看到的,有一个选择[input-file]
。
我们举一个前面的例子,去掉cat
:
reader@ubuntu:~/scripts/chapter_10$ sed 's/wood/stone/g' search.txt
How much stone would a stonechuck chuck
if a stonechuck could chuck stone?
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much wood would a woodchuck chuck
if a woodchuck could chuck wood?
如您所见,通过使用可选的[input-file]
参数,sed
根据脚本处理该文件中的所有行。默认情况下,sed
打印它处理的所有内容。在某些情况下,这会导致行被打印两次,即在使用sed
的print
功能时(我们稍后会看到)。
这个例子演示的另一件非常重要的事情是:这个语法不编辑原始文件;只改变打印到STDOUT
的内容。有时,您会想要编辑文件本身——对于这些场景,sed
有--in-place
( -i
)选项。
确保你明白这个不可逆地改变了磁盘上的文件。而且,和 Linux 中的大多数东西一样,没有撤销按钮或回收站这样的东西!
让我们看看如何使用sed -i
持久化地更改文件(当然是在我们备份之后):
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much wood would a woodchuck chuck
if a woodchuck could chuck wood?
reader@ubuntu:~/scripts/chapter_10$ cp search.txt search.txt.bak
reader@ubuntu:~/scripts/chapter_10$ sed -i 's/wood/stone/g' search.txt
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much stone would a stonechuck chuck
if a stonechuck could chuck stone?
这一次,sed
没有将处理后的文本打印到你的屏幕上,而是悄悄地改变了磁盘上的文件。由于这种情况的破坏性,我们事先创建了一个备份。但是,sed
的--in-place
选项也可以通过添加文件后缀来提供该功能:
reader@ubuntu:~/scripts/chapter_10$ ls
character-class.txt error.txt grep-file.txt grep-then-else.sh search.txt search.txt.bak
reader@ubuntu:~/scripts/chapter_10$ mv search.txt.bak search.txt
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much wood would a woodchuck chuck
if a woodchuck could chuck wood?
reader@ubuntu:~/scripts/chapter_10$ sed -i'.bak' 's/wood/stone/g' search.txt
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much stone would a stonechuck chuck
if a stonechuck could chuck stone?
reader@ubuntu:~/scripts/chapter_10$ cat search.txt.bak
How much wood would a woodchuck chuck
if a woodchuck could chuck wood?
sed
对语法有点吝啬。如果你在-i
和'.bak'
之间放一个空格,你会得到奇怪的错误(这通常适用于选项有参数的命令)。在这种情况下,因为脚本定义紧随其后,所以sed
很难区分什么是文件后缀和脚本字符串。
只要记住,如果你想使用这个,你需要小心这个语法!
sed
的文字操控功能很棒的同时,也让我们可以操控整行。例如,我们可以通过编号删除某些行:
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick"
Hi,
this is
Patrick
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed 'd'
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '1d'
this is
Patrick
通过使用echo -e
结合换行符(\n
),我们可以创建多行语句。-e
在man echo
页面上解释为启用反斜杠转义。通过将多行输出输入到sed
,我们可以使用删除功能,这是一个简单使用字符d
的脚本。
如果我们以行号作为前缀,例如1d
,第一行被删除。如果我们不这样做,所有的行都会被删除,这导致我们没有输出。
另一种通常更有趣的可能性是删除包含某个单词的行:
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/Patrick/d'
Hi,
this is
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/patrick/d'
Hi,
this is
Patrick
就像我们在sed
的搜索替换功能中使用了一个单词匹配的脚本一样,如果有一个单词,我们也可以删除一整行。从前面的例子中可以看出,这是区分大小写的。幸运的是,如果我们想以一种不区分大小写的方式做到这一点,总会有一个解决方案。在grep
中,这将是-i
标志,但是对于sed
来说,这个-i
已经为--in-place
功能预留了。
那我们怎么做呢?当然是通过使用我们的老朋友正则表达式!请参见以下示例:
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/[Pp]atrick/d'
Hi,
this is
reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/.atrick/d'
Hi,
this is
虽然它不像grep
提供的功能那样优雅,但它确实在大多数情况下完成了工作。它至少应该让你意识到这样一个事实,将正则表达式与sed
一起使用会使整个事情更加灵活和强大。
和大多数事情一样,随着灵活性和能力的增加,复杂性也随之增加。但是,我们希望通过这种对正则表达式和sed
的温和介绍,两者的结合不会感到难以管理的复杂。
您可能有一个更好的用例来显示一些文件,而不是从文件或流中删除行。然而,这有一个小问题:默认情况下,sed
打印它处理的所有行。如果您给sed
打印一行的指令(使用p
脚本 ) ,它将打印该行两次——一次用于脚本上的匹配,另一次用于默认打印。
这看起来像这样:
reader@ubuntu:~/scripts/chapter_10$ cat error.txt
Process started.
Running normally.
ERROR: TCP socket broken.
ERROR: Cannot connect to database.
Exiting process.
reader@ubuntu:~/scripts/chapter_10$ sed '/ERROR/p' error.txt
Process started.
Running normally.
ERROR: TCP socket broken.
ERROR: TCP socket broken.
ERROR: Cannot connect to database.
ERROR: Cannot connect to database.
Exiting process.
打印和删除脚本的语法类似:'/word/d'
和'/word/p'
。要抑制打印所有行的sed
的默认行为,请添加一个-n
(也称为--quiet
或--silent
):
reader@ubuntu:~/scripts/chapter_10$ sed -n '/ERROR/p' error.txt
ERROR: TCP socket broken.
ERROR: Cannot connect to database.
You might have figured out that printing and deleting lines with sed
scripts shares the same functionality as grep
and grep -v
. In most cases, you can choose which you prefer to use. However, some advanced functionality, like deleting lines that match, but only from the first 10 lines of a file, can only be done with sed
. As a rule of thumb, anything that can be achieved with grep
using a single statement should be handled with grep
; otherwise, turn to sed
.
sed
还有最后一个用例,我们想强调一下:你有一个文件或流,你需要删除的不是一整行,而是那些行中的一些单词。有了grep
,这就不能(轻易)实现了。sed
有一个非常简单的方法。
什么使搜索和替换不同于简单地删除一个单词?只是替换模式!
请参见以下示例:
reader@ubuntu:~/scripts/chapter_10$ cat search.txt
How much stone would a stonechuck chuck
if a stonechuck could chuck stone?
reader@ubuntu:~/scripts/chapter_10$ sed 's/stone//g' search.txt
How much would a chuck chuck
if a chuck could chuck ?
通过用 nothing 代替一词,我们完全删除了石头一词。然而,在这个例子中,你可以看到一个你无疑会遇到的常见问题:删除一个单词后会有额外的空白。
这给我们带来了sed
的另一个技巧,在这方面对你有帮助:
reader@ubuntu:~/scripts/chapter_10$ sed -e 's/stone //g' -e 's/stone//g' search.txt
How much would a chuck chuck
if a chuck could chuck ?
通过提供-e
,后跟一个sed
脚本,可以让sed
运行多个脚本(按顺序!)越过你的小溪。默认情况下,sed
至少需要一个脚本,这就是为什么如果您只处理一个脚本,就不需要提供-e
了。对于比这更多的脚本,您需要在每个脚本之前添加一个-e
。
正则表达式是硬的。让这在 Linux 上变得更加困难的是,正则表达式被不同的程序(它们有不同的维护者,有不同的观点)稍微不同地实现。
更糟糕的是,一些程序将正则表达式的一些特性隐藏为扩展正则表达式,而其他程序则认为它们是默认的。在过去的几年里,这些程序的维护者似乎已经朝着更加全球化的 POSIX 标准发展,包括正则表达式和扩展的正则表达式,但是直到今天,仍然存在一些差异。
我们对此有一些非常简单的建议:试试吧。您可能不记得星号在 globbing 中代表什么,而不是正则表达式,也不记得问号为什么会有所不同。也许你会忘记用-E
来‘激活’扩展语法,你的扩展搜索模式会返回奇怪的错误。
您肯定会忘记引用一次搜索模式,如果它包含一个字符,如点或$(由 Bash 解释),您的命令将崩溃并烧毁,通常会显示一条不太清楚的错误消息。
只要知道我们都犯过这些错误,只有经验会让这变得更容易。事实上,在写这一章的时候,我们脑子里的命令几乎没有一个能立刻起作用!你并不孤单,你不应该为此感到难过。只要坚持下去,不断尝试,直到成功,直到你明白为什么第一次没有成功。
本章解释了正则表达式,以及在 Linux 下使用它们的两个常用工具:grep
和sed
。
我们首先解释正则表达式是搜索模式**与文本结合使用来查找匹配项。这些搜索模式允许我们在运行时不一定知道其内容的文本中非常灵活地搜索。
*例如,搜索模式允许我们只寻找单词而不寻找数字,寻找行首或行尾的单词,或者寻找空行。搜索模式包括通配符,通配符可以代表一个或多个特定字符或字符类。
我们引入了grep
命令来展示如何在 Bash 中使用正则表达式的基本功能。
本章的第二部分讨论全球化。Globbing 被用作文件名和路径的通配符机制。它与正则表达式有相似之处,但也有一些关键的区别。Globbing 可以用于大多数处理文件的命令(而且,由于 Linux 下的大多数东西都可以被认为是文件,这意味着几乎所有的命令都支持某种形式的 globbing)。
本章的后半部分用egrep
和sed
描述了正则表达式的使用。egrep
,作为grep -E
的一个简单包装器,允许我们为正则表达式使用扩展语法,这一点我们和grep
的一些常用的高级特性一起讨论过。
与默认正则表达式相反,扩展正则表达式允许我们指定某些模式的长度和重复频率,并允许我们使用交替。
本章的最后一部分描述了sed
,流编辑器。sed
是一个复杂但非常强大的命令,它允许我们做比grep
更令人兴奋的事情。
本章介绍了以下命令:grep
、set
、egrep
和sed
。
- 什么是搜索模式?
- 为什么正则表达式被认为是贪婪的?
- 除了换行符,搜索模式中的哪个字符被认为是任何一个字符的通配符?
- 在 Linux 正则表达式搜索模式中星号是如何使用的?
- 什么是线锚?
- 说出三种字符类型。
- 什么是全球化?
- 在 Bash 下,扩展正则表达式语法中有哪些是普通正则表达式无法实现的?
- 决定使用
grep
还是sed
的好的经验法则是什么? - 为什么 Linux/Bash 上的正则表达式这么难?
如果您想深入了解本章的主题,以下资源可能会很有意思:
- 正则表达式 Linux 文档项目:http://www.tldp.org/LDP/abs/html/x17129.html
- 全球 Linux 文档项目:http://www.tldp.org/LDP/abs/html/globbingref.html
- Sed 上的 Linux 文档项目:http://tldp.org/LDP/abs/html/x23170.html*