vim入门--(6)搜索&替换:学会使用正则表达式

作者 by adtxl / 2022-01-10 / 暂无评论 / 441 个足迹

1. 正则表达式搜索

在一个搜索表达式里,或者称为模式(pattern)里,.、*、^、$、~、[]、\是有特殊含义的字符:

  • .可以匹配除换行符外的任何字符:如 a. 可以匹配“aa”、“ab”、“ac”等,但不能匹配“a”、“b”或“ba”。如果需要匹配换行符(跨行匹配)的话,则需要使用 \_.
  • * 表示之前的匹配原(最普通的情况为单个字符)重复零次或多次:如 aa* 可以匹配“a”、“aa”或“aaa”,a.* 可以匹配“a”、“aa”、“abc”等等,但两者均不能匹配“b”。
  • ^ 匹配一行的开头,如果出现在模式的开头的话;在其他位置代表字符本身。
  • $ 匹配一行的结尾,如果出现在模式的结尾的话;在其他位置代表字符本身。
  • ~ 匹配上一次替换的字符串,即如果上一次你把“foo”替换成了“bar”,那 ~ 就匹配“bar”。
  • […] 匹配方括号内的任一字符;方括号内如果第一个字符是 ^,表示对结果取反;除开头之外的 -表示范围:如[A-Za-z]表示任意一个拉丁字母,[^-+*/]表示除了“+”“-”“*”“/”外的任意字符。
  • \的含义取决于下一个字符,在大部分的情况下,包括上面的这几个(.、*、\、^、$、~、[ 和 ]),代表后面这个字符本身;在跟某些字符时则有特殊含义(后面我们会讨论最重要的那些)。

除此之外的字符都是普通字符,没有特殊含义。不过,需要注意的是,如果使用 / 开始一个搜索命令,或者在替换命令(:s)中使用 / 作为模式的分隔符,那模式中的 / 必须写作 \/ 才行,否则 Vim 看到 / 就会以为模式结束了,导致错误发生。

为了避免写模式的困扰,如果模式中使用“/”作为路径的分隔符,在替换命令中可以使用其他模式中没有的符号作为分隔符。比如,想把“/image/”全部替换成“/images/”的话,不要用 :%s/\/image\//\/images\//g,而应该用类似于 :%s!/image/!/images/!g的写法。这只能适用于替换命令,而在使用 / 命令搜索时我们就没什么好办法了,只能把模式里的 / 写作 \/。不过我们也可以取巧一下,用 ? 向上、也就是反向搜索,只要记得 n、N 反过来用找下一个就行。

通过 \ 开始的特殊表达式有不少,如果你需要完整了解的话,可以去看看参考文档(:help pattern-overview)。我们下面先学习一下最基本的 6 个特殊模式项:

  • \? 表示之前的匹配原重复零次或一次:如 aa\? 可以匹配“a”、“aa”,但不能完整匹配“aaa”(可以匹配其前两个字符、后两个或最后一个字符)。
  • \+ 表示之前的匹配原重复一次或多次:如 aa\+ 可以匹配“aa”、“aaa”,但不能匹配“a”或“b”。
  • \{n,m} 表示之前的匹配原重复 n 到 m 遍之间,两个数字可以省略部分或全部:如 a\{3}(可读作:3 个“a”)可以匹配“aaa” ,a\{,3}(可读作:最多 3 个“a”)可以匹配“”、“a”、“aa”和“aaa”;两个数字都省略时等价于*,也就是之前的匹配原可以重复零次或多次。
  • \(\) 括起一个模式,将其组成为单个匹配原:如 \(foo\)\? 可以表示单词“foo”出现零次或一次。\(\) 还有一个附加作用,是捕获匹配的内容,按 \( 出现的先后顺序,可以用 \1\2\9 来引用。如果你不需要捕获匹配内容的话,用 \%(\) 的性能更高。
  • \&是分支内多个邻接(concat)的分隔符,概念上可以和与操作相比,表示每一项都需要匹配成功,然后取最后一项的结果返回:如 .*foo.*\&.*bar.* 匹配同时出现了“foo”和“bar”的完整行。相对来讲,\& 没那么常用。
  • \| 是多个分支的分隔符,概念上可以和或操作相比,表示任意一项匹配成功即可:如 foo\|bar 可匹配“foo”或“bar”两单词之一。

接下来,我再和你分享 13 个特殊模式项。虽然它们相对来说不那么必需,但掌握它们可以大大地提高程序员的编辑效率。

  • \< 匹配单词的开头
  • \> 匹配单词的结尾
  • \s 匹配空白字符 <Space><Tab>
  • \S 匹配非空白字符
  • \d 匹配数字,相当于 [0-9]
  • \D 匹配非数字,相当于 [^0-9]
  • \x 匹配十六进制数字,相当于 [0-9A-Fa-f]
  • \X 匹配非十六进制数字,相当于 [^0-9A-Fa-f]
  • \w 匹配单词字符,相当于 [0-9A-Za-z_]
  • \W 匹配非单词字符,相当于 [^0-9A-Za-z_]
  • \h 匹配单词首字符,相当于 [A-Za-z_]
  • \H 匹配非单词首字符,相当于 [^A-Za-z_]
  • \c 忽略大小写进行匹配

搜索加亮和取消

使用vim搜索时,会对搜索结果进行加量处理。但完成工作有时不需要再加亮了,我们可以再vimrc中这样设置,加入:


" 停止搜索高亮的键映射
nnoremap <silent> <F2>      :nohlsearch<CR>
inoremap <silent> <F2> <C-O>:nohlsearch<CR>

这样一来,在搜索或替换工作完成之后,只要按下 <F2> 就可以取消搜索加亮了。

2. 正则表达式替换

你在看下面这些复杂的替换情况时,也可以同时考虑下自己有没有解决方案:

  • 你可能要保留匹配中的某些字符,而替换另外一些字符
  • 你可能要对匹配出的内容做大小写转换
  • 你可能需要“计算”出替换结果
  • 你可能需要决定一行里要替换单次还是多次,是自动替换还是要一一确认,等等

接下来,我们就分别看看这些复杂情况。在这些情况里,最常用的显然就是在替换结果中保留匹配出的字符串了
前面说到 \(\) 除了将一个模式转变成匹配原外,还有一个作用是捕捉匹配的内容,按 \(的出现顺序依次编号为 1 到 9,并可以在模式和替换字符串中用 \1\9 来访问。如果要在替换字符串中完整使用匹配内容的话,则可以使用 \0&(字符“&”也因此要在替换字符串中写成 \&)。从搜索的角度,我们一般只关心匹配与否,而不关心匹配的大小。举个例子,如果我想找出作为函数调用的 begin,那我可以写成 \&)

从搜索的角度,我们一般只关心匹配与否,而不关心匹配的大小。举个例子,如果我想找出作为函数调用的 begin,那我可以写成 \<begin(,虽然 ( 不是我想匹配的内容(函数名称)的一部分。但从替换的角度,我需要在替换时再处理一下多匹配的内容,也是件麻烦事;在非匹配的内容比较复杂或者会变化的时候,尤其会是这样。所以 Vim 里还有专门标识匹配开始和结束的匹配原,分别是 \zs 和 `ze。对于这个例子,搜索模式就应该是\<begin\ze(。为了巩固前面学到的知识,你应该知道,这个模式也可以啰嗦地写成\<begin(\&begin\<begin(\&.....`。

Vim 里还有一些大小写转换的特殊替换字符串。它们是:

  • \U 把下面的字符变成大写,直到 \E 出现
  • \u 把下一个字符变成大写
  • \L 把下面的字符变成小写,直到 \E 出现
  • \l 把下一个字符变成小写
  • \E 结束大小写转换

Vim 还能用 \= 开始一个返回字符串的表达式,用来计算出一个替换结果。

跟常用的编程语言一样,Vim 的正则表达式中支持 \t\r\n 等特殊转义字符,但在替换表达式中,由于一些技术原因(:help NL-used-for-Nul),\n 插入的是空字符(NUL 或“\0”),而非在模式中出现时代表的 LF。如果要插入正常的行尾符 LF 的话,我们得使用 \r。这意味着如果想把一个回车变成两个的话,我们得别扭地写 :s/\n/\r\r/,略遗憾。如果有特殊需要得插入 CR 的话,就要更别扭地输入 \<C-V><CR> 才行。还好,我们基本不会在替换时遇到要插入 CR 的情况……

Vim 有很多用来控制替换的标志,你可以通过 :help s_flags 查看详细的介绍,我就不一一列举了。今天这一讲中,我们只会用到最常用的一个标志,g,代表可以在一行内进行多次替换;没有这个标志的话,Vim 在一行里只会对第一个成功的匹配进行替换。替换实例同样,我们还是通过例子来巩固一下对正则表达式替换的理解。先来看一个简单的,删除行尾的“//”注释。我们可以用这个命令

:%s!\s*//.*$!!

把零到多个空白字符后面出现的“//”直到行尾全部删除。

如果要删除“/* */”注释,那就复杂多了。首先,匹配内容可以跨行;其次,有跟 HTML 标签类似的问题,需要使用最短匹配。我们需要使用的命令是:

:%s!/\*\_.\{-}\*/!!g

由于一行里可以有多个“/* */”注释,我们在替换命令的尾部还加上了 g 标志,允许一行里进行多次替换。假设我们目前的编码规范规定,所有的函数名应该首字母大写(简单起见,我们假设所有的类名已经是首字母大写了,因而构造函数自动符合该要求,不会发生冲突;但其他很多函数名称仍然是小写字母开头),我们能不能用 Vim 的替换命令做到呢?答案也是肯定的。所有需要的知识点我们都已经讲过了,我就直接公布答案了:

:%s/\<\(_*\)\([a-z]\w*\)\ze(/\1\u\2/g

这个命令比较长,请你慢慢体会一下,尝试去理解每一部分的意图。

独特见解