正则表达式(regular expression,regex)是一种很强大的工具,非常值得学习和掌握。Vim有一套独特的正则表达式语法。
先来了解Vim中常规的搜索和替换命令。
1 搜索和替换
Vim通过:substitute命令实现搜索和替换功能,大部分时候都会将其简写为:s。默认情况下,:s命令将当前行中的一个子字符串替换为其他字符串,其命令形式如下。
:s/<find-this>/<replace-with-this>/<flags>
<选项>参数是可选的。打开animal_farm.py文件,体验此命令。跳转到包含cat的行(如用搜索命令/cat),然后执行:s/cat/dog命令。
如图1所示,当前行中的第一个cat被替换成了dog。
图1
下面介绍:substitute的一些选项。
g表示全局替换,即将匹配到的所有项都替换掉,而不仅仅是第一个。c表示每次替换前需要确认,即弹出一个界面供用户确认是否替换。e表示没有匹配项时不显示错误。i表示忽略大小写,即搜索时不关心大小写。I表示区分大小写。
这些选项可以根据需求结合起来使用(除了i和I之外)。比如,命令:s/cat/dog/gi会将字符串cat.Cat()替换为dog.dog()。
:substitute命令可以作用于一个区间范围,即哪些内容中的匹配项会被替换掉。常用的范围是%,它使:s命令作用于整个文件。
如果要将一个文件中的所有 animal 替换成 creature,则只需要执行:%s/animal/creature/g命令。
如果在文件animal_farm.py中执行此命令,则会看到如图2所示的效果,所有的animal都被替换成了creature。
图2
替换完成之后,:substitute会在屏幕底部的状态栏显示有多少个匹配项被替换掉了。这看起来已经像是一种简单的代码重构了。
:substitute还支持其他区间范围,常用的有以下几种。
- 数字,表示行号。
$表示最后一行。%表示整个文件(最常用的一种)。/search-pattern/,即在下一个搜索结果所在的行操作。?backwards-search-pattern?,与/search-pattern/功能类似,只不过是反向搜索。
此外,这些区间范围可以用;运算符组合起来。比如20;$表示从第20行到最后一行。
下面是一个更复杂的例子,它表示从第12行开始到找到包含dog的行,在这个范围内的所有animal都被替换成creature。
:12;/dog/s/animal/creature/g
如图3所示,第13行和第14行中的两个animal被替换成了creature,但是第10行或第21行中的animal没有发生变化(用:set nu命令显示行号)。
图3
读者还可以在可视模式中将选中的文字作为默认的区间,这时不指定任何区间,直接执行:s命令,会在选中的文字上执行替换操作。更多内容参见:help cmdline-ranges中关于区间的介绍。
![]()
如果读者使用的是Linux风格的路径,或路径中包含
/符号,则可以用反斜划线\符号进行转义,避免与替换命令的分隔符混淆。当然,也可以修改替换命令的分隔符,比如:s+path/to/dir+path/to/other/dir+gc中的命令分隔符被改成了+,它等价于:s/path\/to\/dir/path\/to\/other\/dir/gc。
大部分情况下,读者可以用下面的命令将整个文件中的所有匹配项替换掉。
:%s/find-this/replace-with-this/g
在替换文本的时候,有时候读者可能只想替换那些完整的单词,这时可以用单词界定符\<和\>。比如,在animal_farm.py文件中,若用/animal搜索(先启用:set hlsearch命令来高亮显示搜索结果),可以搜到所有的animal,但是有些却不仅仅是animal单词本身,比如animals,如图4所示。
图4
不过,如果使用/\<animal\>,就能精确匹配单词animal,而将那些包含animal的其他单词排除在外,比如animals,如图5所示。
图5
2 用参数列表来处理多个文件
参数列表(argument list,arglist)支持在多个文件中执行同一操作,而不需要用户预先加载缓冲区(它会帮用户自动加载)。
参数列表支持如下命令。
:arg用于定义参数列表。:argdo对参数列表中的所有文件执行一条命令。:args用于显示参数列表中的文件列表。
如果想递归将每个Python文件中的单词animal替换掉,则可以使用如下命令。
:arg **/*.py
:argdo %s/\<animal\>/creature/ge | update
这两条命令的含义如下。
:arg <pattern>表示在参数列表中添加匹配<pattern>模式的那些文件名,每个文件名对应有它自己的缓冲区。**/*.py是通配符,表示所有.py文件,它会递归匹配当前目录下的所有.py文件。:argdo对参数列表中的每一项执行同一条命令。%s/\<animal\>/creature/ge将每个文件中的每一个(选项g的作用)单词animal都替换成creature,如果哪个文件中没找到,也不会报错(选项e的作用)。update等价于:write,用于保存每个被修改过的缓冲区。
前面已经介绍过,
\<和\>之间的animal表示的是单词animal的精确匹配,并排除那些包含animal的其他单词,比如animal_farm或animals。
这里的update是必要的,因为Vim在切换缓冲区时推荐保存当前缓冲区。另一种方案是使用:set hidden命令,它会隐藏那些警告,读者可以在所有替换完成之后用:wa命令保存所有缓冲区。
读者尝试运行这条命令,可以发现相关文件中的每一个匹配到的单词都被替换掉了(可以通过git status或git diff来查看Git仓库中的文件修改情况)。参数列表中的文件列表可以通过:args命令来查看。
实际上,参数列表是Vi时代的遗留产物,那时的参数列表与今天缓冲区的使用方式类似。只不过现在的缓冲区涵盖了参数列表:每个参数列表项都在缓冲区列表中,但不是每个缓冲区都在参数列表中。
从技术方面而言,读者可以用:bufdo命令来替代:argdo命令(因为参数列表项都在缓冲区列表中),它会对每个打开的缓冲区执行同一操作。但是,这种行为是不明智的,因为生成参数列表之前可能已经无意中打开了其他缓冲区,使用:bufdo命令会对这一部分文件产生误操作。
3 正则表达式基础
正则表达式可以在替换命令和搜索命令中使用。正则表达式引入了一些特殊模式,每种模式匹配一组字符,比如以下几种。
\(c\|p\)arrot同时匹配carrot和parrot,这里的\(c\|p\)表示c或p。\warrot\?同时匹配carrot、parrot,甚至还匹配farro,这里的\w表示所有单词字符,而t\?表示t字符是可选的。pa.\+ot匹配parrot、patriot,甚至还匹配pa123ot,这里的.\+表示一个或多个任意字符。
![]()
如果读者已经熟悉其他正则表达式变体(语法稍有不同),会发现和其他很多正则表达式不同,Vim中的正则表达式的特殊字符需要用
\来转义(默认情况下,大部分字符都不是正则表达式,只有少数例外,比如.或*)。当然,读者可以通过魔法模式改变这种行为,后面会介绍这个模式。
1.正则表达式中的特殊字符
接下来将更深入地介绍正则表达式,表6.1是几个常用的正则表达式符号。
表6.1
| 符 号 | 含 义 |
|---|---|
. |
任意字符,但不包括行尾 |
^ |
行首 |
$ |
行尾 |
\_ |
任意字符,包含行尾 |
\< |
单词开始 |
\> |
单词结尾 |
![]()
这类符号的完整列表可参考文档
:help ordinary-atom。
还有一类正则表达式称为字符类(character class),如表6.2所示。
表6.2
| 符 号 | 含 义 |
|---|---|
\s |
一个空白符(包括Tab和Space) |
\d |
一个数字 |
\w |
一个单词字符(包括数字、字母或下划线) |
\l |
一个小写字符 |
\u |
一个大写字符 |
\a |
一个字母字符 |
这些字符类的大写版本表示它们的反类,比如\D匹配所有非数字的字符,而\L匹配除小写字母外的所有字符(注意,不仅仅是大写字符)。
![]()
字符类的完整列表可参考文档
:help character-classes。
读者也可以显式地指定一个字符集合,供匹配时选择,语法是使用一对方括号[]。比如,[A-Z0-9]匹配所有的大写字母和数字,而[,4abc]只会匹配逗号、数字4和字母a、b、c。
在字符集合中,可以用短横线-来指定一个范围,这适用于构成序列的那些符号(如数字或字母表)。比如[0-7]表示0~7的数字,而[a-z]表示a~z的所有小写字母。
一个更复杂的例子是[0-9A-Za-z_],它匹配字母、数字和下划线。
读者也可以取一个字符集合的补集,只需要在字符集合的前面加上脱字符^即可。如果要匹配所有非字符数字的符号,则可以使用字符集合[^0-9A-Za-z]。
2.交替和分组
Vim还支持一些更特殊的操作符,如表6.3所示。
表6.3
| 符 号 | 含 义 |
|---|---|
\\ |
交替 |
\(\) |
分组 |
交替(alternation)操作起到的是“或”的作用,比如,carrot\|parrot同时匹配carrot和parrot。
分组(grouping)用于将多个字符放在一个组里,这样做有两个好处。首先,分组可以与其他正则表达式组合使用,比如\(c\|p\)arrot是一种同时匹配carrot和parrot的更精准的方式。
另外,分组匹配到的字符串还可以在后面的替换中重用。如果读者希望将cat hunting mice替换成mice hunting cat,则可以使用替换命令:s/\(cat\) hunting \(mice\)/\2 hungint \1/。
显然,分组是有助于代码重构的,比如,可用分组功能对函数的参数进行重排序。后面还会详细介绍这一点。
3.量词和重数
每个字符(无论是字面字符,还是特殊字符)或字符区间后面都可以接一个量词(quantifier),在Vim中称为重数(multi)。
比如,\w\+匹配一个或多个单词字符,而a\{2,4}匹配2~4个连续的字符a(如aaa)。
表6.4中是一些常用的量词。
表6.4
| 符 号 | 含 义 |
|---|---|
|
0或多个,贪婪匹配模式 |
\+ |
1或多个,贪婪匹配模式 |
\{-} |
0或多个,非贪婪匹配模式 |
\?或\= |
0或1个,贪婪匹配模式 |
\{n,m} |
n~m个,贪婪匹配模式 |
\{-n,m} |
n~m个,非贪婪匹配模式 |
![]()
量词的完整列表可参考
:help multi。
在表6.4中,读者应该注意到了两个新术语:贪婪匹配模式(greedy)和非贪婪匹配模式(non-greedy)。贪婪匹配模式指的是尽可能多地匹配字符,而非贪婪匹配模式则是尽可能少地匹配字符。
比如,对于字符串foo2bar2,贪婪正则表达式\w\+2将匹配foo2bar2(尽可能多地匹配,直到最后一个2为止),而非贪婪的\w\{-1,}只会匹配foo2。
4 魔法(magic)详解
如果读者只是偶尔在搜索或替换时使用正则表达式,那么使用反斜划线\对特殊字符转义是没有什么问题的。但是,当需要编写较长的正则表达式时,读者大概不愿意对每一个特殊字符都转义,这时候就需要用到Vim的魔法模式了。
Vim的魔法模式用于确定如何解析正则表达式字符串(如搜索和替换命令)。Vim有3种魔法模式:基本魔法、无魔法和深度魔法。
1.基本魔法(magic)
这是默认的模式,大部分特殊字符都需要转义,少数例外(如.和)。
读者可以显式设置基本魔法模式,在正则表达式字符串前面加上\m即可,比如/\mfoo或:s/\mfoo/bar。
2.无魔法(no magic)
无魔法模式类似于基本魔法模式,只不过每一个特殊字符都需要用反斜划线\转义,包括.和等字符。
比如,在默认的基本魔法模式下,搜索包含任意文本的行的命令为/^.<i class="math-start"></i>,这里的^表示行首,.*表示0个或多个任意字符,而表示行尾。而在无魔法模式中,这个命令则写为/\^\.\<i class="math-start"></i>\$。
读者可以显式地设置无魔法模式,在正则表达式前加上\M即可,比如/\Mfoo或:s/\Mfoo/bar。无魔法模式可以在.vimrc中设置,命令为set nomagic,但不建议这样做,因为修改Vim处理正则表达式的方式将很可能影响读者正在使用的很多插件(因为这些插件的作者可能并没有考虑无魔法模式)。
3.深度魔法(very magic)
深度魔法模式将数字、字母和下划线之外的字符都视为特殊字符。
使用深度魔法的方式是在正则表达式字符串之前添加\v,比如/\vfoo或:s/\vfoo/bar。
深度魔法模式的使用场合是特殊字符比较多的时候。比如,在基本魔法模式下,使用如下命令将cat hunting mice替换成mice hunting cat。
:s/\(cat\) hunging \(mice\)/\2 hunting \1
而在深度魔法模式下,这条命令可写成下列形式。
:s/\v(cat) hunging (mice)/\2 hunting \1
5 正则表达式的实际案例
重构代码中常涉及的操作是重命名和重排序,而正则表达式是实现这些功能的完美工具。
1.变量、方法或类的重命令
重构代码常常需要重命名,而且是在整个代码库中进行重命名。不过,简单的搜索和替换往往无法完成这样的任务,因为有可能会改动一些不相关的东西。
比如,将Dog类重命名为Pitbull。因为这个重命名操作需要在多个文件中进行,所以要用到参数列表。
:arg **/*.py
然后,将光标移动到需要重命名的类上,比如这里的Dog,然后执行如下命令(这里的<[Ctrl + r, Ctrl + w]\>表示先按Ctrl + r组合键,再按Ctrl + w组合键,不要输入中括号)。
:argdo %s/\<[Ctrl + r, Ctrl + w]\>/Pitbull/gec | update
运行之后,每次匹配处都会弹出提示,如图6.6所示。
图6.6
出现提示时,按y表示同意修改,而n表示拒绝修改。这条命令的含义如下。
:argdo表示在参数列表中每一项上执行某个操作(这些参数列表项是用:arg加载的)。%s/…/…/gec表示在整个文件中(%)中替换每一个匹配(g),没有找到匹配项时不提示错误(e),并且在修改前需要进行确认(c)。\<…\>表示匹配整个单词,而不仅仅是局部匹配(否则,类似于Dogfish的类也会被替换掉,与这里的用意不符)。Ctrl + r, Ctrl + w是一个快捷键,它的作用是将光标下的单词插入当前命令中(这里的Dog)。
这个方法有一个缺点,执行命令之后,读者会卡在很多个对话窗口中,无法先查看文件。如果读者希望得到更多控制权,另一种方案是先用:vimgrep命令寻找匹配项。
:vimgrep /\<Dog\>/ **/*.py
然后,读者就可以查看匹配项,并用:cn或:cp命令在多个匹配项之间跳转(或者用:copen打开一个快速恢复窗口,并在该窗口中跳转),如图6.7所示。
图6.7
当跳转到一个匹配项时,读者可以通过常用的修改命令来替换单词(如输入cw,然后输入Pitbull,再按Esc键回到正常模式),然后按句点符.来重复之前的修改操作。读者也可以使用非全局的替换命令:s/\<Dog\>/Pitbull来执行修改操作。
2.函数参数的重排列
另一个常用的重构操作是修改函数的参数。这里仅介绍参数的重排序,相关的操作也同样适用于其他场合。
下面是animal.py中定义的一个方法。
def act(self, target, verb):
return 'Suddenly {kind} {verb} at {target}!'.format(
kind=self.kind,
verb=verb,
target=target)
此方法的参数顺序不是非常直观,读者可能希望将其调整为如下形式。
def****act(self, verb, target):
return 'Suddenly {kind} {verb} at {target}!'.format(
kind=self.kind,
verb=verb,
target=target)
但是,该方法已经在多个地方被调用了,比如farm.py中用到了这个方法(当然,这里的代码都只是用于演示)。
def act(self, target):
for animal in self.animals:
if animal.get_kind() == 'cat':
print(animal.act(target, 'meows'))
elif animal.get_kind() == 'dog':
print(animal.act(target, 'barks'))
elif animal.get_kind() == 'sheep':
print(animal.act(target, 'baas'))
else:
print(animal.act(target, 'looks'))
因此,这个重构任务适合用正则表达式来完成,完整命令如下。
:arg **/*.py
:argdo %s/\v<act>\((\w{-1,}), ([^,]{-1,})\)/act(\2, \1)/gec | update
如图6.8所示,每次匹配时都会弹出一个确认界面(:substitute命令中的c选项的作用)。
图6.8
此命令的含义如下。
\v将后面的字符串设置为深度魔法模式,不需要对每个特殊字符都转义。<act\匹配字符串act(,并且确保act是一个完整单词,也就是说诸如react(之类的部分匹配不符合。(\w{-1,}), ([^,]{-1,})\)定义了用一个逗号和一个空格分隔的两个分组,以及随后的一个右括号。第一个分组是至少包含一个字符的一个单词,而第二个分组是至少包含一个字符的任意字符串(不能包含逗号),这样就可以匹配act(target, ‘barks’),而不会匹配act(self, target, verb)。- 最后,
act(\2, \1)将两个分组交换次序。
本文摘自《 Vim 8文本处理实战》

- Vim8文本处理技术指南,vim实用技巧
- 文本编辑器书籍,程序员编程开发技能
- python语言结合
本书面向的读者群体是所有使用Vim的程序员,书中的示例文本为Python代码,并详细介绍了Git和正则表达式。读者需要对操作系统和程序设计有基本的了解,特别是需要了解Linux操作系统的基本使用。虽然本书尝试兼顾三大操作系统,但毫无疑问书中内容以Linux为主。本书可以帮助读者完善Vim技能,增加程序设计的知识储备。