VIM作为一款编辑软件有着强大的操作指令,灵活的配置方法,通过适当的组合能够实现令人眼花缭乱的功能,而正则表达式作为一门处理文本和数据的重要工具,和VIM异曲同工,通过元字符的简单组合就可以匹配千变万化的文本和数据,它是如此的强大以至于有些任务如果没有正则表达式几乎没有其他好的方法实现。下面看看这两个强大的武器是如何结合在一起的。

本文翻译自http://www.vimregex.com/,算是一篇比较全面的VIM正则表达式介绍。

2.介绍

2.1什么是VIM?

VIM(VI Improve)是VI编辑器的改进版,它在UNIX中无处不在。VIM由Bram Moolenaar发明,是一款免费的编辑器,当然如果你愿意,可以捐助一部分钱。

VIM有自己的网站为www.vim.org邮件列表,上面的资料涵盖VIM的方方面面。目前,VIM能够运行在各大操作系统上,甚至是一些linux发型版本(redhat)的默认编辑器。

VIM拥有现代编辑的许多特点:语法高亮、可以定制化的用户界面、可以方便的与多种IDE集成在一起,从而具有一些更加吸引人的特色,比如故障恢复、自动命令补全、会话管理等。

VIM拥有庞大的用户群,仅linux用户就超过1000万,这个数字还在进一步增加。

2.2关于本教程

之所以写这个教程,只是因为我爱正则表达式,没有什么能比写出一个精心设计满足需要的正则表达式更让人兴奋的了,我希望这能作为一个引言。

不过说真的,正则表达式作为一个处理文本和数据的工具不是独立存在,而是嵌入在其他的程序语言或工具中,比如UNIX中的著名的grep程序,它根据一定的模式查找文件中的内容。你可以把正则表达式看做一种模式匹配语言,用它来处理一些棘手的文本问题会有意想不到的效果。

2.3致谢

感谢Benji Fisher, Zdenek Sekera, Preben “Peppe” Guldberg, Steve Kirkendall, Shaul Karl(排名不分先后)以及所有给我建议的人。

如果你有好的建议或者想法,随时发信给我(olontir at yahoo dot com)。

3.替换命令

3.1查找/替换

:range s[ubstitute]/pattern/string/cgiI
c 每次替换都要确认
g 替换一行当中所有的匹配项(没有g只替换第一个匹配值,pingao注:注意与%区别)
i 忽略大小写
I 不忽略大小写

[]中表示可选项

3.2范围操作、行地址及标记

在讲匹配模式之前,先来了解下行地址。一些命令可以接受行范围,这样命令就会限定在这个范围内执行。行范围通常由逗号(,)或者分号(;)分隔的标示符组成,你也可以使用命令mI在当前位置作一个标记,以方便后面使用,"I"可以是任何字母。

标示符 说明
数字 行号
. 当前行
$ 文件的最后一行
% 整个文件,与1,$相同
't 标记t
/pattern[/] pattern的下一个匹配行
?pattern[?] pattern的上一个匹配行
/ 最近一个搜索pattern的下一个匹配行
? 最近一个搜索pattern的上一个匹配行
& 最近一个替换pattern的下一个匹配行

如果没有指定行,操作只针对当前行。

这里有一些例子,

10, 20

-10到20行

/Section 1/+,/Section 2/-

-所有Section 1和Section 2之间的行,不包括它们所在行,+标示加一,-标示减一,可以重复多个

:/Section/+ y

-复制Section的下一个匹配行

:// normal p

-粘贴到Section下一个匹配行的下一行

Tip1:如果你在pattern里使用/,一定要使用\进行转义,比如,
s/\/dir1\/dir2\/dir3\/file/dir4\/dir5\/file2/g

为了避免这种令人迷惑的转义灾难,VIM中可以自定义分隔符,我喜欢用冒号(:)

Tip2:将下面两个快捷键映射放在你的vimrc文件中,
noremap ;; :%s:::g<Left><Left><Left>
noremap ;' :%s:::cg<Left><Left><Left><Left>

有了这两个快捷键,你会省去不少敲击键盘的时间,它会直接定位到搜索模式那里,输入搜索部分后再输入替换部分然后按回车键。第二个快捷键增加了确认标志。

4.模式说明

4.1锚

假设你想把所有的vi替换为VIM,很容易会想到下面的命令,

s/vi/VIM/g

但是如果你真的这么做了,你会发现,它会把所有vi替换为VIM,甚至vi只是某个单词的一部分,这可能不是你想要的。

你可能还会想到,在vi两边添加空格来达到想要的效果,

s: vi : VIM :g

你会发现结果并没有变化,正确的方法是使用单词边界标志\<\>

s:\<vi\>:VIM:g

行开始和结束有自己的标识符^和$,替换所有在行开始出现的vi,

s:^vi\>:VIM:

如果一行之中只有vi则进行替换,

s:^vi$:VIM:

现在假设你不仅要替换vi还要替换Vi、VI,有几种方法可以实现,

  • 最简单的方法是使用i标志, %s:vi:VIM:gi
  • 定义字符类(character class),:%s:[Vv]i:VIM:将会替换所有的Vi和vi

4.2转义字符或元字符

到目前为止,所有的匹配模式(pattern)都是由一些正常字符组成的,而正则表达式的真正强大之处就在于元字符(metacharacter),元字符是是指一些具有特殊含义的字符,从外观上它们的前面常有一个反斜杠,如下表所示,

# 匹配 # 匹配
. 除换行符之外的任意字符
\s 空白字符 \S 非空白字符
\d 数字 \D 非数字
\x 十六进制 \X 非十六进制
\o 八进制 \O 非八进制
\h 单词头(a-zA-Z_) \H 非单词头
\p 可打印字符 \P 非打印字符
\w 单词字母 \W 非单词字母
\a 字母 \A 非字母
\l 小写字母 \L 非小写字母
\u 大写字母 \U 非大写字母

比如,你想匹配 09/01/2000,可以使用下面的正则表达式,

\d\d/\d\d/\d\d\d\d

匹配一个首字母大写的六字母单词,

\u\w\w\w\w\w

如果你想匹配一个不知道长度的单词或者一个长单词,写出每个\w不是很方便,这就要用到下面介绍的**量词(quantifiers)**概念了。

4.3量词、贪婪匹配与惰性匹配

将一个量词(quantifiers)放置在模式(pattern)一部分后面,就可以限制这部分的重复次数。

量词 说明
* 0个或多个,.*匹配任何东西,甚至一个空行
\+ 1个或多个
\= 0个或1个(pingao注:相当于?)
\{n, m} 匹配n到m次
\{n} 匹配n次
\{, m} 匹配0到m次
\{n, } 至少匹配n次

n和m都必须是正整数

现在很容易就能写出一个匹配任意长度单词的表达式:\u\w\+

上面这些量词都是工作在贪婪模式下的,它们会尽可能多的匹配字符。有时候这会带来意想不到的问题,考虑一个典型的例子,假如你想匹配一个含有某种限定符的文本,比如被引号或者括号包围的文本,因为你不知道这些限定符里有什么,我们可以使用/".*"/

但是这个表达式将会匹配任何处于第一个引号和最后一个引号中间的文本,如粗体标注的部分

this file is normally “$VIM/.gvimrc”. You can check this with “:version”.

这种问题可以使用惰性(non-greedy)量词来解决,

量词 说明
\{-} 0个或多个,尽可能少的匹配
\{-n,m} n个或多个,尽可能少的匹配
\{-n, } 至少匹配n次,尽可能少的匹配
\{-, m} 至多匹配m次,尽可能少的匹配

让我们用\{-}替换上面的*,所以.\{-}将会匹配第一个引号的内容。

this file is normally “$VIM/gvimrc”. You can check this with “:version”.

\{-}确实没有让我们失望,下面看看执行下面的命令将会发生什么,

:s:.\{-}:_:g

执行前:

n and m are decimal numbers between

执行后:

n a_n_d m a_r_e d_e_c_i_m_a_l n_u_m_b_e_r_s b_e_t_w_e_e_n

"尽可能的少的匹配"在这里的意思是匹配0个字符,然而匹配竟然发生在了字符之间,下面我引用Bram自己的话来解释这种行为,

匹配到0个字符也是一种匹配,因此它会将0字符替换为一个"_",然后走到下一个位置,继续匹配到0个字符。

大部分情况下,\{-}没有多大的用处,它的这种运行方式主要是为了和*保持一致,后者也会匹配0个字符,相比之下,x\{-1,}是一种更加没用的写法,它只会匹配一个x,和x功能一样,比较有用的一种写法为x\{70},至于x\{-3,}", "x\{-2,}", "x\{-1,}用处也不大,只是为了和贪婪模式的量词保持一致。

-Bram

但是如果你只想匹配第二个引号的内容呢?或者我们只想改变引号中的一部分内容呢?我们将会用到分组(grouping)和反向引用(backreference),在这之前我们先来看下字符区间的概念(character range)。

4.4字符区间

典型的字符区间:

[012345]将会匹配括号中的任意一个,[0-5]与之等价,类似地,我们可以定义全部小写字母的字符区间[a-z],所有的字母 [a-zA-Z],数字加字母[0-9a-zA-Z],根据你所在的区域,你可以在字符区间添加à, Ö, ß这样的非ASCII字符。

注意字符区间仅仅匹配其中的一个字符,[0123]0123不同,顺序对于一个字符区间不重要,[0123][0231]一样,而01230231是两个截然不同的模式。看看执行下面的句子会发生什么,

s:[65]:Dig:g

执行前:

High 65 to 70. Southeast wind around 10

执行后:

High DigDig to 70. Southeast wind around 10

然后执行

s:65:Dig:g

执行前:

High 65 to 70. Southeast wind around 10

执行后:

High Dig to 70. Southeast wind around 10

通过放置一个反选符号(^)在字符区间的最前面可以很容易的去除不愿匹配的字符,下面将会匹配除大写字母外的任意字符,

/[^A-Z]/

我们可以使用字符区间重写匹配引号内的文本,

/"[^"]\+"/

注意[]内部的元字符会失去其特殊的意义,所以如果你想要一个包含-的字符区间,把-放在最前面,如下表达式将会匹配所有的数字和-

/[-0-9]/

同时^如果不在最前面,也会失去其特殊意义。

现在考虑一个现实的例子,假设有一个语法检测器想找出所有不以大写字母开头的句子,下面的表达式可以实现这一点,

\.\s\+[a-z]

这将会匹配一个句号、一个或多个空格然后是一个小写字母,我们现在知道如何找到错误,下面来看看如何修复它。这里就需要我们记住前面的匹配值以便后面可以重新调用它,这就是反向引用大显身手的地方了。

4.5分组和反向引用

你可以使用\(\)对模式匹配项进行分组,然后通过\1, \2 ... \9来引用。一个典型的例子为交换每一行的头两个单词,

s:\(\w\+\)\(\s\+\)\(\w\+\):\3\2\1:

\1代表第一个单词,\2代表一个或多个空白符,\3代表第二个单词。如何知道哪个数字代表哪个匹配项,从左往右数\(的个数。

# 含义 # 含义
& 模式匹配到的全部内容 \L 将后面的字符都转换为小写
\0 同上 \U 将后面的字符都转换为大写
\1 第一个括号中匹配的内容 \E end of \U and \L
\2 第二个括号中匹配的内容 \e end of \U and \L
\r 将一行分为两行
\9 第九个括号中匹配的内容 \I 将下一个字符转换为小写
~ 前面替换的字符串 \u 将下一个字符转换为大写

看下上面的语法检查问题完整的表达式,

s:\([.!?]\)\s\+\([a-z]\):\1 \u\2:g

我们将0个或多个空白符替换为两个空格。

4.6备选

备选(alternation)是指用\|将多个表达式结合在一起,这样一旦有一个表达式匹配到,则整个表达式匹配成功,返回这个表达式匹配内容。(pingao注:类似于逻辑操作符|)

\(Date:\|Subject:\|From:\)\(\s.*\)

上面的表达式将会把邮件的头部和内容放在\1\2中,对于备选需要注意的是,它不是贪婪匹配的,一旦多个表达式有一个表达式匹配到,后面的表达式将不再匹配,这意味着对于一个备选,表达式的顺序十分重要。

Tip3:将\(\)快速的放在表达式中,
cmap ;\ \(\)<Left><Left>

4.7正则表达式操作符的优先级

和算数表达式一样,正则表达式的运算符也有一定的优先级,下表从高到低列出了各个操作的优先级,

优先级 操作符 说明
1 \(\) 分组
2 \=,\+,*,\{n} 量词
3 abc\t\.\w 字符、元字符
4 “|” 备选

5.全局命令

5.1全局搜索及执行

我想介绍另一个用处广泛功能强大的命令,

:range g[lobal][!]/pattern/cmd
在range的范围内,在pattern匹配行执行Ex cmd(默认为:p[rint]),如果pattern前面加上一个!,表示pattern没有匹配的行。

全局命令的工作原理为,第一遍扫描range范围的每一行,并对pattern匹配行做一个标记;第二遍对每一个标记行执行cmd。range默认为整个文件。

注意:Ex command包括所有你在VIM命令行输入的命令,比如

:s[ubstitute], :co[py] , :d[elete], :w[rite]

非Ex command(normal command)也可以执行,

:norm[al]non-ex command

5.2例子

:g/^$/ d

-删除文件中所以的空行

:g/^$/,/./-j

-将多个空行转换为一个空行

:10,20g/^/ mo 10

-颠倒10到20行的顺序

下面是一个来自 Walter Zintz vi教程的例子,例子有改动

:'a,'b g/^Error/ . w >> errors.txt

-在标记’a和’b之间找到以Error开始的行,然后将这些行追加到errors.txt。注意:w前面的.(当前行)不要漏掉,否则将会把整个文件追加到errors.txt中。

你可以使用|作为分隔符执行多个命令,如果你想在参数中使用|,要用\对其进行转义。 Zintz的另一个例子,

:g/^Error:/ copy $ | s /Error/copy of the error/

将所有Error行拷贝到文件的最后,然后将Error替换为copy of the error。s命令没有指定地址,默认为当前行。

:g/^Error:/ s /Error/copy of the error/ | copy $

将上面的操作顺序颠倒了一下,先替换后复制。

6.更多的例子

6.1小贴士

(1)由Antonio Colombo提供

去掉所有行尾部的空白符,

s:\s*$::或者s:\s\+$::

6.2创建一个大纲

这个例子需要你有点html的背景,我们需要将<h1><h2>标签中的标题和副标题分离出来,做一个表格。

(1)首先我们给每个标签做一个标记,<h1><a name="anchor">Heading</a></h1>,anchor是标签的唯一标示,实现表达式如下,

:s:\(<h[12]>\)\(.*\s\+\([-a-zA-Z]\+\)\)\s*\(</h[12]>\):\1<a name="\3">\2</a>\4:

说明:

(2)接下来,将标题拷贝到一个地方,

:%g/<h[12]>/ t$

上面的命令将会把<h1><h2>标签所在行拷贝到文件的最后。现在文件的样子如下,

<h1><a name="anchor1">Heading1></a></h1>
<h2><a name="anchor2">Heading2></a></h2>
<h2><a name="anchor3">Heading3></a></h2>
..........................
<h1><a name="anchorN">HeadingN></a></h1>

第一步,为了要把表格的元素链接到各自的位置,我们要把name="替换为href="#

s:name=":href="#:

第二步,要让h1h2看起来不同,我们定义"majorhead"和"minorhead"两个CSS类,

g/<h1>/ s:<a:& class="majorhead":
g/<h2>/ s:<a:& class="minorhead":

现在文件看起来像这样,

<h1><a class="majorhead" name="anchor1">Heading1></a></h1>
<h2><a class="minorhead" name="anchor2">Heading2></a></h2>
#(pingao注:我认为此时文件应该是这样的)
<h1><a class="majorhead" href="#">Heading1></a></h1>
<h2><a class="minorhead" href="#">Heading2></a></h2>

我们不再需要<h1><h2>标签,

s:<h[21]>::

替换</h1></h2>标签为<br>

s:/h[21]:br:

现在文件的样子,

<a class="majorhead" name="anchor1">Heading1></a><br>
<a class="minorhead" name="anchor2">Heading2></a><br>
#(pingao注:我认为此时文件应该是这样的)
<a class="majorhead" href="#">Heading1></a><br>
<a class="minorhead" href="#">Heading2></a><br>

6.3处理表格

很多情况下,你需要处理表格形式的文本。例如,下面的文本,

 Asia    America  Africa   Europe
 Africa  Europe   Europe   Africa
 Europe  Asia     Europe   Europe
 Asia    America  Africa   Europe
 Africa  Europe   Asia     Africa
 Europe  Asia     Asia     Europe
 Europe  America  Africa   Asia
 Africa  Europe   Europe   Africa
 Europe  Asia     Europe   Europe

假设你想把第三列的"Europe"替换为 “Asia”,

:%s:\(\(\w\+\s\+\)\{2}\)Europe:\1Asia:

交换前两列,

:%s:\(\w\+\)\(.*\s\+\)\(\w\+\)$:\3\2\1:

未完待续…

7.其他语言正则表达式特点

现在我将VIM的正则表达式与其他语言的正则表达式做一个对比,特别是Perl。提起正则表达式,Perl肯定不得不提。

(在Steve Kirkendall的帮助下整理)Perl和VIM的主要区别为,

  • Perl的大多数元字符不需要反斜杠。个人认为,反斜杠越少越好,这样正则表达式会更加可读。
  • Perl中你可以在量词后加上一个?将贪婪模式的量词转换为非贪婪模式, 比如*?为非贪婪模式的*。
  • Perl的正则表达式支持各种奇怪的选项。
  • Perl的正则表达式可以包含变量,变量将会替换为具体的值,这称作"变量替换"。

8.链接

在正常模式下,输入":help pattern",阅读VIM帮助文档的正则表达式和搜索章节。

市场上有两本不错的介绍VIM正则表达式的书,

  • “Learning the vi Editor” by Linda Lamb and Arnold Robbins.
  • “vi Improved - VIM” by Steve Oualline

Jeffrey Friedl的"Mastering Regular Expressions"是一本正则表达式的权威指南,此书主要介绍Perl的正则表达式,由O’Reilly出版,官网上有一章免费。