sed & awk 第二版学习(二)—— 正则表达式语法

2024-09-05 13:53:14 浏览数 (2)

在计算机术语中,表达式是某些需要被计算的东西。一个表达式描述一种结果。正则表达式描述了模式或特殊的字符序列,尽管没有必要指定一个精确的序列。例如:

代码语言:javascript复制
^  *.*

该表达式使用元字符(metacharacter)(也叫通配符)和空格,匹配一个具有一个或多个前导空格的行。

grep、sed、awk 都使用正则表达式,但这三个程序并不能完全使用正则表达式语法中的所有元字符。为了理解正则表达式语法,必须了解由不同的元字符执行的功能。

一、表达式

一个正则表达式描述了一种模式或字符序列。字符串连接是每个正则表达式的基本操作,也就是,一个模式匹配相邻的一系列字符。例如:

代码语言:javascript复制
ABE

每个字面字符都是一个正则表达式,它只匹配那个单独的字符。这个表达式描述了“B 跟着 A,E 跟着 B”,或者简单称为“字符串 ABE”。术语“字符串”意味着每个字符都与它前面的字符相连接。正则表达式区分大小写,因此“A”不匹配“a”。sed 和 awk 为使用正则表达式提供了不区分大小写的选项。

接受正则表达式的程序必须首先解析正则表达式的语法来产生一个模式。然后逐行读取输入来尝试匹配该模式。输入行是一个字符串,要看字符串与模式是否匹配,程序将字符串的第一个字符与模式的第一个字符进行比较。如果匹配就比较第二个字符。无论何时只要匹配失败,就返回并从字符串中这个字符后面的字符重新开始匹配。下图说明了这个过程,在输入行上尝试匹配模式“abe”。

解释正则表达式

正则表达式不只限于文字字符。例如元字符句点(.)可以作为“通配符”匹配任何单个字符。元字符星号(*)用于与它前面的正则表达式的零个、一个或多个匹配,该表达式通常是一个字符。星号元字符本身不匹配任何字符,它用于修饰它前面的内容。这与它在 shell 中的含义不同。正则表达式 .* 匹配任意数目的字符,而在 shell 中,* 本身就具有这种含义。* 作为一个 shell 元字符,表示“零或多个字符”。

“.”和“*”代表了元字符的两个基本类型:能够被看做单个字符的元字符和被看做如何修饰前面的字符的元字符。使用元字符可以扩展或限制可能的匹配,从而更多地控制匹配什么和不匹配什么。

二、成行的字符

表达式中的两个基本元素是:

  1. 以一个字面值或变量表示的值。
  2. 一个操作符。

在正则表达式中,除下表中的元字符外,任意字符都被解释为只匹配它本身的字面值。

特殊字符

用途

.

匹配除换行符以外的任意单个字符。在 awk 中,句点也能匹配换行符。

*

匹配任意多个(包括零个)在它前面的单个字符,或由正则表达式指定的字符。

[...]

匹配方括号中的字符类中的任意一个。如果方括号中的第一个字符为脱字符(^),则表示否定匹配,即匹配除了换行符和类中列出的那些字符以为的所有字符。在 awk 中,也匹配换行符。连字符(-)用于表示字符类的范围。如果类中的第一个字符为右方括号(])则表示它是类的成员。所有其它的元字符在被指定为类中的成员时都会失去它们原来的含义。

^

如果作为正则表达式的第一个字符,则表示匹配行的开始。在 awk 中匹配字符串的开始,即使字符串包含嵌入的换行符。

$

如果作为正则表达式的最后一个字符,则表示匹配行的结尾。在 awk 中匹配字符串的结尾,即使字符串包含嵌入的换行符。

{n,m}

匹配它前面某个范围内单个字符,或由正则表达式指定的字符的出现次数。{n}匹配n次出现,{n,}至少匹配n次出现,{n,m}匹配n和m之间的任意次出现。

转义随后的特殊字符。

匹配前面的正则表达式的一次或多次出现。

?

匹配前面的正则表达式的零次或一次出现。

|

指定可以匹配其前面的或后面的正则表达式(替代)。

()

对正则表达式分组。

{n,m}

匹配它前面某个范围内单个字符,或由正则表达式指定的字符的出现次数。{n}匹配n次出现,{n,}至少匹配n次出现,{n,m}匹配n和m之间的任意次出现。(用于 POSIX 的 egrep 和 POSIX awk 而不是传统的 egrep 或 awk。)

元字符汇总

元字符在正则表达式中有特殊的含义。下面介绍每个元字符的用法。

1. 反斜杠

元字符反斜杠()将元字符转换成普通字符(或将普通字符转换成元字符)。它强制将任意元字符解释为普通字符,以便匹配该字符本身。

代码语言:javascript复制
# 转义句点:
.

 # 转义反斜杠:
\

# 将普通字符解释为元字符:
() {} n

2. 通配符

句点(.)代表除换行符以外的任意字符的通配符(在 awk 中,句点甚至可以匹配嵌入式换行符),通常放在字面字符或其它元字符的前面或后面。

匹配 Plymouth 后跟任意一个字符:

代码语言:javascript复制
$ grep Plymouth. list 
John Daggett, 341 King Road, Plymouth MA

本例中这个表达式与固定的字符串模式“Plymouth”具有相同的匹配:

代码语言:javascript复制
$ grep Plymouth list
John Daggett, 341 King Road, Plymouth MA

如果句点前的字符出现在行尾,因为句点不匹配换行符,所以不匹配那一行:

代码语言:javascript复制
$ grep MA. list 
$ grep MA list 
John Daggett, 341 King Road, Plymouth MA
Eric Adams, 20 Post Road, Sudbury MA
Sal Carpenter, 73 6th Street, Boston MA

3. 编写正则表达式

正则表达式允许编写简单或复杂的模式描述,而使编写正则表达式困难的因素是应用的复杂性:模式出现在各种不同的情况和上下文中。复杂性是语言本身所固有的。

编写正则表达式的过程涉及 3 个步骤:

  1. 知道要匹配的内容以及它如何出现在文本中。
  2. 编写一个模式来描述要匹配的内容。
  3. 测试模式来查看它匹配的内容。

这个过程实质上与程序员开发程序的过程相似。步骤 1 可以当做规范,它反映理解要解决的问题以及如何解决它。步骤 2 类似于编写程序代码,而步骤 3 相当于运行程序并根据规范测试它。步骤 2 和步骤 3 需重复进行,直到程序令人满意为止。

对匹配描述进行测试可以确保这个描述和所期待的一样。仔细检查测试的结果,比较输出和输入,可以大大提高对正则表达式的理解。可以按下面的方式解析模式匹配的结果:

  • Hits(命中):要匹配的行。
  • Misses(未命中):不要匹配的行。
  • Omissions(遗漏):没有匹配但需要匹配的行。
  • False alarms(假报警):不要匹配但却匹配了的行。

4. 字符类

可以列出要匹配的字符,使用方括号元字符([])将字符列表括起来,其中每个字符占据一个位置。这在处理大小写字符时很有用。例如:

代码语言:javascript复制
[Ww]hat

这个正则表达式可以匹配“what”或“What”。它匹配包含这 4 个字符的字符串的任意行。如果想提取包含 .H1、.H2、.H3 等结构化标题宏的行,可以使用下面的正则表达式:

代码语言:javascript复制
.H[12345]

可以使用字符类在 UNIX 命令中指定文件名。例如为了从一组以章节为文件名的文件中提取标题可能输入:

代码语言:javascript复制
$ grep '.H[123]' ch0[12]

注意必须用引号引住其中的模式,以便把它传递给 grep 而不是由 shell 解释。下面列出了方括号中具有特殊含义的字符。

  • :转义任意特殊字符(只用于 awk 中)。
  • -:当它不在第一个或最后一个位置时,表示一个范围。
  • ^:仅当在第一个位置时表示反转匹配。
(1)字符的范围

连字符(-)用于指定一个字符范围。每个字符类都匹配单个字符,如果指定多个类,可以描述多个连续的字符。

匹配所有大写英文字母:

代码语言:javascript复制
[A-Z]

匹配数字:

代码语言:javascript复制
[0-9]

匹配数字、小写字母、问号、逗号、句点、分号、冒号、单引号或双引号:

代码语言:javascript复制
[0-9a-z?,.;:'"]

匹配“任意后面跟有句点、问号或感叹号的小写或大写字母”:

代码语言:javascript复制
[a-zA-Z][.?!]

如果闭括号(])是作为类中的第一个字符出现,那么它就被解释为类的一个成员。如果连字符在一个类中是第一个或最后一个字符,则失去其特殊含义。

匹配算数操作符:

代码语言:javascript复制
[- */]

匹配 MM-DD-YY 或 MM/DD/YY 两种日期格式:

代码语言:javascript复制
[0-1][0-9][-/][0-3][0-9][-/][0-9][0-9]
(2)排除字符类

类中作为第一个字符的脱字符(^)将类中的所有字符都排除在被匹配之外,或者说匹配除换行符(awk 中换行符也可以被匹配)以外的没有列在方括号中的任意字符。

匹配任意非数字字符:

代码语言:javascript复制
[^0-9]

匹配非小写元音:

代码语言:javascript复制
[^aeiou]

匹配字符串“.DS”其后依次跟随一个空格、一个双引号、一个除了字符 1 以外的单个字符和一个双引号。

代码语言:javascript复制
.DS "[^1]"
(3)POSIX 字符类补充

POSIX 标准定义了两类正则表达式:基本的正则表达式(BRE),grep 和 sed 使用;扩展的正则表达式,egrep 和 awk 使用。

为了适应非英文环境,POSIX 标准增强了匹配不在英文字母表中的字符的字符类的功能。例如,法文 è 是一个字母字符,但使用典型的字符类 [a-z] 不匹配它。该标准提供了附加的字母序列,当匹配和排序字符串数据时,这些字符应该被作为单个单元看待。

POSIX 还改变了常用的术语。“字符类”在 POSIX 标准中称为“括号表达式”。在括号表达式中,除字面字符外,还可以有如下标记:

  • 字符类。由 [: 和 :] 包围 的关键字组成的 POSIX 字符类。关键字描述了不同的字符类,例如文字字符、控制字符等等。
  • 排序符号。排序符号是多字符的序列,表示这些字符应该被看做是一个单元。它由 [. 和 .] 包围的字符组成。
  • 等价类。等价类列出了应该看做是等价的字符集。例如 e 和 è。它由地区化的字符元素(由 [= 和 =] 包围)组成。

所有这三种结构都必须出现在括号表达式的方括号中。例如 [[:alpha:]!] 匹配任意单个字母字符或感叹号,[[.ch.]] 匹配整理元素 ch,但不只匹配字母 c 或字母 h。在法语地区中,[[=e=]] 可以匹配任意 e、è 或 é。下表列出了类及其匹配字符。

匹配字符

[:alnum:]

可打印字符,包括空白字符

[:alpha:]

字母字符

[:blank:]

空格和制表符

[:cntrl:]

控制字符

[:digit:]

数字字符

[:graph:]

可打印的和可见的非空格字符

[:lower:]

小写字符

[:print:]

可打印字符,包括空白字符

[:punct:]

标点符号字符

[:space:]

空白字符

[:upper:]

大写字符

[:xdigit:]

十六进制数

POSIX字符类

GNU awk 和 GNU sed 支持字符类符号,但不支持另外两个括号符号。

5. 重复出现的字符

星号(*)元字符表示它前面的正则表达式可以出现零次、一次或多次。可以使用星号元字符匹配出现在引号中的单词。

不管单词 hypertext 是否出现在引号中都会被匹配。

代码语言:javascript复制
"*hypertext"*

看一系列数字:

代码语言:javascript复制
1
5
10
50
100
500
1000
5000

匹配所有行:

代码语言:javascript复制
[15]0*

匹配除前面两行以外的所有行:

代码语言:javascript复制
[15]00*

第一个 0 是字面值,第二个由星号修饰。常使用类似的方法匹配一个或多个(而不是零个或多个)空格:

代码语言:javascript复制
  *

当星号元字符前面有句点元字符时,表示匹配任意数目的字符。这可用于标识两个固定的字符串之间的字符的跨度。使用“.*”进行匹配的范围总是最大的(贪婪模式)。

匹配引号中的任意字符串:

代码语言:javascript复制
".*"

匹配带有 <> 标记的所有行:

代码语言:javascript复制
grep '<.*>' sample

看下面的 5 行示例文本:

代码语言:javascript复制
I can do it
I cannot do it
I can not do it
I can't do it
I cant do it

匹配以上语句中的否定语句,但不匹配肯定语句:

代码语言:javascript复制
can[ no']*t

匹配所有行:

代码语言:javascript复制
can.*t

技术术语“closure(闭合)”有匹配“零次或多次”的能力。egrep 和 awk 使用的元字符扩展提供了几个非常有用的 closure 的变化。加号( )匹配其前面正则表达式的一次或多次出现。问号(?)匹配零次或一次出现。不要和 shell 中的 ? 通配符混淆。shell 中的 ? 表示单个字符,等效于正则表达式中的“.”。

6. 匹配单词

匹配 book,包括单数和复数:

代码语言:javascript复制
$ cat bookwords 
This file tests for book in various places, such as
book at the beginning of a line or
at the end of a line book
as well as the plural books and
handbooks. Here are some
phrases that use the word in different ways:
"book of the year award"
to look for a line with the word "book"
A GREAT book!
A great book? No.
told them about (the books) until it
Here are the books that you requested
Yes, it is a good book for children
amazing that it was called a "harmful book" when
once you get to the end of the book, you can't believe
A well-written regular expression should
avoid matching unrelated words,
such as booky (is that a word?)
and bookish and
bookworm and so on.

$ egrep "(^| )["[{(]*book[]})"?!.,;:'s]*( |$)" bookwords
This file tests for book in various places, such as
book at the beginning of a line or
at the end of a line book
as well as the plural books and
"book of the year award"
to look for a line with the word "book"
A GREAT book!
A great book? No.
told them about (the books) until it
Here are the books that you requested
Yes, it is a good book for children
amazing that it was called a "harmful book" when
once you get to the end of the book, you can't believe

书中给出的正则表达式是“(^| )["[{(]*book[]})"?!.,;:'s]*( |$)”,很麻烦。试了一下,egrep 支持 b,用这个很简单:

代码语言:javascript复制
$ egrep 'bbook(s)?b' bookwords

7. gres 替换脚本

代码语言:javascript复制
$ cat gres 
if [ $# -lt "3" ]
then
echo Usage: gres pattern replacement file
exit 1
fi
pattern=$1
replacement=$2
if [ -f $3 ]
then
file=$3
else
echo $3 is not a file.
exit 1
fi
A="`echo | tr '12' '01' `"
sed -e "s$A$pattern$A$replacement$A" $file

$ ./gres "A*Z" "00" test
All of us, including 00ippy, our dog
Some of us, including 00ippy, our dog

8. 限制范围

匹配第一个引号里的内容:

代码语言:javascript复制
$ cat sampleLine
.Se "Appendix" "Full Program Listings"

$ ./gres '"[^"]*"' '00' sampleLine
.Se 00 "Full Program Listings"

匹配两个数字之间至少有 5 个句点,并将句点替换为连字符:

代码语言:javascript复制
$ cat sample
1........5
5........10
10.......20
100......200

$ sed 's/([0-9][0-9]*).{5,}([0-9][0-9]*)/1-2/' sample
1-5
5-10
10-20
100-200

0 人点赞