课程表

vim 命令手册

Vimscript 编程参考

工具箱
速查手册

Vimscript Grep运算符实例

当前位置:免费教程 » 软件/图像 » Vim

第一部分

在本章和下一章中,我们将使用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运算符一样,还可以加入到组合键(比如wi{)中。
  • 它将立刻开始搜索并打开quickfix窗口展示结果。
  • 它将_不会_跳到第一个结果,因为当第一个结果不是你想要的时候,这样做会困扰你。

一些你将怎么使用它的用例:

  • <leader>giw: Grep光标下的词(word)。
  • <leader>giW: Grep光标下的词的大写形式(WORD)。
  • <leader>gi': Grep当前所在的单引号括住的词。
  • viwe<leader>g: 可视状态下选中一个词并拓展选择范围到下一词,然后Grep。

有很多,_很多_其他的方法可以用它。看上去它好像需要写很多,很多代码, 但事实上我们只需要实现"运算符"功能然后Vim就会完成剩下的工作。

一个原型

在埋头写下巨量(trickey bits)的Vimscript之前,有一个也许会帮上忙的方法是简化你的目标并实现, 来推测你最终解决方案可能的"外形"。

让我们简化我们的目标为"创造一个映射来搜索光标下的词"。这有用而且应该更简单,所以我们能更快得到可运行的成果。 目前我们将映射它到<leader>g

我们从一个映射骨架开始并逐渐填补它。执行这个命令:

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

如果你阅读过:help grep,你就能轻易理解这个命令。我们之前也看过许多映射,这里没有什么是新的。

显然我们还没做什么,所以让我们一步步打磨这个映射直到它符合我们的要求。

搜索部分

首先我们需要搜索光标下的词,而不是something。执行下面的命令:

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

现在试一下。<cword>是一个Vim的command-line模式的特殊变量, Vim会在执行命令之前把它替换为"光标下面的那个词"。

你可以使用<cWORD>来得到大写形式(WORD)。执行这个命令:

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

现在试试把光标放在诸如foo-bar的词上面。Vim将grepfoo-bar而不是其中的一部分。

我们的搜索部分还有一个问题:如果这里面有什么特殊的shell字符,Vim会毫不犹豫地传递给外部的grep命令。 这样会导致程序崩溃(或更糟:铸成某些大错)。

让我们看看如何使它挂掉。输入foo;ls并把光标放上去执行映射。grep命令失败了, 而Vim将执行ls命令!这肯定糟透了,如果词里包括比ls更危险的命令呢?

为了解决这个问题,我们将调用参数用引号括起来。执行这个命令:

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

大多数shell把单引号括起来的内容当作(大体上)字面量,所以我们的映射现在更加健壮了。

转义Shell命令参数

搜索部分还有一个问题。在that's上尝试这个映射。它不会工作,因为词里的单引号与grep命令的单引号发生了冲突!

为了解决问题,我们可以使用Vim的shellescape函数。 阅读:help escape():help shellescape()来看它是怎样工作的(真的很简单)。

因为shellescape()要求Vim字符串,我们需要用execute动态创建命令。 首先执行下面命令来转换:grep映射到:execute "..."形式:

  1. :nnoremap <leader>g :execute "grep -R '<cWORD>' ."<cr>

试一下并确信它可以工作。如果不行,找出拼写错误并改正。 然后执行下面的使用了shellescape的命令。

  1. :nnoremap <leader>g :execute "grep -R " . shellescape("<cWORD>") . " ."<cr>

在一般的词比如foo上执行这个命令试试。它可以工作。再到一个带单引号的词,比如that's,上试试看。 它还是不行!为什么会这样?

问题在于Vim在拓展命令行中的特殊变量,比如<cWORD>,的之前,就已经执行了shellescape()。 所以Vim shell-escaped了字面量字符串"<cWORD>"(什么都不做,除了给它添上一对单引号)并连接到我们的grep命令上。

通过执行下面的命令,你可以亲眼目睹这一切。

  1. :echom shellescape("<cWORD>")

Vim将输出'<cWORD>'。注意引号也是输出字符串的一部分。Vim把它作为shell命令参数保护了起来。

为解决这个问题,我们将使用expand()函数来强制拓展<cWORD>为对应字符串, 抢在它被传递给shellescape之前

让我们单独看看这一部分是怎么工作的。把你的光标移到带单引号的词(比如that's)上去, 并执行下面命令:

  1. :echom expand("<cWORD>")

Vim输出that's,因为expand("<cWORD>")以Vim字符串的形式返回当前光标下的词。 是时候加入shellescape的部分了:

  1. :echom shellescape(expand("<cWORD>"))

这次Vim输出'that'\''s'。 如果觉得这看上去真可笑,你大概没有感受过看透了各种shell转义的疯狂形式后的淡定吧。 目前,不用为此而纠结。就相信Vim接受了expand的输出并正确地转义了它。

目前我们已经得到了光标下的词的彻底转义版本。是时候连接它到我们的映射了! 执行下面的命令:

  1. :nnoremap <leader>g :exe "grep -R " . shellescape(expand("<cWORD>")) . " ."<cr>

试一下。这个映射不再有问题,即使我们用它搜索带古怪符号的词。

"从简单的Vimscript开始并一点点转变它直到达成你的目标"这样的工作方式将会被你一再取用。

整理整理

在完成映射之前,还要处理一些小问题。首先,我们说过我们不想自动跳到第一个结果, 所以要用grep!替换掉grep。执行下面的命令:

  1. :nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>

再一次试试,发现什么都没发生。Vim用结果填充了quickfix窗口,我们却无法打开。 执行下面的命令:

  1. :nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>

现在试试这个映射,你将看到Vim自动打开了包含搜索结果的quickfix窗口。 我们所做的仅仅是在映射的结尾续上:copen<cr>

最后一点,在搜索的时候,我们要移除Vim所有的grep输出。执行下面的命令:

  1. :nnoremap <leader>g :silent execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>

我们完成了,试一试并犒劳一下自己吧!silent命令仅仅是在运行一个命令的同时隐藏它的正常输出。

练习

把我们刚刚做出来的映射加入到你的~/.vimrc文件。

如果你未曾读过:help :grep,去读它。

阅读:help cword

阅读:help cnexthelp cprevious。修改你的grep映射,试一下它们。

设置:cnext:cprevious的映射,让在匹配内容间的移动更加方便。

阅读:help expand

阅读:help copen

在我们创建的映射中加入height参数到:copen命令中,看看quickfix窗口能不能以指定的高度打开。

阅读:help silent


第二部分

目前为止,我们已经完成了一个原型,是时候扩充它,让它更加强大。

记住:我们初始目标是创建"grep运算符"。我们还需要做一大堆新的东西来达成目标, 但要像前一章的过程一样:从简单的东西开始,并逐步改进直到它满足我们的需求。

在开始之前,注释掉~/.vimrc中在前一章创建的映射。我们还要用同样的快捷键来映射新的运算符。

新建一个文件

创建一个新的运算符需要许多命令,把它们手工打出来将很快变成一种折磨。 你可以把它附加到~/.vimrc,但让我们为这个运算符创建一个独立的文件。我们有足够的必要这么做。

首先,找到你的Vimplugin文件夹。在Linux或OS X,这将会是~/.vim/plugin。 如果你是Windows用户,它将位于你的主目录下的vimfiles文件夹。(如果你找不到,在Vim里使用`:echo $HOME命令) 如果这个文件夹不存在,创建一个。

plugin/下新建文件grep-operator.vim。这就是你放置新运算符的代码的地方。 一旦文件被修改,你可以执行:source %来重新加载代码。 每次你打开Vim,这个文件也会被重新加载,就像~/.vimrc

不要忘了,在你source之前,你_必须_先保存文件,这样才能看到变化!

骨架(Skeleton)

要创建一个新的Vim运算符,你需要从两个组件开始:一个函数还有一个映射。 先添加下面的代码到grep-operator.vim:

  1. nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
  2. function! GrepOperator(type)
  3. echom "Test"
  4. endfunction

保存文件并用:source %source它。尝试通过按下<leader>giw来执行"grep整个词"。 Vim将在接受iw动作(motion)后,输出Test,意味着我们已经搭起了骨架。

函数部分是简单的,没有什么是我们没讲过的。不过映射部分比较复杂。 我们首先对函数设置了operatorfunc选项,然后执行g@来以运算符的方式调用这个函数。 看起来这有点绕,不过这就是Vim工作的原理。

暂时把这个映射看作黑魔法吧。稍后你可以到文档里一探究竟。

可视模式

我们已经在normal模式下加入了这个运算符,但还想要在visual模式下用到它。 在之前的映射下面添加多一个:

  1. vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

保存并source文件。现在在visual模式下选择一些东西并按下<leader>g。 什么也没发生,但Vim确实输出了Test,所以我们的函数已经运行了。

之前我们就见过<c-u>,但是还没有解释它是做什么的。试一下在可视模式下选中一些文本并按下:。 Vim将打开一个命令行就像平时按下了:一样,但是命令行的开头自动添加了'<,'>

Vim为了提高效率,插入了这些文本来让你的命令在被选择的范围内执行。 但是这次,我们不需要它添倒忙。我们用<c-u>来执行"从光标所在处删除到行首的内容",移除多余文本。 最后剩下一个孤零零的:,为调用call命令作准备。

我们传递过去的visualMode()参数还没有讲过呢。 这个函数是Vim的内置函数,它返回一个单字符的字符串来表示visual模式的类型: "v"代表字符宽度(characterwise),"V"代表行宽度(linewise),Ctrl-v代表块宽度(blockwise)。

动作类型

我们定义的函数接受一个type参数。我们知道在visual模式下它将会是visualmode()的返回值, 但是在normal模式下呢?

编辑函数体部分,让代码像这样:

  1. nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
  2. vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
  3. function! GrepOperator(type)
  4. echom a:type
  5. endfunction

Source文件,然后继续并用多种的方式测试它。你可能会得到类似下面的结果:

  • 按下viw<leader>g显示v,因为我们处于字符宽度的visual模式。
  • 按下Vjj<leader>g显示V,因为我们处于行宽度的visual模式。
  • 按下<leader>giw显示char,因为我们在字符宽度的动作(characterwise motion)中使用该运算符。
  • 按下<leader>gG显示line,因为我们在行宽度的动作(linewise motion)中使用该运算符。

现在我们已经知道怎么区分不同种类的动作,这对于我们选择需要搜索的词是很重要的。

复制文本

我们的函数将需要获取用户想要搜索的文本,而这样做最简单的方法就是复制它。 把函数修改成这样:

  1. nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
  2. vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
  3. function! GrepOperator(type)
  4. if a:type ==# 'v'
  5. execute "normal! `<v`>y"
  6. elseif a:type ==# 'char'
  7. execute "normal! `[v`]y"
  8. else
  9. return
  10. endif
  11. echom @@
  12. endfunction

哇。好多新的东西啊。试试按下<leader>giw<leader>g2evi(<leader>g看看。 每次Vim都会输出动作所包括的文本,显然我们已经走上正道了!

让我们把这段代码一步步分开来看。首先我们用if语句检查a:type参数。如果是'v', 它就是使用在字符宽度的visual模式下,所以我们复制了可视模式下的选中文本。

注意我们使用大小写敏感比较==#。如果我们只用了==而用户设置ignorecase"V"也会是匹配的,结果_不会_如我们所愿。重视防御性编程!

if语句的第二个分支则会拦住normal模式下使用字符宽度的动作。

剩下的情况只是默默地退出。我们直接忽略行宽度/块宽度的visual模式和对应的动作类型。 Grep默认情况下不会搜索多行文本,所以在搜索内容中夹杂着换行符是毫无意义的。

我们每一个if分支都会执行normal!命令来做两件事:

  • 在可视状态下选中我们想要的文本范围:
    • 先移动到范围开头,并标记
    • 进入字符宽度的visual模式
    • 移动到范围结尾的标记
  • 复制可视状态下选中的文本。

先不要纠结于特殊标记方式。你将会在完成本章结尾的练习时学到为什么它们会不一样。

函数的最后一行输出变量@@。不要忘了以@开头的变量是寄存器。@@是"未命名"(unnamed)寄存器: 如果你在删除或复制文本时没有指定一个寄存器,Vim就会把文本放在这里。

简明扼要地说:我们选中要搜索的文本,复制它,然后输出被复制的文本。

转义搜索文本

既然得到了Vim字符串形式的需要的文本,我们可以像前一章一样将它转义。修改echom命令成这样:

  1. nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
  2. vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
  3. function! GrepOperator(type)
  4. if a:type ==# 'v'
  5. normal! `<v`>y
  6. elseif a:type ==# 'char'
  7. normal! `[v`]y
  8. else
  9. return
  10. endif
  11. echom shellescape(@@)
  12. endfunction

保存并source文件,然后在可视模式下选中带特殊字符的文本,按下<leader>g。 Vim显示一个被转义了的能安全地传递给shell命令的文本。

执行Grep

我们终于可以加上grep!命令来实现真正的搜索。替换掉echom那一行,代码看起来就像这样:

  1. nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
  2. vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
  3. function! GrepOperator(type)
  4. if a:type ==# 'v'
  5. normal! `<v`>y
  6. elseif a:type ==# 'char'
  7. normal! `[v`]y
  8. else
  9. return
  10. endif
  11. silent execute "grep! -R " . shellescape(@@) . " ."
  12. copen
  13. endfunction

看起来眼熟吧。我们简单地执行上一章得到的silent execute "grep! ..."命令。 由于我们不再把所有的代码塞进单个nnoremap命令里,现在代码甚至更加清晰易懂了!

保存并source文件,然后尝试一下,享受自己辛勤劳动的成果吧!

因为定义了一个全新的Vim运算符,现在我们可以在许多场景下使用它了,比如:

  • viw<leader>g: 可视模式下选中一个词,然后grep它。
  • <leader>g4w: Grep接下来的四个词。
  • <leader>gt;: Grep到分号为止的文本。
  • <leader>gi[: Grep方括号里的文本.

这里彰显了Vim的优越性:它的编辑命令就像一门语言。当你加入新的动词,它会自动地跟(大多数)现存的名词和形容词搭配起来。

练习

阅读:help visualmode()

阅读:help c_ctrl-u

阅读:help operatorfunc

阅读:help map-operator


第三部分

我们新鲜出炉的"grep运算符"工作得很好,但是写Vimscript的目的,就是要体贴地改善你的用户的生活。 我们可以额外做两件事,让我们的运算符更加符合Vim生态圈的要求。

保护寄存器

由于把文本复制到未命名寄存器中,我们破坏了之前在那里的内容。

这并不是我们的用户想要的,所以让我们在复制之前先保存寄存器中的内容并于最后重新加载。 修改代码成这样:

  1. nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
  2. vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
  3. function! GrepOperator(type)
  4. let saved_unnamed_register = @@
  5. if a:type ==# 'v'
  6. normal! `<v`>y
  7. elseif a:type ==# 'char'
  8. normal! `[v`]y
  9. else
  10. return
  11. endif
  12. silent execute "grep! -R " . shellescape(@@) . " ."
  13. copen
  14. let @@ = saved_unnamed_register
  15. endfunction

我们在函数的开头和结尾加入了两个let语句。 第一个用一个变量保存@@中的内容,第二个则重新加载保存的内容。

保存并source文件。测试一下,复制一些文本,接着按下<leader>giw来执行运算符, 然后按下p来粘贴之前复制的文本。

当写Vim插件时,你_总是_应该尽量在修改之前保存原来的设置和寄存器值,并在之后加载回去。 这样你就避免了让用户陷入恐慌的可能。

命名空间

我们的脚本在全局命名空间中创建了函数GrepOperator。 这大概不算什么大问题,但当你写Vimscript的时候,事前以免万一远好过事后万分歉意。

仅需增加几行代码,我们就能避免污染全局命名空间。把代码修改成这样:

  1. nnoremap <leader>g :set operatorfunc=<SID>GrepOperator<cr>g@
  2. vnoremap <leader>g :<c-u>call <SID>GrepOperator(visualmode())<cr>
  3. function! s:GrepOperator(type)
  4. let saved_unnamed_register = @@
  5. if a:type ==# 'v'
  6. normal! `<v`>y
  7. elseif a:type ==# 'char'
  8. normal! `[v`]y
  9. else
  10. return
  11. endif
  12. silent execute "grep! -R " . shellescape(@@) . " ."
  13. copen
  14. let @@ = saved_unnamed_register
  15. endfunction

脚本的前三行已经被改变了。首先,我们在函数名前增加前缀s:,这样它就会处于当前脚本的命名空间。

我们也修改了映射,在GrepOperator前面添上<SID>,所以Vim才能找到这个函数。 如果我们不这样做,Vim会尝试在全局命名空间查找该函数,这是不可能找到的。

欢呼吧,我们的grep-operator.vim脚本不仅非常有用,而且是一个善解人意的Vimscript公民!

练习

阅读:help <SID>

享受一下,吃点零食犒劳自己。

转载本站内容时,请务必注明来自W3xue,违者必究。
 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号