外部命令

Vim 遵从 UNIX “做好一件事”的理念。 把所有能想到的功能都塞进编辑器中并不是一个好的办法,而是应该在适当的时候代理给外部命令。

让我们在插件中添加一些和 Potion 编译器的交互,并开始接触 Vim 的外部命令。

编译

首先,我们会添加一个命令来编译并运行当前的 Potion 文件。 有很多方式来做到这个,但目前我们只会简单地使用一个外部命令。

在插件仓库中创建一个 potion/ftplugin/potion/running.vim 文件。 我们会在这里面创建映射,用来编译并运行 Potion 文件。

if !exists("g:potion_command")
    let g:potion_command = "potion"
endif

function! PotionCompileAndRunFile()
    silent !clear
    execute "!" . g:potion_command . " " . bufname("%")
endfunction

nnoremap <buffer> <localleader>r :call PotionCompileAndRunFile()<cr>

第一段代码检查一个储存执行 Potion 命令的全局变量是否设置过,如果没有,则把它储存起来。 我们之前就看过这种检查方式了。

如果 potion 不在 $PATH 中,这会允许用户在 ~/.vimrc 文件中通过添加一行类似 let g:potion_command = "/Users/sjl/src/potion/potion" 的代码来重写它。

最后一行添加了一个缓冲本地的映射,它调用上面定义的函数。 记住,因为这个文件是在 ftdetect/potion 目录中的, 在每次文件的 filetype 被设为 potion 时,都会被运行。

真正的功能在 PotionCompileAndRunFile() 函数中。 继续,保存文件,打开 factorial.pn 然后按下 <localleader>r 来运行映射,看看会发生什么。

如果 potion 在你的 $PATH 中,文件就会被运行,并且会在终端中 (或者,如果你用的是图形界面的 Vim,会在窗口的底部)看到它的输出。 如果看到一个错误说找不到 potion 命令,你就需要在 ~/.vimrc 文件中设置 g:potion_command, 就像上面提到的那样。

让我们看一看 PotionCompileAndRunFile() 函数时如何工作的。

叹号!

Vim 中的 :! 命令(发音为“bang”)能运行外部命令并在屏幕中显示输出。 试试运行以下命令:

:!ls

Vim 应该会展示 ls 命令的输出,以及“Press ENTER or type command to continue”的提示。

当通过这种方式运行时,Vim 并没有给命令传入任何输入。 运行下面的命令来确认:

:!cat

键入一些行,你会看到 cat 命令会把它们吐回来,就和你在 Vim 外面运行 cat 一样。 用 Ctrl-D 来结束。

要在运行一个外部命令时不出现 Press ENTER or type command to continue 的提示,需要用 :silent !。 运行以下命令:

:silent !echo Hello, world.

如果在一个图形界面的 Vim ,例如 MacVim 或 gVim 中运行,你将看不到 Hello, world. 命令的输出。

如果在一个终端 Vim 中运行,那结果会各种各样,取决于你的配置。 如果运行了一个空的 :silent !,你需要运行 :redraw! 来恢复屏幕。

注意,这个命令是 :silent ! 而不是 :silent!(看到空格了吗?)! 这是两个不同的命令,而我们想要的是前者! Vimscript 是不是很赞?

让我们回头看看 PotionCompileAndRunFile() 函数:

function! PotionCompileAndRunFile()
    silent !clear
    execute "!" . g:potion_command . " " . bufname("%")
endfunction

首先,我们运行一个 silent !clear 命令,这会清空屏幕并且没有 Press ENTER... 的提示。 这将确保我们只会看到这次运行的输出,如果一直运行相同的命令时,将很有用。

下一行用到了我们的老朋友 execute 来动态构建一个命令。 它构建的命令看上去会像这样:

!potion factorial.pn

注意这里没有 silent,所以用户会看到命令的输出,并且需要按下回车键来回到 Vim 中。 对于这个特定的映射,这就是我们想要的,至此已经全部准备好了。

显示字节码

Potion 编译器有一个选项可以让你查看编译时生成的字节码。 如果你要从底层来调试你的程序时,将会很有用。 试试在 shell 提示中运行以下命令:

potion -c -V factorial.pn

你应该会看到大量输出,如下所示:

-- parsed --
code ...
-- compiled --
; function definition: 0x109d6e9c8 ; 108 bytes
; () 3 registers
.local factorial ; 0
.local print_line ; 1
.local print_factorial ; 2
...
[ 2] move     1 0
[ 3] loadk    0 0   ; string
[ 4] bind     0 1
[ 5] loadpn   2 0   ; nil
[ 6] call     0 2
...

我们来添加一个映射,让用户能在一个 Vim 分割窗口中查看当前 Potion 文件生成的字节码, 这样他们就能轻松地操作和检查代码。

首先,把以下代码添加到 ftplugin/potion/running.vim 文件的底部:

nnoremap <buffer> <localleader>b :call PotionShowBytecode()<cr>

这里没什么特别的 —— 只是一个简单的映射。 现在让我们先定好函数的纲要,它将完成重要的工作:

function! PotionShowBytecode()
    " Get the bytecode.

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

既然已经设好了纲要,那就让我们继续讨论如何实现它。

system()

我们有很多方式来实现这个,所以,我会选一个以后也用得上的方式。

运行以下命令:

:echom system("ls")

你应该会在屏幕的底部看到 ls 命令的输出。如果你运行 :messages,你在那儿也能看到它。 Vim 的 system() 函数接受一个命令字符串,并把命令的输出作为一个字符串返回。

你也可以传入第二个字符串作为 system() 的参数。运行以下命令:

:echom system("wc -c", "abcdefg")

Vim 会显示 7 (有一些留白)。 如果你像这样传入第二个参数,Vim 会把它写入到一个临时文件中,并通过管道传给命令的标准输入。 就目前来说,我们并不需要这个,但是了解下总是好的。

回到我们的函数中。编辑 PotionShowBytecode(),填写纲要中的第一部分,如下:

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))
    echom bytecode

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

去试试保存文件,并在 factorial.pn 文件中运行 :set ft=potion 来重载,然后使用 <localleader>b 映射。 Vim 应该会在屏幕底部显示字节码。一旦你确认它能正常工作了,就可以删除 echom 这行了。

空白分割窗口

然后,我们要为用户打开一个新的分割窗口来显示结果。 这能让用户使用所有 Vim 的能力来查看并操作字节码,而不只是从屏幕上阅读一次。

要实现这个,我们要创建一个“空白”分割窗口:包含一个永远不会被保存的缓冲,并且每次运行映射的时候都会被重写。 把 PotionShowBytecode() 函数修改成如下这样:

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.

endfunction

这些新命令应该很容易弄懂。

vsplit 为缓冲创建一个新的垂直分割窗口,名为 __Potion_Bytecode__。 我们用下划线包裹这个名字是要让用户明白这不是一个普通的文件(只是一个保留输出的缓存)。 下划线并不是什么特殊的东西,只是一个约定。

然后,我们用 normal! ggdG 删除缓冲中的所有东西。 这个映射第一次运行时什么都不会做,但随后我们会重用 __Potion_Bytecode__ 缓冲,这就会清空它了。

然后,我们为缓冲设置两个本地设置。 首先,我们把文件类型设为 potionbytecode,只是为了指明它保留的是什么内容。 我们也把 buftype 设为 nofile,告诉 Vim 这个缓冲没有关联到硬盘上的文件,所以不应该被保存。

剩下的就是把保存在 bytecode 变量中的字节码输出到缓冲中。 最后函数完成时看上去如下:

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%") . " 2>&1")

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.
    call append(0, split(bytecode, '\v\n'))
endfunction

Vim 的 append() 函数接受两个参数:一个行号,以及一个要添加的字符串列表。 例如,试试运行以下命令:

:call append(3, ["foo", "bar"])

这会在当前缓存的第3行下面添加两行,foobar。 在本例当中,我们是在第0行下面添加,而这表示“在文件的顶部”。

我们需要一个待添加的字符串列表,但是我们只从 system() 中得到了一个包含了换行符的字符串。 我们用 Vim 的 split() 函数把这个巨大的文本快分割为了一个字符串列表。 split() 接受一个需要分割的字符串和一个寻找分割点的正则表达式。这非常简单。

既然函数已经完成了,那就去试试这个映射。 当你在 factorial.pn 缓冲中运行 <localleader>b,Vim 会打开一个包含了 Potion 字节码的新缓冲。 随意试试修改下源码,保存文件,并再次运行映射,看看字节码是否改变了。

练习

阅读 :help bufname

阅读 :help buftype

阅读 :help append()

阅读 :help split()

阅读 :help :!

阅读 :help :read:help :read! (我们并没有提到这些命令,但它们是非常有用的)。

阅读 :help system()

阅读 :help design-not

现在我们的映射要求用户自行保存文件,使改动生效,然后再运行映射。 而目前撤销是很容易操作的,所以修改我们写的函数,让它可以自动保存文件。

当你在一个 Potion 文件中运行字节码映射时出现了一个语法错误会发生什么? 而这又为什么会发生?

修改 PotionShowBytecode() 函数来检测 Potion 编译器是否返回了错误,并把错误信息展示给用户看。

额外的练习

每次运行字节码映射时,都会创建一个新的垂直映射窗口,即使用户并没有关闭之前的。 如果用户并不耐烦去关掉它们,那结果就会堆积很多额外窗口。

修改 PotionShowBytecode() 来检测是否已经有了一个保留 __Potion_Bytecode__ 缓冲的窗口, 如果有,就切换到那里,而不是创建一个新的。

你可能需要阅读 :help bufwinnr()

更多额外的练习

还记得我们是如何把临时缓冲的 filetype 设置为 potionbytecode 的么? 创建一个 syntax/potionbytecode.vim 文件,并为 Potion 的字节码缓冲定义语法高亮,使得它们易于阅读。

原文地址:http://learnvimscriptthehardway.stevelosh.com/chapters/52.html