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.

151 lines
8.7 KiB

2 years ago
# 第九章 VimL 混合编程
## 9.1 用外部语言写过滤器
### 9.1.1 混合编程场景介绍
本章来讨论 VimL 与其他语言混合编程的话题。这“混合”编程可能不是很准确的定义,也
许涉及不同层面的场景应用。在上一章介绍的异步编程也算是其中一种吧。不过如果所调
用的外部程序是别人已经写好的(或者是系统提供的经典工具),那用户就只能适应其提
供的接口或输出,在 vim 端几乎没什么可干预的。但如果利用通道连接的另一端的程序
,也要自己开发,那就可以从设计开始就考虑如何更好地与 VimL 协作,并且显然另一端
可以使用任何主流语言。这就不再多说了,本章主要着眼于其他场景的(同步)混合编程。
与众所周知的另一件编辑神器相比, vim 是比较纯粹的编辑器,它本身提供的功能(虽
然编辑方面非常丰富)比较集中,也就比较依赖或吻合 Unix 哲学:一个工具把自己的事
做好,并且便于与其他工具配合。所以,当 vim 想处理更复杂的事务时,它天然地倾向于
与其他工具“混用”。比如,从最基本打开文件编辑,vim 也接受从其他工具的管道输入:
```bash
$ ls -l | vim -
```
这个命令表示将当前目录下的文件列表送入 vim 中编辑,譬如打算在每行前面添加 `mv
` 命令,想仔细规划下如何批量重命名。
就像许多 Unix 工具一样,启用 vim 时若用 `-` 取代文件名参数,就表示从标准输入读
入内容,所以它很容易配合管道,作为接收管道输入的末端。但由于 vim 常规运用是作
为可视化交互式全屏编辑,它不再产生标准输出,因而也不便继续产生管道输出至下一工
序。然而, vim 也有批量模式,不会打开交互界面,实际上也是可以强行配合,达到类
`sed` 的流编辑效果——但这就似乎有点旁门左道了,不是 vim 的常规用法。
当然,管道的配合,只是工具的组合与混用,离“可编程”的概念还比较远。
在支持异步的版本之前,`system()` 函数只能在 VimL 这端进行逻辑与流程控制,而对
所调用的外部命令不可控,这可算“半混合”编程。其实还有另一个叫“过滤器”的功能用法
,它是允许与鼓励对所调用的外部脚本进行编程,但在 vim 这端的用法却是固定的,因
而也可算是另一种“半混合”编程。
除了自己写通道服务算“全混合”编程外, vim 在这之前还提供了多种脚本语言的内置接
口,那也算是(同步的)“全混合”编程了。本节先介绍相对简单的过滤器,下一节再介绍
语言接口。
### 9.1.2 过滤器的概念与使用
即使你对过滤器并不熟,但也应该用过 `=` 重缩进命令,那就是个特殊的过滤器。
过滤器的意思是将当前编辑 buffer 中指定范围的文本,当作标准输入调用某个外部程序
,并用其标准输出替换原范围的文本,以此达到修改、编辑的目的。因此,重缩进与格式
化的本质也就是过滤器。
过滤器的标准使用方式是在命令行一对地址范围之后接 `!` 与外部命令,如:
```vim
:n1,n2 ! 外部程序
:1,$ ! 外部程序
:'<, '> ! 外部程序
:. ! 外部程序
```
注意必须在 `!` 前有地址参数,否则 `!外部程序` 就是纯粹切到外部 shell 运行那个
外部程序了。而过滤器并不会打断用户切到外部 shell ,只要不是处理巨量文本,替换
输入输出应该都比较快,虽然是同步,一般没延迟问题。
如果只有一个地址参数,表示只处理一行, `.` 表示当前行。如果是两个地址参数,则
表示起始行到终止行的范围,`1,$` 表示从第一行至最后一行,即全部文本。可以在命令
行手动输入两个数字行号,也在可视模式下选择一定范围后按 `:` 自动添加 `'<,'>`
示所选择的行范围。
使用过滤器还有个快捷键方式,不必先按 `:` 进入命令行,直接在普通模式按 `!` 再接一
个移动命令(文本对象),也会自动帮你选定这个文本对象,并自动进入命令行模式并填
充好地址参数,用户只要继续在 `!` 之后输入想调用的外部程序。
诚然,过滤器可以直接调用别人写好、已经完善的外部程序。然而,由于以标准输出替换
标准输入的模型如此简单,而每个人的编辑任务又可能多种多样各具个性,在一时找不到
合用的外部工具时,用户完全可以用他所熟悉的任一种脚本语言快速写个过滤器。
比如再举那个简单的例子吧,给文本行编号?最简单的需求,其实可以直接用 `cat -n`
命令完成:
```vim
:'<,'>!cat -n
```
注意,从 vim 命令行调用外部过滤器时,可以附加命令行参数传给过滤器程序,选择文
本是标准输入,这两者互无关系。如果要对全文编号,一定别忘了加地址参数: `1,$!`
; vim 有不少可能作用于范围的命令,在缺省时默认表示全文,但过滤器若省了地址参
数就解释为普通 `!` 外部调用命令了。
但是,`cat -n` 的编号似乎不美观,右对齐,空白太多。如果你想编号左对齐,数字后
面最好还能加个符号,如 `1.``1)` 等,再或者想为指定行编号,比如跳过注释行……
等等,不一而足的需求。如果你熟悉某种脚本语言,最好是自己操起脚本语言来写适合的
过滤器。例如,下面这个 perl 脚本,实现为文本行编号:
```perl
" file: catn.pl
my $sep = shift || "";
my $num = shift || 0;
$sep .= ($num > 0) ? (" " x $num) : "\t";
while (<>) { print "$.$sep$_"; }
```
该过滤器脚本接受两个参数,第一参数指定紧接数字编号的后缀符号,第二参数指定之后
隔几个空白,如果缺省,就隔一个制表符。在 while 循环中,`<>` 符号用于从标准输入
读取数据,`$.` 表示行号, `$_` 表示当前行文本,这样语义就明确了,行号与分隔字
符串与原文本拼接起来作为标准输出。(用其他语言写这个脚本也不复杂,只是语法不一
样,总是可以手动累加行号的)
如果将该脚本保存在当前目录中,可以在 vim 命令行尝试一下:
```vim
:'<,'>!perl catn.pl
:'<,'>!perl catn.pl .
:'<,'>!./catn.pl . 2
```
如果给脚本添加了可执行权限,可直接将脚本作为过滤器程序,否则就将脚本文件当作
perl 解释器的第一参数。如果脚本不在当前目录,请替换为脚本全路径,或者若将可执
行脚本放在某个 `$PATH` 路径中,也可以直接使用。然后要注意命令参数,会先后经过
vim 命令行与 shell 命令行两层处理,对特殊字符最好加引号或转义,避免出错。例如:
```vim
:'<,'>!./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 的编辑效率与趣味性。