正则表达式(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技能,增加程序设计的知识储备。