@1405010304geshuaishuai
2017-07-28T11:50:56.000000Z
字数 10975
阅读 784
正则表达式
查找与替换
shell
grep最简单的用法就是使用固定字符串:
$ who | grep -F root
范例中使用-F选项,以查找固定字符串root。事实上,只要匹配的模式里未含有正则表达式的meta字符(metacharacter),则grep默认行为模式就等同于使用了-F。
主要提供有关正则表达式构造与匹配方式的概述。特别会提及POSIX BRE与ERE构造,因为它们想要将大部分UNIX工具里的两种正则表达式基本风格(flavor)加以正式化。
正则表达式是一种表示方式,让你可以查找匹配特定准则的文本。
正则表达式是由两个基本组成部分所建立:一般字符与特殊字符。
字符 | 模式含义 |
---|---|
\ | 通常用于关闭后续字符的特殊含义 |
. | 匹配任何单个的字符,但NUL除外。独立程序也可以不允许匹配换行符 |
* | 匹配在它之前的任何数目(或没有)的单个字符 |
^ | 匹配紧接着的正则表达式,在行或字符串的起始处 |
$ | 匹配前面的正则表达式,在字符串或行结尾处 |
[...] | 方括号表达式(bracket expression),匹配方括号内的任一字符。连字符(-)指的是连续字符的范围。^符号置于方括号里第一个字符则有反向含义:指的是匹配不在列表内(方括号内)的任何字符。作为首字符的一个连字符或是结束方括号(]),则被视为列表的一部分。 |
\{n,m\} | 区间表达式(interval expression),匹配在它前面的单个字符重现(occurrences)的次数区间。\ {n\ }指的是重现n次;\ {n,\ } 则为至少重现(oocurrences)n次,而\ {n,m\ }为重现n至m次。 |
\ ( \ ) | 将\ (与\ )间的模式存储在特殊的“保留空间(holding space)”。最多可以将9个独立的子模式(subpattern)存储在单个模式中。匹配于子模式的文本,可通过转义序列(escape sequences) \1至\9,被重复使用在相同模式里。例如\ (ab\ ).* \1,指的是匹配于ab组合的两次重现,中间可存在任何数目的字符。 |
\n | 重复在\ (与\ )方括号内第n个子模式至此点的模式。n为1至9的数字, 1为由左开始。 |
{n,m} | 与先前提及的\ {n,m\ }一样,只不过方括号前没有反斜杠。 |
+ | 匹配前面正则表达式的一个或多个实例。 |
? | 匹配前面正则表达式的零个或一个实例。 |
I | 匹配于I符号前或后的正则表达式。 |
( ) | 匹配于方括号括起来的正则表达式群。 |
在POSIX的标准下,现在叫做方括号表达式(bracket expression).在方括号表达式内除了字符之外(例如z、;等等)之外,另有额外的组成部分,包括:
字符集(Character class)
以[:与:]将关键字组合括起来的POSIX字符集。关键字描述各种不同的字符集,例如英文字母字符、控制字符等,见下表。
排序符号(Collating symbol)
排序符号指的是将多个字符序列视为一个单位。它使用[.与.]将字符组合括起来。
等价字符集(Equivalence class)
等价字符集列出的应视为等值的一组字符。以[=与=]括住。
这三种构造都必须使用方括号括住表达式。
表3-3:POSIX字符集
类别 | 匹配字符 |
---|---|
[:alnum:] | 数字字符 |
[:alpha:] | 字母字符 |
[:blank:] | 空格(space)与定位(tab)字符 |
[:cntrl:] | 控制字符 |
[:digit:] | 数字字符 |
[:graph:] | 非空格(nonspace)字符 |
[:lower:] | 小写字母字符 |
[:print:] | 可显示的字符 |
[:punct:] | 标点符号字符 |
[:space:] | 空白(whitespace)字符 |
[:upper:] | 大写字母字符 |
[:xdigit:] | 十六进制数字 |
BRE是由多个组成部分所构建,一开始提供数种匹配单个字符的方式,而后又结合额外的meta字符,进行多字符匹配。
注意:
1.在方括号的表达式里,^放在字首表示是取反(complement)的意思;也就是说,不再 方括号列表里的任意字符。所以[^aeiouy]表示的就是所有小写元音字母以外的任何字符。例如:大写元音字母、所有辅音字母、数字、标点符号等。
2.将所有要匹配的字母全列出来是一件无聊又麻烦的事情,例如[0123456789]指所有数字,或[0123456789abcdefABCDEF]表示所有十六进制的数字。因此,方括号表达式可以包括字符的范围。像前面提过的两个例子,就可以分别以[0-9]与[0-9a-fA-F]表示。
排序(collating)是指给予成组的项目排列顺序的操作。由[.与.]括起来。
等价字符集(equivalence class)用来让不同字符在匹配时视为相同字符。等价字符集类别名称以[=与=]括起来。
最后一个特殊zu'che组成部分:字符集,它表示字符的类别,例如数字、小写与大写字母、标点符号、空白(whitespace)等。这些类别名称定义于[:与:]之间。
注意:在方括号表达式中,左右其他的meta字符都会失去其特殊含义。所以[*.]匹配于字面上的星号、反斜杠以及句点。要让]进入该集合,可以将它放在列表的最前面:[]*.],将]增加至词列表中。要让减号字符进入该集合,也请将它放到列表最前端:[-*.]。若你需要右方括号与减号两者进入列表,请将右方括号放到第一个字符、减号放到最后一个字符:[]*.-]。
POSIX明确陈述:NUL字符(数值的零)不需要时可匹配的。这个字符在C语言中是用来指出字符串结尾,而POSIX标准则希望让它直接了当的,通过正规C字符串的使用实现其功能。
BRE提供一种叫向后引用(backreferences)的机制,指的是“匹配于正则表达式匹配的先前的部分”。使用后向引用的步骤有两个。第一步是将子表达式包围在 \ (与\ )里;单个模式里可包括至多9个子表达式,且可为嵌套结构。
下一步是在同一模式之后使用\digit,digit指的是介于1至9的数字,指的是“匹配于第n个前方括号内子表达式匹配成功的字符”。举例如下:
模式 | 匹配成功 |
---|---|
\ (ab \ ) \ (cd \ )[def]*\2\1 | abcdcdab,abcdeeecdab,abcdddeeffcdab |
\ (why\ ).*\1 | 一行里重现两个why |
\ ([[:alpha:]] [[:alnum:]]*\ )=\1 | 简易C/C++赋值语句 |
后向引用在寻找重复字以及匹配引号时特别好用:
\ ([" ']\ ).* \1 匹配以单引号或是双引号括起来的字,例如'foo'或"bar"
匹配多字符最简单的方法就是把它们一个接一个(连接)列出来,所以正则表达式ab匹配ab,..(两个点号)匹配任意两个字符,而[[:upper:]][[:lower:]]则匹配于任意一个大写字符,后面接着任意一个小写字符。
虽然.(点号)meta字符与方括号表达式都提供一次匹配一个字符的很好方式,但正则表达式真正强而有力的功能,其实是在修饰符(modifier)meta字符的使用上。
最常用的的修饰符为星号(*),表示“匹配0个或多个前面的单个字符”。因此,ab*c表示的是“匹配1个a、0或多个b字符以及c”。这个正则表达式匹配的有ac、abc、abbc、abbbc等。
*修饰符最好用的,但它没有限制,你不能用*表示“匹配三个字符,而不是四个字符”。而要使用一个复杂的方括号表示,表明所需的匹配次数。区间表达式(interval expression)可以解决这类问题。
\ {n \ } 前置正则表达式所得结果重现n次
\ {n, \ } 前置正则表达式所得结果重现至少n次
\ {n,m \ } 前置正则表达式所得结果重现n至m次
有了区间表达式,要表达像“重现5个a”或“重现10到42个q”就变得很简单了,这两项分别是: a\ {5 \ }与 q \ {10,42\ }。
还有两个meta字符是脱字符号(^)与货币符号($),它们叫做锚点(anchor),因为其用途在限制正则表达式匹配时,针对要被匹配字符串得开始或结尾处进行匹配(^在此处得用法与方括号表达式里得完全不同)。
^与$也可同时使用,这种情况是将括起来得正则表达式匹配整个字符串(或行)。有时^$这样得简易正则表达式也很有好用,它可以用来匹配空得字符串或行列。
扩展文件(expended file)里头时长包含得空白或空行通常会比原始码更多,因此要排除空行只要:
$ cc -E foo.c | grep -v '^$' >foo.out 预先删除空行
grep 加上 -v选项可以用来显示所有不匹配于模式的行,使用上面的做法,便能够过滤掉文件里的空(empty)行。
表3-5:BRE运算符优先级,由高至低
运算符 | 表示意义 |
---|---|
[..] [==] [::] | 用于字符排序的方括号符号 |
\metacharacter | 转义的meta字符 |
[] | 方括号表达式 |
\ ( \ ) \digit | 子表达式与后向引用 |
|
前置单个字符重现的正则表达式 |
无符号(no symbol) | 连续 |
^ $ | 锚点(Anchors) |
ERE拥有比基本正则表达式更多的功能。
较有名的一个例外出现在awk里:其\符号在方括号表达式内表示其他的含义。因此,如需匹配左方括号、连字符、右方括号或是反斜杠,你应该用[\ [\ -\ ] \ \ ]。
ERE里没有后向引用的。圆括号在ERE里具特殊含义,但和BRE里的使用又有所不同,在ERE里,\ (与\ )匹配的是字面上的左括号与右括号。
ERE在匹配多字符这方面,与BRE又很明显的不同。不过,在*的处理上和BRE是相同的。
注意:*作为ERE的第一个字符是“未定义的”,而在BRE中它是指“符合字面的*”
区间表达式可用于ERE中,但它们是写在花括号里({}),且不需要前置反斜杠字符。因此我们先前的例子“要刚好重现5个a”以及“重现10个至42个q”,写法分别为:a{5}与q{10,42}。而\ { 与 \ }则可用以匹配字面上的花括号,当在ERE里{找不到匹配的}时,POSIX特意保留其含义为“未定义(undefined)状态”。
ERE另有两个meta字符,可更细腻地处理匹配控制:
? 匹配于0个或一个前置正则表达式
+ 匹配于1个或多个前置正则表达式
使用交替(alternation)运算符,即垂直地一条线,或称为管道字符(|)。
|字符ERE运算符里优先级最低的。因此,左边会一路扩展到运算符的左边,一直到一个前置|的字符,或者时到另一个正则表达式的起始。同样地,|的右边也是一路扩展到运算符的右边,一直到后续的|字符,或是到整个正则表达式的结尾。
(why)+匹配一个或连续重复的多个why。
在必须用到交替(alternation)时,分组的功能就特别好用了(也是必需的)。它可以让你用以构建复杂并较灵活性的正则表达式,举例来说[Tt]he (CPU|computer) is 指的就是: 在The(或the)与is之间,含有CPU或computer的句子。要特别注意的一点是,圆括号在这里是meta字符,而非要匹配的输入文本。
举例:(((read|write)[[:space:]]*)+的正则表达式的意思。
结论是:这个单个正则表达式是用以匹配多个连续出现的read和write,且中间可能被空白符隔开。
在[[:space:]]之后使用*是一种判断调用(judgement call)。使用一个*而非+,此时可以匹配在行(或字符串)结尾的单词。但也可以匹配中间完全没有空白的单词。运用正则表达式时常会需要用到这样的判断调用(judgment call)。
当你将交替(alternation)操作结合^与$锚点字符使用时,分组就非常好用了。由于|为所有运算符中优先级最低的,因此正则表达式^abcd|efgh$意思是“匹配字符串的起始处是否有abcd,或者字符串结尾处是否有efgh”,这个^(abcd|efgh)$不一样,后者表示的是“找一个正是abcd或正是efgh的字符串”。
^与$在这里表示的意义与BRE里的相同:将正则表达式停驻在文本字符串(或行)的起始或结尾处。不过有个明显不同的地方就是:在ERE里,^与$永远是meta字符。
运算符 | 含义 |
---|---|
[..] [==] [::] | 用于字符对应的方括号符号 |
\metacharacter | 转义的meta字符 |
[] | 方括号表达式 |
() | 分组 |
* + ? {} | 重复前置的正则表达式 |
无符号(no symbol) | 连续字符 |
^ $ | 锚点(Anchors) |
I | 交替(Alternation) |
最常见的扩展为\ <与 \ >运算符,分别匹配“单词(word)”的开头与结尾。单词是由字母,数字及下划线组成的。我们称这类字符为单词组成(word-constituent)。
表3-7:额外的GNU正则表达式运算符
运算符 | 含义 |
---|---|
\ w | 匹配任何单词组成字符,等同于[[:alnum:]_] |
\ W | 匹配任何非单词组成的字符,等同于[^[:alnum:]_] |
\ < \ > | 匹配单词的起始与结尾。 |
\ b | 匹配单词的起始与结尾所找到的空字符串。这是\ <与\ >运算符的结合 |
\ B | 匹配两个单词组成字符之间的空字符串 |
\ ' \ ` | 分别匹配emacs缓冲区的开始与结尾。GNU程序(还有emacs)通常将它们视为^与$同义 |
从略
一般来说,执行文本替换的正确程序应该是sed--流编辑器(Stream Editor) 。sed的涉及就是用来以批处理的方式而不是交互的方式来编辑文件。
你可能会常在管道(pipeline)中间使用sed,以执行替换操作。做法就是使用s命令--要求正则表达式寻找,用替代文本(replacement text)替换匹配的文本,以及可选用的标志:
sed 's/:.*//' /etc/passwd | 删除第一个冒号之后的所有东西
sort -u 排序列表并删除重复部分
在这里,/字符扮演定界符(delimiter)的角色,从而分隔正则表达式与替代文本(replacement text)。在本例中,替代文本是空的(空字符串 null string),实际上会有效地删除匹配地文本。
find /home/tolstoy -type d -print |寻找所有目录
sed 's;/home/tolstoy/;/home/lt/;' |修改名称;注意:这里使用分号作为定界符
sed 's/^/mkdir /' |插入mkdir命令
sh -x |以Shell跟踪模式执行
上述脚本是将/home/tolstoy目录结构建立一份副本在/home/lt下(可能是为备份而做的准备)。(在本例中它地输出是/home/tolstoy底下的目录名称列表:一行一个目录。)这个脚本使用了产生命令(generating command)的手法,使命令内容成为Shell的输入。这是一个功能很强且常见的技巧。
sed 了解后向引用,而且它们还能用于替换文本中,以表示“从这里开始替换成匹配第n个圆括号里子表达式的文本”:
$ echo /home/tolstoy/ | sed 's;\(/home\)/tolstoy/;\1/lt/;'
最后说明的是:&在替代文本里表示的意思是“从此点开始替代成匹配于正则表达式的整个文本”。举例来说,假设处理Atlanta Chamber of Commerce这串文本,想要在广告册中修改所有对该城市的描述:
mv atlga.xml atlga.xml.old
sed 's/Atlanta/&, the capital of the South/' < atlga.xml.old > atlga.xml
这个脚本会存储一份原始广告小册的备份,做这类操作绝对有必要。
如果要在替代文本里使用&字符的字面意义,请使用反斜杠转义它。例如,下面的小脚本可以转换DocBook/XML文件里字面上的反斜杠,将其转换为DocBook里对应的&bsol:
sed 's/\\/\\/g'
在s命令里以g结尾表示的是:全局性(global),意即以“替代文本取代正则表达式中每一个匹配的”。如果没有设置g,sed只会取代第一个匹配的。这里来比较看看有没有设置g产生的结果。
给予sed多个命令是比较容易的。在命令行上,这是通过-e选项的方式来完成。每一个编辑命令都使用一个-e选项:
$ sed -e 's/foo/bar/g' -e 's/chicken/cow/g' myfile.xml > myfile2.xml
有时,将编辑命令全放进一个脚本里,再使用sed搭配-f选项会更好:
$ cat fixup.sed
s/foo/bar/g
s/chicken/cow/g
s/draft animal/horse/g
...
$ sed -f fixup.sed myfile.xml > myfile2.xml
POSIX也允许使用分号将同一行里的不同命令隔开:
sed 's/foo/bar/g ; s/chicken/cow/g' myfile.xml > myfile2.xml
不过,许多商用sed版本不支持此功能。
sed读取每个文件,一次读一行,将读取的行放到内存的一个区域--称之为模式空间(pattern space)。
-n选项修改了sed的默认行为。当提供此选项时,sed将不会在操作完成后打印模式空间的最后内容。反之,若在脚本里使用p,则会明白地将此行显示出来。
sed -n '/<HTML>/p' *.html 仅显示<HTML>这行
sed默认地会将每一个编辑命令(editing command)应用到每个输入行。它还可以限制一条命令要应用到哪些行,只要在命令前置一个地址(address)即可。因此,sed命令的完整形式就是:
address command
以下为不同种类的地址:
正则表达式
将一模式放置到一条命令之前,可限制命令应用于匹配模式的行。可与s命令搭配使用:
/oldfunc/ s/$/# XXX: migrate to newfunc/ 注释部分源代码
s命令里的空模式指的是“使用前一个正则表达式”:
/Tolstoy/ s//& and Camus/g 提及两位作者
最终行
符号$指“最后一行”。下面的脚本指的是快速打印文件的最后一行:
sed -n '$p' "$1" 引号里为指定显示的数据
对sed而言,“最后一行”指的是输入数据的最后一行。即便是处理多个文件,sed也将它们视为一个长的输入流,且$只应用到最后一个文件的最后一行。
行编号
可以使用绝对的行编号作为地址。
范围
可指定行的范围,仅需将地址以逗号隔开:
sed -n '10,42p' foo.xml 仅打印10-42行
sed '/foo/,/bar/ s/baz/quux/g' 仅替换范围内的行
第二个命令为“从含有foo的行开始,再匹配是否有bar的行,再将匹配后的结果中,有baz的全换成quux”。
这种以逗号隔开两个表达式的方式称为范围表达式(range expression)。
否定正则表达式
有时,将命令应用于不匹配于特定模式的每一行,也是很有用的。通过将!加在正则表达式后面就能做到。
/used/ !s /new/used/g 将没有used的每个行里的new改成used
POSIX标准指出:空白(whitespace)跟随再!之后的行为是“未定义的(unspecified)”,并建议需提供完整可移植性的应用软件,不要在!之后放置任何空白字符。
正则表达式匹配可以匹配整个表达式的输入文本最长的,最左边的子字符串。除此之外,匹配的空(null)字符串,则被认为是比完全不匹配的还长。再者,POSIX标准指出:“完全一致的匹配,指的是自最左边开始匹配、针对每一个子模式、由左至右,必须匹配到最长的可能字符串”。(子模式指的是在ERE下圆括号里的部分。为此目的,GNU的程序通常也会在BRE里以\ (...\ )提供此功能)。
在文本查找时有可能会匹配到null字符串。而在执行文本替代时,也允许你插入文本:
大部分简易程序都是处理输入数据的行,像grep与egrep,以及sed大部分的工作都是这样。在这种情况下,不会有内嵌的换行字符出现在将要匹配的数据中,^与$则分别表示行的开头与结尾。
然而,对可应用的正则表达式的程序语言,例如awk、Perl以及Python,所处理的就多半是字符串。若每个字符串表示的就是独立的一行输入,则^与$仍旧可分别表示行的开头与结尾。不过这些程序语言,起始可以让你使用不同的方式来标明每条输入记录的定界符,所以有可能单独输入的“行”(记录)里会有内嵌的换行字符。这种情况下,^与$无法匹配内嵌的换行字符;它们只用来表示字符串的开头与结尾。
字段(field)指的就是记录的组成部分。
在文本文件下,一行表示一个记录。各字段都以任意长度的空格(space)或制表(Tab)字符隔开。第二种惯例是使用特定的定界符来分隔字段,例如冒号。两种惯例都有其优缺点。
cut命令是用来剪下文本文件里的数据,文本文件可以是字段类型或是字符类型。后一种数据类型在遇到需要从文件里剪下来特定的列时,特别方便。请注意:一个制表字符在此被视为单个字符。
上面的命令可显示系统上每个用户的登录名称及全名。
通过选择其他字段编号,还可以取出每个用户的根目录:
通过子都列表做剪下操作有时是很方便的。例如,你只要取出命令ls -l的输出结果中的文件权限字段:
不过这种用法比使用字段的风险要大。因为你无法保证行内的每一个字段长度总是一样的。一般来说,我们偏好以字段为基础来提取数据。
join命令可以将多个文件结合在一起,每个文件里的每条记录,都共享一个键值(key),键值指的是记录中的主字段。
每条记录都有两个字段:工人的名字与其相对应的量。
下面的脚本程序merge-sales.sh即为使用join结合两个文件。
首先,使用sed删除注释,然后再排序个别文件。排序后的缓存文件成为join命令的输入数据,最后删除缓存文件。这是执行后的结果:
awk主要的设计是要在Shell脚本中发挥所长:做一些简易的文本处理,例如取出字段并重新编排这一类。
awk 'program' [file ...]
awk读取命令行上所指定的各个文件(若无,则为标准输入),一次读取一条记录(行)。再针对每一行,应用程序所指定的命令。awk程序基本架构为:
pattern {action}
pattern {action}
pattern部分几乎可以是任何表达式,但是在单命令行程序里,它通常是由斜杠括起来的ERE。action为任意的awk语句,但是在单命令行程序里,通常是一个直接明了的print语句。
省略pattern,则会对每一条输入记录执行action;;省略action则等同于{print},将显示整条记录。
大部分单命令行程序为这样的形式:
... | awk '{print some-stuff} ' | ...
对每条记录来说,awk会测试程序里的每个pattern。若模式值为真(例如某条记录匹配于正则表达式,或是一般表达式计算为真),则awk便执行action内的程序代码。
awk读取输入记录(通常是一些行),然后自动将各个记录切分为字段。awk将每条记录内的字段数目,存储到内建变量NF。
awk特别之处就是:也可以设置它为一个完整的ERE,这种情况下,每一个匹配在该ERE的文本都将视为字段分隔字符。
如需字段值,则是搭配$字符。通常$之后会借一个数值常数,也可能是接着一个表达式,不过多半是使用变量名称。
awk '{ print $1 }' 打印第1个字段(未指定pattern)
awk '{ print $2, $5 }' 打印第2与第5个字段(未指定pattern)
awk '{ print $1, $NF }' 打印第1个与最后一个字段(未制定pattern)
awk 'NF > 0' { print $0 }' 打印非空行(指定pattern与action)
awk 'NF > 0' 同上(未制定action,则默认为打印)
比较特别的字段是编号0:表示整条记录。
在一些简单程序中,你可以使用-F选项修改字段分隔字符。例如,显示/etc/passwd文件里的用户名与全名,你可以:
$ awk -F: '{print $1, $5}' /etc/passwd
-F选项会自动地设置FS变量。
你会发现每一个空格来分隔的。
必须设置OFS变量,改变输出字段分隔字符。方式是在命令行里使用-v选项,这会设置awk的匾蛉。
$ awk -F: -v 'OFS=**' '{ print $1, $5}' /etc/passwd
上面提到简单明了的print语句,如果没有任何参数,则等同于print $0, 极限是整条记录。
在混合文本与数值的情况下,多半会使用awk版本的printf语句。
$ awk -F: '{ printf "User %s is really %s\n", $1, $5 }' /etc/passwd
awk的print语句会自动提供最后的换行字符。如果是printf则需要用户自己提供\n。
注意
在print的参数间用逗号隔开!否则,awk将连接相邻的所有值。
BEGIN与END这两个特殊的“模式”,它们提供awk程序起始(startup)与清除(cleanup)操作。
常见于awk程序中,且通常写在个别文件里,而不是在命令行上:
BEGIN { 起始擦欧总程序代码(startup code) }
pattern1 { action1 }
pattern2 { action2 }
END { 清除操作程序代码(cleanup code) }
BEGIN与END的语句块是可选用的。
以简单程序来看, BEGIN可用来设置变量:
$awk 'BEGIN { FS = ":"; OFS = "**" } 使用BEGIN设置变量
> { print $1, $5 }' /etc/passwd 被引用的程序继续到第二行