本文实验环境 notepad++ v7.5, 不同编辑器/语言中会有细微的差别

20200715发现一个 notepad++ 的 bug:
正则模式下退出重新打开 notepad++,此时 Backward direction 变成可勾选状态, 若勾选上,正则表达式中的组号功能不可用

关于正则表达式,维基百科是这么说的:

正则表达式(英語:Regular Expression,常简写为regex、regexp或RE),又称正規表示式、正規表示法、規則運算式、常規表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器裡,正則表达式通常被用来检索、替换那些符合某个模式的文本。

简单的说就是如果你要查找或者替换大量的字符串或者文本文件,正则表达式就是非常高效的方法。
基础教程大家在网上搜下好了,我这里记录一下容易忘记或者不太常见的例子。

例1:

需求
去掉每行最后的 制表符、空格等空白符

查找
[\t ]+$ 或者 \s+$

替换
[空]



例2

需求
“check_begin:$ P E R D A C K ” (其中$之后的不是空格而是 0x00(NULL) ),将0x00去除

查找
\x00

替换
什么都不输入,即可去除 NULL

结果
“check_begin:$PERDACK”

说明
\xnn 表示ASCII代码中十六进制代码为nn的字符



例3

需求
如下一整行字符串
CMediaControl.cpp:Active:beforeGetInitInfor.CMediaControl.cpp:GetInitInfor:off.CMediaControl.cpp:ActiveEnd.USBWindow.hpp:active USB2.CMediaControl.cpp:SP_GetInitInfor:beforeGetInitInfor.CMediaControl.cpp:GetInitInfor:off.
对其中“.”后面添加换行,但“.hpp”和“.cpp”除外。

查找
\.(?![hc]pp)

替换
\.\n

结果
CMediaControl.cpp:Active:beforeGetInitInfor. CMediaControl.cpp:GetInitInfor:off. CMediaControl.cpp:ActiveEnd. USBWindow.hpp:active USB2. CMediaControl.cpp:SP_GetInitInfor:beforeGetInitInfor. CMediaControl.cpp:GetInitInfor:off.

说明
(?![hc]pp)匹配“.”之后的非“hpp”“cpp”的情况。

引申
参见正则表达式的先行断言(lookahead)和后行断言(lookbehind)

(?=exp): 匹配 exp 前面的位置(零宽正向先行断言(zero-width positive lookahead assertion))
(?<=exp): 匹配 exp 后面的位置(零宽正向后行断言(zero-width positive lookbehind assertion))
(?!exp): 匹配非 exp 前面的位置(零宽负向先行断言(zero-width negative lookahead assertion))
(?<!exp): 匹配非 exp 后面的位置(零宽负向后行断言(zero-width negative lookbehind assertion))

以上是四种 匹配位置 的断言,匹配位置的意思是不消耗字符。
(?:exp) 不仅匹配位置还消耗字符,所以在本例中 \.(?:[hc]pp)\.[hc]pp效果一样。
以下三种都消耗字符(就是说进行替换时这些字符会被替换掉)

(exp) 匹配exp,并捕获文本到自动命名的组里
(?<name>exp) 匹配exp,并捕获文本到名称为 name 的组里
(?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号

注意一下 (?:exp) 和其他两个的区别,不捕获不分配组号,这样 exp 所匹配的字符串不会缓存,可以提高正则表达式匹配的性能



例4

需求
查找 c 代码中的整块注释(即以 /* XXX */ 表示的块)

查找
\/\*(\*(?!\/)|[^*])*\*\/

替换
[空]

说明
这里需要注意一点,正则表达式多数情况下是贪婪匹配(即尽可能多的匹配字符),所以不能简单的使用如下写法

1
2
\/\*(.|\n)*\*\/   if . exclude newline  
\/\*.*\*\/        if . include newline  

首先分成三个部分:\/\*, (\*(?!\/)|[^*])*, \*\/
开头和结尾部分好理解,就是查找 /* 开头, */ 结尾的字串,每个符号前面都多个\表示转义。
中间部分是这种形式:(P1|P2)*, 其中 P1P2 分别是 \*(?!\/)[^*]
意思是匹配任意数量的 以 * 开始但是不以 / 结尾的字符 或者任意数量的 非 * 字符
这样就可以完整理解这个表达式了。
还有这样的写法 \/\*(?:\*(?!\/)|[^\*])*\*\/
比之前的写法多了 <code>(?:exp)</code> 的形式,参考上面的例子,好处是可以提高性能。

等等!关于贪婪和懒惰,这样其实也可以 \/\*(.|\r|\n)*?\*\/
其中 *? 表示重复任意次,但是尽可能少匹配。



例5:

需求
如下的代码块

1
m_pTitleBar->setMediaInfo(IDS_SetupItem_Repeat_All, true, DISC_TITLE_TIME);

若在一个cpp文件中有多处调用,现要去掉setMediaInfo函数的第二个参数,即改为如下形式

1
m_pTitleBar->setMediaInfo(IDS_SetupItem_Repeat_Off, DISC_TITLE_TIME);

查找
m_pTitleBar->setMediaInfo\((.*?),(.*?),(.*?);

替换
m_pTitleBar->setMediaInfo\((\1),(\3);

说明
*? 表示懒惰匹配(即尽可能少的匹配字符),这个例子中有多个 , 符号,所以需要懒惰匹配。
小括号内表示分组,从左到右按照123… 顺序分配组号,上面例子中函数三个参数分别分配了1,2,3三个组,替换时去掉第二组,留下了第一组和第三组,也就满足了需求。

PS:在 vscode 中使用组号是 $1 这样的。



例6:

需求
去掉含有某个字符串所在行(比如m_pTitleBar)

查找
^.*m_pTitleBar.*\r\n
注意:是去掉「所在行」,如果查找部分最后不是 \r\n 而是 $ ,则只能去掉该行内容,该空白行还在。 PS: 神奇的是,在vs2008环境下,最后部分不是 \r\n 也不是 \r 而是 \n 。目前看似乎是vs2008不识别 \r

替换
[空]

延伸
若是要去掉不含有某字符串所在行

查找
^(?!.*m_pTitleBar).*\r\n

替换
[空]

注意不能写成 ^.*(?!m_pTitleBar).*\r\n
详情参考: 利用正则表达式排除特定字符串