9.1 用外部语言写过滤器

第九章 VimL 混合编程 #

9.1 用外部语言写过滤器 #

9.1.1 混合编程场景介绍 #

本章来讨论 VimL 与其他语言混合编程的话题。这“混合”编程可能不是很准确的定义,也 许涉及不同层面的场景应用。在上一章介绍的异步编程也算是其中一种吧。不过如果所调 用的外部程序是别人已经写好的(或者是系统提供的经典工具),那用户就只能适应其提 供的接口或输出,在 vim 端几乎没什么可干预的。但如果利用通道连接的另一端的程序 ,也要自己开发,那就可以从设计开始就考虑如何更好地与 VimL 协作,并且显然另一端 可以使用任何主流语言。这就不再多说了,本章主要着眼于其他场景的(同步)混合编程。

与众所周知的另一件编辑神器相比, vim 是比较纯粹的编辑器,它本身提供的功能(虽 然编辑方面非常丰富)比较集中,也就比较依赖或吻合 Unix 哲学:一个工具把自己的事 做好,并且便与其他工具配合。所以,当 vim 想处理更复杂的事务时,它天然地倾向于 与其他工具“混用”。比如,从最基本打开文件编辑,vim 也接受从其他工具的管道输入:

$ ls -l | vim -

这个命令表示将当前目录下的文件列表送入 vim 中编辑,譬如打算在每行前面添加 mv 命令,想仔细规划下如何批量重命名。

就像许多 Unix 工具一样,启用 vim 时若用 - 取代文件名参数,就表示从标准输入读 入内容,所以它很容易配合管道,作为接收管道输入的末端。但由于 vim 常规运用是作 为可视化交互式全屏编辑,它不再产生标准输出,因而也不便继续产生管道输出至下一工 序。然而, vim 也有批量模式,不会打开交互界面,实际上也是可以强行配合,达到类 似 sed 的流编辑效果——但这就似乎有点旁门左道了,不是 vim 的常规用法。

当然,管道的配合,只是工具的组合与混用,离“可编程”的概念还比较远。

在支持异步的版本之前,system() 函数只能在 VimL 这端进行逻辑与流程控制,而对 所调用的外部命令不可控,这可算“半混合”编程。其实还有另一个叫“过滤器”的功能用法 ,它是允许与鼓励对所调用的外部脚本进行编程,但在 vim 这端的用法却是固定的,因 而也可算是另一种“半混合”编程。

除了自己写通道服务算“全混全”编程外, vim 在这之前还提供了多种脚本语言的内置接 口,那也算是(同步的)“全混合”编程了。本节先介绍相对简单的过滤器,下一节再介绍 语言接口。

9.1.2 过滤器的概念与使用 #

即使你对过滤器并不熟,但也应该用过 = 重缩进命令,那就是个特殊的过滤器。

过滤器的意思是将当前编辑 buffer 中指定范围的文本,当作标准输入调用某个外部程序 ,并当其标准输出替换原范围的文本,以此达到修改、编辑的目的。因此,重缩进与格式 化的本质也就是过滤器。

过滤器的标准使用方式是在命令行一对地址范围之后接 ! 与外部命令,如:

:n1,n2 ! 外部程序
:1,$ ! 外部程序
:'<, '> ! 外部程序
:. ! 外部程序

注意必须在 ! 前有地址参数,否则 !外部程序 就是纯粹切到外部 shell 运行那个 外部程序了。而过滤器并不会打断用户切到外部 shell ,只要不是处理巨量文本,替换 输入输出应该都比较快,虽然是同步,一般没延迟问题。

如果只有一个地址参数,表示只处理一行, . 表示当前行。如果是两个地址参数,则 表示起始行到终止行的范围,1,$ 表示从第一行至最后一行,即全部文本。可以在命令 行手动输入两个数字行号,也在可视模式下选择一定范围后按 : 自动添加 '<,'> 表 示所选择的行范围。

使用过滤器还有个快捷键方式,不必先按 : 进入命令行,直接在普通模式按 ! 再接一 个移动命令(文本对象),也会自动帮你选定这个文本对象,并自动进入命令行模式并填 充好地址参数,用户只要继续在 ! 之后输入想调用的外部程序。

诚然,过滤器可以直接调用别人写好、已经完善的外部程序。然而,由于以标准输出替换 标准输入的模型如此简单,而每个人的编辑任务又可能多种多样各具个性,在一时找不到 合用的外部工具时,用户完全可以用他所熟悉的任一种脚本语言快速写个过滤器。

比如再举那个简单的例子吧,给文本行编号?最简单的需求,其实可以直接用 cat -n 命令完成:

:'<,'>!cat -n

注意,从 vim 命令行调用外部过滤器时,可以附加命令行参数传给过滤器程序,选择文 本是标准输入,这两者互无关系。如果要对全文编号,一定别忘了加地址参数: 1,$! ; vim 有不少可能作用于范围的命令,在缺省时默认表示全文,但过滤器若省了地址参 数就解释为普通 ! 外部调用命令了。

但是,cat -n 的编号似乎不美观,右对齐,空白太多。如果你想编号左对齐,数字后 面最好还能加个符号,如 1.1) 等,再或者想为指定行编号,比如跳过注释行…… 等等,不一而足的需求。如果你熟悉某种脚本语言,最好是自己操起脚本语言来写适合的 过滤器。例如,下面这个 perl 脚本,实现为文本行编号:

" file: catn.pl
my $sep = shift || "";
my $num = shift || 0;
$sep .= ($num > 0) ? (" " x $num) : "\t";
while (<>) { print "$.$sep$_"; }

该过滤器脚本接受两个参数,第一参数指定紧接数字编号的后缀符号,第二参数指定之后 隔几个空白,如果缺省,就隔一个制表符。在 while 循环中,<> 符号用于从标准输入 读取数据,$. 表示行号, $_ 表示当前行文本,这样语义就明确了,行号与分隔字 符串与原文本拼接起来作为标准输出。(用其他语言写这个脚本也不复杂,只是语法不一 样,总是可以手动累加行号的)

如果将该脚本保存在当前目录中,可以在 vim 命令行尝试一下:

:'<,'>!perl catn.pl
:'<,'>!perl catn.pl .
:'<,'>!./catn.pl . 2

如果给脚本添加了可执行权限,可直接将脚本作为过滤器程序,否则就将脚本文件当作 perl 解释器的第一参数。如果脚本不在当前目录,请替换为脚本全路径,或者若将可执 行脚本放在某个 $PATH 路径中,也可以直接使用。然后要注意命令参数,会先后经过 vim 命令行与 shell 命令行两层处理,对特殊字符最好加引号或转义,避免出错。例如:

:'<,'>!./catn.pl ')' 2
:'<,'>!./catn.pl '*' 2
:'<,'>!./catn.pl \% 2
:'<,'>!./catn.pl '\#' 2

如果 ) 不加引号,会出现 shell 语法错误;如果 * 不加引号,在 shell 中会展开 为当前文件的所有文件名,这可能不是想要的;当然这想两个字符也可以用 \ 转义, 安全地从 shell 命令行传入 catn.pl 过滤器脚本。

Vim 命令行中的 % 符号会被展开为当前文件名,即使用引号,也是将文件名字符串括 在引号中传给 shell (如果文件名中有空格,有无引号影响 shell 将其作为几个参数) ,如果要将百分号传给 shell ,就得用 \% 反杠转义。# 在 vim 命令行中会被展开 为“上一个编辑过的”文件名,仅用 \# 可以将 # 传给 shell ,但在 shell 中这符 号是注释,那又会有问题,所以必须用 '\#' 两层保护,才能将 # 符号传入过滤器脚 本中,输出类似 1# 2# 的编号效果。

记不住这许多特殊符号规则怎么办,很简单呀,多试试就好,或者用保守的 '\#' 就差 不多了。而且在 vim 试错了过滤器(参数问题,或脚本本身 bug)不要紧,如果意外修 改了文本,按撤销命令 u 就好。

当然,有时特意利用 vim 特殊符号的替换意义也可能是有用的,例如你又想文件名放在 行号之前了,类似 file:1 的效果。那么就可以在 vim 命令行中传入 '%' 参数,如 果确认当前文件名中没空格,也可以不用引号。当然了,这个过滤器脚本本身的逻辑功能 也要作相应修改了。

所以你看,只要你经常脑洞大开,需求总是在不断变化。然而只要掌握一门脚本语言,哪 怕只会写简单的教科书式的标准输入输出的小程序,运用过滤器思维,就能极大地扩展 vim 的编辑效率与趣味性。