You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

8.7 KiB

第九章 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 的编辑效率与趣味性。