2009年6月1日星期一

vim实用技术(2)

寄存器

通常的编辑器有一个剪贴板,以存储复制和剪切的内容。Vim中的类似概念叫做寄存器(register)。除了有一个无名寄存器外,Vim还有一大 堆有名的寄存器,可以通过"""(参见":help "")或"Ctrl-R"(参见":help i_CTRL-R"和":help c_CTRL-R")加寄存器名(字母、数字和某些特殊字符,参见":help registers";"无名"寄存器的名字是""")来访问。比如,你先使用""ayy"复制了一行,然后使用"dd"删掉了一行,然后移动光标到要复 制到的位置,就可以使用""aP"把先前复制的内容粘贴上去了。手工编辑是有名寄存器的作用还不是很大,但当你想让Vim通过类似于宏的方式自动完成工作 时,有名寄存器就变成不可缺少的重要功能了。下面我们还会用到。

在使用X Window系统时,有两个特殊的寄存器是需要注意一下的:""*"访问的寄存器是X的主选择区域(primary selection),""+"访问的寄存器是X的剪贴板(clipboard)。如果你要在Vim和其它的X应用程序之间复制文本内容,你可以试一下这 两个寄存器。

还有一个很特殊的"寄存器":"="。在插入模式或命令模式中,键入"Ctrl-R=",Vim会提示你输入一个表达式,普通的整数运算在此完全有效。如果想要进行浮点运算,请参见第3.2节中的技巧。

搜索、替换和正则表达式

大家应该都已经知道Vim里使用"/模式"(或"?模式")进行搜索,使用":s/模式/字符串/标志"进行替换,其中的"模式"是一个正则表达 式。关于正则表达式,不熟悉的话可以边用边学,本节也不打算对Vim的正则表达式作完整的阐述(那可能可以专门写一本小册子了),而只抛砖引玉式地给出一 些有用的例子加以说明,以及一些实用技巧。

先说一点点搜索。搜索里最最有用的一个快捷方式是"*"(向下完整匹配光标下的单词)。把光标移动到你要搜索的词(变量名、函数名等)上,比如 "test",然后按"*",Vim将自动产生一个对"\<test\>"(参见":help /\<"和":help /\>")的搜索,也就是说,搜索完整的单词"test"。不要小看这个技巧,它经常可以大幅度地提高搜索的速度。事实上,这是Vim网站上公布的 第1号技巧,也是被评价最高的技巧。相似的技巧还有"#" (向上完整匹配光标下的单词)、"g*" (向下部分匹配光标下的单词)等,请自行查看(":help #"等)。

Vim在搜索和替换时会对匹配成功的文本进行加亮,在已经完成搜索和替换任务后,这种加亮有时反而会妨碍显示。Vim专门提供一个命令取消这种加亮 (直到用户再一次使用搜索或替换命令):":nohlsearch"。建议用户创建一个键盘映射(key mapping)加入到.vimrc中,如:

nmap  :nohlsearch
以上命令表示,在正常模式下按F2键相当于输入":nohlsearch"后面跟一个回车,即取消搜索加亮显示。

再看几个搜索替换的实用例子。

  • 去掉所有的行尾空格:":%s/\s\+$//"。"%"表示在整个文件范围内进行替换,"\s"表示空白字符(空格和制表符),"\+"对前面 的字符匹配一次或多次(越多越好),"$"匹配行尾(使用"\$"表示单纯的"$"字符);被替换的内容为空;由于一行最多只需替换一次,不需要特殊标 志。这个还是比较简单的。

  • 去掉所有的空白行:":%s/\(\s*\n\)\+/\r/"。这回多了"\("、"\)"、"\n"、"\r"和"*"。 "*"代表对前面的字符(此处为"\s")匹配零次或多次(越多越好;使用"\*"表示单纯的"*"字符),"\n"代表换行符,"\r"代表回车符," \("和"\)"对表达式进行分组,使其被视作一个不可分割的整体。因此,这个表达式的完整意义是,把连续的换行符(包含换行符前面可能有的连续空白字 符)替换成为一个单个的换行符。唯一很特殊的地方是,在模式中使用的是"\n",而被替换的内容中却不能使用"\n",而只能使用"\r"。原因是历史造 成的,详情如果有兴趣的话可以查看":help NL-used-for-Nul"。

  • 去掉所有的"//"注释:":%s!\s*//.*!!"。首先可以注意到,这儿分隔符改用了"!",原因是在模式或字符串部分使 用了"/"字符,不换用其他分隔符的话就得在每次使用"/"字符本身时写成"\/",上面的命令得写成":%s/\s*\/\/.*//",可读性较低。 命令本身倒是相当简单,用过正则表达式的人估计都知道"."匹配表示除换行符之外的任何字符吧。

  • 去掉所有的"/* */"注释:":%s!\s*/\*\_.\{-}\*/\s*! !g"。这个略有点复杂了,用到了几个不太常用的Vim正则表达式特性。"\_."匹配包含换行在内的所有字符;"\{-}"表示前一个字符可出现零次或 多次,但在整个正则表达式可以匹配成功的前提下,匹配的字符数越少越好;标志"g"表示一行里可以匹配和替换多次。替换的结果是个空格的目的是保证像 "int/* space not necessary around comments */main()"这样的表达式在替换之后仍然是合法的。

希望上面的这些简单的例子能够引起你使用Vim的正则表达式高效完成任务的兴趣。进一步的信息可参考":help regexp"。

自动完成和路径设定

Vim支持单词的自动完成。比如,你前面使用了一个很长的变量名,叫aLongVariable,下面你在输入时,就不用完整键入了。很可能,你只 需要键入"aL",然后按下"Ctrl-P"(向前搜索可匹配的单词并完成)就可以得到完整的变量名(没有得到想要的结果的话,多按几下"Ctrl- P";或者前面多输入几个字符,如"aLongV")。类似的命令还有"Ctrl-N"(向后搜索可匹配的单词并完成)、"Ctrl-X Ctrl-L"(搜索可匹配的行并完成)、"Ctrl-X Ctrl-F"(搜索可匹配的文件名并完成)等,具体可参看":help ins-completion"。

如果你并不熟悉这些功能,但也并不觉得这有什么稀奇的话,下面这个例子可能会让你觉得吃惊。请尝试打开一个空白的C文件(vim test.c),并输入:

#include 
int main()

{
pri

最后一行不要回车,直接在"pri"后面输入"Ctrl-P",你将看到"printf"出现。是的,虽然文件里没有"printf",但Vim知 道到哪里去寻找它!在作关键字匹配完成时,如果当前文件和其它打开的文件中没有想要的结果,Vim会自动到"#include"的文件中进行进一步的搜索 (为什么是"#include"呢?请查阅":help 'include'"),搜寻的目录则由选项path决定,其缺省值在Unix(含Linux)下为".,/usr/include,,",代表搜索的目 录依次是文件所在目录、/usr/include和当前目录。根据实际情况,你可能需要在.vimrc文件中设置该选项,加入项目相关的包含目录,注意一 般要保留最后的",,",除非你不需要在当前目录下搜索。

设置了合适的path后,另外带来的一个便利就是可以使用"gf"命令方便地跳转到光标下的文件名所代表的文件中。在上面的例子中,把光标移到 "stdio.h"的任一字符上,键入"gf",则Vim会自动打开/usr/include/stdio.h文件。使用"Ctrl-O"(参见": help CTRL-O")可返回到原先的文件中。

文件跳转和Tags

大家一般都知道,在Vim的帮助窗口中的关键字上双击鼠标或者键入"Ctrl-]"即可跳转至该关键字相关的帮助主题。不过,"跳转至匹配的关键字 "这一功能并不仅仅局限于帮助文件。只要有合适的tags文件(参见":help tags-file-format"),我们同样可以在源代码中使用这个方便的功能,跳转到与关键字匹配的"标记"处(通常是源代码中某一函数、类型、变 量或宏的定义位置)。

要产生tags文件,通常我们使用Exuberant Ctags [15]。一般的Linux发布版中均带有这一工具。Ctags带有的选项数量极多,此处我们仅简单介绍如何在一个典型的多文件、多层目录的项目中使用其 基本功能:我们只需在项目的根目录处键入"ctags -R .",Ctags即可自动在文件中查找、识别支持的文件格式、生成tags文件。目前Exuberant Ctags支持多达33种编程语言[16],包括了Linux下常用的C、C++、Java、Perl、PHP等。有了tags文件,以下的Vim命令就 可以方便使用了(进一步的信息可参考":help tags-and-searches"):

  • :tag 关键字(跳转到与"关键字"匹配的标记处)
  • :tselect [关键字](显示与"关键字"匹配的标记列表,输入数字跳转到指定的标记)
  • :tjump [关键字](类似于":tselect",但当匹配项只有一个时直接跳转至标记处而不再显示列表)
  • :tn(跳转到下一个匹配的标记处)
  • :tp(跳转到上一个匹配的标记处)
  • Ctrl-](跳转到与光标下的关键字匹配的标记处;除"关键字"直接从光标位置自动获得外,功能与":tags"相同)
  • g](与"Ctrl-]"功能类似,但使用的命令是":tselect")
  • g Ctrl-](与"Ctrl-]"功能类似,但使用的命令是":tjump")
  • Ctrl-T(跳转回上次使用以上命令跳转前的位置)

当我们在项目的根目录下工作时,上面这些命令工作得很好。但如果我们进到多层目录的里层再运行Vim打开文件时,这些命令的执行结果通常就变成了错 误信息"E433: No tags file"。这是因为缺省Vim只在文件所在目录和当前目录下寻找tags文件,而我们前面只在项目的根目录下生成了tags文件,Vim无法找到该文 件。解决方法有好几种,我认为一般较简单的做法是对每个项目都在.vimrc文件中增加一个路径相关设定。假设我们有两个项目,位置分别是 /home/my/proj1和/home/my/proj2,那我们可以使用

au BufEnter /home/my/proj1/* setlocal tags+=/home/my/proj1/tags
au BufEnter /home/my/proj2/* setlocal tags+=/home/my/proj2/tags
Vim选项tags用于控制检查的tags文件,缺省值为"./tags,tags",即前面所说的文件所在目录下和当前目录下的tags文件。上 面两行自动命令告诉Vim,在打开项目目录下的文件时,tags选项中的内容要增加项目的tags文件的路径。进一步信息可参看":help 'tags'"。

Make 和 grep

Make [17]和grep [18]应当算是Unix世界里无人不晓的基本工具了吧。很自然的,Vim对它们有着特殊的支持。该支持主要通过访问一个特殊的快速修订窗口 (quickfix window)来实现。直接在Vim的命令模式里输入相应的make或grep命令(如":grep foo *.c")即可将命令的执行结果放入该窗口,同时根据返回的结果跳转到第一个错误(make的情况;在使用grep时是匹配成功之处)。以下是常用的"快 速修订"命令:

  • :cn(显示下一个错误)
  • :cp(显示上一个错误)
  • :cl(列出所有的错误及其编号)
  • :cc(跳转到指定编号的错误)
  • :copen(打开快速修订窗口,在其中显示所有错误,可在错误上双击鼠标或按回车键跳转至该错误;示例参见下图)


  • :cclose(关闭快速修订窗口)

Vim 的这个特性也可以与make和grep以外的程序一起工作(事实上,在Windows XP上,":grep"命令一般调起的是"findstr /n")。具体调用那个程序由选项makeprg(Linux下缺省为"make")和grepprg(Linux下缺省为"grep -n $* /dev/null")控制,而如何解析返回的内容则由选项errorformat和grepformat控制。鉴于在Unix/Linux下一般不需更 改这些选项的内容,此处不再详述。

执行外部命令

在":make"这样的命令中,Vim会自动调用外部的程序。用户当然也可以自己执行外部的程序:估计很多的人都已经知道了用":!命令"可以在 Vim中执行一个外部命令。不过,估计大部分人都不知道,还有其它一些命令可以执行外部命令,并且,即使":!"命令里面也有一些技巧可以使用。

最正规的执行外部命令的方法,如前所述,就是":!"。比如,我们想要显示当前目录下的所有文件,就可以直接执行:":!ls"。Vim会在一个终 端窗口中进行文件列表,然后提示我们按键返回Vim中。事实上,这种方式对于"cp"、"rm"这样基本不需要输出的命令比较实用,而对于"ls"这样关 注于输出的命令并不太适用。

如果想把外部命令执行的结果插入到当前编辑的缓冲区中,可以考虑使用":r!"。比如,我们使用":r!ls",就可以把"ls"命令的执行结果插入到缓冲区中光标所在行下面。在使用宏时,这可能会特别有用。

Vim的":!"命令还有一个特别强大的技巧可以使用。拿一个实际例子,我们需要对在一个文件的每一行之前插入一个编号,该怎么做呢?--用Vim 的宏或者脚本可以完成这一工作,但这不是最高效、最灵活的工作方式。Linux下一般带有的GNU的nl,可以用非常灵活的方式来完成这一任务--要对所 有的非空行进行编号,只需要":%!nl";要对包含空行的所有行进行编号?OK,":%!nl -ba"。

稍作一点解释。当使用可视模式选中文本行后然后键入":!"(命令行上将出现":'<,'>!",表示命令的范围是选定的文本),或者 使用":%!"(表示命令的范围是整个缓冲区中的文本),Vim在执行后面的命令时,将把命令范围里的文本行作为后面执行的命令标准输入,并用命令执行后 的标准输出替换当前缓冲区中的这些文本行。这就是上面的命令行的工作原理。

定宽文本排版

在传统的Unix环境下,文本文件的定义是具有一定长度限制的文本行的组合[19]。虽然Vim本身对行的长度没有任何实际的限制,但有一些工具有 这样的限制。为了最大程度的兼容性,也为了在显示、打印等处理上比较方便,一般推荐在邮件和源代码中一般不要超出72列(最多不超出80列)。Vim在处 理定宽的文本方面具有特殊的支持能力。下面是一个在Vim中把行宽(使用选项textwidth)设为40后输入Harry Potter and the Half-Blood Prince的第一句话的结果:

It was nearing midnight and the Prime
Minister was sitting alone in his
office, reading a long memo that was

slipping through his brain without
leaving the slightest trace of meaning
behind.

输入时我只使用了英文字母和空格,换行符都是Vim自动插入的。如果在某一行加入或删除了一些字符后行不就不齐了吗,该如何处理?很简单,把光标移 到要重新格式化的文本开头,使用"gq"命令后面跟一个光标移动命令确定重新格式化的范围。比如"gq}"(格式化一段),"gq5j"(格式化5行), "gqG"(格式化至文件末尾)。

除了选项textwidth外,选项formatoptions确定了跟文本格式化有关的基本选项,常用的数值有:

  • t:根据textwidth自动折行;
  • c:在(程序源代码中的)注释中自动折行,插入合适的注释起始字符;
  • r:插入模式下在注释中键入回车时,插入合适的注释起始字符;
  • q:允许使用"gq"命令对注释进行格式化;
  • n:识别编号列表,编号行的下一行的缩进由数字后的空白决定(与"2"冲突,需要"autoindent");
  • 2:使用一段的第二行的缩进来格式化文本;
  • l:在当前行长度超过textwidth时,不自动重新格式化;
  • m:在多字节字符处可以折行,对中文特别有效(否则只在空白字符处折行);
  • M:在拼接两行时(重新格式化,或者是手工使用"J"命令),如果前一行的结尾或后一行的开头是多字节字符,则不插入空格,非常适合中文

上面提到的注释,可以是C/C++中的"//"和"/*",也可以是邮件中引用原文使用的">"等字符(具体由comments选项控制;参 见":help 'comments'")。Vim在遇到这些字符时,能够相当智能地进行处理,足以完成日常编辑源代码和邮件的需要。在使用一些处理纯文本不够强大的邮件 客户端时,我通常使用Vim编辑邮件(特别是英文邮件),然后把结果贴回到邮件编辑窗口中进行发送。

Vim中formatoptions的缺省值是"tcq",一般我会在.vimrc文件中加入一行"set formatoptions+=mM"来确保Vim能在中文字符之间折行而不要求空格的存在,并且在大部分情况下可以正确地处理中文重新格式化。

其它小技巧

也许你会觉得这些很有用:

  • %(跳转到与之匹配的括号处)
  • .(重复上次的修改命令)
  • `.(跳转到最近修改过的位置)
  • ZQ(无条件退出)
  • ZZ(存盘退出)
  • ga(显示光标下的字符在当前使用的encoding下的内码)
  • guw(光标下的单词变为小写)
  • gUw(光标下的单词变为大写)
  • :TOhtml(根据Vim的语法加亮的方式生成HTML代码;在图形界面中也可以使用菜单"Syntax-Convert to HTML"达到同样效果)

无聊的时候,还可以试试(呵呵!):

  • :help!
  • :help 42
  • :help holy-grail