9.2 外部语言接口编程

优质
小牛编辑
113浏览
2023-12-01

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 源码编译安装程序的标准三部曲执行如下命令了:

$ 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