Vimscript编程指南

实例学习:Grep操作符之一

从本章开始,我们将会来构造一些很复杂的Vimscript。我们将会讨论一些我们之前没有看到过的东西,并且把它们和我们之前学习的东西结合起来使用。

在你学习这些例子的时候,如果碰到不熟悉的东西,记得用:help命令来查看一下。如果你没有完全理解所有的东西的话,你就不会学到很多东西的。

Grep

如果你从来没有用过:grep,你需要花几分钟时间来阅读一下:help :grep和:help :make。如果你没有用过快速补全窗口,那么也阅读一下:help quickfix-window。

简单的来说,:grep ...调用外部命令来处理你输入的参数,解析返回结果,并填充在快速补全窗口里,以方便在vim里使用。

在我们的例子里,为了方便调用,我们会添加一个“grep操作符”,使得你能够在任何vim的内置动作来选择你要搜索的文本。

用法

当你在写一段不简单的Vimscript时,你首先要考虑的事情是:“这个功能会被怎么使用呢?”。试着去想出一个对你以及你的代码的使用者而言更加流畅,更加舒服,更加直观的方式。

在这个例子里,我会帮你做上门那一步:

  • 我们会创建一个“grep操作符”,并且和g进行绑定。
  • 它会像w以及i{这些Vim操作符一样,会接受一个动作。
  • 它会立马执行这个搜索,并且把结果显示在快速补全窗口里。
  • 它不会跳到第一个匹配的文本上去,因为那不一定是我们要的操作。

一些你可能会使用这个功能的例子有:

  • giw:搜索当前光标下的单词
  • giW:搜索当前光标下的WORD
  • gi' :搜索光标当前所在单引号里的内容
  • viweg:在visual模式下选择一个单词,并且把选中的区间扩展到下一个单词的结尾处,然后搜索选中的文本。

    当然,还有其他很多可以用到这个的地方。看起来实现这些功能需要写很多代码,其实我们只要实现这个操作符,其他的东西,vim会帮我们处理的。

    一个基本的框架

当你需要写一些棘手的Vimscript时,一个比较好的方式就是,先简化你的目标,并且实现它来让你对你的最终目标有个基本的了解。

让我们把目标简化为“创建一个映射来搜索当前光标下的单词”。这个也很有用,并且也比较简单,这样我们就能够更快的运行我们的代码。我们会暂时把这个命令映射为g。

我们会先从这个脚本的基本模型开始,然后再慢慢完善它。运行下面的命令:


     :nnoremap g :grep -R something .

如果你阅读过:help grep的话,那么这个命令就很容易去理解了。我们之前已经看过很多映射了,这个就没什么新的东西了。

很显然这个还没有完成我们的功能,所以我们需要继续完善它以至于能够达到我们简化的目标。

搜索的内容

首先,我们要搜索的是当前光标下的单词,而非是someting。运行下面的命令:


     :nnoremap g :grep -R  .

现在试试它。是vim的一个特殊标识符,它会在运行命令之前被vim替换成光标下的单词。

你可以用来获取当前光标下的文本串。运行下面的命令:


     :nnoremap g :grep -R  .

现在把你的光标放在类似于“foo-bar”的文本上,再试试上面的命令。vim会搜索“foo-bar”,而不只是这个文本的一部分。

但是这里还有另外一个问题,如果选择的文本里有shell的关键词的话,vim也是会传过去的,但是这样会出问题的(比较坏的情况是做一些很可怕的事情)。

你可以去试试这样做,以加深印象。在一个文件里输入foo;ls,保持光标在这个文本串上,然后运行上面那个映射。这时,grep命令会运行失败,但是vim会同时运行一个ls命令。很显然,如果是其他比ls更加危险的命令的话,那就比较可怕了。

我们可以通过给grep的参数加上引号来解决这个问题。运行下面的命令:


     :nnoremap g :grep -R '' .

大部分的shell也会把单引号里的字符串当作常量来处理,所以我们的映射就更加健壮了。

转义shell命令的参数

但是,对于搜索的内容,这里还存在一个问题。试着在“that's”文本上使用这个映射。它不起作用,因为搜索文本里的单引号对grep命令的单引号造成了影响。

为了解决这个问题,我们可以用Vim的shellescape函数。阅读:help escape()和:help shellescape()来看看它是如何工作的(这个非常简单)。

因为shellescape()函数只对vim字符串有效,所以我们需要动态拼接命令和execute。首先运行下面的命令来把:grep映射成:execute "..."的形式:

     
     :nnoremap g :execute "grep -R '' ."

试试上面的命令,保证它还能够正常工作。如果不行的话,找到输错的地方并进行纠正。然后运行下面的命令,这个命令里用shellescape函数来修复搜索内容上的问题:

     
     :nonoremap g :execute "grep -R " . shellescape("") . " ."

试着对一个正常的单词例如“foo”执行上面的命令,它很好地起了作用。再试试一个带引号的单词,例如“that's”。它又失效了,到底是怎么一回事呢?

这是因为Vim会在特殊字符串被替换之前就先进行了shellescape()函数。所以Vim直接对字符串“”进行了转义(只是往里面添加单引号来进行转义),然后把它和grep命令串拼接起来。

你可以通过下面的命令来验证上面的说法:

     
     :echom shellescape("")

Vim会输出''。注意两边的双引号也是字符串的一部分,因为vim这样做是为了让它用做一个shell命令的参数。

可以用expand()函数来修复它,expand函数可以在字符串传给shellescape之前,强制把它转换成它实际上是表示的文本。

我们可以先看看expand函数的功能。把你的光标放在一个带引号的字符串串上,例如“that's”,然后运行下面的命令:

     
     :echom expand("")

Vim会输出“that's”,因为expand("")会把当前光标所在位置的单词作为vim字符串返回。现在把shellescape加入到我们的命令里:

     
     :echom shellescape(expand(""))

这次Vim会输出'that'\''s'。这个看起来有点复杂,相信你不愿意去理睬shell的复杂语法。现在,不用担心这个,只要相信vim从expand里拿到字符串,并正确的进行了转义即可。

现在我们知道了怎么取得当前光标下的文本,并且进行转移了。现在只需要把它和我们的影射结合起来就可以了!运行下面的命令:


     :nnoremap g :execute "grep -R " . shellescape(expand("")) . " ."

再试试看。这次我们的命令不会再因为我们搜索的文本里包含特殊字符而崩溃了。

这个通过一个小的Vimscript,来慢慢进行转换成接近你目标的版本的方法会对你很有用的。

清理一些小问题

这里还有一些小问题我们需要关注。首先,我们说过,我们不不需要自动跳到第一个匹配的文本上去,我们可以用grep!代替grep来实现它。运行下面的命令:

  
:nnoremap g :execute "grep! -R " . shellescape(expand("")) . " ."

试试这个命令,不过什么都不会出现。Vim已经在快速补全窗口里填充了结果,但是我们还没有打开它。运行下面的命令:

     
     :nnoremap g :execute "grep -R " . shellescape(expand("")) . " .":copen

在试试上面的命令,你可以看到vim会打开快速补全窗口,并且里面有搜索的结果。我们所做的只是在映射的末尾加上:copen

作为最后一步的工作,我们需要清除在搜索时,vim打印出的所有grep命令的输出。运行下面的命令:


     :nnoremap g :silent execute "grep! -R " . shellescape(expand("")) . " ."     

现在,我们的工作都完成了,现在试一试。silient命令的功能是只运行它后面的命令,但是忽略它所有应该输出的信息。

练习

  • 把刚刚创建的影射添加到你的vimrc文本里。
  • 如果你之前没有阅读:help :grep的话,阅读一下它。
  • 阅读:help cnext和:help cprevious。在使用了上面的映射之后,试试它们。
  • 添加:cnext和cprevious的映射,以便于快速切换搜索结果。
  • 阅读:help expand。
  • 阅读:help copen。
  • 给映射里的:copen命令添加一个高度,是得每次它打开的时候都是你觉得合适的高度。
  • 阅读:help silent。