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.
 
 
 
 
 
 

17 KiB

第九章 VimL 混合编程

9.2 外部语言接口编程

9.2.1 语言接口介绍

Vim 支持其他诸多语言接口。这意味着,你不仅可以写 VimL 脚本,也可以使用被支持的 语言脚本。这就相当于在 vim 中内嵌了另一种语言的解释器。当然你不能完全像其他语 言的解释器来使用 vim ,毕竟还是遵守 vim 制定的一些规范,那就是 vim 为该语言提 供的接口。

在 Vim 帮助首页,专门有一段 Interfaces 的目录,列出了 Vim 所支持的语言接口,大 都以 if_lang.txt 命名,其中 lang 后缀指某个具体的(脚本)语言。笔者较熟悉 的脚本语言有 lua、python、perl ,而其他如 ruby、tcl 较少了解。因而在本章打算简 要介绍下 if_lua if_pythonif_perl 这几个语言接口。(因 python 有两个 版本,故在帮助文档中其实用 if_pyth.txt 命名,避免 python 狭义地指 python2, 不过本文仍习惯使用 python 统称)

一些功能复杂的插件,为了规避 VimL 语言的不足,都倾向于按语言接口采用其他语言来 完成一部分或主要功能。比如,unite 就采 用了 if_lua 接口,后来的升级版 denite 则采用 if_python 接口,另外推荐一个插件 LeaderF 也是用 if_python 写的。这都是不错的实际项目源码,想深入学习的可以参考。

不过采用 if_perl 接口的现代插件较少,笔者鲜有看到。但是笔者偏爱 perl ,所以 在本章剩余篇幅将重点以 if_perl 为主,也算略微弥补一点空白。而且, Vim 为各语 言提供的接口大同小异,思路是一致的。介绍一种语言接口,也期望读者能举一反三。真 要用好某种语言接口,除了要仔细学习 vim 相关的 if_lang.txt 文档,还需要对目标 语言掌握良好,才能方便地在两种环境中来回游弋。

9.2.2 自定义编译 vim 支持语言接口

默认安装的 vim 一般不支持语言接口,需要自己重新从源码编译安装。这也其实很简单 ,只要修改一些编译配置即可。首先从 vim 官网或其 github 镜像下载源代码包,解压后进入 src/ 子目录, vi Makefile 查找并取消如下几行注释:

CONF_OPT_LUA = --enable-luainterp
CONF_OPT_PERL = --enable-perlinterp
CONF_OPT_PYTHON = --enable-pythoninterp

原来这几行是被 # 注释的,表示相关语言接口是被禁用的,你所需做的只是删去 # 符号启用功能。当然每个语言接口在 Makefile 都提供了好几个不同的(被注释)选项 备用,各有不同的含义,典型的如动态链接或静态链接。上面示例是打开静态链接编译选 项,含 =dynamic 的表示动态链接编译选项。你只需打开(取消注释)其中一条选项, 一般建议用静态链接编译。动态链接只是减少最后编译出的 vim 程序的大小,或许也略 微减少 vim 运行时所需的内存。在硬盘与内存都便宜的情况下,这都不算问题,用静态 链接可减少依赖,避免版本不兼容的麻烦。

不过 python 语言接口分 python2 与 python3 两个选项,它们既像一个语言又像两个语 言。打开 python3 接口的编译选项是 --enable-python3interp 。注意,你不能同时 打开 python2 与 python3 的静态编译选项,如果想同时支持,只能都用动态链接编译选 项。除非你有绝对理由想同时使用 python2 与 python3 ,还是建议你只使用其中之一。 而且 python2 都是历史原因,以后的趋势都应该都是转向 python3 。

在自定义安装 vim 时,还有个选项推荐打开,就是安装到个人家目录下,不安装到系统 默认的路径下,也就不影响系统其他用户使用的 vim 。只要指定 prefix 即可,一般 也就是打开(取消注释)如下这行:

prefix = $(HOME)

然后,就可以按 Unix/Linux 源码编译安装程序的标准三部曲执行如下命令了:

$ make configure
$ make
$ make install

如果你运气足够好,应该直接 make 成功的。如果 make 失败,最可能的原因是系统没有 安装相应的语言开发包,请用系统包管理工具(yumapt-get)安装语言开发包, 如 perl-dev ,注意有些系统为语言开发包名的命名后缀不同,也可能是 perl-devel 。 安装好了所需语言开发包(及可能的其他依赖),再重新 confire make 应该就能成 功了。

在编译成功之后,make install 安装之前,最好检查一下新编译的 vim 是否满足你所 需的特性。执行如下命令:

$ ./vim --version

vim 命令之前添加 ./ 表示使用当前目录(src/ 编译时目录)的 vim 程序, 否则可能会查找到系统原来的 vim 程序。如果打印的版本信息,包含 +perl (或 +lua +python),就表示成功编进了相应的语言接口。当然,你也可以直接不带参数 地启动 ./vim 体验一下,并可在 vim 的命令行查看如下命令的输出:

: version
: echo has('perl')
: echo has('python')
: echo has('python3')

:version 命令与 shell 命令参数 --version 的输出基本类似。has() 函数用于 检测当前 vim 是否支持某项特性,如果支持返回真值(1),否则假值(0)。 has() 函数也经常用于 VimL 脚本尤其是插件开发中,为了兼容性判断,根据是否支持 某项特性执行不同的代码。

确认无误后,就可以 make install 安装。所谓安装也不外是将刚才编译好的 vim 程 序及其他运行时文件与手册页等文件,复制到相应的目录中。安装的根目录取决于之前 $prefix 选项,如果按之前指导选择了 $(HOME) ,那 vim 就会安装到 ~/bin/vim 中。一般建议将个人家目录下的 ~/bin 添加到环境变量 $PATH 之前,这样在 shell 启动命令时,首先查找 ~/bin 目录下的程序。

当然了,在你决定手动编译 vim 之前,最好在目前默认使用的 vim 中用 :versionhas() 检测下它是否已经支持相应的特性了,如果已经支持,那就可跳过这里介绍的手 动编译流程了。

9.2.3 语言接口的基本命令

测试某个语言接口是否真的能正常工作,也可直接以相应语言名作为 vim 的命令,执行 一条目标语言的简单语句,例如:

: perl print $^V
: perl print 'Hello world!'
: lua print('Hello world!')
: python print 'Hello world!'
: python3 print 'Hello world!'

其中第一条语句是打印 if_perl 接口使用的 perl 版本,其后就是使用不同语句打印 喜闻乐见的 Hello world! 了。

语言名如 :perl 也就是相应语言接口的最基本接口命令了,可见它们保持着高度的一 致性,vim 调用相应的语言解释器执行其参数所代表的代码段,所不同的只是各语言的语 法文法了。下面,如无特殊情况,为行文精简,就基本只以 if_perl 为例说明了。

基本命令 :perl 只适合在命令行执行简短的一行 perl 语句(当然,对于 perl 语言, 单行语句也可以很强大)。如果要执行一大块 perl 语句,短合在脚本中用 here 文档 语法,即 VimL 也像许多语言一样支持 << EOF 标记:

perl << EOF
print $^V; # 打印版本号
print "$_\n" for @INC; # 打印所有模块搜索路径
print "$_ = $ENV{$_}" for sort keys %ENV; # 打印所有环境变量
EOF

EOF 只是约定俗成的标记,其实可以是任意字符串标记,甚至可以省略默认就是单个点 . 号。Vim 会从下一行开始读入,直到匹配某行只包含 EOF 标记,将这块内容(长 串字符串)送给 :perl 命令作为参数。换用其他标记的理由,一般是内容本身包含 EOF 避免误解。

不过良好的实践,不推荐将 perl << EOF 裸写在某个 *.vim 脚本文件中,而应该封 装在一个 VimL 函数中,最好再用 if has 判断保护,如:

function! PerlFunc()
    if has('perl')
        perl << EOF
        print $^V;
        print "$_\n" for @INC;
        print "$_ = $ENV{$_}" for sort keys %ENV;
EOF
    endif
endfunction

注意:EOF 不能缩进,只能顶格写,即整行只能有 EOF 才表示 here 文档结束。 这样封装之后,更能提高代码的健壮性与兼容性。然后就可按普通 VimL 函数一样调用了 :call PerlFunc()

当然,每次都写 if has 判断可能有点繁琐,那么可以将这个判断保护提升到更大的范 围内,如:

if has('perl')

function! PerlFunc1()
    perl code;
endfunction

function! PerlFunc2()
    perl code;
endfunction

endif

或者将所有利用到语言接口的代码收集到一个脚本,然后在最开始判断:

if !has('perl')
    finish
endif

if_luaif_python 接口中,还提供执行整个独立的 *.lua*.py 脚本 文件的命令,如下:

:luafile script.lua
:pyfile script.py

但是比较奇怪,if_perl 并没有类似的 :perlfile 命令,要实现类似功能,可以用 :perl require "script.pl" 命令,并且要注意 perl 的模块搜索路径问题。而在 :luafile:pyfile 命令中,查寻命令行中提供的脚本文件,还是 vim 的工作, 取决于 vim 的搜索路径。

另外一个很有用的命令是 :perldo , 它会遍历指定当前 buffer 范围的每一行(默认 是 1,$ ),将 perl 的默认变量 $_ 设为遍历到的那行文本(不包括回车换行符) ,如果 :perldo 命令参数的代码段修改了 $_ ,它就会替换“当前”行文本。例如:

:perldo s/regexp/replace/g
:%s/regexp/replace/g

上面两行语句其实是一样的意义,都是执行全文正则替换,只不过第一行 :perldo 采 用 perl 风格的正则语法,它实际执行的是 perl 语句;第二行 :%s 就是执行 VimL 自己的正则替换。如果你想体会 perl 正则与 VimL 正则有什么异同,或对 perl 正则比 较熟悉,觉得某些情况下用 perl 正则更舒服,就可以用 :perldo s 代替 %s 试试 。

当然,:perldo 所能做的事情远不只 s 替换,s 在 perl 语言中只是一个操作符 。perl 语言的单行语句非常强大,尤其是支持后置 if/for/while 的条件判断或循环 ,这就取决于用户的 perl 语言造诣了。

不过 :perldo 命令,与上一节介绍的过滤器机制略有不同,尝试用它实现给文本行编 号的功能,最初的想法可能是:

:perldo $_ = "$. $_"

但这不能达到要求,$.:perldo 遍历的每一行中都输出 0 ,这说明 perl 并 没有把文本行当成标准输入(或其他输入文件)处理,并没有给 $. 变量自动赋值。改 成如下语句能达到编号需求:

:perldo $_ = ++$i . " $_"

看起来有点像 perl 的黑魔法,其实不过是借助了一个变量 $i ,未定义变量当作数字 用时被初始化 0 ,然后也支持像 C 语言的前置 ++i 语法,然后又将该数字通过点 号 . 与一个字符串连接,代表行号的数字自动转化为字符串。这样创建使用的 $i 将是 perl 的全局变量,在执行完这条语句后,可以再用如下语句:

:perl print $i

查看 $i 的值,可见它仍保留着最后累加到的行号值。如果再次执行上面的 :perldo 语句对文本行编号,那起始编号就不对了。需要手动 :perl $i = 0 重置编号。但这也 正意味着,如果要求编号从任意值开始,上述 :perldo 语句就很容易适应。

在 lua 或 python 语言接口中,也有类似 :perldo 的命令。但是它们没有类似 $_ 默认变量的机制,:luado:pydo 实际是在循环中为每行隐含调用一个函数,传入 linelinenr 参数代表“当前”行文本与行号,然后在参数的代码段中可以利用这 两个参数进行操作,并可用 return 返回一个字符串,取代“当前”行。在写法上没 perl 那么简洁,而且在单行语句中不像函数的地方使用 return 也多少有点违和与出 戏感。

9.2.4 目标语言访问 VIM

显然,如果使用一种语言接口,只是换一门语言自嗨诸如打印 Hello world 这种是没 有前途的。决定使用一种语言接口时,总是期望能利用那种语言更强大的能力,如更快的 运算速率或更丰富的标准库第三方库功能,完成一系列数据与业务逻辑处理后,最终还是要 通过某种形式反馈到 vim ,对 vim 有所影响才是。

为此,if_luaif_python 都提供了专门的 vim 模块,在目标语言中将 vim 视 为一个逻辑对象,可从那语言代码中直接访问、控制 vim ,如设置 vim 内 buffer 的 文本,执行 vim 的 Ex 命令等。if_perl 也提供类似的模块,名叫 VIM,使用语法 与常规点号调用方法不同而已,perl 使用 ::-> 符号。

if_perl 为为例,其 VIM 模块提供了如下实用接口:

  • VIM::DoCommand({cmd}) 从 perl 代码中执行 vim 的 Ex 命令;
  • VIM::SetOption({arg}) 设置 vim 的选项,相当于执行 :set 命令;
  • VIM::Msg({msg}, {group}?) 显示消息,相当于 :echo ,但可以指定高亮颜色;
  • VIM::Eval({expr}) 在 perl 代码中计算一个 vim 的表达式;
  • VIM::Buffers([{bn}...]) 返回 vim 的 buffer 列表或个数;
  • VIM::Windows([{wn}...]) 返回 vim 的窗口表表或个数。

其中,前三个接口方法只是执行 vim 的命令,perl 代码中不再关注其返回值。后三个方 法是计算与 vim 相关的表达式,需要获得并利用其返回值。而 perl 语言的表达式是有 上下文语境的概念的。

VIM::Eval() 方法在标量环境中获得一个 vim 表达式的值,并转化为 perl 的一个标 量值。所谓 vim 表达式,比如 @x 表示 vim 寄存器 x 的内容,&x 表示 vim 的 x 的选项值。当然简单的 1+2 也是 vim 的表达式,但这种平凡的表达式直接在 perl 代码中求值也是一样的意义,没必要使用 VIM::Eval() 了。Vim 中的环境变量 $X 也与 perl 中 $ENV{X} 等值。 perl 的标量值具体地讲就是数字或字符串。但如 果该方法在列表语境中求值,则结果也是一个列表,特别地是二元列表:

($success, $value) = VIM::Eval(...);
@result = VIM::Eval(...);
if($result[0]) { make_use_of $result[1] };

返回结果的第一个值表示 Eval 求值是否成功,毕竟参数给定的 vim 表达式有可能非 法,如果成功,第二值才是实际可靠的求值结果。如果确信求值有意义,可直接用标量变 量接收 VIM::Eval() 的返回值,那就是求值结果,可简化写法,省略成功与否的判断 。

VIM::Buffers()VIM::Windows() 的上下文语境就更易理解了,它符合 perl 的 上下文习惯:本来是数组的变量,在标量上下文表示数组的大小。所以不带参数的 VIM::Buffers() 返回所有 buffer 的列表,或在标量语境下返回 buffer 数量。如果 提供参数(可以一个或多个),就根据参数筛选 buffer 列表。如果想获取某个特定的 buffer,也得通过在列表结果中取索引,例如:

$mybuf = (VIM::Buffers('file.name'))[0]

你得保证 file.name 至少匹配一个 buffer,否则返回空列表,再对空列表取索引 [0] 是未定义的值。而且一般建议参数给精确,能且只能匹配一个 buffer ,否则如果匹配多 个,按 vim 的 bufname() 函数的行为,在歧义时也返回空。如果给的参数是表示 buffer 编号的数字,一般能保证唯一,只要是有效的 buffer 编号。给这个方法传多个 参数时,就返回相应参数个数的 buffer 列表,例如:

@buf = VIM::Buffers(1, 3, 4, 'file.name', 'file2.name')

就将取得一系列指定的 buffer 对象,存入于 @buf 数组中。

一旦获得 buffer 对象,就可以用对象的方法,操作它所代表的相应的 vim buffer:

  • Buffer->Name() 获得 buffer 的文件名;
  • Buffer->Number() 获得 buffer 编号;
  • Buffer->Count() 获得 buffer 的文本行数;
  • Buffer->Get({lnum}, {lnum}?, ...) 获取 buffer 内的一行或多行文本;
  • Buffer->Delete({lnum}, {lnum}?) 删除一行或一个范围内的所有行;
  • Buffer->Append({lnum}, {line}, {line}?, ...) 添加一行多多行文本;
  • Buffer->Set({lnum}, {line}, {line}?, ...) 替换一行或多行文本;

Window 对象也有自己的方法,请查阅相应文档,这里就不再罗列了。此外,还提供两个 全局变量用于操作当前 buffer 与当前窗口:

  • $main::curbuf 表示当前 buffer ;
  • $main::curwin 表示当前窗口。

由于 :perl 命令执行的 perl 代码,就默认在 main 的命名空间(包)内,所以一 般情况下可简写为 $curbuf$curwin