Vimscript 实例研究:Grep 运算符(Operator),第一部分
在本章和下一章中,我们将使用Vimscript来实现一个相当复杂的程序。我们将探讨一些闻所未闻的东西, 也将在实战中把之前学过的东西联系起来。
在本实例研究中,遇到不熟悉的内容,你得用:help
弄懂它。如果你只是走马观花,就将所获无多。
Grep
如果你未曾用过:grep
,现在你应该花费一分钟读读:help :grep
和:help :make
。 如果之前没用过quickfix window,阅读:help quickfix-window
。
简明扼要地说::grep ...
将用你给的参数来运行一个外部的grep程序,解析结果,填充quickfix列表, 这样你就能在Vim里面跳转到对应结果。
我们将会添加一个"grep运算符"到任意Vim的内置(或自定义!)的动作中,来选择想要搜索的文本, 让:grep
更容易使用。
用法
在写下每一个有意义的Vimscript程序的第一步,你需要思索一个问题:“它会被用户怎么使用呢?”。 尝试构思出一种优雅,简易,符合直觉的调用方法。
这次我会替你把这活干了:
- 我们将创造一个"grep运算符"并绑定到
<leader>g
。 - 它将表现得同其他任意Vim运算符一样,还可以加入到组合键(比如
w
和i{
)中。 - 它将立刻开始搜索并打开quickfix窗口展示结果。
- 它将_不会_跳到第一个结果,因为当第一个结果不是你想要的时候,这样做会困扰你。
一些你将怎么使用它的用例:
<leader>giw
: Grep光标下的词(word)。<leader>giW
: Grep光标下的词的大写形式(WORD)。<leader>gi'
: Grep当前所在的单引号括住的词。viwe<leader>g
: 可视状态下选中一个词并拓展选择范围到下一词,然后Grep。
有很多,_很多_其他的方法可以用它。看上去它好像需要写很多,很多代码, 但事实上我们只需要实现"运算符"功能然后Vim就会完成剩下的工作。
一个原型
在埋头写下巨量(trickey bits)的Vimscript之前,有一个也许会帮上忙的方法是简化你的目标并实现它, 来推测你最终解决方案可能的"外形"。
让我们简化我们的目标为"创造一个映射来搜索光标下的词"。这有用而且应该更简单,所以我们能更快得到可运行的成果。 目前我们将映射它到<leader>g
。
我们从一个映射骨架开始并逐渐填补它。执行这个命令:
:nnoremap <leader>g :grep -R something .<cr>
如果你阅读过:help grep
,你就能轻易理解这个命令。我们之前也看过许多映射,这里没有什么是新的。
显然我们还没做什么,所以让我们一步步打磨这个映射直到它符合我们的要求。
搜索部分
首先我们需要搜索光标下的词,而不是something
。执行下面的命令:
:nnoremap <leader>g :grep -R <cword> .<cr>
现在试一下。<cword>
是一个Vim的command-line模式的特殊变量, Vim会在执行命令之前把它替换为"光标下面的那个词"。
你可以使用<cWORD>
来得到大写形式(WORD)。执行这个命令:
:nnoremap <leader>g :grep -R <cWORD> .<cr>
现在试试把光标放在诸如foo-bar
的词上面。Vim将grepfoo-bar
而不是其中的一部分。
我们的搜索部分还有一个问题:如果这里面有什么特殊的shell字符,Vim会毫不犹豫地传递给外部的grep命令。 这样会导致程序崩溃(或更糟:铸成某些大错)。
让我们看看如何使它挂掉。输入foo;ls
并把光标放上去执行映射。grep命令失败了, 而Vim将执行ls
命令!这肯定糟透了,如果词里包括比ls
更危险的命令呢?
为了解决这个问题,我们将调用参数用引号括起来。执行这个命令:
:nnoremap <leader>g :grep -R '<cWORD>' .<cr>
大多数shell把单引号括起来的内容当作(大体上)字面量,所以我们的映射现在更加健壮了。
转义Shell命令参数
搜索部分还有一个问题。在that's
上尝试这个映射。它不会工作,因为词里的单引号与grep命令的单引号发生了冲突!
为了解决问题,我们可以使用Vim的shellescape
函数。 阅读:help escape()
和:help shellescape()
来看它是怎样工作的(真的很简单)。
因为shellescape()
要求Vim字符串,我们需要用execute
动态创建命令。 首先执行下面命令来转换:grep
映射到:execute "..."
形式:
:nnoremap <leader>g :execute "grep -R '<cWORD>' ."<cr>
试一下并确信它可以工作。如果不行,找出拼写错误并改正。 然后执行下面的使用了shellescape
的命令。
:nnoremap <leader>g :execute "grep -R " . shellescape("<cWORD>") . " ."<cr>
在一般的词比如foo
上执行这个命令试试。它可以工作。再到一个带单引号的词,比如that's
,上试试看。 它还是不行!为什么会这样?
问题在于Vim在拓展命令行中的特殊变量,比如<cWORD>
,的之前,就已经执行了shellescape()
。 所以Vim shell-escaped了字面量字符串"<cWORD>"
(什么都不做,除了给它添上一对单引号)并连接到我们的grep
命令上。
通过执行下面的命令,你可以亲眼目睹这一切。
:echom shellescape("<cWORD>")
Vim将输出'<cWORD>'
。注意引号也是输出字符串的一部分。Vim把它作为shell命令参数保护了起来。
为解决这个问题,我们将使用expand()
函数来强制拓展<cWORD>
为对应字符串, 抢在它被传递给shellescape
之前。
让我们单独看看这一部分是怎么工作的。把你的光标移到带单引号的词(比如that's
)上去, 并执行下面命令:
:echom expand("<cWORD>")
Vim输出that's
,因为expand("<cWORD>")
以Vim字符串的形式返回当前光标下的词。 是时候加入shellescape
的部分了:
:echom shellescape(expand("<cWORD>"))
这次Vim输出'that'\''s'
。 如果觉得这看上去真可笑,你大概没有感受过看透了各种shell转义的疯狂形式后的淡定吧。 目前,不用为此而纠结。就相信Vim接受了expand
的输出并正确地转义了它。
目前我们已经得到了光标下的词的彻底转义版本。是时候连接它到我们的映射了! 执行下面的命令:
:nnoremap <leader>g :exe "grep -R " . shellescape(expand("<cWORD>")) . " ."<cr>
试一下。这个映射不再有问题,即使我们用它搜索带古怪符号的词。
"从简单的Vimscript开始并一点点转变它直到达成你的目标"这样的工作方式将会被你一再取用。
整理整理
在完成映射之前,还要处理一些小问题。首先,我们说过我们不想自动跳到第一个结果, 所以要用grep!
替换掉grep
。执行下面的命令:
:nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>
再一次试试,发现什么都没发生。Vim用结果填充了quickfix窗口,我们却无法打开。 执行下面的命令:
:nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>
现在试试这个映射,你将看到Vim自动打开了包含搜索结果的quickfix窗口。 我们所做的仅仅是在映射的结尾续上:copen<cr>
。
最后一点,在搜索的时候,我们要移除Vim所有的grep输出。执行下面的命令:
:nnoremap <leader>g :silent execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>
我们完成了,试一试并犒劳一下自己吧!silent
命令仅仅是在运行一个命令的同时隐藏它的正常输出。
练习
把我们刚刚做出来的映射加入到你的~/.vimrc
文件。
如果你未曾读过:help :grep
,去读它。
阅读:help cword
。
阅读:help cnext
和help cprevious
。修改你的grep映射,试一下它们。
设置:cnext
和:cprevious
的映射,让在匹配内容间的移动更加方便。
阅读:help expand
。
阅读:help copen
。
在我们创建的映射中加入height参数到:copen
命令中,看看quickfix窗口能不能以指定的高度打开。
阅读:help silent
。