第九章 VimL 混合编程
9.3* Perl 语言接口开发
本节将专门讲一讲
if_perl
接口的开发指导与实践经验,虽然只讲 perl ,但其基本 思路对于其他语言接口也可互为参照。
9.3.1 VimL 调用 perl 接口的基本流程
典型地,假如要使用(perl)语言接口实现某个较为复杂的功能或插件,其调用流程大概 可归纳如下:
-
定义快捷键映射,
nnoremap
,这不一定必要,可能直接使用命令也方便; -
快捷键调用自定义命令,
command
; - vim 自定义命令调用 vim 自定义函数;
-
在 vim 函数中使用
:perl
命令调用 perl 函数; - 在 perl 函数中实现业务运算,可能有更长的调用链或引入其他模块;
- 在 perl 函数使用 VIM 模块将运算结果或其他效果反馈回 vim 。
在以上流程中,前三步是是纯 VimL 编程(细究起来,前两步准备动作还只是使用 vim ),第 5 步是纯 perl 编程,而第 4 步与第 6 步就是 VimL 与 perl 的接口过渡。接 口的使用只能按标准规定,打通一种可能,而要直接实现有意义的功能,重点还是回归到 第 5 与第 3 步两门语言的掌握程度上。
整个流程是同步的,当 perl 代码执行完毕后,堆栈上溯,一直回到第 1 步的命令完成 ,才算一条 vim 的
Ex
全部完成,然后 vim 继续响应等待用户的按键。
但凡编程,要有作用域的意识,在这第 4 步中,首先是在 VimL 的函数的局部作用域中 ,首次进入的 perl 代码,是在 perl 的
main
命令空间。如果在 perl 的后续调用链 中,进入了其他命名空间,再想引用本次 vim 命令(第 2 步)或之前 vim 命令中在 perl
main
命名空间定义的变量,就得显式加前缀
main::
或简写
::
也可。在 perl 代码中,使用 VIM 模块,只能直接影响 vim 的全局变量,它无法获知调用
:perl
命令所处的函数作用域或脚本作用域。如果有这个需求,请约定使用的全局变量,并在
:perl
代码同步返回时,及时从被影响的全局变量更新局部变量保存下来。
另一个基本意识是有关程序的输入输出。从
:perl
开始执行的代码,它的标准输出被 重定向到 vim 的消息区。所以如果打印简单字符,
:perl print
与
:echo
效果差不 多。在这里执行的 perl 不应试图从标准输入读取数据,如果需要输入,可以打开文件的 方式(如临时文件,或确定的目标文件),或者利用 VIM 模块直接读取 buffer 内容。
9.3.2 Perl 代码与 VimL 代码解耦
虽然语言接口允许你将两种语言混用写在一起,但当真正想实现一些较复杂功能时,将两 种语言的代码分别保存在独立的
*.vim
或
*.pl
是更好的代码维护与项目管理方式。 而且也尽量将使用了
VIM
模块的 perl 脚本与未使用
VIM
模块的代码分开。
因为
VIM
模块只能是从 vim 执行的 perl 代码才可用。将那些未使用
VIM
模块的 纯数据运算逻辑的 perl 代码独立开来,方便独立测试,也便于将其复用在非 vim 环境 下的常规 perl 脚本开发中。使用了
VIM
模块的 perl 代码,只方便在 vim 环境下测 试。如果一定要在外部独立测试调试,只能自己提供一个简易模拟版的
VIM.pm
,将在 脚本用到的
VIM::
方法都实现出来(比如就打印调试信息之类)。
如下代码段可以判断 perl 是否运行在 vim 环境(是否通过
:perl
调用的):
package main;
our $InsideVim = 0;
eval { VIM::Eval(1); };
$InsideVim = 1 unless $@;
perl 的
eval
语句块,有类似的
try ... catch
的功能,就是尝试执行
VIM
模 块的随便一个有效的方法,最简单就是
VIM::Eval(1)
了。如果不是从 vim 环境执行 ,
eval
会出错,出错信息保存在
$@
变量中。如果确实在 vim 环境中,
eval
正 常执行,
$@
为空,
unless
是条件取反,变量
$InsideVim
被置为 1 标记之。
然后就可以根据
$InsideVim
的值来做分支判断了。如果代码只设计在 vim 环境中使 用,当
$InsideVim
为假值时可直接 return 或 exit 。如果特意还是想在非 vim 环 境下通过测试,那就可以在
$InsideVim
为假时引用自写的简易调试版
VIM.pm
。
只为调试用的模拟
VIM
模块大致结构可以如下:
# File: VIM.pm
package VIM;
sub DoCommand{
my $cmd = shift;
print "Will do Vim Ex Command: $cmd\n";
sub Eval{
my $expr = shift;
print "Will eval Vim expression: $expr\n";
return $expr;
也许还应该为
Eval()
函数添加自适应列表环境与标量环境的返回值,还有 Buffer 与 Window 对象的方法,模拟实现都会更复杂。故没必要求全,只根据实际情况,待测试的 脚本用到哪些方法,首先让脚本能编译能运行,再考虑进一步模拟精度的必要性。当然最 可靠的还是在 vim 中整合起来测试效果,只是在 vim 只能交互地手动测试,有时略有不 便。
顺便提一下,使用
if_perl
时,不必显式声明
use VIM;
就能在相关代码中使用
VIM
模块。但使用
if_python
,还是要显式声明
import vim
的。
9.3.3 Perl 与 VimL 数据交换的几种方式
首先,简单的 perl 代码,如果 print 至标准输出的,在被 vim 调用时是打印到消息区 的,因而可以用重定向消息的方法,将 perl 的标准输出内容捕获至 vim 变量中。例如 ,专门写个
ifperl.vim
存些基本工具函数,如:
" File: ifperl.vim
function! s:execute(a:code) abort
let l:perl = 'perl ' . a:code
redir => l:ifstdout
silent! execute l:perl
redir END
return l:ifstdout
endfunction
这个函数将封装执行一段 perl 代码,将其标准输出当作一个变量返回(为简明起见,省 略了错误等特殊情况处理)。一般更推荐调用 perl 函数,如此利用
s:execute()
也 很容易封装函数调用:
function! s:call(func, ...) abort
let l:args = join(a:000, ',')
let l:code = printf('%s(%s);', a:func, l:args)
return s:execute(l:code)
endfunction
实际上,在 vim 命令行向 perl 函数传参数还得注意引号问题,这里也从略。然后,模 拟
:pyfile
实现并未内置支持的
:perlfile
功能,也可简单封装成一个函数,如果 也想关注执行一个
*.pl
可能的输出,可以改用上面的
s:execute()
函数:
function! s:require(file) abort
execute printf('perl require("%s");', a:file)
endfunction
function! s:use(pm) abort
execute printf('perl use "%s";', a:pm)
endfunction
function! s:uselib(path) abort
execute printf('perl use lib("%s");', a:path)
endfunction
注意,在 perl 中,
require
与
use
语句有区别,各有用途。但都涉及搜索路径, 在程序中推荐用
use lib
动态添加。可以将用于 vim 调用的 perl 脚本收集在一个目 录(或专门的插件目录),并用
use lib
添加这个目录,便于 vim 使用。
其次,如果要用到的 perl 脚本,主要是一些工具函数,要利用其返回值的,而不是打印 到标准输出的。这种情况下,若强行在 perl 处加一层打印函数,在 vim 处重定向消息 ,那是比较低效也不优雅的。另一个可考虑的替代的办法是专门设计几个全局变量槽让 perl 访问。例如;
" File: ifperl.vim
let g:useperl#ifperl#scalar = ''
let g:useperl#ifperl#list = []
let g:useperl#ifperl#dict = {}
# File: ifperl.pl
sub ToVimScalar
my ($val) = @_;
VIM::DoCommand("let g:useperl#ifperl#scalar = '$val'");
sub ToVimList
my ($array_ref) = @_;
VIM::DoCommand("let g:useperl#ifperl#list = []");
foreach my $val (@$array_ref) {
VIM::DoCommand("call add(g:useperl#ifperl#list, '$val')");
sub ToVimDict
my ($hash_ref) = @_;
VIM::DoCommand("let g:useperl#ifperl#dict = {}");
foreach my $key (keys %$hash_ref) {
my $val = $hash_ref->{$key};
VIM::DoCommand("let g:useperl#ifperl#dict['$key'] = '$val'");
在 perl 中的三种数据类型,标量、列表、散列,分别可对应 VimL 变量的字符串、列表 与字典,并且字符串在可能的情况下都可当作数字使用。当 perl 里的数据需要发往 VimL 时,临时借助事先规定好的这几个全局变量做缓存,只多调用一层转接函数,不影 响原来 perl 函数的使用方式。
最后,其实要考虑的问题,是否真有必要将 perl 数据发还 VimL 。在协作完成一个功能 时,得盘算好哪部分必须在 VimL 处完成,哪部分可集中在 perl 处完成,没必要的中间 结果就别传回 VimL 处理了。
如果真要从 perl 频繁传出大量文本,自己用变量接收也不如用 VIM 内部的 Buffer 方 法有效率。例如,也专门设计一个 buffer,取名
IFPERL.buf
,在 perl 中将需要查 看的文本直接附加到这个 buffer 的末尾:
" File: ifperl.vim
let g:useperl#ifperl#buffer = 'IFPERL.buf'
# File: ifperl.pl
sub ToVimBuffer