Vim进阶索引[6]::外部程序
Windows的用户可能会因为这一篇里面用了大量的Unix工具而倍感沮丧。但大可不必如此,这一篇里面我们更多的是讲一种使用Vim的理念──用户可以依据自身对外部工具的掌握程度适时地使用这些工具来减少工作量。常的工具都可以找到对应的Winodws版本,而且通常很小。
unix工具箱哲学的一个核心思想是每个工具只完成各自相对简单的任务,这些工具的真正威力来自于它们之间关系。你可以组合这些工具来完成复杂的工作。Vim虽然是一个交互式的编辑器,但这种思想仍在它身上得了很好的体现。它能很好地与其他工具配合来扩展功能或完成相对复杂的编辑工作。
这一篇教程我们将讨论与Vim中与外部程序有关的议题。
1 使用外部程序的Vim命令
Vim中有一些功能是通过外部程序来实现的。比如:make命令,Vim没有内置make工具。但是用户可以为:make命令指定一个外部程序,比如:gnu make或Windows下的nmake。这样我们在Vim中就可以使用:make命令了。设置make工具所用的设置项是'makeprg'。我们先看一下跟外部程序有关命令和其对应设置项。
:cscope
|
=
|
gq
|
:grep
|
:make
|
K
|
:shell
|
提示:这些设置的一个共同点是它们都不能在模式行中进行设置。出于安全的原因在模式行中不能使用这些设置项。
Vim的Diff模式就是使用了工具GNU diff,这个程序是Vim的重要组成,Vim并未提供相应的设置项让用户更改。与vim相关的外部程序还有ctags类的程序,这类程序用来生成关键字的索引。但Vim用到的是tags文件所以也没有提供相应的设置项。(即使要生成tags,ctags也不是必需的)
这些就是使用或可设置为外部程序的命令。总的来说这些命令在某些方面提供了方便。但从功能性来说它们并非是必不可少的。不过既然有了我们还是利用起来吧。
由于这些命令的特性我们可以用来做一些有用的事情,现在来看一下我们还能怎么用这些命令。
-
这两条命令都是在一般模式中使用的过滤命令(一般的过滤在行模式中使用)。它们的工作方式都是一样的:对一定的范围使用过滤程序/内部命令,所以我们放在一起。通过指定合适的过滤程序我们就能决定这命令的作用。
比如:设置为sort可以用来排序` se equalprg=sort‘,设置为` se equalprg=sort/|uniq‘,排序并去除重复。
总之任何过滤程序都可以在这里使用──而决不仅仅可以使用缩进或文本格式化的工具。
-
与前面两个命令不同,这个命令的目的并非改变当前文本的内容或格式。这个命令以当前字为或选区(至多一行)为参数运行命令 。自动传递参数给相应的程序。利用它自动传递参数的特点我们可以运行一些需要参数的程序。
除了用来查找Vim文档,man文档外最直觉的一个用法就是用来查字典了。只要是以一个字词为输入的所有程序都可以定义为'keywordprg'。如:stardict、locate、which等。下面是用K来运行当前圈选1的网址的例子(windows平台):
se kp=start/ c://progra~1//opera//opera.exe
-
这两条命令的真正有用的/有效的特性在于Vim可以根据运行的结果生成quickfix窗口。而且你可以用:cn, :clist, cw…等quickfix命令在不同的位置间跳转。要用好两条命令首先要了解这两个设置项:grepformat errorformat。这两个设置项的作用是捕捉并分析输出。在使用make工具时由于在使用不同编译器时输出的错误信息的格式也不尽相同,所以在设置好了grepprg或makeprg后还要教它“读懂输出”。
在缺省情况下Unix平台的Vim的'grepprg'是'grep -n'而Windows下则是'findstr /n',不过它们运行的结果都一样。假设你是命令行下(shell中)用前面的命令在当前目录查找:grep -n goes *.txtl,返回信息是以下面的格式出现的:
a_text_file.txt:52: something goes here .........
这条信息中包括了:文件名a_text_file,行号52,该行的内容共三个部分。这三个部分是由:隔开的。
我们再看一下缺省情况下的gfm::se gfm? grepformat=%f:%l:%m, ....
同时设置的多种不同格式的grepformat间用,隔开。其中%f、%l、%m分别表示文件名,行号和提示信息(这个例子中就是该行的内容)。中间的:是帮助对应不同部分的关键,Vim在输出的信息中查找:并将冒号分隔起来的三个部分与gfm中的三部分对应起来。这样我们是Vim中使用grep后,它就能自动解读输出的信息并带我们到不同文件的特定行去了。要了解%f、%l等更多占位符的意义及用法见*error-file-format*
提示:在Vim中通过程序的输出信息,使用cn cl cw等命令在文件中快速移动的这一特性称为quickfix。使用quickfix时,文件名是必需的——这样才能打开正确的文件,有行号的话更好——精确地定位到行。
可以看到在设置好程序的前提下,要用好这两条命令的关键是设置好合适的gfm/efm。Vim文档中提供了相当多的现成的gfm和efm,几乎含盖了各种类型的编译器。所以在你设置"合适的gfm/efm"之前先看一看有没有现成的。
这里再来看一下makeinfo的输出格式,
somethingwrong.texi:5: Unknown command `blah'.
与我为它设置的“错误信息格式”:
se efm=%f:%l:/ %m se makeprg=makeinfo
很眼熟不是?它跟grep是一样的只是刚才的查找结果现在成了“错误信息”,本质上:grep跟:make是一样的。除了命令名称和设置项名称的不同它们的其他地方是一样。在设置这些后我只要使用:make,Vim就会带我到出错的行去——如果有错的话。
提示:se makeprg=makeinfo这条命令不是必需的,你可以将命令放到makefile中,然后使用make工具。当然直接用makeinfo编译文档还是使用make工具由你自己决定。当然efm是要设置的。同样的还有tex文档,你一样可以使用make工具但efm要设置正确。
这两条命令的作用可不仅局限于使用各种make工具或grep类工具。想想下面的设置会产生什么样的效果:
:se gfm=%f :se grepprg=find
上面的设置让你可以快速定位特定的文件并打开。将find改为ls就有了一个简易的Explorer插件了。
看到下面的命令,你可能会一头雾水,
:se grepprg=cat :se gfm=%f :grep blah.txt
但如果你知道blah.txt有着下面的内容后,你就会知道了——这是个项目文件。
farsi.c farsi.h feature.h glbl_ime.cpp proto/buffer.pro proto/charset.pro
再想一下如果blah.txt的内容是一些网址,上面的命令又会产生什么结果?就像这样:
ftp://ftp.vim.org/pub/vim/ http://sf.net/
动手实验一下,然后:h netrw。
最后提供用来调试sed/awk脚本用的'efm':
" sed se efm=sed:/ file/ %f/ line/ %l:/ %m " awk 仅限于使用脚本文件时。 se efm=awk:/ %f:%l:/ %t:/ %m
=
gq
K
:grep
:make
对于make和grep的设定可以在ft文件中设置或是在vimrc中使用au(:help :autocmd)进行设置。K,gq,=的功能也可以用map的方式来达到(这就是它们并非必不可少的原因),并且map也带来了灵活性。但使用keywordprg你不能指定Vim的内部命令,同时使用外部命令时map通常意味着更复杂的设置过程。有利有弊。
另外,Vim处理make和grep和cscope的方式是内建一个命令的接口(quickfix),要达到同样的效果又需要用户写相当长度的脚本。可以说这点来说使用内置的接口还是要方便些的。
尽管我们可以通过这些Vim命令灵活地使用外部程序,但Vim与外部程序的互动更多地来自:!命令。
2 !命令
Vim为用户提供了极大的可能性,让用户可以自由的操纵他们的文本。尤其是通过内置的脚本解释器,用户只要写Vim脚本就可以扩展Vim的功能。但是Vim知道大量经得住时间考验的Unix工具如果闲置起来就太可惜了。通过提供方便的接口,Vim在保持体积小巧的同时也让用户有选择的余地。
Vim的插件机制是以两种方式实现的:一是通过内置的脚本解释器。一是通过保留与外部程序协作的接口。本篇我们会看看如何在Vim中使用外部程序或shell脚本。
一般而言我们可以通过使用多个软件来满足我们的特殊要求:先用A程序处理一部分保存,再由B程序接着处理,……但是Vim提供的接口让我们在使用外部程序就象在使用内建的功能一样方便快捷,你不必在程序之间来回切换。
Vim调用外部程序主要是通过!实现的。!命令可以在行模式下也可以在一般模式中使用。带地址/范围地使用这条命令时(即作为过滤命令时),当前地址范围内的内容会被命令的输出所替代。否则只是在shell中运行该命令。详细用法见:! 和 :! 。
在你决定花点时间写Vim脚本前也许你应该先考虑以下这几个问题
- 我需要什么样的结果?
- 有这样的外部程序存在并且能更好或更简单地满足我的要求吗?
- 最后就是你对哪种方式(Vim脚本或外部工具程序)更熟悉。
比如你可能在整理一份软件列表,你需要某个目录的文件列表。毫无疑问你会选择用外部程序(或命令):
:r !dir ... :r !ls ...
这是另一种情况,你要将文本区中每一行文本复制一行在该行下面,你可以使用下面的任意一条命令:
%!sed p g/^/norm Yp
使用两命令看上去没什么差别你只需要照自己的习惯来决定使用哪条命令。但如果你要对文本排序或是更复杂的工作呢?这时写Vim脚本会是很累人的工作。
2.1 !的两种使用风格
!命令在shell中运行命令,所有能在命令行中运行的程序/工具都可以。对Linux来说这代表了几乎所有的可执行程序都可以从vim中启动运行。而Windows中则与路径的设置有关,但通常来说在windows目录下和system32目录下的程序都可以直接在Vim中运行。此外还有命令解释器的内部命令。能在shell中运行的程序脚本或批处理文件都能在Vim中使用。但我们可以作一些简单的分类。
在Vim中使用命令不外乎有两种情况。
- 是把Vim当成一个shell接口(这种情况下要注意命令参数中的特殊字符。见*cmdline-special*)。如
!touch abbcc !rm abbcc
当然你也可以运行图型介面程序:
" 用Opera预览效果 !opera % " 或者你想要后台运行firefox !firefox % &
简单说这种使用方式就是把Vim当成shell用。但是在Vim中方便地运行其他程序不是我们的首要目标。Vim是一个编辑器,所以我会着重在编辑相关的内容──就是下面要讲的第二。
- 是为了获取程序的输出。又可细分为两种。过滤程序(filter)产生的输出;非过滤程序的输出。
- 获取非过滤程序的输出。前面的读入目录的命令就是在Vim中使用非过滤命令的例子,这里再举几个例子:
r !date "当前行下插入日期 " 在当前位置插入脚本运行时间的测试结果 .!time myscript " 运行用户的shell脚本或批处理程序,并获取输出 r !myshellscript
能够在命令行产生某种输出的程序(批处理/脚本)都能与Vim配合(如前面例子中的date),而其中一些从stdin读取数据处理后输出到stdout的程序,即filter程序,在Vim中的应用范围要更大一点。相对而言一个图形介面程序在这种情况几乎毫无用处可言,无法在命令行下运行也就代表着难以与其他程序配合。因为当一个程序在处理的结果是在对话框中显示的话,表示这个结果无法直接为Vim所用。我们必须至少要经过一个复制粘贴的过程:-(
- 获取过滤程序输出。与非过滤程序不同的是过滤程序对给定范围内的文本进行“加工”,然后才产生输出。下面是一些使用过滤命令的例子。
" gggqG %!fmt " 选择打印部分“列”数据 8,20!cut .. " 删除选区中的重复行,改变次序 '<,>'!sort |uniq " 同上,但不改变行的次序 '<,>'!awk '{if ($0 in a) next}{a[$0]++}8'
- 获取非过滤程序的输出。前面的读入目录的命令就是在Vim中使用非过滤命令的例子,这里再举几个例子:
Unix下有相当多丰富的过滤程序资源,Windows下这类的工具较少(sort、more和findstr)。好在Unix下的最好的命令行工具(尤其是Gnu版本)通常都有Windows版本。它们通常很小——可以在这里下载。其中最强大的两个工具是awk、sed和CoreUtils工具包,稍大一点的选择是perl。
2.2 实例
由于这一篇更多地是涉及到工具软件的使用而非Vim本身,所以不会对这些例子进行很详细的解说。这里演示一下实际应用中外部程序的用法。这一次仍是以成绩单为例,这是其中的三条数据:
姓名 期中 期末 李阿月 72 70 林小丽 91 93 王小明 46 56
共三栏栏与栏之间用一个或多个空格隔开。
看一下外部命令是如何逐一完成任务的,思考一下用纯Vim的方式要怎么达到同样的目的。
- 首先是成绩单排序。按期末成绩,然后期中成绩:
:2,$!sort -k3n -k2,2n
- 根据前面排序的结果,写上排名(在Unix上可以输出序号的工具很多如nl cat等,不过这次我们要用的是grep):
" 工具之间也可以协作 :2,$!grep -n .|tr : ' '
- 计算平均成绩。在寄存器篇中我们用了下面的命令来更新目录中的行号:
:1,25s/[0-9]/+$//=submatch(0)+25/
我们可以用同样的方式计处平均成绩,但Vim不支持浮点运算。
而成绩通常会精确到0.5,所以我们使用了下面的脚本::2,$!gawk '{$0=$0 " " ($3+$4)/2}8'
- 如果我们打算在下面增加一栏计算总人数。总平均成绩,用Vim脚本就没那么容易了。
:2,$!gawk '{sm+=$2;sf+=$3}8;END{print "人数:" NR " 期中平均:" sm/NR " 期末平均:" sf/NR}'
- 我们还能在成绩单后加入简单的统计,
:2,$--!gawk -f myawkscript.awk
上面的命令后会在成绩单后面加入下面的内容:
-- 期末考成绩分布图示 -- 优 4人 %16■■■■ 良 8人 %32■■■■■■■■ ....
提示:对myawkscript.awk有兴趣的话可以见附录。
- 导出为CVS(这样就方便在其他数据处理分析工具中打开):
" 选择要导出的范围,然后输入…… :'<,'>w !tr -s ' ' , >成绩.cvs
3 相关议题
- 要以某一部分的文本,如圈选区的文本作为某个外部程序的输入但又不捕捉该程序的输出要怎么做呢?我们需要用到:w命令,格式如下:
:3,5w !cmd
我们前面导出为cvs用的就是这种方法。
- 很多情况下我们需要在Vim脚本中运行并捕捉外部程序的输出。这时我们可以用system()函数(*system()*)。
:let files = system("ls")
上面的例子是文档中带的例子。看了这个例子大家应该就知道这个函数的用用法了。
4 小结
Unix的这些工具软件有着比Vim脚本更多的应用,对于个人的使用目的来讲写Vim脚本不应做为首选。别忘了从Vi诞生之始开始工具软件就一直是扩展Vi功能的主要途径。充分利用现有的外部程序资源一直是用好Vi的关键之一,除了节省大量时间外,使用对的工具正是Unix风格的一贯体现。如果你使用Linux/Unix的话花点时间在这些工具上会是很好的投资。
最后一点补充:在写完这一篇后,我才发现关于!的部分的篇幅与其在实际应用中的广泛和重要程度完全不符(这当然是因为我前面说过的“这更多是涉及外部程序和而非Vim的使用)。所以我想我得说明一下:对于相当一部分的老手来说!是他们解决一些棘手编辑问题的第一个选择,其次才是脚本。而其中有些人(尤其是perl老手)根本不愿意写超过5行的Vim脚本。
Appendix A
# /usr/bin/gawk -f # 从期末成绩计算各成绩段的人数、比例及图示 # 计算各成绩段的人数 {$3>=90?++a[1]:$3>=80?++a[2]:$3>=70?++a[3]:$3>=60?++a[4]:++a[5]} # 显示输入的数据 8888 END{ split("优 良 中 及 不",rank) print "/n-- 期末考成绩分布图示 --" # 显示统计信息 for(i=1;i<=5;i++){ if(a[i]=="")a[i]=0 printf "%s/t%d人/t%d%%",rank[i],a[i],a[i]*100/NR # 显示图示 while(a[i]-->0)printf "■"; print "" } }