【Mastering Vim 2_07】第六章:正则表达式和 Vim 宏在代码重构中的实战应用

news/2025/2/26 5:19:12

最新版《Mastering Vim》封面,涵盖 Vim 9.0 版特性

【最新版《Mastering Vim》封面,涵盖 Vim 9.0 版特性】

文章目录

  • 第六章 正则表达式和 Vim 在代码重构中的应用
    • 1 substitute 替换命令
    • 2 关于 substitute 的精确匹配
    • 3 参数列表 arglist 在跨文件操作中的应用
    • 4 Vim 正则表达式基础
    • 5 关于 magic 模式
      • 5.1 magic 模式
      • 5.2 no magic 模式
      • 5.3 very magic 模式
      • 5.4 very nomagic
    • 6 批量重命名变量名、方法名或类名
    • 7 Vim 的应用
      • 7.1 Vim 在代码重构中的应用
      • 7.2 批量添加前缀
      • 7.3 的递归调用
      • 7.4 Vim 在 arglist 中的应用

写在前面
本篇为第六章自学笔记,主要介绍了正则表达式和 Vim 录制的基础知识,并结合几个典型应用场景进行了演示,包括变量名的批量重命名、Python 代码模块的完整重构等。但相较于 JetBrains 家族的成熟 IDE 工具生态,Vim 在代码重构领域仍然稍显稚嫩,暂时还没有一统江湖的杀手锏级别的通用工具,不过这方面的进展仍然非常值得关注。

第六章 正则表达式和 Vim 在代码重构中的应用

本章概要

  • substitute 命令在查询替换中的用法;
  • 借助正则表达式让查询替换更加智能化;
  • 巧用 arglist 实现多文件批量操作;
  • 重构技巧演示:方法的重命名以及参数的重新排序;
  • 的录制与按键组合回放技巧。

本章源码:https://github.com/PacktPublishing/Mastering-Vim-Second-Edition/tree/main/Chapter06

本章对 substitute 命令、正则表达式、以及 arglist 参数列表进行了深入探讨,不过书中说的重构和我理解的重构在概念上相差较大,有点虎头蛇尾的感觉。与之前章节一样,与《Vim Masterclass》专栏相似的基础内容不再赘述,仅梳理有差异的知识点。


1 substitute 替换命令

substitute 命令用于同一行内的文本替换,其语法格式为:

:s/<find-this>/<replace-with-this>/<flags>

具体用法详见《Vim Masterclass》专栏 第 10 篇笔记。这里仅补充常见的 flags 标记:

  • g:全局替换标记(global),用于替换行中出现的所有匹配项;
  • c:确认标记(confirm),在替换文本前提示用户是否执行下一步操作。其中——
    • y:表示确认替换(yes);
    • l:确认替换然后退出(last);
    • n:跳过本次替换(no);
    • a:替换当前及后续所有匹配项(all);
    • q<Esc>:退出本轮替换;
    • ^ECtrl-E:表示上翻一屏;
    • ^YCtrl-Y:表示下翻一屏;
  • e:不显示错误标记(error),如果未找到匹配项,则不显示错误;
  • i:忽略大小写标记(ignore case);
  • I:区分大小写检索标记。

更多 flags 标记用法,详见 :h s_flags

2 关于 substitute 的精确匹配

通常 :s 命令匹配到的关键词都是 模糊匹配。例如 :s/ingredient/demo_target 既能匹配 ingredient 本身,也能匹配 prepare_ingredient

图 6.1 substitute 命令的默认模糊匹配模式举例(ingredient)

【图 6.1 substitute 命令的默认模糊匹配模式举例(ingredient)】

如果需要精确匹配,需使用 /\<ingredient\>

图 6.2 通过人为控制检索范围实现精确匹配

【图 6.2 通过人为控制检索范围实现精确匹配】

3 参数列表 arglist 在跨文件操作中的应用

如果启动 Vim 时使用了多个文件名,则该文件名列表会被记入 Vim 的参数列表(argument list),即 arglist

arglist 常见操作:

  • :arg <pattern>:定义 arglist
  • :argdo <commands>:对 arglist 的所有文件批量执行指定命令;
  • :args:显示 arglist 列表内容。

例如,对本章练习源码文件夹下的所有 *.py 文件执行批量替换,将精确匹配的 ingredient 全部替换为 food,需要在 Vim 环境下先后执行如下两条命令:

:arg **/*.py
:argdo %s/\<ingredient\>/food/ge | update

注意第 2 行命令末尾必须加上 update,否则变更缓冲区内容后无法顺利切换到其他缓冲区。这里的 update 相当于 write,仅在缓冲区存在变更时保存该文件。

随着替换命令的批量执行,用 :ls 查看缓冲区列表可以看到当前 Vim 会话中存在多个缓冲区:

图 6.3 执行批量替换后看到的缓冲区列表情况

【图 6.3 执行批量替换后看到的缓冲区列表情况】

注意

上述需求也可以在 Vim 外直接实现:

$ vim **/*.py -c ":argdo %s/<ingredient>/food/ge | update"

实测结果(自动打开 Vim):

图 6.4 在 Vim 外通过 -c 选项实现批量替换

【图 6.4 在 Vim 外通过 -c 选项实现批量替换】


这里的 -c 选项表示执行指定的命令脚本。如果需要批量替换后退出 Vim,则用 -c 再跟一个 qa 命令即可:

$ vim **/*.py -c "argdo %s/\<ingredient\>/food/ge | update" -c qa

是否修改成功,可以通过 git status -sgit diff 进行检查(需提前初始化 Git 项目)。

4 Vim 正则表达式基础

特殊字符:

特殊字符含义
.任意字符(不含行尾字符)
^一行的起点位置
$一行的终点位置
\_.任意字符(包括行尾字符)
\<词首
\>词尾

更多详情,参考 :h ordinary-atom

常见字符类(character classes):

字符类含义
\s空白(制表符和空格符)
\d任意数字
\D任意非数字字符
\w任意单词字符(数字、数字或下划线)
\l任意小写字符
\L除小写字符外的任意字符
\u任意大写字符
\a任意字母字符(alphabetic character)

更多详情,参考 :h character-classes

常见正则量词:

量词符号含义
*0 次及以上,贪婪匹配
\+1 次及以上,贪婪匹配
\{-}0 次及以上,非贪婪匹配
\?\=0 次或 1 次,贪婪匹配
\{n,m}n 次到 m 次,贪婪匹配
\{-n,m}n 次到 m 次,非贪婪匹配

更多详情,参考 :h multi

关于贪婪与非贪婪搜索

贪婪搜索(greedy):指尽量匹配尽可能多的字符;

非贪婪搜索(non-greedy):指尽量匹配尽可能少的字符。

例如,给定字符串 foo2bar2\w\+2 按贪婪搜索将匹配到 foo2bar2;而 \w\{-1,}2 按非贪婪搜索仅匹配 foo2

常见正则序列:

符号含义
[A-Z0-9]匹配 AZ09 的任意字符
[^A-Z0-9]对上述序列取反
[,4abc]匹配逗号符、4abc

正则中的分组与或操作:

  • \|:正则或操作,例如:carrot\|parrot 匹配 carrotparrot
  • \(\):正则分组操作,常与或操作连用,例如:\(c\|p\)arrot 匹配 carrotparrot

cat hunting mice 替换为 mice hunting cat,执行命令:

:s/\(cat\) hunting \(mice\)/\2 hunting \1

其中 \1 包含第一个捕获组(cat),\2 包含第二个捕获组(mice)。

5 关于 magic 模式

可以看到 Vim 中的很多正则表达式写法都需要转义字符处理,对于需要大量使用正则表达式的场景,可以通过切换不同的 magic 模式简化书写。

Vim 中的 magic 模式是指正则表达式中元字符的特殊行为,分别对应四种状态:magicnomagicvery magicvery nomagic(经 DeepSeek 增补)。它们决定了哪些字符被视为特殊元字符,哪些字符需要转义。

5.1 magic 模式

该模式也是 Vim 的默认模式,除了 .*^$ 等特殊字符无需转义外,其余特殊字符(如 +?(){})均要转义,例如:\+\(\)

该模式也可以用 \m 显式声明,如:/\mfoo 或者 :s/\mfoo/bar

5.2 no magic 模式

该模式下,所有特殊字符均需转义,可用 \M 启用该模式,例如:默认的 /^.*$ 对应的 no magic 模式写法为:/\M^\.\*$

此外也可以在 vimrc 配置文件中指明使用 no magic 模式:

set nomagic

5.3 very magic 模式

该模式下,除字母、数字、下划线以外的所有字符,都将被视为特殊字符,此时无需手动输入转义字符。该模式可通过 \v 显式启用,适用于存在大量特殊字符的场景,例如刚才的换位案例:

# 默认 magic 模式:
:s/\(cat\) hunting \(mice\)/\2 hunting \1
# 启用 very magic 模式:
:s/\v(cat) hunting (mice)/\2 hunting \1

5.4 very nomagic

此时所有字符都按字面意义匹配,除非显式转义。该模式适合匹配纯文本,避免正则表达式的特殊行为。可用 \V 显式启用,例如:

/\Vfoo.bar

这里的 . 只是一个普通的句点字符,而不是一个通配符。

更多用法,参考 :h magic

6 批量重命名变量名、方法名或类名

案例演示:用 Vim 批量替换当前文件夹下的所有 *.py 文件,使得类名 Egg 被统一替换为 Omelette

具体实现:

由于需要实现跨文件批量查找替换,这里需要先定义参数列表:

:arg **/*.py

执行上述命令后,所有 *.py 文件就都被加载到了 Vim 的缓冲区内。此时切到一个包含原类名的缓冲区(如 welcome.py),并将光标定位到 Egg 上:

图 6.5 定义 arglist 后将光标定位到待替换的类名 Egg 上

然后执行以下命令:

:argdo %s/\<[Ctrl + r, Ctrl + w]\>/Omelette/gec | update

注意:上述命令中的 [Ctrl + r, Ctrl + w]一组按键操作,不是实际输入的文本内容;它表示先按 Ctrl + R、再按 Ctrl + W,这样就能自动录入当前光标所在的完整单词(本例即为 Egg),以避免手动输入较长的类名而引入不必要的笔误(实现方案有很多种,但这样写恐有炫技之嫌)。因此,本例最终批量执行的命令为:

:argdo %s/\<Egg\>/Omelette/gec | update

由于开启了确认模式,执行命令后 Vim 在成功匹配到类名 Egg 后,会在下方状态栏让用户确认下一步操作:

图 6.6 执行命令并匹配到目标关键字后,Vim 将在下方提示用户进行下一步操作

【图 6.6 执行命令并匹配到目标关键字后,Vim 将在下方提示用户进行下一步操作】

提示栏中的字符含义在本篇第一小节中介绍过,这里直接输入 a 进行批量替换。这样当前文件的所有匹配项都将被替换为指定内容(即 Omellete);接着继续查找下一个文件,再进行二次确认……直到匹配替换完全结束。

此时通过 :Git status -s 命令可以快速查看受影响的文件列表(需提前用 Git 初始化并安装 vim-fugitive 插件):

图 6.7 批量替换结束后,利用 fugitive 插件和 Git 环境查看所有受影响的文件列表

【图 6.7 批量替换结束后,利用 fugitive 插件和 Git 环境查看所有受影响的文件列表】

上述方案虽然完成了既定目标,但无法提前获知需要替换的文件列表。要想提前了解需要替换哪些文件,可以使用命令 :vimgrep /\<Egg\>/ **/*.py,然后执行 :copen + Enter 查看匹配到的文件列表:

图 6.8 利用 <a class=vimgrep + copen 命令提前获知需要替换的文件列表" />

【图 6.8 利用 vimgrep + copen 命令提前获知需要替换的文件列表】

其他实用替换技巧:

  • :%s/<[^>]*>//g:批量删除文档中的所有 HTML 标记;
  • :%s#//.*$##:删除单行注释(以 // 开头)。

7 Vim 的应用

关于 Vim 的基础知识与用法,可完全参考《Vim Masterclass》专栏 第 15 篇笔记,这里仅梳理具体演示案例。

7.1 Vim 在代码重构中的应用

需要重构的源码文件如下(Chapter06/welcome.py):

#!/usr/bin/python

from kitchen import bacon, egg, sausage
import random

INGREDIENTS = [egg.Egg(), bacon.Bacon(), sausage.Sausage()]

def prepare_ingredient(ingredient):
    has_spam = random.choice([True,  False])
    if isinstance(ingredient, egg.Egg) and has_spam:
        return 'spam eggs'
    if isinstance(ingredient, bacon.Bacon) and has_spam:
        return 'bacon and spam'
    if isinstance(ingredient, sausage.Sausage) and has_spam:
        return 'spam sausage'
    return ingredient.name

def main():
    print('Scene: A cafe. A man and his wife enter.')
    print('Man: Well, what\'ve you got?')
    menu = []
    for ingredient in INGREDIENTS:
        menu.append(prepare_ingredient(ingredient))
    print('Waitress: Well, there\'s', ', '.join(menu))


if __name__ == '__main__':
    main()

重构目标:改造 L8 至 L16 的多重 if 分支判定逻辑。

总思路:将各分支的返回值重构为一个父类方法的返回值,再让各子类在继承父类时重写该方法,从而彻底消除 if 判定。

以下是具体实现步骤:

  1. 先在父类新增一个成员属性 custom_spam_name,然后修改 prepare 方法:
# Chapter06/solution/ingredient.py
class Ingredient(object):

    def __init__(self, name):
        self.name = name
        self.custom_spam_name = None

    def prepare(self, with_spam=True):
        """Might or might not add spam to the ingredient."""
        if with_spam:
            return self.custom_spam_name or 'spam ' + self.name
        return self.name

  1. 改造子类:将原方法 prepare_ingredient 中的各分支返回值重构Ingredient 各子类的 custom_spam_name 属性中:
# Chapter06/kitchen/egg.py
from kitchen import ingredient
class Egg(ingredient.Ingredient):
    def __init__(self):
        self.name = 'egg'
        self.custom_spam_name = 'spam eggs'
        
# Chapter06/kitchen/bacon.py
from kitchen import ingredient
class Bacon(ingredient.Ingredient):
    def __init__(self):
        self.name = 'bacon'
        self.custom_spam_name = 'bacon and spam'
        
# Chapter06/kitchen/sausage.py
from kitchen import ingredient
class Sausage(ingredient.Ingredient):
    def __init__(self):
        self.name = 'sausage'
        self.custom_spam_name = 'spam sausage'
  1. 最后完成对 welcome.py重构(L8 到 L10):
#!/usr/bin/python

from kitchen import bacon, egg, sausage
import random

INGREDIENTS = [egg.Egg(), bacon.Bacon(), sausage.Sausage()]

def prepare_ingredient(ingredient):
    has_spam = random.choice([True,  False])
    return ingredient.prepare(with_spam=has_spam)

def main():
    print('Scene: A cafe. A man and his wife enter.')
    print('Man: Well, what\'ve you got?')
    menu = []
    for ingredient in INGREDIENTS:
        menu.append(prepare_ingredient(ingredient))
    print('Waitress: Well, there\'s', ', '.join(menu))


if __name__ == '__main__':
    main()

书中演示的 Vim 重构操作,其实是通过录制 "a,将原来的多重 if 判定逻辑(光标初始定位到第一个 if 处):

def prepare_ingredient(ingredient):
    has_spam = random.choice([True,  False])
    if isinstance(ingredient, egg.Egg) and has_spam:
        return 'spam eggs'
    if isinstance(ingredient, bacon.Bacon) and has_spam:
        return 'bacon and spam'
    if isinstance(ingredient, sausage.Sausage) and has_spam:
        return 'spam sausage'
    return ingredient.name

逐步改造为:

def prepare_ingredient(ingredient):
    has_spam = random.choice([True,  False])
    return ingredient.prepare(with_spam=has_spam)

的过程;并且在逐一删除 if 逻辑的过程中,需要同步修改各子类的 custom_spam_name 的取值;另外,由于整个过程需要借助 Ctrl-] 跳转到各子类的定义文件,因此还需要提前装好 ctags 工具(sudo apt install universal-ctags),并在项目根路径下提前生成 tags 文件(ctags -R .)。

一切就绪后,就可以将光标定位到第一个 if 处,并录制 Vim 到寄存器 "a 中。最终实测结果如下:

img6.9

完整的代码摘录如下(书中最后还漏掉了保存 welcome.py 的关键步骤,这里一并更正):

j_w"by$kf)b^]/self.name^Moself.custom_spam_name = ^["bp:w^M^^2dd:w^M

7.2 批量添加前缀

本例较为简单,可作为练手题。通过录制,在下列列表的每一项字符串前加注前缀 spam (注意末尾有个空格符):

dish_names = [
    'omelet',
    'sausage',
    'bacon'
]

最终效果:

dish_names = [
    'spam omelet',
    'spam sausage',
    'spam bacon'
]

7.3 的递归调用

本节通过演示将示例字典的键值对互换来介绍 Vim 递归调用(强烈不推荐使用):

处理前:

dish_names = [
    'egg': 'spam omelet',
    'sausage': 'spam sausage',
    'bacon': 'bacon and spam'
]

处理后:

dish_names = [
    'spam omelet': 'egg',
    'spam sausage': 'sausage',
    'bacon and spam': 'bacon'
]

所谓的递归调用,就是在某个寄存器中,例如在 "d 中出现类似 @d 的语句来调用自身。这无疑将引入堆栈溢出风险,这类做法也 明显不符合最佳实践。因此实际应用时应尽量避免这样 走捷径 的方案。

7.4 Vim 在 arglist 中的应用

利用 :argdo 命令可以实现对多个文件批量执行命令,格式为(假如代码位于寄存器 "a 内):

:arg **/*.py
:argdo execute ":normal @a" | update

后记
尽管 Vim 在代码重构方面还没有公认的高效处理模式和适用于所有语言环境的通用插件,但相关进展仍然非常值得关注。这就好比 DeepSeekOpenAI 的竞合关系,一旦 Vim 诞生了专门用于代码重构的通用插件,完全开源的吸引力也许很快就会让 JetBrains 这样的 IDE 霸主迅速跌落神坛。


http://www.niftyadmin.cn/n/5868017.html

相关文章

AI创作教程:用deepseek和猫箱做互动故事游戏

年轻的时候我看过典型的玛丽苏文学、小妞文学&#xff0c;老了虽然识破这是给女孩编织的琉璃般的梦&#xff0c;看起来梦幻美丽其实一击就碎&#xff0c;会伤人的碎渣渣。【叠甲完毕】 本来我想用橙光的&#xff0c;但是橙光的话&#xff0c;最好把剧本和立绘都多打磨一下。快…

自动化部署工具Jenkins和Jpom的区别及优缺点,你选择用哪个?

Jenkins和Jpom都是常用的自动化部署工具&#xff0c;但它们在功能、使用场景和架构上有显著差异。以下是它们的优缺点对比&#xff1a; Jenkins 优点&#xff1a; 成熟稳定 &#xff1a;Jenkins是开源CI/CD工具&#xff0c;拥有庞大的社区支持和丰富的插件生态。 高度可扩展 &a…

如何把图片或者图片地址存到 MySQL 数据库中以及如何将这些图片数据通过 JSP 显示在网页中

如何优雅地管理图片&#xff1a;从MySQL数据库存储到JSP展示的全流程解析 在互联网时代&#xff0c;一张引人入胜的图片往往能为网站带来巨大的流量。而作为开发者的我们&#xff0c;如何高效地管理和展示这些图片资源则成为了一项重要的技术挑战。今天&#xff0c;我们就一起…

理解 MHA、GQA、MQA 和 MLA:多头注意力的变种及其应用

在深度学习、自然语言处理&#xff08;NLP&#xff09;和计算机视觉&#xff08;CV&#xff09;中&#xff0c;多头注意力&#xff08;Multi-Head Attention, MHA&#xff09;是 Transformer 结构的核心。近年来&#xff0c;MHA 产生了多个变体&#xff0c;如 GQA&#xff08;G…

Maven导入hutool依赖报错-java: 无法访问cn.hutool.core.io.IORuntimeException 解决办法

欢迎大家来到我的博客~欢迎大家对我的博客提出指导&#xff0c;有错误的地方会改进的哦~点击这里了解更多内容 目录 一、报错二、解决办法 一、报错 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-captcha</artifactId> </de…

MATLAB应用介绍

MATLAB 数据分析 MATLAB 在数据分析方面的强大功能和优势&#xff0c;涵盖数据处理、分析、可视化、结果分享等多个环节&#xff0c;为工程师和科学家提供了全面的数据分析解决方案。 MATLAB 数据分析功能概述&#xff1a;工程师和科学家利用 MATLAB 整理、清理和分析来自气候学…

常用搜索引擎命令大全

常用搜索引擎命令大全 1.1、双引号 关键词在双引号中&#xff0c;代表完全匹配&#xff0c;搜索结果返回的页面包含双引号中出现的所有词&#xff0c;顺序也匹配。baidu、google 支持 例&#xff1a;“百度” 1.2、减号 代表不包含减号后面的词的页面&#xff0c;减少前面…

Solidity study

Solidity 开发环境 Solidity编辑器&#xff1a;Solidity编辑器是一种专门用于编写和编辑Solidity代码的编辑器。常用的Solidity编辑器包括Visual Studio Code、Atom和Sublime Text。以太坊开发环境&#xff1a;以太坊开发环境&#xff08;Ethereum Development Environment&am…