碎碎念

2006, April 27

寄存器::Vim进阶索引[4]

Filed under: vim




Up: (dir)

Vim进阶索引[4]::寄存器

以前经常要安装剪贴板的软件来支持多次的剪切粘贴操作。现在这些步骤可以省下了,Vim的寄存器就可以当成多个剪贴板来使用。但是寄存器可不这么简单……

寄存器是Vim用来储存文本的临时空间。当我们使用y或d指令时被复制或删除的文本会被送到寄存器,而我们可以通过p指令插入刚删除或复制的内容。寄存器在这里的作用就跟Window的剪贴板相似,但Vim的寄存器要更多,作用也更多。

在Vim中不同寄存器有固定的名称。我们可以通过这个名称访问它们的。寄存器的名称由单个字符组成——只有一个例外,大部分的寄存器名称是单个数字或字母有几个特殊的寄存器以其他字符为名。使用时需要在寄存器名称前加上"号以区别于一般命令以及标记(mark)。

:help registers
:help :registers
:help :copy-move
:help c_Ctrl-R
:help s\=





Next: ,
Up: Top

1 数字与字母寄存器





Next: ,
Up: Top

1.1 数字寄存器

有些寄存器是有特殊作用的如数字寄存器。在介绍数字寄存器前先看一个命令:reg。现在输入这个命令:reg。有没有看到许多"号开始的数字或字符呢,这些就是寄存器。这里面有你以前删除的文本和最近复制的文本。

寄存器"0到"9就叫做数字寄存器。寄存器"0存着上一次复制操作所复制的文本。寄存器"1到"9分别保存着你最近删除的文本。"1的内容总是你上一次删除的内容。每删除一次这些寄存器的内容就往下传递。刚删除的文本到了"1,而原来的寄存器"1的内容到了"2,原来"2的内容到了"3,……,原来"9的内容则被丢弃。数字寄存器只保留最近9条删条的文本和一条复制的文本。

现在做一下下面的实验,在Vim中分四行输入123:

1
2
3

使用两次dd指令,依次将1、2两行删除。输入:reg观察寄存器"1、"2。可以看到:

"1   2
"2   1

再使用dd指令将第三行删除。再输入:reg看看结果……现在你应该知道数字寄存器的工作方式了吧。

而我们经常使用的p指令,就是将最近一次删除或复制的文本添加到当前位置。如果最近一次操作是复制则p就添加"0的内容,如果最近一次操作是删除就添加"1的内容。





Next: ,
Previous: 数字寄存器,
Up: Top

1.2 字母寄存器

现在看一下今天要讲的第二种寄存器:字母寄存器(named register)。字母寄存器的名称是单个英文字母。可以用这种方式表示一个字母寄存器:"a,"b,…,"z。同一个字母的大写形式与小写形式表示的是同一个寄存器,但它们在“行为”会有所不同,这点稍后说明。字母寄存器只有在用户指定时才被使用。

一般模式(normal mode)下要访问寄存器只要在使用复制和删除指令y和d时,在前面加上寄存器的名称即可。比如要将当前行及随后两行(1+2=3)复制到寄存器c中:"c3yy。要将刚保存到寄存器c的内容“粘贴”出来:"cp。数字寄存器也是一样的使用方式,要将数字寄存器3的内容粘贴出来:"3p。

提示:可是怎么知道那个寄存器有自己想要的内容呢?使用:reg或:display。在命令后加上寄存器名称则显示相应寄存器的内容。

字母寄存器的名称大写时有特殊用途。当我们使用大写的寄存器进行复制或删除文本时,寄存器原来的内容会被保留,刚删除或复制的内容则附加到原来内容的后面。如:`"Cdd‘时当前删除行会添加到寄存器c原有内容的后面。大小写的寄存器仅在复制和删除时有区别。而当使用p时,大小写寄存器名的作用是一样的。

说明:在一般模式下Vim的删除和复制命令相当灵活。有各种使用方式,下面是一些删除命令的例子——将d改为y就是复制的例子了。如

3dd
删除3行
d2l
向右删除2个字符
v2]d
删除两个段落
df。
从当前位置删除到句号
d`c
删除至标记(:help :mark)c

除些之外x命令和p命令也有各种形式。所有这些命令及其不同形式都可以与寄存器一起使用,方法是在d、y、x、p前面加上寄存器。篇幅所限我们不可能将与寄存器一起使用的删除复制命令一一列举。虽然我们举的例子者是yy或dd这样删除的最简单形式,但无特别说明在与寄存器一起使用时yy和dd可以是任何删除、复制或是粘贴形式,以下同。详细的删除复制及粘贴命令可以见:help deleting :help copy-move

2 其他寄存器

除了上面的两种寄存器外,Vim还有很多种寄存器。这是Vim的文档中对寄存器的分类:

""
无名寄存器。保存最近一次复制或删除的文本。就是p命令默认使用的寄存器。

"-
短删除寄存器(The small delete register)。事实上刚删除的文本并不一定被送到数字寄存器,如果删除的文本不含换行符(不足一整句)则文本被送至这个寄存器。如x、d2h这两条命令删除的文本都会被送到这个寄存器。注意下在这条命令虽然删除了一整行的文本但因不含换行符所以也被送到这个寄存器`0d$‘。

": ". "% "#
只读寄存器。它们分别用来保存最近一次在命令行窗口使用的命令、最近一次插入的文本、当前编辑的文件名、当前的替代文件名。

"=
表达式寄存器。

"* "+ "~
选择与拖放的寄存器。在Windows中这几个寄存器就是剪贴板。在Linux中它们也是剪贴板——但这几个寄存器是有所区别的。

"_
黑洞寄存器
删除操作会影响现有数字寄存器的内容。前一个数字寄存器的值传给后一个数字寄存器,"9的内容被丢弃,新删除的文本则放入"1。这至少有两个直接的影响,一是"9的内容被丢弃;二是寄存器中文本的位置都发生了变化。而复制操作会改变"0的值。如果你不希望删除或复制的操作影响数字寄存器的话就使用这个寄存器。使用这个寄存器进行删除或复制的内容都会被丢弃——这还可以提高一点速度节省一点空间。

"/
搜索式样寄存器。保存上一次搜索所使用的式样。注意这也包括了s命令中所使用的搜索式样。





Next: ,
Previous: 其他寄存器,
Up: Top

3 寄存器相关命令

前面已经说了一般模式下下的各种x、d、y、p命令都可以与寄存器一起使用,如"ayy。现在看一下在命令窗口中(或Ex模式)下怎么访问寄存器。
命令行中复制、删除和粘贴分别是`:y‘、`:d‘、`:pu‘。寄存器的使用方式是直接在上述命令后面加上寄存器的名称——不需要在寄存器前加入"号。如:

:2,4y a
将第2至4行的文本复制到寄存器a中。
:'<,'>d A
将选中的行删除并将其内容附加到寄存器a中。
:pu! a
将寄存器a的内容粘贴到当前行之前。

寄存器也是一种变量可以在表达式中使用,因而也经常配合:exec构造复杂的命令。在替换命令:s的替换式样部分可以使用表达式寄存器(:h sub-replace-expression)。一般模式命令q(:help q)可以用来录制宏,而所录的击键序列就保存在寄存器中。此外:redir命令也可以与寄存器一起使用。具体见Vim文档(:help :redir)。





Next: ,
Previous: 寄存器相关命令,
Up: Top

4 寄存器的特殊性质

我们已经知道数字寄存器可以保留复制和删除的内容供用户使用。但这个功能实际用途已大不如前,因为Vim支持无限的undo操作。而且由于引入了Vim脚本后可以使用变量来代替寄存器的作用。但寄存器并非毫无存在价值。

4.1 是临时的存储空间

这正是寄存器出现的目的。有时候我们需要一些临时存储空间我们就可以使用寄存器而不需要新建一个临时文件。比如写作时你也许会发现有一整段的文字也许应该删除或放到其他位置。这时你可以把它放到寄存器中。然后在需要时再把它贴出来——没错就象Windows的剪贴板。但更好用,因为你有26个字母寄存器可以使用;可以使用大写字母将文本附加到已有内容后。如果在你关闭文件之前还没想到这将这些内容贴在哪里也没关系用`:wviminfo my_viminfo‘命令。下一次编辑时输入`:rviminfo! my_viminfo‘或者在命令行用这个命令运行`gvim -i my_viminfo myfile‘,:reg看寄存器的内容是不是都还在呢1

4.2 寄存器也是变量

在上一篇“脚本”中我们说过了是个变量——特殊的变量,只要在前面加上一个@号就可以用变量的方式访问寄存器。
所以,变量的操作也同样适用于寄存器。

" 给寄存器赋值
let @e="开始\<CR>"
let @E="结束"
echo @e
开始
结束
" 将寄存器作为表达式的一部分
let my_var=@a . @c
" 和
echo @e+4
" 清空寄存器。
" 注意:不能用unlet清除寄存器。
:let @e=""

4.3 在编辑窗口与命令窗口间交换内容

编辑窗口的文本可以放进寄存器。搜索式样和上一条Ex命令被放进了只读寄存器"/和":。
已知寄存器的内容可以在贴到编辑窗口。可以在命令窗口作为变量使用。那有没有办法在命令窗口插入寄存器的内容呢?有没有办法在搜索式样中插入寄存器的内容呢?

比如,假设在寄存器e中保存着一个文件名:“这是一个保存在寄存器中的很长的文件名.txt”。而我想使用:w命令保存一个当前编辑文件的副本——使用寄存器e中的那个文件名。如果使用`:w @e‘的话,文件名将是“@e”而不是“这是一个保存在寄存器中的很长的文件名.txt”。这时该怎么办呢?考虑到寄存器也是变量,我们可以使用寄存器的传统办法。

" 方法一。使用:execute命令
" 写入以"e为名的寄存器中
:exe "w " . @e

那搜索呢?如果我们要在搜索式样中使用寄存器的内容呢?对于s命令的搜索式样上面的:exe大法仍然适用,但如果只是普通的搜索操作(在一般模式中按/)呢?我们要用到组合键Ctrl-R,用Vim的写法就是<C-R>。

" 方法二。使用Ctrl-R转义。
" 搜索寄存器e的内容。<Ctrl-R>表示用户在这里按了组合键Ctrl-R——不要直接输入<Ctrl-R>这8个字符。
/<Ctrl-R>e/

使用<C-R>的方式可适用于各种输入的环境中:在插入模式输入时、在命令窗口输入时、在搜索时。在插入模式时要输入寄存器内容并不需要退回到一般模式再使用p指令,可以直接按`<Ctrl-R>e‘当然e可以改成相应的寄存器名。在命令窗口与搜索时也是一样:按Ctrl-R输入寄存器名。

提示:除了一些不接受变量作为参数,不能使用寄存器名称的情况外,还有一些情况也要求插入寄存器的内容。有时我们插入寄存器的内容而不使用寄存器变量是因为我们可能还需要手工对寄存器的内容进行一些编辑。

无名寄存器总是保存着最近一次复制或删除的内容。不带寄存器名地使用p就可以添加该寄存器的内容到当前位置了。但是既然“无名”该怎么在命令窗口使用这个存器呢?又怎么插入无名寄存器的内容呢?答案是使用@",插入也是一样按Ctrl-R再按输入"就可以了。

现在总结一下:":保存了上一条Ex命令。"/保存了上一条搜索式样。字母寄存器及数字寄存器中可以保存编辑的文本。并且我们也可以在不同的环境中插入寄存器的内容。通过寄存器我们可以方便地在命令窗口编辑窗口以及搜索中交换内容。相对而言一般的变量就没这么方便,你只能在命令行中使用变量也只能是命令行中给变量赋值。

4.4 在buffer之间及程序之间交换内容

寄存器是全局的变量。在Vim中打开的所有文件2,共享这些寄存器。你可以在不同的文件之间交换内容。

通过寄存器"*和"+,Vim可以与其他程序交换信息。在Windows中这两个寄存器是一样的。在Linux中这两个寄存器则有所不同。

:help gui-selections
:help x11-selection

4.5 寄存器可以做为宏

跟一般的变量相比寄存器还有一个最大的特点就是寄存器本身可以做为宏使用。如果你有用过一般模式命令q的话就会发现q录制的击键序列就是存在寄存器中的,并且可以直接使用寄存器执行命令。现在做做实验,新建一文档随便输入几行文字。输入:

qeggddq

上面这条命令录制了一个宏并保存到寄存器e中。这个宏的作用是回到第一行并删除该行。现在看一下寄存器的内容:

:reg e

就是你刚才的键盘命令ggdd。要运行刚录制的键盘操作在一般模式输入@e就可以运行了,输入3@e会将前三行删除。

当然你不一定要用q来录制宏——因为寄存器也是变量。

:let @e="/删除本行/^Mdd:w^M"
@e

上面的^M表示的是回车键。可不是输入^再输入M,而是输入Ctrl-V(Windows是Ctrl-Q)再按回车键这时就会出现^M表示这是一个回车键。常见的还有^[表示的是<ESC>键。输入的方法也是一样按Ctrl-V再按Esc键。这样输入控制字符的方式是传统的Vi方法。在Vim中也支持用按键名表示这些控制字符。比如<CR>表示回车键3所以上面的命令也可表示为:

:let @e="/删除本行/\<CR>dd:w\<CR>"

这里一定要用双引号,我们在“脚本”一篇中已经讲到了,在单引号中的字串会被当成普通字串。后面这种表示控制字符的方式与'cpoptions'的设置有关,虽然在默认情况下都是可行的但是建议使用第一种方式。不过为了更好的可读性在教程中我们还是可能使用后面这种方式表示控制字符。

正因为寄存器可以直接执行所以":可以用来执行上一条在命令窗口使用的命令:

:@:

记得最后要按回车执行。当然现在由于命令行的历史功能这种用法没有什么实用价值。

关于宏的更多用法我们将另外解说。

4.6 在重定向命令中使用

重定向命令(:redir)是一个较常用的技巧。所有的字母寄存器、@*、无名寄存器(@")都可以在重定向命令中使用。还是用个例子说明好了:

假设你的小说家朋友寄了一本小说的初稿给你,但显然他没有整理文本的习惯——好消息是他这次竟然没用Word写。在你往下看之前你决定先将文档做适当的整理。使用Vim作这种事当然是小菜一碟,只用了10分钟你就将他的小说整理成一份格式整齐的文档了。

第六章 为山九仞
===============
	
  小明是从不在午时之前离开被褥的,今天却是个例外。他一夜没睡不
  过他却觉得精神比任何时候都好……
	
  < 省略800行 >
  ……
	
第七章 功亏一篑
===============
	
  小明已经很久没像今天这样开心了。从那时到现在已有二十年又一天
  了。对他来讲二十年并不长,能在二十年又三天之内报仇已经是出乎
  自己意料了。何况对方是可是威峦镖局的大当家。想到这里他的眼睛
  眯得更小了……再过两天……只要两天!
	

但你发现这份初稿没目录,而你看小说的习惯是从目录看起。于是你决定整理一份目录。于是你用了寄存器:

:let @a=""
:g/^第.\{1,3}章 /y A

这两条命令将所有章的标题放到寄存器A中。你可以在需要目录的地方"ap。不过你还想在每章标题后加上该章对应的行号,你知道这时可以用:redir:

:redir @a
:echo "目录:"
:g/^第.\{1,3}章 /echo getline(".") . "\t\t\t" . line(".")
:redir END

现在你的寄存器a中有了一个带行号的目录了。只用了几行命令你就漂亮地完成任务了,想到这这里你的眼睛眯得更小了……

注:这里用到的函数是我们在讲折叠时说过的getline("."),表示返回当前行。line(".")则返回当前的行号。这两个函数的详细用法见文档。通过对这个脚本进行扩充我们甚至可以让它抓取含小节的目录。

上面的例子演示了通过:redir用户能对寄存器的内容进行进一步的加工而不只是简单的摘录,它增加了寄存器的使用范围。这正是与redir之所以成为寄存器重要性质之一的重要原因。在Vim7.0之前的版本中不支持重定向内容到变量,所以寄存器成了唯一选择。考虑到:redir是比较重要的命令,寄存器吃香也就不足为奇了。但在Vim7开始支持重定向内容到变量后,寄存器就没那么重要了——当然如果你希望方便地将重定向的内容插入到文件中的话寄存器仍是理想之选。关于:redir的更多内容,将会另外解说。

4.7 表达式寄存器

虽说是寄存器但从各种角度来看这都是个冒牌的寄存器。它的主要作用是实时计算表达式的值。适用的场合:在编辑输入时、在命令窗口输入时、在搜索时。使用的方式是按Ctrl-R再按等号(<C-R>=),接着输入表达式,原来输入的位置就会插入表达式的值。只要是合法的表达式都可以使用。我们知道字串可以做为合法的表达式,所以在插入模式下按Ctrl-R =然后输入"abc"(注意包括")当前位置就插入了abc。当然我们不会为了输入字串而使用这个寄存器。现在寄存器a中保存着一个数字,你想在当前文档中搜索该数字4倍的另一个数,你当然不想自己计算。这时使用表达式寄存器:/<Ctrl-R>=@a*4<Enter>/<Enter> 。其中<Ctrl-R>不是让你输入这8个字符而是按组合键Ctrl-R,同理<Enter>表示这里按了回车。任何时候当你需要插入一个表达示的值时都可以使用这个寄存器。

如果在输入=号后直接按回车没有输入表达式的话默认使用上一次使用的表达式。

在上一个例子中,如果你把刚才的目录贴在文件的开头(当然是开头),会发现行号不准了因为所有的内容都被往下移了——第一行现在变成在目录后面了。假设增加的目录有25行(不知道有几行?:se nu),现在文章的第一行(是空行)成了第26行。当然这样问题难不倒你,让表达式寄存器重新计算一下行号就行了——将原来的行号加上25。

注:下面几个控制字符的输入方式:^I, ^R, ^M, 分别表示的是Tab键,Ctrl-R,回车。它们的输入方式是按Ctrl-V(或Ctrl-Q)再输入各自所表示的键。

:1,25norm $T^I"ty$:s/[0-9]\+$/^R=@t+25^M/^M

:1,25norm                                                 在1到25行之间(目录区)执行一般模式命令
          $T^I                                             移到行末,将光标定位到最后一个制表符后(也就是第一个数字的位置)
              "ty$                                         将数字复制到寄存器t中
                  :s/[0-9]\+$/                             将行末的数字(每一章的行号)
                              ^R=                          插入表达式
                                  @t+25                    将寄存器t中的数字加上25
                                     ^M                    插入回车结束表达式
                                         /^M               结束s命令并在插入回车键
                                            <Enter>        在全部输入完成后别忘了按回车执行命令

还有一个特殊的地方可以用上表达式寄存器(Vim文档没说这是一个表达式寄存器,但它的使用方式与表达式寄存器完全一样),就是:s命令。:s命令的命令格式为::s/lhs/rhs/,表示搜索lhs并替换为rhs。一个特殊用法就是当rhs的开头为`\=‘时,这rhs将被视为表达式。lhs将被替换为表达式的值。

:" 例:将当前行中的算术式'42x31'替换为算术式的结果。
:s/42x31/\=42*31/

再回到刚才的例子,中现在我们可以用一种相对优雅的方式计算更新该目录的行号:

:1,25s/[0-9]\+$/\=submatch(0)+25/

注:submatch()只在:s命令rhs的表达式中使用。submatch(0)与原来的&在rhs的作用是一样的。submatch(1)就相当于原来rhs中的\1,依此类推。





Previous: 寄存器的特殊性质,
Up: Top

5 小结

当我们在进行交互编辑时,寄存器可以提高效率。不过正因为它的这样交互特性,它并不经常在脚本中使用。因为我们完全可以在脚本中使用变量来替代寄存器。它的优点是能方便地与buffer的内容互动。当我们想把buffer的内容赋予某个变量时比较麻烦因为没有直接的一般模式命令可以做到这时寄存器则更方便点。




Footnotes

[1] 通常寄存器的内容会保留到下一次打开Vim,但不一定会保留到下一次打开同一文档所以需要人为的保存
[2] 准确地说是buffer,这与文件的概念并不完全一样
[3] :help key-notation

2006, April 22

脚本::Vim进阶索引[3]

Filed under: vim




Up: (dir)

Vim进阶索引[3]::脚本

在这一系列的进阶教程中我们会不时地遇到Vim脚本的内容,也许是配置文件也许是在“宏”中使用的自定义函数。虽然我假设Vim用户会随着这系列的教程,慢慢地掌握脚本的知识。但实际上很多人在学习Vim的过程中“本能地”避开Vim脚本语言的内容;习惯性地认为脚本语言是洪水猛兽。但是却没意识到他们在学习的Vim命令就是脚本语言的主要部分,并且这一部分已经足以让他们写出功能完整的脚本了。本文的目的不在于让用户看完后就掌握了Vim脚本语言,而在于引导读者对Vim脚本语言有个认识:知道怎么使用已有的知识来写脚本;知道怎么运行脚本文件;知道Vim脚本语言中还有哪些主要的概念。最理想的情况下读者从今天开始能随着这系列教程的深入自然而然地掌握Vim的脚本语言。本中提供了相关的帮助索引,用户可以用这些帮助命令找到更完整的信息。

对了这一系列教程使用的Vim版本是6.3。

实际上本文假定读者有一定的Vim基础,当然也包括掌握了一部分的命令行命令。为了能更好地掌握Vim的进阶技巧我们这篇中将涉及Vim的脚本。

:help usr_41.txt
:help eval.txt





Next: ,
Up: Top

1 什么是脚本?脚本的作用是什么?

狭义的脚本指含一条或多条编辑命令的独立(脚本文件)文件。通过在编辑器中调用脚本执行在脚本中定义的一条或多条编辑命令。如无明确说明,本文的脚本指的是可在脚本中运行的一条或多条命令而不是指文件本身。当我说“写脚本”时,我指的是写出以某一特定编辑任务为目的而组织起来的一条或多条命令1

既然你掌握了一些命令你当然会写脚本。我们一起来写一个脚本吧。将下面的命令放入一文件中,假设文件名为myscript.vim2

" 在文件的最后一行加入签名
:$a
月下独酌
========================
暂伴月将影,行乐须及春。
我歌月徘徊,我舞影零乱。
醒时同交欢,醉后各分散。
永结无情游,相期邈云汉。
========================
.

这个脚本的作用是在文件的最后一行加入签名档。现在用Vim随便打开一文件,输入

:so myscript.vim。

完成。

可以看到上面的“脚本”实际上只是一行注释和一条命令行命令,注释在这里并不是必需的3,所以只要你会命令就可以写脚本。在脚本中以`"‘开始的行都是注释。当然脚本很少会只有一条命令,我们看一下脚本还能为我们做什么?现在我们还想为我们的脚本加入日期怎么办?在脚本的最后一行加入`:$r !date4

通常我们使用脚本是为了减少重复的劳动,如果前面例子中的签名你只用一次,你当然不会想为此写脚本文件。但这并不是绝对的。事实上,很难具体地说明脚本有什么作用,因为每个人都有自己独特的需要,脚本对每个人有不同的作用。总的来说:脚本赋予了一些人满足自己需要的能力





Next: ,
Previous: 什么是脚本,
Up: Top

2 两种类型的脚本

Vim中脚本有两种基本的类型即Ex脚本和一般模式(normal mode)脚本。

2.1 Ex脚本

在Ex脚本中所有的命令都被默认为Ex命令,而命令前面的:是可有可无的。我们开头的例子,就是一Ex脚本。如果在Ex脚本中使用一般模式命令时,需要使用:normal命令。这是Vi的传统脚本格式。





Next: ,
Previous: Ex脚本,
Up: 两种类型的脚本

2.2 一般模式脚本。

与Ex脚本相反一般模式脚本。Vim在一般模式中运行这种脚本,所以脚本中的命令就如同你在一般模式中输入指令一样。Ex命令需要在前面加上:。下面是一个例子:

3G02f,lct,家庭地址^[:w

这条命令/脚本将文件中第三行的第二及第三个,之间的内容更改为“家庭地址”。分解一下就是:

3G                      跳到第三行
  0                     跳到本行开始处
   2f,                  找到第二个,
      l                 右移一位
       ct,              修改第二个,和第三个,之间的内容
          家庭地址       改为“家庭地址”
                  ^[    按<ESC>回到一般模式。这个符号是通过输入
                        指令序列<Ctrl-V><ESC>产生的,
                        Windows是<Ctrl-Q><ESC>。表示这里按了<ESC>键。
                    :w  保存修改

这种脚本紧凑但不易读,也不适合写复杂的脚本,一般只在5中使用。





Next: ,
Previous: 两种类型的脚本,
Up: Top

3 运行脚本

所以在继续之前我们先学会如何使用脚本。之所以在本篇的开始处就写如何运行脚本是因为:

  • 你会一些命令所以你已经具备写脚本的能力了
  • 运行脚本大多数情况下要比写脚本容易
  • 这样你才能边根据你对Vim脚本语言掌握的程序使用脚本,而不用学完全部脚本知识后才知道怎么运行脚本。

记住,你并不需要一次掌握所有的脚本知识,而是需要多少学多少,学会多少用多少。

实际上我们一开始的时候就运行过开始的示例脚本了。这里总结一下在Vim中运行脚本的几种方式。





Next: ,
Up: 运行脚本

3.1 Vim环境

先看一下下面四个命令:

:source
这是最传统的运行方式。读入Ex脚本文件并运行。我们一开始的例子就是一Ex脚本文件。一次只能运行一个文件。
:source!
读入Vim的一般模式命令。注意这个!号并不表示强制运行脚本,而是读入不同的脚本。
:runtime
从'runtimepath'设置的路径中读取脚本文件。可使用通配符或多个文件名参数,但只有第一个符合的文件被运行。
:runtime!
跟上一命令一样但运行所有的符合文件。慎用!

一些例子:

:so! c:/normal_mode_script.vim
[Windows环境]Vim中路径可以使用/或\。运行一般模式脚本。
:so ../vscript/samp1.vim
[windows或Unix环境]运行Ex模式脚本。
:ru samp[0-9].vim
[Unix环境]在runtimepath中搜索以形如samp0.vim、samp1.vim、……、samp9.vim的脚本并运行找到的第一个脚本文件。





Next: ,
Previous: Vim环境,
Up: 运行脚本

3.2 命令行

Vim的脚本也可以在命令行中使用,用户不需要经过运行Vim、打开文件、运行脚本这三个步骤,相反的同户同要在命令行上给出合适的参数Vim就可以自动处理。在命令行运行Vim的脚本可通过以下几种方式:

参数 作用 示例
-s 读入一般模式脚本 vim -s 脚本文件名 文件
-e -s
-E -s
使用vim的ex命令 vim -e -s <脚本文件名 文件
vim -es <脚本文件名 文件
vim -E -s <脚本文件名 文件

现在我们可以在命令行运行本文开头的脚本了,但在如果你现在运行的话可能会发现什么也没发生。这是因为脚本中没有保存的命令,我们前还需要对脚本进行一些修改——加入命令`wq!6。这是修改后的脚本文件(前面的:号是多余的已经去掉了):

" 在文件的最后一行加入签名
$a
月下独酌
========================
暂伴月将影,行乐须及春。
我歌月徘徊,我舞影零乱。
醒时同交欢,醉后各分散。
永结无情游,相期邈云汉。
========================
.
$r !date
wq!

这个简单的脚本完成的任务就是在文件的末尾加入签名和日期。现在在命令行运行试试:

$ vim -es <myscript.vim 文件.txt

通过在命令行运行脚本我们还可以批量修改文件:

for a in `ls *.txt`
do
    vim -es <myscript.vim $a
done

如果脚本文件使用MS-Dos的'fileformat'可能会出现不理想的结果,最好将'fileformat'设为Unix。当使用`-e -s‘和`-E -s‘在Windows会失去响应7,不过`-s‘不会。

一般模式脚本除了自已写以外,Vim提供了两个命令行参数来“录制”命令8

参数 作用 示例
-w 将操作录制起来,如果脚本文件已存在则覆盖 vim -w script.vim 文件
-W 同上但是如果脚本文件已存在则添加到末尾 vim -W script.vim 文件

现在跟我做以下实验,先找一文本文档如 f1.txt 复制为f2.txt。使用以下命令运行Vim:
gvim -w mysc.vim f1.txt
然后,进行几个简单的编辑操作并保存退出。现在f1.txt是编辑过的而f2.txt是未经编辑的。用以下命令运行vim:
gvim -s mysc.vim f2.txt
再打开f2.txt看看是不是与f1.txt进行了一样的操作了呢。现在再打mysc.vim看看,可以看到刚才的所以键盘操作都被“录”下来了。你也可以对脚本文件mysc.vim进行进一步的修改以适应实际需要。

好了到目前为止你已经完全可以使用脚本了:你已经掌握了一些命令,你也知道如何运行脚本了。当你掌握的编辑命令越来越多时,你写出来的脚本也就越强大。如果你还想学更多脚本的技巧时那就往下看吧。





Next: ,
Previous: 运行脚本,
Up: Top

4 脚本进阶

学习一门编程脚本语言最有效的方法就是动手进行实验。下面的内容中有相当多简单的例子,建议读者跟着例子做。当然你不用保存为脚本文件再运行,一般直接在Vim中输入命令就行了。如果有多条命令可以进入Ex模式运行,方法是按在一般模式中按Q——如果接Q不行的话那就按gQ,输入visual退出Ex模式。

Vim5.0以前的版本与Vi(ex)的脚本是一到多条编辑命令的集合,脚本的功能就是依序执行脚本中的各种编辑命令。那时的脚本没有变量也没流程控制等程序语言所具有的要素。

对于重复或者复杂的编辑工作Vi通常使用脚本、宏和外部工具来完成。而且一般也认为通过使用宏及外部工具对于大部分的编辑任务而言就以足够了,然而无论是宏还是脚本都只是一系列的编辑指令组成,它们无法与Vi交互并且缺乏足够的灵活性。

从Vim5开始,Vim引入了内建的“脚本语言”,其实就是加入了变量、表达式及流程控制的命令。同时Vim提供了许多的内置函数和内置变量,这些函数和变量我们在后面的教程中会慢慢接触到。通过这些编程特性用户可以写出更灵活更强大的脚本9。现在来看一下都有哪些内容。





Next: ,
Up: 脚本进阶

4.1 变量

:h variables
:h internal-variables

Vim的变量有两种类型,一是字串类型,一是数字类型。这没什么好解释的了,需要注意的是在我们为变量赋字串类型值时用单引号和双引号表示的字串值是不一样。在双引号字串中是可以使用转义字符\的,而单引号字串中不进行任何的转义。下面是两条命令及他们的运行结果:

:echo "|\t|"
|    |
:echo '|\t|'
|\t|

变量名。Vim中合法的变量名由下划线_、大小字母和数字构成,但变量名不能由数字开头。

赋值语句

:help 41.2
:help :let
:help :unlet

Vim中为变量赋值需要使用:let命令,要释放变量则使用:unlet命令。这是一些例子:

let a=1
let b=3+1
let c='你好'
let d="blahblah"
unlet c

变量前缀
在Vim在可使用不同的变量前缀10,来区别不同变量的作用范围。

g:全局变量
s:脚本文件内部的变量
l:只在函数内部使用的变量
a:变数变量
v:预定义变量

完整列表见`:help internal-variables‘。
预定义变量是Vim为用户提供的变量,这些变量包括了各种有用的信息,如语言设置、Vim版本、当前行号……等等。这里有祥细列表`:help v:var

可访问的特殊变量

&设置项
@寄存器
$环境变量

看一下一下下面的例子:

:let a="abc"
定义一个全局变量a
:let g:a="abc"
同上
:let l:a=a:marg
自定义函数的局部变量。将参数marg的值传给变量a。因为只在自定义函数内部使用所以l:可以省略。
:echo v:lang
显示预定义变量lang的值。预定义变量是只读的。
:echo &tabstop
显示设置项'tabstop'(制表符宽度)的值。
:echo $home
显示环境变量$home。
:echo @3
显示数字寄存器3的值
:let &l:tabstop=4
将'tabstop'设为本地变量,并赋值4。见`:help let-star‘与`:help :setlocal





Next: ,
Previous: 变量,
Up: 脚本进阶

4.2 表达式

:help expression-syntax
:help eval

表达式是比较抽象的概念,在Vim中所有可以表示字串/数值或返回字串/数值的变量名11、函数、算术运算式、逻辑运算式都是表达式——当然还有字串/数字本身也是。而它们所表达的字串/数字就是表达式的值。下面这些都是表达式:

表达式 类型
"123木头人" 字串 "123木头人"
123 数值 123
mode() 函数 表示当前模式的字串
abc 变量 变量的值。如果变量未赋值则表达式错误。
"123"==123 逻辑表达式 1。这里发生了类型转换,见:help expr6
"123"=="321" 逻辑表达式 0
"123木头人"+3 算术表达式 126。这里发生了类型转换
32 / 2 算术表达式 16
name . '你好' 字串 假设name的值为'小明',则值为'小明你好'

怎样判断一个表达式是不是合法的呢?一个技巧是使用:echo命令。:echo后接表达式会显示表达式的值当表达式不合法是则显示错误信息。
Vim支持的算术运算及逻辑运算与C语言相似。要注意的是Vim不支持浮点运算,算术运算结果的小数点将被省略。所以像7/2的结果会是3而不是3.5。Vim多了一些字串比较的运算符。其中最重要的是正则匹配的运算符:

=~
!~

另外在这些运算符后加上#表示区分大小写,而?表示不区分大小写。如:

==?    大小写不敏感
==#    大小写敏感

在没有#或?时则,根据'ignorecase'设置项决定是否区分大小写。

详细说明请参阅Vim文档
:h expr4





Next: ,
Previous: 表达式,
Up: 脚本进阶

4.3 流程控制

Vim中的控制语句有两种形式分别是选择语句和循环语句。它们相应的命令是:if与:while,结束命令分别是:endif与:endwhile。
它们都根据条件表达式的值来决定是否执行选择体或循环体中的命令,条件表达式的值是数值——0表示条件不成立,非0数值表示条件成立12。其中条件表达式可以是任意合法的表达式,因为条件表达式的值是数值,所以如果表达式的值是字串时会进行自动类型转换,见:help expr6。

选择语句。这是选择语句的形式:

:if 条件表达式
  "Ex命令
:endif

例:

" 如果变量user_name的值为"小明"则显示“小明你好”否则不做动作。
:if user_name=='小明' | echo '小明你好' | endif

选择语句与循环语句都支持嵌套使用。除了嵌套外选择语句还有一种形式可以进行多分支的选择。下面是多分支选择语句的一种形式,表示的是当表达式为真时执行第一组命令否则转到第二组表达式,当第二组表达式为真时执行第二组命令,……当前面的表达式全为假时执行其他命令:

:if 条件表达式
  " 第一组命令
:elseif 第二组条件表达式
  " 第二组命令
:elseif 第三组条件表达式
  " 第三组命令
" ......
:else
  " 其他命令
:endif

上面的:elseif及:else命令都是可选的,可视情况添加。下面再看个例子:

:if user_name=='小明'
: echo '小明你好'
:elseif user_name=~'^李'
: echo '李先生/女士你好'
:else
: echo '你好'
:endif

循环语句
循环语句在形式上与选择语句相似,所不同的是当条件表达式的值为真时选择语句中的命令会被执行一次然后退出而循环语句会命令命令并回到循环的开始处重新计算表达式如果为真就再次执行……如此重复。

:while 条件表达式
 " 命令
:endwhile

假设有变量user_1、user_2、… 、user_10分别存了10个不同的用户名,我们可以用这种方式将10个用户名列出来13

:let num=1
:while num<=10
"   使用了{}模拟数组下标。:help curly-braces-names
:   echo user_{num}
:   let num=num+1
:endwhile

这是两个与:while相关的命令`:help :continue‘、`:help :break





Next: ,
Previous: 流程控制,
Up: 脚本进阶

4.4 函数

Vim中的函数可以分为两种一种是Vim提供的内置函数。一种是自定义函数。
内置函数可以认为是有特殊用法的命令。不同的是命令不能做为表达式而函数可以。Vim提供了相当多的内置函数,具体列表见:help function-list。内置函数一般提供了返回值可以在表达式的环境中使用如:

" 例一、函数line(".")的作用是返回当前行的行号。函数的结果(行号)将保存在变量line_num中。
:let line_num=line(".")
" 例二、echo接受的参数是字串表达式。而toupper()返回的正是大写字串。
:echo toupper("abc")
ABC

自定义函数是用户根据自己需要写的函数。形式如下:
:function! 函数名()
" 脚本
:return
" 脚本
:endfunction
函数名的命令规则与变量名一样,但只有大写字母开头的函数名可以从脚本外部访问。:function命令后的!号表示如果也存在同名函数则覆盖原函数——!号是可选的。:return语句不是必须的,如果没有:return语句则函数的返回值总为0。没有:return语句的函数一般不在表达式中使用,要调用这些函数的可以使用:call命令。

" 例,定义两个函数
:function! Sayhello( words )
:  return a:words
:endfunction
:function! Sayhello2( words )
: echo a:words
:endfunction

然后分别运行以下命令,观察结果看有什么不同。

:call Sayhello("abc")
:call Sayhello2("abc")
:echo Sayhello("abc")
:echo Sayhello2("abc")





Next: ,
Previous: 函数,
Up: 脚本进阶

4.5 零散内容

.操作符。字符串可以用.操作符连接。

	
" 例1,将'你    好'值给变量hello
let hello = '你' . "\t" . '好'
" 例2
:echo 'ab' . (3+4)
ab7
	
" 例3,不支持浮点运算+自动类型转换
:echo 123.4
1234
:echo 9/4.4
24
" 例4
:let name="小明"
:let addr="龙门客栈"
:echo "姓名:\t".name."\r地址:\t".addr
姓名:    小明
地址:    龙门客栈

[]操作符。虽然Vim不直接支持数组,但允许通过[]析取字串中的某个字母(Vim7已经加入了数组的支持)。这跟C语言也是一样的,例:

:echo 'ab'[ 0 ]
a
:echo 'ab'[1]
b
:echo 1234[2]
3
:let a="abcd" | echo a[0] . a[2]
ac
" 一个中文有两个字节因而
:echo '中文'[0].'中文'[1]
中

`|
连接多条命令这样就可以在同一行中执行多条命令——实际上我们前面已经多次用过这种技巧了。注意有些命令以|为参数因而不能使用|来分隔,祥细列表见`:help :bar

`:exec
以表达式的结果为命令运行。这条命令经常做为一些复杂技巧的一部分,建议用户详细了解这条命令。

let cmd='echo' | exec cmd . ' "abc"'
abc

至此Vim脚本的主要内容已经讲完了。这些内容目前还比较零散,我们会在以后的教程中将今天学的这些知识一点一点拼起来。





Previous: 脚本进阶,
Up: Top

5 小结

对没有编程基础的人来讲学习一门编程语言尤其是通用编程语言最难的有两点其一是使用流程控制语句和各种表达式。二是他们学到的东西不能在日常生活中运用,这让他们很容易就将辛苦学来的知识忘得一干二净。好在在Vim中你很容易就可以将所学的脚本语言知识用上——只要你会用命令就能用脚本。




Footnotes

[1] 事实上命令与脚本是一体两面,当你在编辑器作使用命令它就是命令,当你把命令以某种方式保存起来以便再次使用时它就成了脚本。
[2] 文件名也可以是中文,但也许用英文文件名会更符合一般人的阅读习惯。另外Vim脚本文件一般是以Vim为后缀名,但实际上可以使用任意后缀名
[3] 但写注释是个好习惯,至少可以练习打字。
[4] Windows的用户需要在date后面加入选项`/t‘。另外用Vim内置的strftime()也可以达到同样的目的,但!date显然是更好也更简便的方法——除非你需要跨平台运行
[5] Vi中的宏一般是指以map命令定义的指令
[6] `q‘对Vim来说不是必需的,不过多打一个字母也不费什么事
[7] Cygwin中可以正常运行
[8] 你可能已经想到了,这跟q命令的作用相近。不同的是使用这两个参数录制的命令将写入到文件中而不是寄存器中
[9] 一个副作用是用户可能会倾向于以Vim来完成所有任务,而不考虑是否有更好的或现在的方式。
[10] 就是命名空间,不过我想抽象的词还是少用点
[11] 必须是已经赋过值的变量名
[12] 其实就是逻辑值,0为假,非0为真。但Vim中逻辑值不是独立的数据类型
[13] Vim7之前不能使用数组,但是可以用模拟的方式。这就是一个模拟数组的方式。

2006, April 19

对《Unix编程艺术》的一点不满

Filed under: 五四三

对《Unix编程艺术》的一点不满

几天前在书店看到了Eric S. Raymond的《Unix编程艺术》1中文版,一直听说这书不错就买下了……

书这是一本Unix的宗教书籍——至少从书名和前15页来看是这样的,作为Unix的信徒将这样一本宗教书籍带回家就像基督徒的家中有一本圣经一样自然。令人失望的是整本书只有前30页还有点价值,其余的部分由自我崇拜+目空一切所组成。虽然买这书时就没想过要作者写得客观,因为这种书(或者所有关于Unix文化的书)当然会带有主观甚至是强烈的个人色彩。但看完书后有些话还是不吐不快……

通篇看下来看出作者要表达的可以归结为三点:

Unix哲学不朽
Unix哲学及在这一理念下的Unix体系中各个部件所体现出来的和谐一致性令人惊叹。关于Unix哲学的更多更深入阐述正是我希望在书中看到的。
信我者得永生
作者将自己的价值观作为任何判断的准绳,这从他的各种评论可见一斑。遗憾的是他的价值观在一些时候并不能代表Unix社团。当然也有可能作者只是在炫耀。
Emacs是最好的并且能与Unix哲学相容
……

关于上面的最后一点实在是不敢赞同。作者将Unix哲学概括为17条原则。其中,写其他原则时不容妥协的态度形成鲜明对比的是作者对吝啬原则隐约其辞(他还特地写了一篇“软件的适度原则”),看到后来才明白原来作者是Emacs用户。并且与前面偏向技术分析写作风格不同的是在写编辑器的章节时作者倾向于从哲学地角度来看问题。

虽然我们都知道在功能及操作取向上的差异这才是Vi与Emacs的最大不同——对了还有体积!然而作者试图将这些讨论带入哲学层面,然后在读者入坠五里云雾时带出结论。除了使用了更多更抽象的词汇写这一篇(译者应该也挺头痛的吧),他还做了一些比较来支撑他的观点:

  • Vi比ED加入了太多功能带来了复杂性,还导致了体积上升。所以更小功能更少的sam要胜出一筹。
  • Emacs比wily更大功能更多……(含糊带过)……。所以更多功能更“大”的Emacs胜出。

然后得到了Emacs(比Vi)更胜一筹的结论。这里面的逻辑令人费解。

作者明白光一味地贬低Vi还不足以说服人们停止对Emacs体形肥大功能庞杂的指责。他为此加入了一节“Emacs是个反Unix传统的论据吗?”为Emacs辨解2

  • Emacs出身于贝尔实验室而不是Unix所以可以公然地违反Unix的设计原则。
  • Emacs的功能很多,所以应该被享有被区别对待的特权。
  • 众多的Emacs扩展不是Emacs的错,用户可以选择不用。(注:就算没那些扩展Emacs还是很大。Emacs之所以为Emacs就是因为那些扩展。管理众多的扩展带来了附加的成本。)
  • 最后作者还试图暗示读者Emacs之所以如此臃肿还有更深层的原因3……

对于他怎么吹捧或者怎么为Emacs辩解我并不怎么在乎,但他对Vi的说法让人无法接受。他关于Vi的论述中只有一个专用陷阱还算站得住脚。他认为专用性陷阱存在的情况下Vi必须不断添加代码来获得新的功能,而大部分用户不得不为他们没用到的代码买单。

但Vi用户都知道的是Vi的增加的代码都是针对编辑需要而增加的,而不是加入对图像声音的支持4。而讽刺的是就算是功能最强大的Vim——在增加了无数代码后也只是Emacs的一小部分。在Vim的最新(2006年4月)PC版本7.0e在+Big编译选项下有1400KB5,而GNU Emacs(Gnu Emacs V21.3,2003年)有9000KB,完整的包有近20MB。更不用说Vim的1400KB已经包括了几乎所有功能,而Emacs在没有扩展的情况下虽然有9000KB却也比Wily好不到哪去。注意:该书中文版在P310最后一行是明显的误译:

中文:“Vim有1500KB而GNU Emacs是900KB”
原文:“1500KB for GNU Emacs versus 900KB for vim”
应为:“GNU Emacs有1500KB而Vim是900KB”

从另一方面看Emacs的Lisp代码在离开Emacs几乎没用。Emacs的扩展与Vi不断增加的c代码在本质上并无不同,只是一边是以二进制存在一边是以文本的方式存在。Emacs虽然可以选择不加载扩展但在Vi的体积也进入9MB之前我不认为这是一个多明显的优点。

最后,我们来看一下作者是怎么根据Emasc来扩展最简原则的定义的(中文版的P316):

最简原则暗示:选择需要管理的上下文环境,并且按照边界所允许的最小化方式构建程序。这就是“尽可能简单,而不过于简单”,……

最矫正这种趋势的方法直接来自于旧学派Unix的赞美诗集。这就是吝啬原则:只有实证了其它方法行不通时才写庞大程序…………如果两者都失败了,才可以自由地构建一个巨型程序(或一个新框架),而不会觉得已经完败于设计挑战。

这是原文:

This suggests a Rule of Minimality: Choose the shared context you want to manage, and build your programs as small as those boundaries will allow. This is “as simple as possible, but no simpler”, ……

The corrective to this tendency comes straight from the old-school Unix hymnbook. It is the Rule of Parsimony: Write a big program only when it is clear by demonstration that nothing else will do—that is, when attempts to partition the problem have been made and failed. ……Only if both approaches fail are you free (in the Unix tradition) to build a large program (or a new framework) without feeling you have failed the design challenge.

如果上面的中+英文还不够清楚地话那我解释一下好了,这些话的大意是:如果Emacs不符吝啬原则的话,那就赋于吝啬原则新的含义。这样Emacs就符合新的吝啬原则了。

这本书的其他部分就不做评论了6,大家可以在网上找找应该可以找到很多关于这本书的书评……




Footnotes

[1] 这本书使用CC授权,可以免费在线阅读地址是http://www.faqs.org/docs/artu/

[2] 这些都可以在书中的13章看到

[3] P315,“在我们对Emacs复杂度种类和动机的分析中,还有什么更意味深长的暗示?又有什么理由相信那些从教训归纳出来的东西?”

[4] Latest stable release: 21.4 (Feb 6, 2005) Emacs version 21 supports variable width and height fonts, playing sounds and the inclusion of images in a document, as well as tool bars, plus nicer menus and scroll bars.

[5] 1.4MB对于编辑软件来说的确是大了点

[6] 一次只做一件事并做好

2006, April 13

折叠::Vim进阶索引[2]

Filed under: vim




Up: (dir)

Vim进阶索引[2]::折叠

折叠对我来说原本不是什么必不可少的功能。但现在却越来越感觉到这实在是个体贴/方便的功能,我经常要保存一些资料但我不喜欢零散的一堆文本文件所以同样的某些特定类型的资料我将它们放在同一个文件中,通过折叠用我可以在同一个文件中管理管理多个文档而不至于混乱。与Vim的其他功能相比折叠是很容易掌握的(至少manual、indent和marker规则是这样的),如果你还没用上这项功能的话赶快往下看吧……

这一篇我们将要讲的是折叠。什么是折叠呢?复制一段文本到一新建文件中,在Vim中输入`:set foldmethod=manual‘。输入`:3,8fo‘,看第三行到第八行是不是折叠起来了呢?这就是Vim的折叠功能,很容易理解吧。先记住相关的帮助命令:

:help folding
:help foldmethod
:help fold-methods

在Vim中输入`:help folding‘,可以看到Vim中折叠的相关文档。往下翻页你可以看到这里面的内容非常多,尤其是以z开头的那一部分指令。好消息是要用好折叠并不要求全部掌握这些命令。当然至少要掌握这几个命令:zM zR zo zc zf折叠的用法的精髓在该页的第一部分`fold methods'。它决定Vim进行折叠所依据的规则。本文的目的是使用户对创建折叠有一个了解。并学会按自己想法来制定折叠规则。





Next: ,
Up: Top

1 折叠有什么用途?

如果你用过字处理软件Word的话那你可能会知道Word有一个大纲视图将“标题一”“标题二”作为大纲显示出来而相应的正文则呈现折叠状态。双击一个标题会展开对应的正文。通过这个视图用户可以对文档的结构有一个全局的了解,更可以有效地组织文本。折叠作用与大纲视图相近,同样让你可以有效的管理文档结构;方便地在文档的不同部分移动;让文档更清爽。此外折叠可以隐藏信息将不需要修改或不想看内容折叠起来。另外结合modeline、filetype或autocmd我们可以在更多的场合使用折叠

Note:请注意我们可以折叠任意行,所以折叠不等同于显示大纲。另外与Word大纲依靠手工套用样式生成不同,折叠可以用手工生成也可以是用户指定的规则生成。通过指定合适的规则我们使折叠成了一种脑力劳动。

折叠让自己的文档看上去更整洁。同时可以方便地选取感兴趣的内容或屏蔽(如果不能删除的话)不想要的内容。如果你的文档有10行的话你可以不想这做,但如果是成百上千行呢?

使用折叠通常要求用户对文档进行某种方式的格式化,这使用户的文档既使在其他文本编辑器上也有较好的可读性。

当然,没有人规定折叠该怎么用,所以发挥你的创意吧。





Next: ,
Previous: 折叠有什么用途,
Up: Top

2 折叠的生成

首先要了解foldmethod设置项,它指定了折叠产生的方式。详细的文档请参考这两个命令:

:help 'foldmethod'
:help fold-methods

其中的manual、indent、expr、syntax、diff、marker规则分别表示根据手工设置、缩进、表达式、语法、diff、标记的方式生成折叠。在了解Vim如何生成折叠之前我们要先了解一个概念:折叠层级

2.1 折叠层级

折叠层级是用数字表示的折叠标志,Vim根据折叠层级来决定是否折叠及怎样折叠某一行或某几行。当我们下折叠命令时(zM)Vim对每一行计算折叠层级,然后将折叠层级大于或等于`1‘(默认情况下)的行折叠起来。如果低折叠层级的几行中有高折叠层级的行时就形成折叠的嵌套。

那这个折叠层级是如何计算出来的呢?折叠层级的计算方式取决于我们设定的`foldmethod‘。下面是不同折叠规则对应的计算方式:

`manual
手工规则下,折叠层级由折叠区域的嵌套关系计算。当我们手工指定一个折叠的区域后,Vim对这个区域的开始行和结束行做记号,多个区域的开始行和结束行形成了嵌套关系。如果一个折叠区域不包含在其他区域之中,则其折叠层级为1;当这个区域直接包含于另一个区域时则其为折叠层级为另一个区域的层级加1;依些类推。
`indent
行的缩进宽度除以`shiftwidth‘,并向下取整得到每一行的折叠层级。同一折叠层级及更高折叠层级的连续行形成折叠。而其中的更高折叠层级的行——如果有的话,形成嵌套的折叠。
`marker
当使用标记规则折叠时,层级的计算跟手工规则相似。除了它是根据文件中的标记来划分一个折叠区域而不是手工指定。然后根据这些区域间的嵌套关系计算折叠层级。具体使用的标记通过`foldmarker‘设置。默认是使用'{{{,}}}'。
`syntax
跟`marker‘差不多,只是所用的标记是在语法文件中定义的,而不是通过`foldmarker‘设置。
`diff
除了差异行及其前后三行1外,其余行折叠(层级为1)。
`expr
由用户指定折叠层级的计算方式。方法是对`foldexpr‘进行设置。具体用法稍后说明。





Next: ,
Previous: 折叠层级,
Up: 折叠的生成

2.2 手工规则(manual)

首先设置设置foldmethod为manual或者marker(没错marker也行)。然后高亮选择可折叠的行,输入指令`zf‘。就这么简单你已经折叠了文本。还可以在命令行用:fold用法跟其他Vim指令一样,如`:1,.fo‘表示将第一行到当前行都折叠起来。需要注意的是如果使用marker折叠规则的话新建折叠时Vim会为指定范围的开始和结束行添加标记(marker)。

注意:如果只折叠一行的话,你可能不会立即看到效果。





Next: ,
Previous: 手工规则,
Up: 折叠的生成

2.3 缩进规则(indent)

首先,设置规则`:set foldmethod=indent‘。然后在编辑过程中依需要进行缩进2

缩进挺容易理解的,将下面的例子复制到文件中保存关闭再用Vim打开看看:

开始
    第一行         折叠层级为1,无嵌套
    第二行
        第三行     折叠层级为2,第1层嵌套
        第四行
            第五行 折叠层级为3,第2层嵌套
            第六行
        第七行     折叠层级为2,第1层嵌套
    第八行         折叠层级为1,无嵌套
            第九行 折叠层级为3,无嵌套
            第五行
结束
vim: shiftwidth=4:foldmethod=indent

注意:正如前面提到的折叠的层级并不仅仅取决于你按了几个空格或制表符还与`shiftwidth‘有关。改变上面例子中的`shiftwidth‘看有什么不一样。





Previous: 缩进规则

折叠的生成

2.4 标记规则(marker)

手工规则





Next: ,
Previous: 折叠的生成,
Up: Top

3 expr规则

expr要比前面的几种方式复杂点所以我们在这里单独讨论。使用expr时,我们需要对一设置项进行设置——`foldexpr‘。它是用来保存我们设定的表达式,也就是我们指定的计算折叠层级的公式。我们将设置项设为特定的表达式 -> 然后Vim通过`foldexpr‘这个项得到我们所指定的表达式 -> Vim逐行处理,并对每一行使用这个表达式计算出一个数值。这个数值就是该行的折叠层级 -> 根据层级进行折叠。举个例子:假设我们在Vim中使用了如下命令:

:set foldmethod=expr
:set foldexpr=1

所有的行都会被折叠(如果没有的话再输入指令`zM‘)。Vim在每一行用foldexpr指定的表达式计算结果。这个例子中我们的表达式是1,所以每行得到的折叠层级都是1,于是所有行折叠成一行。同理如果你将它赋于2的话则所有行的折叠层级为2,所有行被折叠成一行。当我们的表达式最后返回一个数值时这个数值就是折叠层级。为了让这表达式更灵活,更能满足我们的需要我们需要补足一些Vim脚本知识3,这里是三个在折叠的表达式比较常用几点:

`v:lnum
内置变量,表示是“当前行的行号”。:help v:var查看更多内置变量。
`getline()
函数用以返回指定行的内容。
`?:
三元条件语句。见:help expr1

另外如果需要构造复杂的表达式,我们可以在自定义的函数中定义。但这又是另一篇教程了。
看一下现在我们能构造出怎么样的表达式:

:set foldexpr=v:lnum

指定每行的行号为折叠层级,这样所有行会折叠成一行。试试看用指令`zo‘逐层打开折叠,打开20层之后就没有折叠了——而不是我们想的“嵌套数=行数”。这是因为Vim对折叠的嵌套数是有限制的,默认最深可以20层。但我们可以用foldnestmax这个项来进行自定义设置,如`set foldnestmax=10‘。
注意:Vim最多支持20层的嵌套,所以设置超过20的值会被当成20。

将第8至第20行折叠。也就是让expr在第8至20行时返回数值1(也可以是2、3、4……),其他时候为0。我们知道Vim是逐行处理的,所以如果我们知道Vim正在处理的行的行号我们就可以进行比较了。当然你已经知道了,我们需要的就是`v:lnum‘:

v:lnum>=8&&v:lnum<=20?1:0
v:lnum>=8\ &&\ v:lnum<=20\ ?\ 1:0

下面是用expr规则模拟indent规则的例子。基本上没有任何实用价值,但至少能让我们又expr的强大有个认识:

" source this.vim
set foldmethod=expr
set foldexpr=Myindent(v:lnum)
" 用expr模拟indent规则
func! Myindent(lnum)
" 用indent()函数得到当前缩进
let s:idt=indent(a:lnum)
" 用&操作符得到一个设置项shiftwidth的值
let s:sw=&shiftwidth
" 如果有缩进宽度超过阈值(shiftwidth)
if s:idt>=s:sw
   " 则计算折叠层级
   return (s:idt-s:idt%s:sw)/s:sw
else
   return 0
endf

除了可以向foldexpr返回数值外还可以返回一些特殊的值,分别是=, a, s, <, >(详细说明见`:help fold-expr‘,这里不现重复)。Vim的文档不推荐使用=, a, s。
这里再给两个简单的例子:

" 如果一行以@samp{#}开始,折叠。
:set foldexpr=getline(v:lnum)=~/^#/?1:0
" 以每5行为一组折叠
set v:lnum%5-1?1:'>1'





Next: ,
Previous: expr规则,
Up: Top

4 实例演示

折叠在写程序时是比较常用的比如python、C语言之类可以将规则设置为`indent‘进行折叠。但我说过了折叠可以有很多不同的用法。下面简单的举几个例子演示一下折叠还能怎么用。由于这里面有些内容需要用到Vim脚本的知识,我不会在这里详细解说,有不明白的地方可以发邮件给我。





Next: ,
Up: 实例演示

4.1 唐诗

这里我借唐诗的例子演示一下expr规则和`foldtext‘设置项的用法。下面是一个文件的内容,这是一个有80首诗的文件每首诗之间有一空行。我的阅读的习惯是看一下目录然后挑自己感兴趣的部分。所以我决定用折叠来模拟目录的效果。

《感遇其一》
作者:张九龄
兰叶春葳蕤,桂华秋皎洁。
欣欣此生意,自尔为佳节。
谁知林栖者,闻风坐相悦。
草木有本心,何求美人折?
	
《感遇其二》
作者:张九龄
江南有丹桔,经冬犹绿林。
岂伊地气暖,自有岁寒心。
可以荐佳客,奈何阻重深。
运命唯所遇,循环不可寻。
徒言树桃李,此木岂无阴。
	
《下终南山过斛斯山人宿置酒》
作者:李白
暮从碧山下,山月随人归。
却顾所来径,苍苍横翠微。
相携及田家,童稚开荆扉。
绿竹入幽径,青萝拂行衣。
欢言得所憩,美酒聊共挥。
长歌吟松风,曲尽河星稀。
我醉君复乐,陶然共忘机。

首先,确定使用的折叠规则。大概的看一下前面的几种规则,看来我们只能用`expr‘规则了。因为我们要将空行以外的所有部分折叠起来。所以我们可以构造这样的表达式:

" 用getline(v:lnum)得到当前行
" 用正则表达式@samp{.}判断当前行是否含有文字。
set foldexpr=getline(v:lnum)=~'.'?1:0

将上面的唐诗复制到一文件中,用Vim打开,设置折叠。是不是看到所有的诗折叠了。不过还有一个问题,在折叠文本(`foldtext‘)中没有注明诗的作者这样的话这个目录似乎有点美中不足。不过我们可以定义折叠文本(:help 'foldtext')的:

" 想显示诗人?
set foldtext=foldtext().v:folddashes.getline(v:foldstart+1)

通过设置`foldcolumn‘,我们还可以使用鼠标来打开或折叠一首诗。最后我们可以在该文件的最后加入一模式行这样我们每次打开都有折叠的效果了:

vim: ro: fdm=expr: fde=getline(v\:lnum)=~'.'?1\:0: foldtext=foldtext().v\:folddashes.getline(v\:foldstart+1): foldcolumn=2

上面的折叠表达式中不同的折叠(诗)之间有一空行,如果希望显示效果更紧凑一点的话试一下这个表达式:
set foldexpr=getline(v:lnum)=~'\\S'&&getline(v:lnum-1)!~'\\S'?'>1':'='





Next: ,
Previous: 唐诗,
Up: 实例演示

4.2 笔记

在阅读文本文件或者多人共同创作说明文档时,经常需要作一些笔记。这些笔记可以是读书心得、体会,也可以是创作说明或者作为以后扩充内容的占位符。如果有很多的笔记的话会影响原文档的可读性,通过折叠笔记我们可以在保留这些笔记的前提下,保证文档的可读性。

  1. 使用模式行vim: se fdm=marker
  2. 选择合适的地方写下自已的想法或注释。
  3. 使用`zf‘或`:fo‘进行折叠。

这样自己做笔记的地方总是高亮显示。
同时可以使用

:folddoclosed .w! >>笔记.txt

来导出笔记。
注意::folddoclosed4只对当前关闭的折叠有效,所以如果要导出所有折叠可以先使用指令`zR‘。如果要删除所有的marker,`:g/{{{/norm zD





Previous: 笔记,
Up: 实例演示

4.3 邮件

我习惯在本地保存一份邮件的副本但有时候在翻以前的邮件时很不方便。因为邮件头通常很长尤其是邮件列表(maillist)中的邮件,有时要看正文得先按好几次CTRL-F。当然在学了折叠后我们当然有更好的办法。

我们以下面这封精简过邮件头并作了适当的修改(防垃圾邮件:()的邮件为例:

Received: from sino.cmm (unknown [202.108.xx.230])
Received: (qmail 25748 invoked by uid 99); 30 Dec 2004 04:42:47 -0000
Message-ID: <200412300xxx47.25xxx.qmail@sino.cmm>
From: chxxxx <chxxxx@sino.cmm>
To: hqxxe@123.cmm
Subject: RE: XX报告
MIME-Version: 1.0
Date: Thu, 30 Dec 2004 12:42:47 +0800
X-Priority: 3
	
hqxxe:
	
    你的报告已经收到,谢谢!
	
                                      陈
	
----- Original Message -----
> ………………
> blah blah
>> blah blah
>> blah blah

在'.vimrc'(windows中是'_vimrc')中加入,下面的内容:

" 根据邮件的后缀名进行相关的设置。如果打开的文件后缀名是'.eml',则当成邮件处理。
autocmd! BufReadPre *.eml se fdm=expr fde=v:lnum==1?1:getline(v:lnum)=~'^$'?0:'=' fdt=Mailfdt(v:foldstart,v:foldend) ft=mail | syn on
	
" 定义函数,用来返回折叠的标题。
" 以折叠的第一和最后一行的行号为参数
func! Mailfdt(fst,fen)
 let fst=a:fst
 " 保存邮件的标题和发信人
 let hfrom=''
 let hsub=''
 let tline=''
 while a:fen!=fst
    let tline=getline(fst)
    " 判断当前行是否是我们感兴趣的行
    " 如果是则保存
    if tline=~'^From: '
       let hfrom=tline
    elseif tline=~'^Subject: '
       let hsub=tline
    endif
    let fst=fst+1
 endwhile
" 返回相关信息
if strlen(hfrom) || strlen(hsub)
    return hsub . "\t\t\t" . hfrom
else
    return getline(a:fst)
endif
endfunc

在加入上面的内容后,我们现在用Vim打开邮件(实际是以.eml作后缀名的文件)看看,是不是清爽多了!





Next: ,
Previous: 实例演示,
Up: Top

5 使用提示

使用manual一般是临时性的折叠。如果每次编辑特定文件都需要做同样折叠时时建议结合modeline使用其他折叠规则。如果不得不使用manual方式时,你可以用:mksession保存包括折叠在内的一切当前编辑设置或者用:mkview保存当前窗口。具体说明见文档。

indent和marker的折叠规则可以用于程序文件或格式文本。一般可以配合modeline使用。

syntax通常用于程序或特定格式的文本。并且由于是在语法文件中定义的所以一般与autocommand一起使用。:help filetype :help command

diff用于比较文件内容后对照修改时使用。可用:mkview或:mksession保存设置。

expr在上面几种方式无法满足需要时我们要使用expr方式。expr可以在模式行中保存设置,也可以保存在Vim的脚本中。相关命令:mkvimrc :mksession





Previous: 使用提示,
Up: Top

6 小结

这一篇中我们简单的介绍了Vim中进行折叠的原理,及几种相关的折叠规则及其使用方法。expr规则实际是一种自定义规则,在学习了Vim脚本后我们还能构造出更复杂的规则。但就现在而言用户掌握了manual、indent和marker就行了(虽然在前面我们举了许多expr的例子,但在实际运用中expr用得比较少,因为通常用户不会绞尽脑汁只为了将文本折叠起来——包括我自己。当然你想要成为进阶用户这是一定要掌握的!)syntax和diff方式会另外讲解,敬请期待。

下一篇我们将开始讨论与编辑相关的一些内容。下次见。

Appendix A

这是比较不常用但又可能比较有用的内容。使用:help查看相关信息。

v:foldstart 内置变量 只读变量记录只前所在折叠的起始行号
v:foldend 内置变量 结束行号,其余同上
foldlevel() 函数 返回指定行的折叠层级
'foldlevel' 设置项 只有高于这个值的折叠层级才会进行折叠
'foldnestmax' 设置项 指定最深的嵌套数
'foldignore' 设置项 在indent规则中以这个值开始的行的将根据前后行的值来设定折叠层级
:folddoclose 命令 对当前闭合的行运行命令
:folddoopen 命令 对未折叠的行或定义了折叠但未闭合的行运行命令


Footnotes

[1] 具体行数可通过`diffopt‘选项变更

[2] 可以试试自动缩进的功能`:se ai‘——如果你还没用上的话

[3] 更多内容参考`:help vim-script

[4] :help :folddoopen :help :folddoclosed


2006, April 5

模式行::Vim进阶索引[1]

Filed under: vim, vi, 编辑器, 教程




Up: (dir)

Vim进阶索引[1]::模式行

这是Vim进阶索引的第一篇,本文假设用户已经掌握了Vim的基本用法(正因为这个假设所以你里的一些内容你可能已经掌握了)。作为第一篇我们要讲的是modeline

相关帮助:

:help modeline
:help 'modeline'
:help 'modelines'





Next: ,
Up: Top

1 什么是模式行?

模式行(modeline)大家应该已经见过了。在Vim文档底部通常会有这么一行`vim:tw=78:ts=8:ft=help:norl:‘,这一行就叫模式行。模式行用来在文档中保存设置,Vim在读入文档后根据模式行的指示对文档对行设置。这样通过在文档中写入模式行我们可以对文档进行个性化设置,定制文本的编辑环境。此外,模式行作为文档的附加信息有助于我们和别人了解一个文档——推荐的文本宽度、使用的语法格式、缩进的方式等等。我们先看一下模式行的用法,再讨论我们可以做什么样的设置。

2 模式行的格式

几乎所有用set命令可以使用的设置项都可以在模式行中使用(有少数设置项是不能在模式行中使用的,如formatprg)。但在进行设置之前必须在设置项前加上`vim:‘(也可以是`vi:‘或`ex:‘,注意一定要小写。),这是Vim判别一模式行的关键。比如有一个文件我们希望每次打开后自动设置为只读,可以在文件中加入模式行`vim:readonly‘。如果有多个设置项的话每个项之间用冒号或空格隔开。现在我们再看一下刚看到的Vim底部的模式行:`vim:tw=78:ts=8:ft=help:norl:‘,现在我们知道在我们打开Vim帮助后它自动进行了如下设置:设置文本宽度为78;设置制表符宽度为8;文件类型为帮助文件;阅读顺序为左到右。

3 模式行中的附加文本

如果我们在自己的日记中或者写作的过程中我们当然可以随意的在文件前几行或后几行添加模式行。但是在一些情况下我们不能直接地在文件开头或最后一行添加模式行——比如程序的源代码,模式行最好是以注释的形式出现。要让模式行以注释的形式出现,只要在前面加入表示注释的命令或符号就行了。例如要在C++源文件末尾加入模式行:

// vim:ft=cpp:

很简单对吧,事实上模式行的`vim:‘之前允许有任意的文字串。唯一的要求是文字串与`vim:‘之间必须至少有一个空格。所以,像这样的模式行是允许的:

*****今天是X月X日星期X***** @#$%$^%^&*()   |   vim:encoding=utf8  tw=80

不过下面的模式行则会出错:
/* vim:ro */
原因是后面的`*/‘被当成一个设置项了,而事实上这个设置项不存在。为了能在模式行后面添加文字串(或者说为了使用c风格的注释),我们要用到模式行的另一种形式。

4 模式行的另一种形式

实际上模式行有两种形式,第一种就是前面说的设置项之间用冒号或空格分隔,缺点是不能后设置项后面添加其他文本。第二种要求使用set命令。不能使用多条set命令。但一条命令可以设置多个选项。选项之间用空格分隔。举例而言,像前面提到的Vim文档的模式行以第二种形式写出来就是:

vim:set tw=78 ts=8 ft=help norl:

这种形式的好处就是或以是模式行末随意添加文本。下面的模式行都是允许的:

/* vim:set ro */
<!-- vim:set ft=html: 模式行示例-->

需要注意的是第一种形式中最后一设置项后的冒号或空格是可有可无的。而第二种形式中最后一设置项后一定要有冒号——不管冒号后有没有文字。在设置项中如果要使用空格、制表符或冒号可以在前面加上转义符-`\‘。其他需要转义符的情况见`:help option-backslash‘。





Next: ,
Previous: 模式行的另一种形式,
Up: Top

5 模式行应用

由于几乎所有的Vim设置项都可以在模式行中使用所以模式行的使用完全视个人的需要而定。因为自己常用的设置可以放在.vimrc文件中所以实际使用中放在模式行中的设置通常是针对某一文档的专有设置。需要在不同的电脑上编辑/查看同一份文档时为了使用同样的设置也会使用模式行(如Vim的文档)。

vim: tabstop=4
正确显示制表符,使文档在不同的机子上有一样的制表符宽度
vim: tw=48 fo=tqamn ts=4 expandtab
写文章。使用48个半角字符文本宽度(24个汉字);序号缩进及汉字支持的格式选项;用4个空格代替制表符;
vim: enc=latin1 guifont=terminal\:oem\:h10\:w8
查看ANSI编码的图形字符(Windows环境)时所用的设置。用来写/看nfo文件。
vim: nowrap tw=0 wm=0 backup
我在行对行翻译时经常使用的设置
vim: fdm=marker
设置折叠规则。关于折叠(folding)我们在以后会讲到。
vim: ft=xxx
设定filetype。关于filetype在以后会讲到。

6 下一篇

模式行的使用方式是比较容易掌握的,但它的价值也比较容易被忽视。希望通过这一篇教程能让大家对模式行有所了解。下一篇我们要讲的是折叠(:help Folding),下次见。





Previous: Top

Appendix A 为什么我添加了模式行却没作用?

  • 首先检查一下`vim:‘与前面的文本之间有没有一个(半角)空格。
  • 其次模式行要求在文件开头的前N行或后N行中,如果没在这个范围内模式行就不起作用。modelines用来设置N的具体数目。比如`:set modelines=8‘这个命令将模式行的有效范围设在了前8行和后8行。
  • 最后看有没有设置`'modeline'‘选项。modeline选项在Vim中默认是开的但也有可能因为一些原因被关上了,比如设置了nomodeline或是没有设置nocompatible

[ -结束- ]

Get free blog up and running in minutes with Blogsome
Theme designed by Jay of onefinejay.com