Vim中常规的搜索和替换命令

异步社区官方博客

正则表达式(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

..\19-0741 图\6-1.png{60%}

图1

下面介绍:substitute的一些选项。

这些选项可以根据需求结合起来使用(除了iI之外)。比如,命令:s/cat/dog/gi会将字符串cat.Cat()替换为dog.dog()

:substitute命令可以作用于一个区间范围,即哪些内容中的匹配项会被替换掉。常用的范围是%,它使:s命令作用于整个文件。

如果要将一个文件中的所有 animal 替换成 creature,则只需要执行:%s/animal/creature/g命令。

如果在文件animal_farm.py中执行此命令,则会看到如图2所示的效果,所有的animal都被替换成了creature

..\19-0741 图\6-2.png{60%}

图2

替换完成之后,:substitute会在屏幕底部的状态栏显示有多少个匹配项被替换掉了。这看起来已经像是一种简单的代码重构了。

:substitute还支持其他区间范围,常用的有以下几种。

此外,这些区间范围可以用;运算符组合起来。比如20;$表示从第20行到最后一行。

下面是一个更复杂的例子,它表示从第12行开始到找到包含dog的行,在这个范围内的所有animal都被替换成creature

:12;/dog/s/animal/creature/g

如图3所示,第13行和第14行中的两个animal被替换成了creature,但是第10行或第21行中的animal没有发生变化(用:set nu命令显示行号)。

..\19-0741 图\6-3.png{60%}

图3

读者还可以在可视模式中将选中的文字作为默认的区间,这时不指定任何区间,直接执行:s命令,会在选中的文字上执行替换操作。更多内容参见:help cmdline-ranges中关于区间的介绍。

..\TS.tif{8%} 

如果读者使用的是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所示。

..\19-0741 图\6-4.png{55%}

图4

不过,如果使用/\<animal\>,就能精确匹配单词animal,而将那些包含animal的其他单词排除在外,比如animals,如图5所示。

..\19-0741 图\6-5.png{55%}

图5

2 用参数列表来处理多个文件

参数列表(argument list,arglist)支持在多个文件中执行同一操作,而不需要用户预先加载缓冲区(它会帮用户自动加载)。

参数列表支持如下命令。

如果想递归将每个Python文件中的单词animal替换掉,则可以使用如下命令。

:arg **/*.py
:argdo %s/\<animal\>/creature/ge | update

这两条命令的含义如下。

前面已经介绍过,\<\>之间的animal表示的是单词animal的精确匹配,并排除那些包含animal的其他单词,比如animal_farmanimals

这里的update是必要的,因为Vim在切换缓冲区时推荐保存当前缓冲区。另一种方案是使用:set hidden命令,它会隐藏那些警告,读者可以在所有替换完成之后用:wa命令保存所有缓冲区。

读者尝试运行这条命令,可以发现相关文件中的每一个匹配到的单词都被替换掉了(可以通过git statusgit diff来查看Git仓库中的文件修改情况)。参数列表中的文件列表可以通过:args命令来查看。

实际上,参数列表是Vi时代的遗留产物,那时的参数列表与今天缓冲区的使用方式类似。只不过现在的缓冲区涵盖了参数列表:每个参数列表项都在缓冲区列表中,但不是每个缓冲区都在参数列表中。

从技术方面而言,读者可以用:bufdo命令来替代:argdo命令(因为参数列表项都在缓冲区列表中),它会对每个打开的缓冲区执行同一操作。但是,这种行为是不明智的,因为生成参数列表之前可能已经无意中打开了其他缓冲区,使用:bufdo命令会对这一部分文件产生误操作。

3 正则表达式基础

正则表达式可以在替换命令和搜索命令中使用。正则表达式引入了一些特殊模式,每种模式匹配一组字符,比如以下几种。

..\TS.tif{8%} 

如果读者已经熟悉其他正则表达式变体(语法稍有不同),会发现和其他很多正则表达式不同,Vim中的正则表达式的特殊字符需要用\来转义(默认情况下,大部分字符都不是正则表达式,只有少数例外,比如.*)。当然,读者可以通过魔法模式改变这种行为,后面会介绍这个模式。

1.正则表达式中的特殊字符

接下来将更深入地介绍正则表达式,表6.1是几个常用的正则表达式符号。

表6.1

符  号 含  义
. 任意字符,但不包括行尾
^ 行首
$ 行尾
\_ 任意字符,包含行尾
\< 单词开始
\> 单词结尾

..\TS.tif{8%} 

这类符号的完整列表可参考文档:help ordinary-atom

还有一类正则表达式称为字符类(character class),如表6.2所示。

表6.2

符  号 含  义
\s 一个空白符(包括Tab和Space)
\d 一个数字
\w 一个单词字符(包括数字、字母或下划线)
\l 一个小写字符
\u 一个大写字符
\a 一个字母字符

这些字符类的大写版本表示它们的反类,比如\D匹配所有非数字的字符,而\L匹配除小写字母外的所有字符(注意,不仅仅是大写字符)。

..\TS.tif{8%} 

字符类的完整列表可参考文档:help character-classes

读者也可以显式地指定一个字符集合,供匹配时选择,语法是使用一对方括号[]。比如,[A-Z0-9]匹配所有的大写字母和数字,而[,4abc]只会匹配逗号、数字4和字母abc

在字符集合中,可以用短横线-来指定一个范围,这适用于构成序列的那些符号(如数字或字母表)。比如[0-7]表示07的数字,而[a-z]表示az的所有小写字母。

一个更复杂的例子是[0-9A-Za-z_],它匹配字母、数字和下划线。

读者也可以取一个字符集合的补集,只需要在字符集合的前面加上脱字符^即可。如果要匹配所有非字符数字的符号,则可以使用字符集合[^0-9A-Za-z]

2.交替和分组

Vim还支持一些更特殊的操作符,如表6.3所示。

表6.3

符  号 含  义
\\  交替
\(\) 分组

交替(alternation)操作起到的是“或”的作用,比如,carrot\|parrot同时匹配carrotparrot

分组(grouping)用于将多个字符放在一个组里,这样做有两个好处。首先,分组可以与其他正则表达式组合使用,比如\(c\|p\)arrot是一种同时匹配carrotparrot的更精准的方式。

另外,分组匹配到的字符串还可以在后面的替换中重用。如果读者希望将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个,非贪婪匹配模式

 

..\TS.tif{8%} 

量词的完整列表可参考: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所示。

..\19-0741 图\6-6.png{60%}

图6.6

出现提示时,按y表示同意修改,而n表示拒绝修改。这条命令的含义如下。

这个方法有一个缺点,执行命令之后,读者会卡在很多个对话窗口中,无法先查看文件。如果读者希望得到更多控制权,另一种方案是先用:vimgrep命令寻找匹配项。

:vimgrep /\<Dog\>/ **/*.py

然后,读者就可以查看匹配项,并用:cn:cp命令在多个匹配项之间跳转(或者用:copen打开一个快速恢复窗口,并在该窗口中跳转),如图6.7所示。

..\19-0741 图\6-7.png{60%}

图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选项的作用)。

..\19-0741 图\6-8.png{60%}

图6.8

此命令的含义如下。

本文摘自《 Vim 8文本处理实战》

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