案例分析:grep操作符,第一部分

在本章节和后面的章节中,我们会完整地创建一段足够复杂的 Vimscript 代码。 我们会讨论几种之前没看过的东西,以及如何把之前学过的东西在实战中组合在一起。

在你完成本次案例分析的过程中,碰到了任何不熟悉的,都应该用 :help 查阅。 如果似懂非懂地完成了所有的东西,你将学不到多少东西。

grep 命令

如果从来没用过 :grep,你现在就应该花上一分钟来阅读 :help :grep:help :make。 如果之前从来没有用过 quickfix 窗口,那就阅读 :help quickfix-window

简而言之::grep ... 会运行一个外部grep程序来处理输入的任何参数,解析结果, 然后填充到quickfix列表,这样你就可以在 Vim 中跳到那些结果上了。

我们的例子就是要让 :grep 更容易使用,通过添加一个“grep操作符”,可以用任何 Vim 内置(或自定义)的移动来选择想要搜索的文本。

用法

在创建任何有意义的Vimscript代码时,第一件需要思考的事情就是: “这个功能会被如何使用?”。尝试着想出一个平滑、简单、直观的方式来调用它。

在这个例子中,我会帮你做这一步:

一些你可能会用到的例子:

有很多,很多其他方式来使用它们。这看起来好像需要很多代码, 但实际上我们所要做的所有事情就是实现“操作符”的功能,而 Vim 会处理余下的。

初步设计

在编写一些棘手的 Vimscript 时,有个办法有时会很有用,那就是简化目标,并实现, 让你对最终解决办法的“样子”有所了解。

让我们把目标简化为:“创建一个映射来搜索光标处的单词”。这很有用处,而且更简单, 这样就可以更快的执行了。我们暂时把这个映射到 <leader>g

我们将从一个映射的纲要开始,并逐步填充它。运行命令:

:nnoremap <leader>g :grep -R something .<cr>

如果你已经阅读过 :help grep ,这应该很容易理解。我们之前已经看过很多的映射, 而这里并没有什么新的东西。

显然我们还没有完成,所有让我们重新定义这个映射,直到它达到了我们简化过的目标。

搜索词

首先我们需要搜索光标处的单词,而不是 something 这个字符串。运行以下命令:

:nnoremap <leader>g :grep -R <cword> .<cr>

现在试试看。<cword> 是在 Vim 的命令行模式中可以使用的特殊文本,而 Vim 会在运行命令之前把它替换为“光标处的单词”。

你可以用 <cWORD> 来得到一个大写单词而非小写。运行命令:

:nnoremap <leader>g :grep -R <cWORD> .<cr>

现在当你的光标处在类似 foo-bar 的字符串时,试试这个映射。 Vim 会搜索 foo-bar 而不只是这个单词的一部分。

我们的搜索词仍然有一个问题:如果里面有任何特殊的shell字符,Vim 都会毫无顾忌的传给外部的grep命令, 这会爆发出问题(或者更糟:引起一些可怕的事情)。

去试试看,让它造成些破坏。在文件中键入 foo;ls,这时光标在这些文本处,然后运行那个映射。 grep命令会失败,Vim 实际上还会执行一个 ls 命令!很明显,如果这个单词包括一个比 ls 更危险的命令,那就严重了。

为了试图修复这个问题,我们在grep调用中给参数加上引号。运行命令:

: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 就转义了字面字符串 "<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

在我们创建的映射中给 :copen 命令添加一个高度,使得 quickfix 窗口能以任何你喜欢的高度打开。

阅读 :help silent

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