碎碎念

2007, September 30

缩进::Vim进阶索引[8]

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


Up: (dir)

缩进::Vim进阶索引[8]

缩进可以使用文本结构更清晰易读。在Vi中,这通过是使用专用的外部程序(如:indent或c beautifier类的程序)实现的。Vim除保留了原有外部程序支持外更增加了一些内部的支持。包括了插入模式下的交互进行的缩进与'='指令的缩进操作。

1 基础知识

:h indent.txt
:h =
:h 'equalprg'
:h indentkeys

Vim中缩进有三种基本的使用方式。一是在普通(正常)模式下使用'='指令。可以圈选范围后使用可以在指令后加上移动的指令。使用的方式与其他编辑指令是一样的(比如'd')。'=='表示对当前行进行缩进。看下面的例子:

=ip
对当前段落缩进
=G
将由当前行至文章末尾的范围缩进
30==
缩进由当前行开始的30行

二是在编辑的过程(插入模式)使用某些键触发。比如,使用'autoindent'时,在插入模式中输入回车(即按回车键)时Vim自动对新行应用缩进规则。
三是粘贴文本时,使用']p'指令对粘贴文本强制运用缩进。详见::h ]p

此外,'gq'或ex命令':left'也能用来缩进文本。由于它们属于文本格式化的内容,这里不作讨论。

注意:当'equalprg'不为空时,'='/'gq'总是使用equalprg中设置的外部工具。除此之外其他的缩进操作不影响。

在equalprg中使用的外部程序通常是整理(过滤)文本的工具,很少是单独用于缩进的工具。此外,如果通过外部程序实现缩进,有一些缺点不可避免:

  • 使用上不方便。如果要实现交互方式的缩进(即边输入边根据输入实现缩进),要不断运行外部程序,运行效率低。通过cinkeys/indentkeys的设置Vim可以在输入时计算缩进。
  • 对大多数的一般应用而言,用户只需要最基本的缩进支持——如autoindent。而你却很难找到这样的程序。
  • 许多工具不跨平台。
  • 不够灵活。你其实不想为一些简单的缩进而写新的程序。

Vim应用缩进的过程如下:

  1. 依据设置使用缩进规则计算缩进宽度。

    在不同的缩进规则同时开启时只能有一个起作用。在所有开启的缩进项中只有优先级最高的起作用。它们的优先级排列如下:
    indentexpr > cindent > smartindent > ai

    缩进宽度:以一个半角字符的宽度为基本单位计算的总缩进的量。缩进时,Vim会在行首增加相应宽度的空格或制表符。举例而言,如果缩进宽度为4,则Vim在行首增加4个半角空格;如果缩进宽度为8,Vim在行首增加一个制表符。

  2. 删除目标行首的制表符与空格。
  3. 根据expandtab与tabstop的设置及缩进宽度添加相应的空格或制表符。

    制表符的宽度与'tabstop'的设置有关。默认值是8,所以8个半角空格(或其他字符)的宽度与一个制表符一样。如果将'tabstop'设为4,那么如果缩进宽度为9则Vim在行首增加2个制表符与1个半角空格。如果不想使用制表符可以:se noet

如果要实验各种缩进方式的话,建议定义如下的快捷键以便随时按<F9>查看缩进的设置:

map <F9> :se autoindent? smartindent? cindent? lisp? indentexpr? equalprg? paste? cpoptions?<CR>

有些选项,如'paste'会影响缩进,所以需要查看这个设置项的情况。各个设置项的情况可以见各自的文档。

2 预设规则

为了方便用户Vim提供了一些预置的缩进规则:自动缩进(autoindent)、智能缩进(smartindent)、c缩进(cindent)、lisp缩进(lisp)。

2.1 autoindent

autoindent的缩进规则是最简单的。它使用与上一行一样的缩进量。换言之如果你为当前行加了3个空格的缩进,则开始下一行时Vim会自动添加3个空格的缩进。写python脚本时,使用这种缩进就够了。

使用autoindent,只要开启相应的选项::se autoindent 或 :se ai

注意:indentexpr、lisp、cindent、smartindent中的任一项开启都会覆盖autoindent的设置。

2.2 smartindent,cindent

:h C-indenting
:h smartindent
:h cindent
:h cinkeys
:h cinwords
:h cinoptions
:h cinkeys-format

smartindent的缩进规则可应用于与c语法类似的语言如AWK、JavaScript等,当然也可以用在c语言。它的规则是将{}块内的语句缩进一定宽度。嵌套的{}块内的语句则相对于上一层语句缩进一定宽度。

cindent的缩进规则是专门用于c语言的缩进。与smartindent相比,cindent除了更严格地对应c语言的语法外,还增加了风格选项——为了适合不同的c语言风格,Vim提供了相当多的定置项改变cindent的缩进方式。设置项包括了:

cinkeys
这个选项定义了一组可以触发缩进的按键。在遇到这些按键是Vim会根据缩进规则重新计算当前行的缩进。定义按键的格式可以见:h cinkeys-format
cinwords
定义了一组让下一行相对对当前行增加缩进的关键字。在遇到定义在cinwords中的字时,Vim为接下来一行增加缩进。
cinoptions
缩进风格选项。参考::h cinoptions-value、

2.3 lisp

:h lisp
:h lispwords

根据lisp语法缩进,我懂得很少,所以——详见帮助。

3 进阶规则(indentexpr)

:h cpo
:h indentexpr
:h indentkeys

与折叠一样,缩进也支持使用表达式定义缩进。这个表达式可以是任意表示数值的表达式也可以是返回数值的自定义/内置函数,这个数值将做为缩进的宽度。也与折叠一样Vim使用v:lnum表示目标行的行号。其它常用的函数包括了indent()、getline()、prevnonblank()、nextnonblank()等等。与折叠不一样的是使用缩进表达式不用另外指定缩进方式,只要赋于indentexpr项一个值,就会覆盖autoindent或smartindent/cindent的设置。

Vim的缩进表达式要比折叠表达式直观得多。我们直接通过例子了解缩进表达式的使用。

3.1 简单缩进

先看几个简单的缩进表达式:

" 缩进宽度总为4
:se indentexpr=4
" 不使用表达式缩进
:se indentexpr=
	
" 将缩进宽度设为与&sw设置一致
:se inde=&shiftwidth
	
" 逐渐增加缩进
:se inde=v:lnum

这一组表达式还是比较容易理解的,都是直接将一某个数值(不需要什么计算)作为缩进量。 此外,三元条件表达式在折叠篇中也已经看了不少:

" 偶数行缩进4格
:se indentexpr=v:lnum%2?0:4
	
" 取消注释行的缩进
:se inde=getline(v:lnum)=~'^\\s*#'?0:indent(v:lnum)
	
" 行首缩进:
:se inde=(getline(v:lnum-1)=~'^\\s*$')?4:0
" 悬挂缩进:
:se inde=(getline(v:lnum-1)=~'\\S')?4:0
	
" 相对于上一行缩进行首带着-号的行
:se inde=getline(v:lnum)=~'^\\s*-'?indent(v:lnum-1)+4:0
	

这一小节的的最后一个例子是个常用到的缩进:根据编号缩进。

1. statement
1.1. substatement
2. statement
2.1. substatement
2.1.1. subsubstatement
2.2. substatement

在看需求文档时几乎每一行都是编号的。程序员从不同的渠道获得这些文档,可能是从某个需求管理系统,电子邮件或者SKYPE。它们有不一样的缩进,有一些甚至没缩进。自动缩进工具此时显得特别有用。

对于写需求文档的人来讲他们除了要能智能的缩进他们可能还需要一个可以自动编号(根据缩进或者行首的*字符的个数)的编辑器。当然Vim用户是不需要再花时间找这样的工具的!

要将这些编号可以用以下的脚本:

" 根据编号缩进
:se inde=len(split(substitute(getline(v:lnum),'^[\ \\t]*\\([0-9.]\\+\\).*','\\1',''),'\\.'))*&sw

将函数写成单行形式的最大挑战是要加上非常多的转义符。而且记住:使用单引号,不要用双引号。具体的原因See 附录的解释.

如果不想记转义规则可以用函数将它包装起来。当然,这样也就没办法在模式行中使用了:

func! GetIndent(lnum)
  let ind=len(split(substitute(
       \ getline(a:lnum),'^[ \t]*\([0-9.]\+\).*','\1',''),'\.'))
  return ind
endfunc
se inde=GetIndent(v:lnum)*&sw

3.2 indentkeys

在定义了缩进表达式后,我们可以在文本输入完成后使用'='或'=='进行缩进。如果要在编辑的过程中实时地缩进,我们需要定义合适的'indentkeys'。考虑下面的表达式:

" 将字串长小于20的句子右对齐
" 这条命令实际等价于:right 20
se inde=20-len(substitute(getline(v:lnum),'^[\\t\ ]\\+','',''))

因为默认的indentkeys中包含了o,O,所以在开启新行时,Vim就已经计算了缩进——但这时我们的输入还没完成,所以缩进宽度是错的。我们需要让Vim在句子输入完成后再计算缩进宽度。也就是在我们按下回车后先计算并应用缩进再插入换行符。同时还需要定义一个在插入模式中可以使用的缩进命令,以随时强制Vim计算缩进。就像所有Vim的其他功能一样,Vim也为这个功能提供了设置项,这次是indentkeys,

se indentkeys=*<CR>,!^F

*<CR>表示在插入模式下按回车键时,先重新计算缩进再加入换行符。如果只有<CR>则Vim会先加入换行符再计算缩进——这时新增行成了目标行。
!^F表示在插入模式下按Ctrl-F时,重新计算缩进但不插入字符。关于*和!在indentkeys(及cinkeys)中的意义可以见::h indentkeys-format

4 缩进进阶

在处理缩进时经常会遇到嵌套的格式文本,幸好它们都大同小异。考虑下面的文本嵌套结构:

[marker]
  block
  [marker]
    block
  [end marker]
[end marker]

这种类型的文件很常见xml(<xxx>block</xxx>),C代码({block}),opera书签文件(opera6.adr)等1

下面我们将一起为两个使用这种结构的文本的写缩进脚本。

4.1 例2

在一些情况下'marker'与'end marker'不那么明显。下面是一个文本目录树:

+ item1
- item2
  + subitem
  - subitem
    * subsubitem
  -
-
* item3

这里的marker是跟减号跟随文字,end marker则是一个减号(后面没有文字)。事实上将这个end marker改成一个空行,在处理上也不会有什么不同。2

但无论是哪种形式只是对marker的判别方式有一些区别,其结构并无本质区别。

状态及对应的处理方式;

  • 当前条目如果只有一个减号(^\s*-\s*$),则当前条目相对上一条目减少缩进量。
  • 上一条加号或星号开始(^\s*[+*]) 当前条目与上一条目的缩进一样
  • 上一条如果只有一个减号(^\s*-\s*$),则当前条目减少缩进量。
  • 上一条如果由一个减号开始(^\s*-\s*\S\+$),则当前条目增加缩进量。

这样脚本就很清楚了,

func! MyIndent(lnum)
  let lastline=getline(a:lnum-1)
  if a:lnum==1 | return 0 | endif
	
  if getline(a:lnum)=~'^\s*-\s*$'
    let diff=-1
  elseif lastline=~'^\s*[*+]'
    let diff=0
  elseif lastline=~'^\s*-\s*$'
    let diff=-1
  elseif lastline=~'^\s*-\s*\S'
    let diff=1
  endif 
	
  return indent(a:lnum-1)+diff*&sw
endfunc
	
se inde=MyIndent(v:lnum)

4.2 例2

最后是一个完整的例子仍是嵌套的文本块,看一下下面的文本,

[
outer block
[[
inner block
]
]
[
[inner block]
]
]

这个仍然是marker与end marker的格式。[是marker,]是end marker。我们要写一个使之能正确缩进的脚本,其中的关键在于判断嵌套的深度来决定缩进宽度。我们可以使用一个buffer变量保存嵌套深度,遇到[增加,遇到]减少深度。但因为=命令可以多次不连续地对不同文本块使用,所有变量的存在可能会导致不正常的结果。为此,仍像前面的例子一样我们将根据前一行的状态判断缩进深度。根据上一行的marker,计算当前行的缩进宽度。描述如下,

  • 如果上一行是[,当前行增加宽度
  • 如果上一行是],当前行减少宽度
  • 否则,保持上一行的宽度

还要考虑到一点,同一行可能数量不等的多个]或[——事实上这是这个例子与上一个例子唯一的不同之处。因为一对[]的缩进刚好可以抵消,我们可以通过它们的差决定缩进的宽度。另外,如果当前行有[或]还要相应增加或减少当前行的缩进。所以改进后的描述如下,

  • 将上一行[的数量减去]的数量,得到初始的缩进宽度
    • 结果为正,则为当前行增加相应数量的缩进
    • 否则,为当前行减少相应数量的缩进
  • 在前面计算的基础上计算当前行的[与]的差,得到缩进的增量。
    • 结果为正,则为当前行增加相应数量的缩进
    • 否则,为当前行减少相应数量的缩进

现在我们可以写脚本了,

" 其中,根据[]数量计算宽度这一段是重复的,
" 我们可以写成一个单独的函数
	
func! IndentSum(lnum,incre)
" 两个参数分别表示目标行行号与缩进的初始量
    let line=getline(a:lnum)
    " 通过'['与']'的数量计算缩进宽度
    " 每多一个[则增加一个单位的缩进
    " 每多一个]则减少一个单位的缩进
    " 没有]或[的行使用与上一行一样的缩进
    let in=len(split('x'.line.'x','['))-1
    let ou=len(split('x'.line.'x',']'))-1
    " [的数量减去]的数量
    return &sw*(in-ou)+a:incre
endfunc
	
func! BIndent(lnum)
    " 上一非空行的行号
    let llnum=prevnonblank(a:lnum-1)
    if llnum==0 | return 0 | endif
    " 由上一行得到初始的缩进宽度
    let ind=IndentSum(llnum,indent(llnum))
    " 计算当前行的的缩进增量
    let ind=IndentSum(a:lnum,ind)
    return ind
endfunc
	
se inde=BIndent(v:lnum)

现在,你已经可以写c缩进的脚本了(将上面脚本中的[]换成{} :) )。这种缩进的计算方式几乎是一个套路了。基于同样的模式,同样的工作流程的一个xml的缩进的例子可以见Vim安装目录中indent/xml.vim。

5 进阶提示

这一章是关于缩进的一些零散的内容。

5.1 去除缩进

:h g@
:h operatorfunc

现在你可以用'='进行缩进了你可能还需要一个可以去除缩进的指令。当然你可以用'<<',但这个命令一次只缩进一层。很遗憾你并不能使用'4<<'将文本向左移4次(这条命令将4行文本往左移一次),你只能一次一次来(或者按1次<<,再按3次.)。如果你确实需要一次去除许多缩进的话,可以使用下面的map宏:

:nnoremap <<< :left<CR>
:vnoremap <<< :left<CR>

这个宏有两个主要缺点,一是会使<<命令变慢,因为Vim要等等看后面是否还有一个<。这可以通过减小设置项'timeoutlen'的值,减少等待时间,但你的操作也要相应变快才行。或者另外定义一个按键序列,不使用<<<。二这条命令不支持对象选择。这不算是个很大的缺点,但如果支持的话显然会更方便一点。我们可以用g@包装上命令,使之具有对象选择的功能:

func! Deindent(dummy)
  exe 'normal! ' . "'[V']:left\<CR>"
endfunc
se opfunc=Deindent
	
nnoremap <<< g@
vnoremap <<< g@

现在试一下<<<G, <<<aB或<<<ip。g@的用法见Vim文档。

5.2 缩进与格式化选项

:h gq
:h formatoptions
:h formatprg

缩进与文格式化(gq)紧密相关,你可能会有兴趣看一下这一部分的内容。

5.3 缩进与折叠

在折叠篇我们知道可以以缩进作为折叠的规则。因此这实际给我们一种同时定义缩进与折叠的方式。在学了这一篇之后这个折叠的缩进规则就能派上用场了!

:se fdm=indent

Appendix A 表达式与沙箱


直接写表达式跟将表达式包装在一个函数中有一个最主要的不同是,前一种方式中的\及空格需要进行转义。所以在模式行中要使用很多的\。另外要注意的是"与'是不一样的。

:h expr-quote
:h expr-'

在写Vim脚本或命令时"的字串是允许使用转义字符的,而'的字串则不进行转义。如"\t"表示的是一个制表符而'\t'表示的是一个斜杠和一个字母t。'\t'等价的双字号字串是"\\t"。
在脚本篇我们就讲过了一个例子:

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

但对于indentexpr及其它在沙箱中计算表达式的设置项来讲,数值表达式在进入沙箱中先进行了一次表达式的计算——只是计算字串值。例如,当你执行:se inde=len('abc\\t')时,先计算字串的“安全值”。即在计算函数的值之前,Vim会先计算字串表达式的值,所以函数现在成了,

"len('abc\\t')"

这个字串表达式的值,大家都知道是(通过:echo &inde可以观察字串表达式的值):

len('abc\t')

然后,再计算函数的值,结果是4(3个字母加一个制表符)。

在执行=命令时,Vim首先计算了字串表达式的值,再eval字串的值(即执行len('abc')并返回数值)。

注意,在写表达式时你并不需要在前后加上引号,Vim会自动为你加上双引号并进行计算字串的值。这个过程中Vim还进行了一些处理以确保值是“安全的”。其中包括移掉未转义的\。可以简单的记为这些表达式中不能有未转义的双引号",空格和斜杠。还是例子比较实在,

命令 字串表达式 字串值
:se inde=len('\\t') "len('\\t')" len('\t')
:se inde=len('\t') "len('\t')" len('t') 未转义斜杠会被忽略掉
:se inde=len(' abc') 空格未被转义,不合法表达式
:se inde=len('ab"c') "len('ab"c')" len('ab 双引号"未被转义,所中间的"后的字串被省略
:se inde=len(\"\\t\") "len(\"\\t\")" len("\t")

正因为在计算len()之前已经先计算了字串值一次,所以本来,

func! GetIndent(lnum)
	return len(split(substitute(getline(v:lnum),'^[ \t]*\([0-9.]\+\).*','\1',''),'\.'))*&sw
endfunc
se inde=GetIndent(v:lnum)

不用转义的表达式,直接放到:se inde命令后,就成了:

:se inde=len(split(substitute(getline(v:lnum),'^[\ \\t]*\\([0-9.]\\+\\).*','\\1',''),'\\.'))*&sw

可以看到多了一堆的反斜杠('\')。不与这些转义规则打交道的方法是尽量将它们包装在独立的函数中(这样不用使用沙箱)。如果一定要用的话尽量少用空格与双引号。


Footnotes

[1] 事实上几乎所有的嵌套结构都是这样的

[2] 但有个'-'号会比空行直观一点。

2007, August 4

TAGS::Vim进阶索引[7]

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




Up: (dir)

Vim进阶索引[7]::TAGS

tags最广为人知的应用是taglist。但大家对tags本身的关注却少得多。我希望这一篇文章能帮你懂得使用tags、生成tags和扩展tags的应用范围。与之前的几篇不一样的是这一篇的脚本使用了许多Vim7的新特性——大部分是关于List的。

这几个是相关的文档:

:h tagsrch
:h taglist()

当我们在查看Vim文档时,只要在关键字(即两边带有"|"的字)上按CTRL-]就可以跳转到指定的位置——Vim是如何做到这一点的呢?它怎么会知道目标位置在哪呢?

其实在进行跳转时,Vim借助了一个叫"tags"的文件。tags文件是一个表格,表格中的记录以行为单位。每一条记录包含一个tag(标签)及一条对应该标签的位置信息。当我们按CTRL-]时,Vim在"tags"中查找相应的标签——即当前光标下的字。如果找到则读取该条记录的位置信息。并根据该信息转到相应的位置。标签就相当于HTML的链接文本,而tags文件则记录了每个链接的链接文本(标签)及其对应的href信息(位置信息)。在任意文件的任意位置,都可以使用通过标签跳转到该标签所对应的位置——只要该标签存在于某个Vim可以找到的tags文件中。

通常Vim会在当前目录下(:pwd)查找tags文件。但你可以指定某个目录下的特定文件为tags文件——不一定要命名为tags,但无论你为它取什么文件名,为了指代方便这里我们还是叫它为tags文件。当你需要为它指定其他名字或位置时,你可能用得着modeline或filetype功能。

如果你还没见过tags文件,现在打开Vim文档所用的tags文件看一下:

:e $VIMRUNTIME/doc/tags




Next: ,
Previous: Top

1 基本的tags用法

如果你没用过Vim中的相关功能,你可以通过这一节的例子了解tags的基本用法。如果你用过——看下一章。
新建一个工作目录,我们会在这个目录下建三个文件做实验。

先建立如下文件,命名为“file.txt”:

第一行
第二行
第三行
第四行
vim:tags=./tags

接下来建立一个tags文件。tags文件有三栏,第一栏是标签,第二栏是文件名,第三栏表示在文件中的第几行。我们先建立四个标签:A,B,C,"A B"(这个例子中标签名不重要,可以随便取。)分别用来表示上面文件中的第一、二、三、四行。新建名为“tags”的文件,并输入如下内容——注意栏与栏之间用制表符(tab)分隔:

A	file.txt	1
A B	file.txt	4
B	file.txt	2
C	file.txt	3

最后,在新建一个文件,并在任意位置输入"A B C"并保存——字符之间要留有空格。将光标移到字母B上按CTRL-],现在你应该在file.txt的第二行了也就是我们在tags文件中设定的位置上了。现在按CTRL-T可以回到原来的位置。然后再按CTRL-]又跳到file.txt的第二行了。现在,你可以反复地按CTRL-OCTRL-I后退前进。还有一个问题,怎么使用 "A B"这个标签呢?方法是先选中再按CTRL-]——也就是先将光标放到字母A上,指令序列如下:vfB<C-]>

中文字与字之间没有空格,所有在中文环境中也经常要使用选中再按CTRL-]的方式跳转。




Next: ,
Previous: tags1,
Up: Top

2 tags文件格式

:h 'tags'
:h tags-file-format

让我们从上面的例子总结一下tags文件的格式:共有三栏(一般也都是三栏),栏与栏之间用一个制表符(Tab)分隔。最前面一栏是标签,后面是具体的位置信息——为表示方便我们估且称之为锚点。第二栏用于定位到特定文件,第三栏将位置具体到特定的行。

B file.txt 2

可解读为标签“B”所指代的位置是“file.txt”文件的第2行(或者说标签“B”对应的锚点在“file.txt”的第2行。)

但上面例子使用最简单的tag格式。实际上,你可能会遇到下面这样子的tags文件:

A	file3.txt	10
ANSWER	file3.txt	norm 30G4|
B	file2.txt	2;"	kind:f	kkk:vvv
C	file3.txt	32
C	file7.txt	/somewhere

我们看一下跟原来的tags文件有什么不一样:

  • 这个tags文件,包含的位置信息分属不同的文件。这意味着通过tags文件,你不仅可以转到同一文件的不同位置,还可以在一堆文件中跳转。
  • 它包含了不只一种定位的方式。在原来的例子中,行的位置是直接用数字表示的。但实际上Vim可以懂Ex命令。所以遇到"norm 30G4|"Vim会将光标定位到第30行的第4列。而遇到“/somewhere”,Vim会在file7.txt中查找第一个somewhere出现的位置。通过正则表达式的字元就可以进行更复杂的定位。对于不需要修改的文档可以使用行号而需要编辑的文档可以使用正则表示式,这样即使做了修改也能定位到正确的位置。
  • 有两个同样的标签C指向了不同位置。这种情形当你使用C标签时,Vim会提示有多个匹配,可以:tn跳到下一个匹配。
  • 标签B的行号后面有“尾巴”。Vi或其他编辑器中“;"”后的内容被视为注释。但Vim可以读取这部分的内容,所以我们可以通常这一部分为tags添加额外的信息。看下面的解释。

只要遵循一定的规范就可以让注释变为有用的信息。我们一起再做个实验,将上面的示例文本,用Vim保存为"tags"。然后在Vim中输入如下命令,观察结果:

:echo taglist("^")
:echo taglist("^B")
:echo taglist("^B")[0]['kind']
:echo taglist("^B")[0]['kkk']

可以看到在返回的字典列表中,多了一个kkk项,可以通过它取得kkk的值vvv。我们可以继续往tags里增加“键:值”对。只要记住,每一项之间要用制表符分开(包括;"与第一项之间),键与值之间用冒号(:)分隔。 另外,Vim支持中文键值但不支持中文键名。如果需要遍历列表可使用:for(:h :for)语句。而且每个tag都有kind项,不论有无给出kind的值,它的值可以是任意的单个字母。为kind赋值时“kind:”也可以省略。所以,标签B也可写为:B file2.txt 2;" f kkk:vvv

注意:tags文件的编码(encoding)要与Vim的默认编码一致,不然可能会出现中文文字工作不正常的情况。




Next: ,
Previous: tags2,
Up: Top

3 相关命令

就像往常一样,Vim提供了非常多的命令——但本文的目的不在帮用户背命令。关于这些命令可翻看文档。

:h tag-commands

下面这几个命令是一定要掌握的:

     :h tag
     :h CTRL-]
     :h CTRL-T
     :h CTRL-O
     :h CTRL-I

提示:在使用:tag时是可以使用tab键补全的,此外还可以使用正则表达式搜索标签。如::tag /agname

现在我们已经了解它的原理、它的格式,也知道怎么样使用这些tags文件了。现在我们要进入进阶的部分了——我们要根据需要定制自己的tags。




Next: ,
Previous: tags3,
Up: Top

4 tags进阶

可以使用的tag命令是固定的,就是Vim提供的那些命令。但生成什么样的tags文件用户是可以控制的。因而用好这个功能的关键也在于怎么生成tags、生成什么什么样的tags。生成什么样的tags取决于用户的意图,怎么生成则依赖于我们的工具。使用ctags程序无疑是最方便的选择,对于每种语言基本上都可以找到对应的ctags工具。只是生成编程用的tags相对用户的多样需求来讲稍嫌不足。下面的内容将通过例子演示怎么根据自己的意图生成合适的tags,也会涉及如何扩展ctags的功能。

Vim本身就可以生成tags(:h :helptags)。但是大多数情况下你用ctags类工具生成tags文件。但是你也可以借助sed,awk,perl……等工具,甚至是手工编辑。:h tags-file-format





Next: ,
Previous: tags4

4.1 编写/转换为Vim文档

我们已经知道tags可以在文档间建立关联。这样既方便用户查询又可以方便地在文档间切换。如果你希望为自己的文档增加tags,你只需要定义好锚点和标签的样式(其实就是为锚点和标签增加独特的标识或分隔符,这样工具才能分辨关键字与普通字词)。好的样式可方便工具找到生成tags的规则,减少调试正则表达式的时间。在标签样式中尽量不在要标签中使用空格,使用命令才方便。对于中文我们可能需要定义一个可以将标签与其他文字分隔的样式。这样才能方便地使用CTRL-]

我们的第一个例子是基于大家都已经熟悉Vim在线文档系统。在这个系统中大家使用:help命令找到相关主题的文档,还能在关键字之间方便地跳转。现在我们知道这个系统的幕后英雄就是tags。

这一节就要讲怎么用Vim文档格式制作出自己的文档(准确地说是使用Vim的tags约定写自己的文档)。当然,先回答个问题——为什么要制作Vim格式的文档呢?

  • 重用已有设计是省时省力的方案。
  • Vim文档的格式很简单。
  • 有现在的tags工具——:helptags。
  • 可以使用Vim的文档系统。除了在开发插件的时候用得着外,你可曾想过通过输入:h 01082007查看自己2007年8月1日的日记呢?额……我承认这个想法有点“非常规”。那你有没有想过在Vim中输入:h Best-Tips呢?:)

这是制作Vim文档的步骤:

  1. 写作并在适当的地方加入锚点和标签。写Vim文档的惟一要求就是对标签和锚点应用特定的样式:
    • 锚点样式:在关键字两边加上“*”号和空格表示一个锚点;另外,关键字不能包含空格;如: *vim:*
    • 标签样式:在关键字两边加上“|”。如:|vim:|
  2. 在文档的最后插入modeline。这是可选步骤。
  3. 在文档中使用了上面的规则后,用:helptags .生成tags文件。
  4. 使用:tag等命令访问文档

下面是一个完整的(但无用的)Vim格式文档,看完大家就知道怎么一回事了。

*花花幼儿园班级手册.txt*
	
*小明*
是个很乖的小孩。他跟|小强|是好朋友。
	
*小强*
是个聪明的小孩。他跟|小丽|是同桌。
	
*小丽*
是个可爱的小女孩。她跟|小明|是邻居。
	
vim:set ft=help: 使用Vim文档的高亮配置

将上面的文本保存.txt文件。现在使用这条命令生成tags
:helptags .

使用tag命令打开文件(不知道为啥使用中文标签有时会失败——虽然补全可以补出来。使用正则语法,也就是标签前加一个“/”可以增加成功率)
:tag /小强

需要的话,你还可以将这个文件整合到Vim的文档中,这样你可以通过:h命令访问自己的文档。方法一是直接将你的文档复制到vimfiles下面的doc目录下,然后重新运行:helptags。这种方法简单但是你可能不想让自己的文档跟其他的文档混在一起,这时候你需要用方法二:首先新建一个名为“doc”的目录——必须命名为doc,将你的文档放进去,doc目录可放在任何你觉得合适的位置。假设目录的位置是:/foo/bar/doc。使用:helptags生成tags。最后在vimrc中加入一行:

let &rtp=&rtp . ',/foo/bar'

上面这一行脚本将doc的上级目录加进Vim的runtimepath中(:h 'rtp'),只有这样:h命令才找得到你的文档。如果你的路径中有空格记得加上“\”。

你当然不会将“花花幼儿园班级手册.txt”整成Vim的帮助——我也不会。这个技巧的价值依赖于应用的场合。我用Vim写gawk脚本,所以我想如果gawk的文档也能用Vim的线上文档系统打开的话那会方便得多。当然这是完全可以做到的,只要依照Vim的格式,在gawk文档中插入适当的锚点。这只需要一些自动替换加上少量手工调整。这样我可以在任何时候输入:h GWK-dcngettext()查询gawk函数dcngettext的细节。如图所示:

vim_tut_gwk_help

这样做的好处是你在写awk脚本时可以随时查看gawk文档——就像查看Vim文档一样。





Next: ,
Previous: tags_sec1,
Up: tags4

4.2 exuberant ctags

说到tags的应用当然要谈到ctags工具。ctags工具预先定义了一组用来抓tags的规则。这样我们在写程序时可以使用tags的便利特性,而不用花时间收集tags——ctags会帮我们做。ctags类软件的操作方式大同小异,exuberant ctags1是最流行的版本。接下来讲的ctags指的就是exuberant ctags。我们将扩充ctags使之支持我们的应用。

ctags支持的语言非常多可以使用下面命令查看:ctags –list-languages2最常见的用法是:ctags -R *。这条命令在在当前目录及子目录中找文件并生成tags。ctags根据扩展名判断所使用的语言。但即便ctags支持众多的语言,还是有许多语言,如 Smalltalk, D, Rebol, Haskell, Basic或者你新开发的语言等不在ctags的支持之列。因此,你需要学会扩展ctags的语言以便让Vim(或者其他用到tags的工具)的功能可以被利用起来。

关于ctags的祥细信息可以在下面的链接中找到:
ctags手册 里面包含有使用这个工具的祥细信息。
为ctags增加语言支持里面介绍了如何扩展ctags的语言支持(包括从源码上增加语言)。

步入正题,让我们先了解一些定义新的语言需要用到的“选项”:

`–langdef=<lang>
告诉ctags,你要定义一种新的语言。如: –langdef=texinfo
`–langmap=<lang>:<ext1>[<ext2><…>][,<lang2>:<ext…>]
将该语言与特定的扩展名关联起来。如:–langmap=texinfo:.texi.texinfo将扩展名“.texi”与“.texinfo”都关联到新定义的语言“texinfo”
`–regex-<lang>=/regexp/replacement/[kind-spec/][flags]
<lang>部分放上新定义的语言的名称。“regexp”是用来抓锚点的正则表达式,而“replacement”部分定义了与锚点对应的标签。这一部分的语法与Vim的:s命令一样,只是正则语法上有些区别。比如, 用这个正则 /class\W(foobar)/\1/ 抓锚点“class foobar”后,可用标签foobar引用该锚点。“kind-spec”可以设定该tag的“kind”项,See kind, Taglist插件用kind项的值对tag进行分类。“flags”指定所使用的正则版本及是否区分大小写。默认使用扩展的正则表达式(Extended Regular Expression)即egrep所使用的正则语法。加上b可使用基本的正则语法——grep的默认的正则语法。加上i,则不区分大小写。如:–regex-texinfo=/^@chapter\W+(.+)$/\1/c/

下面是一个完整的例子。我们会为ctags增加VBScript的支持。3 我们的目的是在看到某个函数时可以方便地跟到定义该函数的地方。为此我们需要抓函数的定义作为锚点,以函数名为标签。明确了目标后剩下的就是写正则表达式了!

在这一节写作过程中,我想到可能已经有人写过了这方面的内容,然后找到了这个:ctags增加VB支持。总的来说它VBS与VB的语法相似,正则也相近,除了VBS要简单得多。考虑了许久为了文章的完整性还是留下这一部分的内容,而不是只留下链接。

如果你不熟悉VBScript的话,先看一下VBS中定义函数的两种方法:

' 无返回值的以Sub关键字定义函数,没参数的话可以不加括弧
Sub mysub(argument1,argument2)
 ' 语句放在这里
End Sub 
	
' 有返回值的以Function关键字定义函数
Function myfunction(argument1,argument2)
 ' 语句放在这里
 ' 返回值是字串foobar
 myfunction="foobar"
End Function

对于这两种函数,我们在抓tag时候将它们的kind分别设为s和f,分别表示subroutine和function。使用这个式样就可以抓大部分的sub锚点了/sub +(\w+)/\1/s/i,但我们需要考虑到一些特殊的情况,而且5.6之前的ctags不支持\w字元。这是完整的命令:

:: 在命令行中使用时如果正则中含有空格需要用引号括起来(Linux用单引号,Windows双引号)
ctags --langdef=VBS --langmap=VBS:.vbs --regex-VBS="/^[ \t]*sub[ \t]+([a-z][a-z0-9_]*)/\1/s/i" --regex-VBS="/^[ \t]*function[ \t]+([a-z][a-z0-9_]*)/\1/f/i" *

但我们不希望每次都使用这么长的命令,而且实际使用时我们可能需要抓更多锚点。我们将选项放到单独的文件中,像这样:

--langdef=VBS
--langmap=VBS:.vbs
--regex-VBS=/^[ \t]*sub[ \t]+([a-z][a-z0-9_]*)/\1/s/i
--regex-VBS=/^[ \t]*function[ \t]+([a-z][a-z0-9_]*)/\1/f/i

然后,使用这条命令(将options.txt改为相应的路径与文件名):

ctags --options=options.txt *

现在,在编辑VBScript的时候你也可以使用tags的功能了。不过你可能还希望让taglist也支持VBScript(或者其他你新定义的语言)——这是下一节的内容。





Next: ,
Previous: tags_sec2,
Up: tags4

4.3 增加taglist支持

Taglist确实是很好用的Vim插件。我们将对它进行适当改造使之支持我们新定义的语言。在ctags中定义完语言后。你需要:1. 下载安装taglist。2. 修改taglist,一些情况下还要修改Vim的filetype设置。3. 使用。

仍以VBScript为例,这是修改的步骤:

首先,在taglist的安装目录中搜索“s:tlist_def_{vim_ftype}_settings”。这里是taglist定义不同文件类型,对应的类型的地方——因为kind项的那个字母,在不同文件类型中有不同的含义。我们定义的VBS的kind(类型)有两种s和f表示subroutine和function。往下翻可以找到这一行:let s:tlist_def_awk_settings = 'awk;f:function' 这是awk的定义,意思是如果Vim设置的filetype为awk的话,那就将f解读为function。我们依葫芦画瓢:

let s:tlist_def_VBS_settings = 'VBS;s:subroutine;f:function'

这里是最容易出错的地方,这边的{vim_ftype}既要与ctags里定义的语言名称一致(也就是VBS),同时也要与Vim中的filetype一致。Vim中*.vbs的filetype是vb而不是单独的VBS或VBScript。所以,你要么需要改变*.vbs的filetype为单独的VBS,使用Vim能识别VBS的文件类型。要么更改变前面ctags中定义的语言名,也就是将VBS全部改为vb——这样就不需要修改Vim的filetype定义。这里我选择前者,因为VBS与vb还是差别挺大的。

所以,在$VIMRUNTIME/filetype.vim中查找:

au BufNewFile,BufRead *.vbs,*.dsm,*.ctl		setf vb

并改为(注意大小写),

au BufNewFile,BufRead *.vbs,*.dsm,*.ctl		setf VBS

其次,在taglist中搜索:

let ctags_args = ' -f - --format=2 --excmd=pattern --fields=nks '

并增加–options=options.txt,记得将options.txt改为实际的路径名和文件名。改完后像这个样子(注意字串前后都要留有空格):

let ctags_args = ' -f - --format=2 --excmd=pattern --fields=nks --options=options.txt '

大功告成。现在你可以在VBScript中使用taglist了。试一下,……语法高亮没掉了。没事,将$VIMRUNTIME/syntax/vb.vim复制为vbs.vim就可以了。





Previous: tags_sec3,
Up: tags4

4.4 生成导航窗口

在折叠篇我们用了一个唐诗的例子。在上一次我们使用折叠让显示更紧凑一点——就像是目录,我们只要点击一首诗就可以查看内容。但我们现在还是遇到了问题,我们收集的诗越来越多,为了管理方便我们把它们放在不同的文件中。只是这样一来我们没办法在同一个视图中查看所有的诗。

这次我们不仅要利用tags方便地在不同文件不同诗之间跳转,我们还要利用tags做一个导航窗口出来——这样我们只要双击某首诗的标题就可以在另一个窗口显示该诗。

《感遇其一》
作者:张九龄
兰叶春葳蕤,桂华秋皎洁。
欣欣此生意,自尔为佳节。
谁知林栖者,闻风坐相悦。
草木有本心,何求美人折?
	
《感遇其二》
作者:张九龄
江南有丹桔,经冬犹绿林。
岂伊地气暖,自有岁寒心。
可以荐佳客,奈何阻重深。
运命唯所遇,循环不可寻。
徒言树桃李,此木岂无阴。

我们选择标题作为锚点。因为所有的标题都含有《》很容易从这些锚点抓出标签。

" 一条简单的命令就可以从单个文件中抓取这种形式的tag
" 因为不需要编辑可以直接使用行号表示位置
g/《/ echo getline('.') ."\t". expand('%') ."\t". line('.')

生成的tags:

《感遇其一》	唐诗1.txt	1
《感遇其二》	唐诗1.txt	8

但我们希望在tag中增加作者的信息——这正是用ctags做不到的,同时要能处理多个文件:

" vim7 脚本
" 抓取当前目录下所有.txt的文件名,并逐一传给变量f
for f in split(glob('*.txt'),'\n')
  " 以只读方式打开一个文件
  exe 'view '. f
  " 保存以下操作的输出到tags文件
  redir >> tags
    " 抓取如下格式的tag:{标签}	{文件.txt}	{行号};"	author:{作者}
    g/《/ let nl = getline(line('.')+1) |
      \ let author = (nl=~'作者:' ? substitute(nl,'作者:','author:','') : '') |
      \ echo getline('.') ."\t". expand('%') ."\t". line('.') .';"'."\t". author
  redir END
endfor
" 任务完成
qa!

在shell/命令行使用这条命令,

" 将xxx.vim换成脚本的名称
vim -S xxx.vim

这将会在当前目录生成如下内容的tags:

《感遇其一》	唐诗1.txt	1;"	author:张九龄
《感遇其二》	唐诗1.txt	8;"	author:张九龄
  ...

事实上这个例子中用gawk或perl会更自然也更方便(虽然Vim脚本也不长,但操作起来步骤比较烦琐),而且快得多。记住,Vim的用户总是选择合适的工具完成工作——这同样也是我们选择Vim的理由。

现在我们有所有诗的tags了。现在用Vim打开tags文件。在任一首诗的标题上按CTRL-]就可以转到相应的位置。再按CTRL-T回到tags文件——似乎这样就可以收工了。

不过我们有一些附加的要求,我们希望能控制导航窗口的行为和内容。首先,这个导航窗口应该是持续打开的。其次可以使用双击在其他的窗口打开一首诗(而不是在导航窗口中)。最后,要能按我们的格式显示导航的内容。这需要20行左右的Vim代码:

" 用来更新目标窗口内容及更新目标文件名的函数。
" 当双击或者输入CTRL-]时调用这个函数。
function! SToc(tag)
  " 高亮标题
  exe 'match Todo /\%' . line(".") . 'l/'
  " 获取目标窗口当前的编号
	let nr=bufwinnr(bufname(g:xbn))
  " 跳到目标窗口
	exe nr."wincmd w"
  " 在目标窗口中打开tag
	silent! exe "tag " . a:tag
  " 更新目标窗口中的文件名(全局变量)
	let g:xbn=bufname('%')
endfunction
	
" 负责初始化的函数
function! IToc()
  " 如果当前编辑区无文件,则打开一个临时窗口
  if bufname('%')=="" | view _blah_  | endif
  " 初始化全局变量,这个变量用来跟踪当前编辑区的文件名
	let g:xbn=bufname('%')
  " 打开一个窗口并做导航
  vsp __目录__
  " 不需要实体文件
  setlocal buftype=nofile
  " 简单的语法高亮
  syn match Comment "[^-]"
  " 从tags读取信息并转换成“用户友好”的格式显示
  call append(line('$'),
    \map(taglist("^"),
    \'substitute(printf("%-30s%s",v:val["name"],' .
    \'(has_key(v:val,"author")?v:val["author"]:""))," ","-","g")'
    \))
  " 定义导航键
  map <2-LeftMouse> :call SToc('/'.expand("<cword>"))<CR>zt
  nmap <C-]> <2-LeftMouse>
endfunction
	
" 定义打开导航窗口的命令
command! -nargs=0 Toc call IToc()

这20行脚本中有几个写Vim程序常用到的技巧。首是多个窗口的管理控制。其次是建立临时文件。还有taglist()函数的使用。最后是用command命令建立宏(command 的好处是可以使用参数,不过这里没有用到。)

将脚本保存起来,用下面的命令运行

" 用下面的命令载入脚本,将xxx.vim改为实际的文件名
:so xxx.vim
" 使用这条命令打开导航窗口
:Toc

也可将脚本放到plugin目录下,这样只要随时输入:Toc就可以开始阅读了——当然,必须确保tags文件在'rtp'或当前的目录中。现在我们可以使用导航窗口中使用双击打开任意一首诗了……

vim_tut_poem

你可能会好奇为什么我们要大费周章写这样的一个创建导航窗口的脚本呢?在我们最初的版本中,我们只用了:g命令就生成了第一个tags文件。在第二个版本中我们使用脚本在tags文件中增加了额外的信息(作者/诗人)。我们只需要在Vim中打开tags文件就可以实现导航。但我们想要控制显示的内容,而tags文件只需要在后台控制跳转。为此我们写了上面的脚本。在使用tags做导航时,我们需要控制显示在导航区的内容——而这一是实现更复杂应用的基础。附图是一个查看邮件的应用,是基于同的的技巧实现的。

vim mail reader




Previous: tags4,
Up: Top

5 小结

tags的作用无非是帮助Vim查找定位跳转,但同样是跳转在不同的应用下起的作用也不一样:

  • 在文档或文档之间的不同部分建立连系。例如我们可以在一个文件的底部放上其他文件的标签,让用户方便地移动,就像是网页的“上一页”“下一页”。
  • 提供交叉引用。方便用户使用参考。如同Vim的文档,每一个词条对应着该词条的解释。最更要的也许是:无论是使用ctags还是其他工具,只要能找到自动生成tags的规则,我们就不需要人为的维护不同文件之间的关系了。这在Vim文档的例子中尤为明显——写Vim文档时只要遵循约定,Vim就能自动维护文档之间的引用关系。手工维护这些交叉引用是不可想像的。
  • 为文档建立结构图或导航图。就像前面唐诗的例子所展现的一样。
  • 特殊应用。作为补全的匹配源;信息汇总,等等……比如,抓一个网页的所有链接,再集中显示在一个新窗口。 4

在文档间建立联系或提供交叉引用都是只要有tags就可以,而显示结构图或导航图则需要一点额外的努力——因为我们需要多开一个窗口。这种情况下应首先考虑taglist是否能做为现成的方案。

tags能起什么作用取决与tags文件都有什么内容。因为生成什么样的tags才是决定tag功能的关键。如果出于编程的需要而用tags的话,那可能已经有某种ctags程序支持你所用的语言了。否则你可以用ctags的定义语言功能为某种语言生成tags文件。在一些应用中我们可能需要Vim或awk,perl这类工具生成一些特殊的tags。但总的来说无论使用哪种工具都是关于正则表达式的运用。


Footnotes

[1] exuberant ctags != etags

[2] 较早的版本会直接显示支持的语言在帮助中,只要输入:ctags –help。就可以在最后的部分看到–{language}-types=…

[3] 为什么是VBScript?我们需要在ctags不支持的语言中找一种语法简单方便讲解,而又相对为人所知的语言做例子说明——其实选择不多。

[4] 在HTML文档中使用taglist时,会显示所有锚点的name

2006, June 27

Vim7中的万能补全(Omni completion)

Filed under: vim, 编辑器, 教程




Up: (dir)

Vim7中的万能补全(Omni completion)

自动补全是Vim的一项重要功能。但由于中文的特性,这项功能对很多中文用户来讲显得不那么实用。在中国这项功能几乎只局限于写程序时使用了。
Vim支持多种形式的补全。补全的使用方法是按C-X,再根据所使用的补全输入相应的键。

  • 整句补全对中文用户还是比较有用的C-l(这条命令在所有的buffer中查找匹配的行)。
  • 单词补全基本没用因为中文的书写并不以词为单位(没有分隔)C-n(倒是也可当成整句补用)。
  • 字典补全c-k和thesaurus补全。同样的对于中文只能当成整句补用。
  • 单词补全还有一种C-k,除了在当前文件中找匹配外也在包含文件中查找匹配──对编程来讲比较有用。
  • tags补全C-j。
  • 文件名补全C-f。
  • 定义和宏补全C-d。
  • Vim命令补全C-v。
  • 用户定义补全C-U。万能补全C-O。



1 万能补全基础

本文关于万能补全(omni completion)的所有内容同样适用于,用户定义补全(user completion)。

什么是万能补全?万能补全有什么作用呢?
万能补全是一种按自定义规则进行补全的补全功能。它的好处在于他给了用户满足特殊需要的灵活性。

万能补全的使用方式是在插入模式下输入<C-x><C-o>(或者<C-x><C-u>)。不过在你使用这项功能前你得先在自定义函数中定义补全所使用的规则。并相应地设置'omnifunc'(或'completefunc')。比如你自定义的规则在自定义的UCompl()函数中则设置:

se ofu=UCompl

那这个自定义的函数或者说自定义的补全规则要怎么写呢?这就是我接下去要讲的最主要的内容。

这里先看一下自定义补全函数的基本框架:

func! Mycomp(start,base)
    if a:start
        " 返回欲匹配字的起始位置。对于英文就是往前找到第一个非字母字符的位置。
    else
        " 返回匹配列表。
    endif
endfunc

之所以有这种奇怪的结构是因为这个函数实际要干两件事,就是上面注释中写的那两件事。这个程序会被调两次。
a:start可以认为是一个flag,第一次调用时这个值为1第二次调用时值为0。关于这个参数我们稍候再介绍.
如果第一次调用时未返回值则base为整行。返回负数base为空。
第二次所返回的将是匹配列表。空列表(list)或负值视为无匹配。

这里有两个参数是Vim传给omnifunc的他们是start,base,(当然在自定义函数内部,你可以任意取名)这两个参数是只读的。start刚已经讲过了,而base表示的是要进行补全的内容。如

how ar_
^^^ ^^

这里用'_'来表示在插入模式下的光标(下面如无说明最后的'_'都是表示插入模式下的光标位置),在使用C-X,C-P后会出现补全列表──如果有的话。其对应的base就是'ar',然后C-P在当前文档中寻找匹配base的单词。而omni与C-P的不同之处在于base的产生方法是由omnifunc决定的──即由用户决定的。前面我们说了omnifunc被调用了两次第一次返回的值,Vim会将之视为欲匹配字的起始位置,这个位置到光标所在栏之间的字串就是base。

使用万能补全用户需要在函数中定义起始位置的计算方式,而base会由编辑器自动计算,并在第二次调用函数时给出。还有一点要注意的就是当我们使用了补全功能后base部分的字串将会被补全所使用的项所替代

我再给一个例子,输入:

abcd_

在上面的文本中,光标的位置是5(即该行第5个字符的位置)而'a'的位置是1。如果第一次调用函数返回的值是2,即字符'b'的位置,则column 2跟5之间的内容即"cd"就是第二次调用时base的值。现在做一个试验:

func! Mycomp(st,base)
	if a:st
		return 2
	else
		echo "base=" . a:base
	endif
endfunc
se omnifunc=Mycomp

运行上面的脚本并输入:

abcd_

因为是在插入模式,_表示光标所在的位置。这时如果我们输入C-X,C-O使用万能补全,可以从命令窗口看到base就是‘cd’。

现在你文该明白第一次调用是怎么一回事了。这个函数的前半部分定义了base的计算方式。那后半部分也很容易理解了,这一部分用来返回补全的列表
现在我们可以写出一个有完整功能的补全函数了:

func! Mycomp(st,base)
	if a:st
		return 2
	else
		echo "base=" . a:base
		return ["XXX YYY"]
	endif
endfunc
se omnifunc=Mycomp
abcd_

补全的结果如下:

abXXX_

这个函数看上去不怎么实用,但功能完整。它“计算”base起始位置(其实它只是简单地返回2即返回'b'的位置,根本没进行什么计算),然后给出两个用来代替base的固定选项——只是它根本不考虑你前两个字符输入的是什么;-)



2 在返回的列表中使用字典

自动补全返回的是列表,但列表项可以使用字典的形式。如:

return [{"word":"abc","kind":"v","info":"变量"}, {"word":"eee","info":"也是变量"}]

其中word就是补全的值,而info是一些附加信息将会在preview窗口中显示。其他可以在返回列表项中使用的(key)可以见*complete-items*



3 更多

为了做出更智能的补全函数,我们要先赋于函数判断base的能力。对于编程或是英文书写的需要来说判断base很简单只要往前找到第一个空格的位置。为此我们需要取前一个位置的字符并判断是否是空格。不是则重复。文档中已经有了一段示例代码:*complete-functions*

	" locate the start of the word
	let line = getline('.')
	let start = col('.') - 1
	" 直到找到非字母字符或行首
	while start > 0 && line[start - 1] =~ '\a'
		let start -= 1
	endwhile
	return start

计算起始位置的代码依不同的目的而有所不同但大体都是以这种形式出现。对大部分的应用来说也许更为重要的是如何返回有效的列表。

	" find months matching with "a:base"
	let res = []
	for m in split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec")
		if m =~ '^' . a:base
			call add(res, m)
		endif
	endfor
	return res

其实Vim并不在乎你返回的结果是怎么计算出来的,它只在乎有没有返回列表。要计算返回的列表,有许多方式。在一般的情况下我们会先找(至少)一个大的匹配源,然后在这个源中找出匹配base的项并返回。而不同补全方式的主要差别也在于此。可以认为,C-P就是以当前buffer为匹配源。C-L以所有buffer为匹配源(同时,它的start总是0)。字典、tags以外部文件为匹配源。上面这个例子中匹配源在脚本中直接以列表的形式给出。

在omni中我们可以选择其中一种匹配源,也可以混合多种——这完全取决于我们的需要。比如我们写一个补全当前目录名的函数,当前目录就是匹配源。

下面的脚本根据已输入的字串补全目录名。

func! Dircompl(st,base)
if a:st
    " 未进行起始位置的判断所以只限于在行首使用。
    return 0
else
    let res=[]
    " 列出所有目录,并测试是否匹配
    " (也可以直接用find)
    for f_name in split(system("ls -m"),",")
    " for f_name in split(system("dir /b"),"\n")
        " 目录必须是以base开头的
        if f_name=~ '^' . a:base
        " 目录只须含base
        " if f_name=~ a:base
            call add(res, f_name)
        endif
    endfor
    return res
endif
endfunc
	
se ofu=Dircompl
"se cfu=Dircompl



4 使用外部文件

前面的两个例子中我们用了外部程序来产生匹配源,但更多情况下我们会将匹配源置于外部文件之中。补全函数再对外部文件进行过滤产生补全列表。那我们要如何在Vim脚本中读入或者说是使用外部文件呢?

  1. 简单的方式是使用外部程序读入数据。不足是可移植性稍差。下面是三种不同格式的数据及其对应的读入命令:
    每行为一条数据,

         "hack"
         "hand"
         "heep"
         "hit"
         "hold"
         "hope"
         "how"
    

    数据直接定义为列表格式,

         ["hack","hand","heep","hit","hold","hope","how"]
    

    数据定义为以字典为列表项的列表格式:

         [{"word":"hack","info":"这是hack的info"},{"word":"hand","info":"这是hand的info"}]
    

    上面的三种格式可以分别用下面的三条命令读入,

         exe 'let wordlist=['. system('cat words|tr "\n" ,') . ']'
         exe 'let wordlist='. system('cat words')
         exe 'let wordlist='. system('cat words')
    

    读入数据后还需要对wordlist进行过滤并返回合适的匹配项作为补全列表。

    提示:外部数据的格式很大程序上决定了函数的复杂程度,为了简化解析数据所需的代码应该尽可能地使用易于使用Vim进行处理的数据格式。

  2. 以Vim脚本的格式保存数据
    上面的第二个外部文件的格式使用的就是列表的格式。如果我们直接将外部文件以Vim脚本的格式保存又会如何呢?下面是新的数据文件,同时也是脚本文件。

         let g:wordlist=["hack","hand","heep","hit","hold","hope","how"]
    

    使用so或ru我们就有了匹配源,接下来就是过滤了。

         so words
         if exists("g:wordlist")
             call filter(g:wordlist,'v:val =~ '.a:base)
         	return g:wordlist
         endif
    
  3. 使用函数readfile()
    readfile()是个很好的解决方案:读入的每一行自动成为一个list项。使用了这个函数的脚本是平台独立的。

         for line in readfile(fname)
         	if line=~a:base
         	  let res=add(res,line)
         	endif
         endfor
         	return res
    
  4. 以tags格式保存数据
    tags格式保存的数据可以直接抓出为dictionary类型的数据。方法是使用*taglist()*

         let tl=taglist("thetag")
    

    但是光抓出来还不行,tags使用的与自动补全接受的有些不同。比如:taglist使用name作为第一栏数据的名,而在补全函数支持的是word键。所以我们需要重新分配一下结果,使用能为我们所用:

         let trs["word"]=tl["name"]
    

    taglist()完整的用法可以参考下面的例子。



5 实例

最后我们用一个简单的例子来结束关于万能补全的讨论。这个脚本根据外部文件对html标签进行自动补全。这个外部文件共有三栏,栏与栏之间用<tab>隔开——与tags文件的格式一样,这样我们就可以使用taglist()读入数据。

<body 	html4	<body>标签
<blink 	deprecated	闪烁标签,w3c不推荐使用此标签
<br>	html	换行标签
<br />	xhtml	换行标签
<table 	html	table 表格标签,用来新建一个表格
<th 	html	table header
<td 	html	table cell
<tr 	html	table row

下面是代码:

func! Mycomp(st,base)
    if a:st
        let start=col('.')
        let line=getline('.')
        " 往前找到第一个'<'字符的位置
        while start>0
            if line[start-1]=='<'
                " 返回该字符的前移一位的位置
                return start-1
            endif
          let start-=1
        endw
        " 没找到。返回0这样在新起一行时可以显示全部列表
        return 0
    else
        let res=[]
        " 如果html标签文件的名称不是'tags',可用用下面注
        " 释掉的代码更改供taglist()抓tag的文件名
        " let oritags=&tags
        " se tags=./htmltags
        " 抓出匹配base的tag
        let tl=taglist("^" . a:base)
        " let &tags=oritags
        " 将抓的结果改为补全功能能接受的形式
        " 为了避免逐一地修改,需要使用map()
        call map(tl,'s:T2l(v:val)')
        return tl
    endif
endfunc
	
func! s:T2l(val)
    let res={}
    let res['word']= a:val['name']
    let res['menu']= a:val['filename']
    let res['info']= a:val['cmd']
    let res['kind']= a:val['kind']
    return res
endfunc
	
" se cfu=Mycomp
se ofu=Mycomp

这个脚本的作用就是根据tags文件的内容补全html标签。功能很简单但已经用到了许多构造强大补全函数所需的元素。更复杂的例子可以在vim7的autoload文件夹中找到。有兴趣的用户不防了解一下Vim自带的补全函数是如何工作的。截图见这里



Appendix A 中文议题

中文使用补全时的难点主要有三点,一个是起始位置的计算;二是字节与编码;三是数据文件的编码。
中文的字词之间没有空格要判断起始位置不好判断。(而单词补全的功能还不如输入法带的字词联想有用)。二是依编码的不同字有可能是两个字节三个字节。而起始函数返回的不是该行的第几个字而是第几个字节的位置。一个方法是定义快捷键,使不同的热键表示不同的起始位置(往前两位,往前三位,往前六位)但这样却失去了灵活性。三Vim不自动判别数据文件使用的编码,这样补全函数使用外部的数据文件时提示列表中可能会出现乱码。

以utf-8编码为例,如果要以当前光标的前一个字为base:

这是示例文_

utf-8的中文有三个字节为了使上面例子中的base为“文”,必需返回col(".")-4,而对于euc-cn/cp936编码则是col('.')-3,因些在处理汉字时需要对编码进行判断。这显然又增加了复杂度。

最后是如果正在编辑的文件与匹配源使用的编码不同,同一个汉字也会出现不匹配或者列表为乱码的情况。为些在进行匹配操作时要先对匹配源的编码进行转换。

最后我们写一个使用字典文件的补全函数。这个字典文件是以utf-8编码保存的(即&enc=="utf-8")。对于英文单词仍按一般方法返回base的起始位置——即往前找到第一个非字母字符的前一位置。而中文我们一律返回当前光标的前一个字。

先看一下字典,这是个中英文混合的字典(注意:是utf-8编码的):

hack
head
hello
jack
joke
joseph
情形
情况
文化
文本
文明
文人

这是最后的函数:

func! Mycomp(start,base)
    if a:start
    let start=col('.')
    let line=getline('.')
	
    " 如果是半角字符则照一般的英文规则,
	" 往前找到第一个非字母字符
    if line[start-2]=~'\a'
        let start-=1
        while start>0 && line[start-1]=~'\a'
            let start -= 1
        endwhile
        return start
    elseif line[start-2]=~'[[:punct:]\s\d ]'
        " echo "space"|sleep 1
        return -1
    endif
	
    " 如果不是字母也不是数字或空格标点则假设为汉字
    " 固定返回前一个字符
    if &encoding=="cp936" || &encoding=="euc-cn"
        return start-3
    elseif &encoding=="utf-8"
        return start-4
    endif
	
    else
    " echo a:base|sleep 1
    let res = []
        for line in readfile("words")
    " 字典文件以utf8格式保存,需要进行编码转换
     call add(res,iconv(line, "utf-8",&enc))
    endfor
    return filter(res,'v:val =~"^'.a:base.'"')
    endif
endfunc
se ofu=Mycomp

提示:上面的自定义补全函数的行为与字典补全(<C-x><C-k>)的行为相似,除了两点:一处理汉字时我们的自定义补全函数并不是以前的第一个非空字符的位置作为base的起始位置(因为汉字并不以空格作为字词之间的分隔符),而是简单的往前移一个汉字。二字典补全在比较前并不进行编码的转换,因此在字典文件与当前编辑文件的编码不同时Vim不能正确给出匹配的汉字列表。当然这个函数的目的不在于取代字典补全而是演示在万能补全中处理汉字的一些注意事项。



Appendix B 用自动补全来计算结果

自动补全并不一定要用来“补”。下面的函数通过外部程序进行计算并以补全的方式给出结果:

func! Mycomp(st,base)
if a:st
    " 未进行起始位置的判断所以只限于在行首使用。
    return 0
else
    let res=[]
    call add(res,system("echo " . a:base .'|bc|tr -d \r\n'))
    return res
endif
endfunc

运行结果(下划线表示插入模式下光标所在位置):

2+2*3+(2+2)*2_

按<C-x><C-o>返回16。


附图:
vim_oc

外部程序::Vim进阶索引[6]

Filed under: vim, 编辑器, 教程




Up: (dir)

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

  • 设置项:'cscopeprg'
  • 说明:cscope是c/c++的源代码分析工具。'cscopeprg'默认设置即为“cscope”。要使用这项功能除了在编译时+cscope外,还要求系统上已安装了cscope类的程序。:cscope命令在Vim有一个专有的运行介面和选项,只适用于cscope或类似的工具。cscope虽然是为c/c++而生的,但存在着许多类似工具。如果在Vim的搜索路径或在系统的路径上只要将相应程序的名称传结csprg,否则必须写上完整路径。
  • 用法:在vim中使用这条:cs命令之前需要先在命令行下运行cscope生成数据文件。然后再在Vim中使用:cs add载入数据文件。输入:cs可看到在线帮助。当然还可以*cscope*。详细用法见文档。

=

  • 设置项:'equalprg'
  • 说明:=这是条一般模式下的过滤命令。用来对c程序进行缩进。在缺省的情况下,这条命令用来进行C缩进。通过对'indentexpr'进行设置我们还可以决定缩进的方式。

gq

  • 设置项:'formatprg'
  • 说明:这也是一条一般模式下的过滤命令,用来整理段落。缺省情形下使用内建的功能重排段落,可以通过调整,'tw''formatoptions'等项细化重排功能。缺省情况下调用Vim内置函数进行格式重排。

:grep

  • 设置项:'grepprg'
  • 说明:grep查找文本,并返回结果(到错误列表中)。正如它的名称一样这条命令的作用就是跟!grep是一样了,因为在Unix下它就是调用grep程序──除了一点:Vim会为:grep命令生成一个窗口和匹配列表。用户可以在结果文件中跳转。Unix下默认设置为"grep -n",Win32环境中默认设置为"findstr /n"。相关的设置项有'grepformat',缺省下不需要设置。

:make

  • 设置项:'makeprg'
  • 说明:写程序的话对这个应该不会陌生。默认情况下设置为make。这条命令在Vim中运行各种的make类工具。而它存在的理由和grep一样,Vim中我们可以通过结果方便地导航(通常是在“出错”的情况下)。

K

  • 设置项:'keywordprg'
  • 说明:这是个一般模式命令用以运行查找Vim文档或man的命令。这条命令的特别之处在于传递参数给外部程序的过程将由Vim自动完成。它将当前“词”做参数传给'keywordprg'。缺省情况下,使用man(Unix)或内置的:help命令(Windows)。相关的设置项有:keywordprg iskeyword

:shell

  • 设置项:'shell'
  • 说明:Linux下一般是根据环境变量`$SHELL‘或设置为`sh‘。Win32下默认设置为`command.com‘或`cmd.exe‘。如无特殊说明这一系列教程中所使用的命令解释器分别为Linux下的`sh‘(或`bash‘)和Windows Xp下的`cmd.exe‘。这个命令涉及的还有外部程序和脚本的使用!,所以无特殊需要最好不要改动。我们在Vim中的:sh命令,!过滤命令等都是通过shellprg 的设置来运转的。

    Linux下一般是根据环境变量`$SHELL‘或设置为`sh‘。Win32下默认设置为`command.com‘或`cmd.exe‘。如无特殊说明这一系列教程中所使用的命令解释器分别为Linux下的`sh‘(或`bash‘)和Windows Xp下的`cmd.exe‘。这个命令涉及的还有外部程序和脚本的使用!,所以无特殊需要最好不要改动。

提示:这些设置的一个共同点是它们都不能在模式行中进行设置。出于安全的原因在模式行中不能使用这些设置项。

Vim的Diff模式就是使用了工具GNU diff,这个程序是Vim的重要组成,Vim并未提供相应的设置项让用户更改。与vim相关的外部程序还有ctags类的程序,这类程序用来生成关键字的索引。但Vim用到的是tags文件所以也没有提供相应的设置项。(即使要生成tags,ctags也不是必需的)

这些就是使用或可设置为外部程序的命令。总的来说这些命令在某些方面提供了方便。但从功能性来说它们并非是必不可少的。不过既然有了我们还是利用起来吧。

由于这些命令的特性我们可以用来做一些有用的事情,现在来看一下我们还能怎么用这些命令。

=
gq
这两条命令都是在一般模式中使用的过滤命令(一般的过滤在行模式中使用)。它们的工作方式都是一样的:对一定的范围使用过滤程序/内部命令,所以我们放在一起。通过指定合适的过滤程序我们就能决定这命令的作用。
比如:设置为sort可以用来排序`se equalprg=sort‘,设置为`se equalprg=sort\|uniq‘,排序并去除重复。
总之任何过滤程序都可以在这里使用──而决不仅仅可以使用缩进或文本格式化的工具。
K
与前面两个命令不同,这个命令的目的并非改变当前文本的内容或格式。这个命令以当前字为或选区(至多一行)为参数运行命令 。自动传递参数给相应的程序。利用它自动传递参数的特点我们可以运行一些需要参数的程序。
除了用来查找Vim文档,man文档外最直觉的一个用法就是用来查字典了。只要是以一个字词为输入的所有程序都可以定义为'keywordprg'。如:stardict、locate、which等。

下面是用K来运行当前圈选1的网址的例子(windows平台):

     se kp=start\ c:\\progra~1\\opera\\opera.exe

:grep
:make
这两条命令的真正有用的/有效的特性在于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

对于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中使用命令不外乎有两种情况。

  1. 是把Vim当成一个shell接口(这种情况下要注意命令参数中的特殊字符。见*cmdline-special*)。如
         !touch abbcc
         !rm abbcc
    

    当然你也可以运行图型介面程序:

         " 用Opera预览效果
         !opera %
    	
         " 或者你想要后台运行firefox
         !firefox % &
    

    简单说这种使用方式就是把Vim当成shell用。但是在Vim中方便地运行其他程序不是我们的首要目标。Vim是一个编辑器,所以我会着重在编辑相关的内容──就是下面要讲的第二。

  2. 是为了获取程序的输出。又可细分为两种。过滤程序(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'
           

Unix下有相当多丰富的过滤程序资源,Windows下这类的工具较少(sort、more和findstr)。好在Unix下的最好的命令行工具(尤其是Gnu版本)通常都有Windows版本。它们通常很小——可以在这里下载。其中最强大的两个工具是awk、sed和CoreUtils工具包,稍大一点的选择是perl。

2.2 实例

由于这一篇更多地是涉及到工具软件的使用而非Vim本身,所以不会对这些例子进行很详细的解说。这里演示一下实际应用中外部程序的用法。这一次仍是以成绩单为例,这是其中的三条数据:

姓名    期中  期末
李阿月  72    70
林小丽  91    93
王小明  46    56

共三栏栏与栏之间用一个或多个空格隔开。

看一下外部命令是如何逐一完成任务的,思考一下用纯Vim的方式要怎么达到同样的目的。

  1. 首先是成绩单排序。按期末成绩,然后期中成绩:
         :2,$!sort -k3n -k2,2n
    
  2. 根据前面排序的结果,写上排名(在Unix上可以输出序号的工具很多如nl cat等,不过这次我们要用的是grep):
         " 工具之间也可以协作
         :2,$!grep -n .|tr : ' '
    
  3. 计算平均成绩。在寄存器篇中我们用了下面的命令来更新目录中的行号:
         :1,25s/[0-9]\+$/\=submatch(0)+25/
    

    我们可以用同样的方式计处平均成绩,但Vim不支持浮点运算。
    而成绩通常会精确到0.5,所以我们使用了下面的脚本:

         :2,$!gawk '{$0=$0 "  " ($3+$4)/2}8'
    
  4. 如果我们打算在下面增加一栏计算总人数。总平均成绩,用Vim脚本就没那么容易了。
         :2,$!gawk '{sm+=$2;sf+=$3}8;END{print "人数:" NR " 期中平均:" sm/NR " 期末平均:" sf/NR}'
    
  5. 我们还能在成绩单后加入简单的统计,
         :2,$--!gawk -f myawkscript.awk
    

    上面的命令后会在成绩单后面加入下面的内容:

         -- 期末考成绩分布图示 --
         优	4人	%16■■■■
         良	8人	%32■■■■■■■■
         ....
    

    提示:对myawkscript.awk有兴趣的话可以见附录

  6. 导出为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

myawkscript.awk的代码:

# /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 ""
   }
}



Footnotes

[1] 通过自定义一个识别网址的脚本,可以省略圈选的步骤。不过这是题外话了。

2006, June 18

Vi/Ex编辑器教程[4]

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

Vi/Ex编辑器

作者:Walter Alan Zintz
译者:hq00e (at) 126.com
原文:The Vi/Ex Editor



第四章 替换命令

除了在这篇教程的上一篇中讲过的全局命令(:global)外在行模式命令中就数“:substitute”命令也就是替换命令最为精细和复杂了。在讲完复杂的部分后我们就会开始接触那些用以构建强力行模式命令串的技巧和窍门。




下一个: ,
上层: substitute

对当前行进行替换

你们中的大多数人应该已经知道替换命令的最简略的写法是“:s”并且已经以这种形式:

     s/previous/former/
     %s/Smith/Lee and Smith/

分别用来为当前所在行做替换和对文件中的所有行作替换。你如果已经在使用这两种形式的替换了,那你的学习进度已经超前了。有太多的课堂讲师和教课书的编撰者都告诉你要在所有行中替换某个短语要使用类似下面的命令:

     global/Smith/s//Lee and Smith/

这只是在浪费时间。两种的形式干的是一样的活,但第二种形式要用掉更多的输入时间和电脑解释命令的时间。在两个版本的命令中不管文件中是否每行都有要替换的“Smith”亦或整篇文档中只有一个“Smith”,替换命令都能很正常并且安静地完成工作。

但两种形式的命令都不保证对文件中所有的“Smith”进行替换。替换命令在默认情况下只对行中出现的第一个目标字串进行替换,因此像下面的这一行:

     inure to Smith's benefit only if Smith shall

在运行完两个版本中任一版本的替换命令后成了:

     inure to Lee and Smith's benefit only if Smith shall

这个问题有一个行模式内建的办法:要对一行中所有的目标字串进行替换可以在替换命令后加上小写字母“g”,就加在最后一个“/”后。因此使用如下命令:

     % substitute /Smith/Lee and Smith/g

原来的行会被替换成:

     inure to Lee and Smith's benefit only if Lee and Smith shall

上面的命令稍加变化就更应用在其他场合上。假如正在处理一些表格,并想每行中的最后一个“k37”改为“q53”。可以用这条命令:

     % substitute /\(..*\)k37/\1q53

这样就行了吗?如果你觉得讶异的话,那就请记住:在搜索中使用通配符时,编辑器总是让通配符所匹配的字串尽可能长。这个例子中被匹配的字串从行开头的第一个字符开始一直到最后一个“k37”。

现在你应该试着举一反三。使用什么命令才能仅替换每行中的倒数第二个“k37”呢?这有点小难,所以我提供了一个参考答案可以和你自己的答案做个比较(见附录)。




下一个: ,
上一个: sect1,
上层: Top

更多的字元

替换命令用搜索式样(即替换命令的left-hand-side pattern,简记为lhs)来指示要被替换的文本。而,也许你已经知道了,你并不是每次都要输入同一个搜索式样。如果你想要重用上一次使用的搜索式样可以使用空的搜索式样来表示上一个搜索式样――不一定要在替换命令中,在其他情况也一样。因此下面的两条命令是一样的。

     /Murphy/ substitute /Murphy/Thatcher/
     /Murphy/ substitute //Thatcher/

两条命令都会到包含“Murphy”的行,并将第一个“Murphy”改为“Thatcher”。

用来表示地址的搜索式样中使用的字元,同样可以在替换命令中的搜索式样中使用。此外还有两个字元是替换命令独有的:字元“\(”和“\)”。 这两个字元本身并不匹配任何字符,因此:

     substitute /^The cat and small dog show/
     substitute /^The \(cat\) and \(small dog\) show/

从给出的部分来看他们是一样的命令。但替换命令除了查找匹配式样的字串外还能记住字元对“\(”和“\)”中的搜索式样所匹配的字串,以便在替换文本中使用。当替换式样中包含“\1”时,编辑器会将该字元(\1)替换成在字元“\(”和字元“\)”之间的式样所匹配的字符或字串。而替换式样中的“\2”则会被第二个“\(”和“\)”字元对中间的式样所匹配的字符或字串所替代。依此类推――在一个替换命令中最多可使用9对这样的字元。在被替换文本中的字元对甚至能嵌套:左起出现的第一个括号对用“\2”,依此类推。所以如果将上面的命令补充完整:

     substitute /^The \(cat\) and \(small dog\) show/My \2-\1 fair

在被替换后的行的开头部分就像这样:

     My small dog-cat fair

又或者你输入了:

     substitute :up \(and \)\(over \)\(the sky\):\2\1\2\1\2\3

会对下面的第一行替换,替换结果如第二行所示:

     up and over the sky
     over and over and over the sky

(我在上面的例子中用了冒号“:”来做为分隔符来分隔替换命令的不同部分。之所以没有用“/”则完全是为了方便读者阅读――因为读者很容易将“/”、“\”或“l”和“1”弄混。)

如前一个例子所示,对于简单的文本搜索式样而言“\(”和“\)”的用处不大。它们真正的价值只有在搜索一些容易输错的文本时才体现出来,可以避免因手工输入而在被替换式样中输错的可能。

有时你会用一些文本或式样来帮助搜索定位,这部分的内容将在替换时被放回原来的位置。这时这两个元字符就能派上用场了。(为了准确地圈定被替换文本我们通常需要在式样中包含该段文本周围的文本以帮助定位,这些帮助定位的文本在替换后会被放到原来的位置上。)这里有三个这种替换方式的例子:

     % substitute :\([Ss]ection\) \([0-9][0-9]*\):\1 No. \2:g
     /\([Ss]ection\) \([0-9][0-9]*\)/ substitute ::\1 No. \2
     % substitute ,[Aa]nswer: \([TtFf] \),ANSWER: \1,g

第一个例子中,仅仅是将“No.”插入到文档中的“Section”与号码之间。“\(”和“\)”的作用就只是将原来的section和其后的号码保留起来,这样在替换后它的第一个字母S的大小写就不会改变。

第二条命令的作用与第一条命令差不多,但第二条命令只对跟当前行最近的一个匹配项进行替换(不对当前行的匹配项进行替换)。这里的特殊之处在于我在地址式样中使用了“\(”和“\)”。当然,行地址本身并不使用这两个字元,但这也不会影响它找到正确地地址。行地址在寻找自己所匹配的行时会将这两个字元忽略,但在接下来的替换命令重用了上一个搜索式样时,这两个字元也会做为搜索式样的一部分一起传给替换命令。第二条命令就是这样情形。

第三例子对整理习题答案很有用。它查找每一个“Ture”或“False”判断题的答案,并将词“answer”转成大写。这条命令的创新之处在于:它在单词“answer”后面查找“T”、“t”、“F”和“f”(即上面提到的帮助定位的文本),这样当后面的答案是数字而非“true”或“false”时,“answer”的大小写就不会发生变化。另外用来表示“true”与“false”的字母(“t”“f”),在替换后并不会发生改变。但是这个例子的教育意义大于实际意义――因为稍后我们会看到有些字元可以轻易地完成这个例子所完成的任务。




下一个: ,
上一个: sect2,
上层: Top

在替换式样中使用的字元

当你想通过替换命令来添加一些字时,你可以使用一些仅在替换式样(替换式样即替换命令的right hand side,简记为rhs)中使用的字元。它们与搜索式样所使用的字元完全不同。

&
在替换式样中,“&”表示替换命令中的被替换部分的文本。当你纯粹想加入而不是替换一些文本时就使用这个字元。例如:要将“kit”改为“kit and kaboodle”(忽略kit首字母的大小写)时,可使用如下命令:

          % substitute /[Kk]it/& and kaboodle/g
     

如果你关掉了“magic”选项,就必须在“&”前加上反斜杠以使用它的元值。而在打开magic选项后,在“&”前面加上反斜杠可以让它成为一个普通的字符。

~
“~”用来表示在上一个替换命令中的替换式样。使用这个字元的一个范例是用来改正某一个字词的各种拼写错误:

          % substitute /[Ff]anstock clip/Fahnestock clip/g
          % substitute /[Ff]ahnstock clip/~/g
          % substitute /[Ff]ahnstocke clip/~/g
          % substitute /[Ff]annstock clip/~/g
          % substitute /[Ff]anestock clip/~/g
          % substitute /[Ff]aenstock clip/~/g
     

如果你关掉了“magic”选项,就必须在“~”前加上反斜杠以使用它的元值。而在打开magic选项后,在“~”前面加上反斜杠可以让它成为一个普通的字符。

\U
“\U”字元使其后一直到替换式样结束的所有字母转成大写,除非中间遇到了可将大写功能关闭的其他字元。下面是将某段文本全部转成大写的命令:

          1 , substitute /.*/\U&
     

\L
“\L”字元与“\U”的作用正好相反:其后的所有字母将被转换成为小写。可用如下命令将文本替换为小写形式:

% substitute /FORTRAN and COBOL/\L&/g

\E
“\E”字元用来限制“\U”或“\L”的作用范围。在“\E”之后的字母将保持原来的大小写形式。下面的命令将当前行用大括弧括起来,并将第一个单词转为大写形式:

          substitute :\([^ ]*\)\(.*\):{\U\1\E\2}
     

如果你想要将“\U”换为“\L”你不需要在它们之间加一个“\E”,反之亦然。当“\U”或“\L”的某一个出现时,另一个字元的作用停止了。假设你有一份书名的清单――每行一个书名并且只有每个单词的首字母大写,你现在想将书名中出现在冒号(“:”)之前一部分大写,而其余部分小写,只要输入:

          % substitute ,\(.*\):\(.*\),\U\1:\L\2
     

\u
这个字元将紧随其后的那一个字母转为大写形式。如果在该字元后的不是一个字母,则“\u”没有作用。

\l
与“\u”一样,不同之处在于“\l”将紧随其后的那个字母变为小写。

在替换命令中重用式样还有一点需要注意。当含被替换文本的式样(lhs)被部分或全部用于替换命令中的替换式样(rhs)时,该重用命令引用的是前者所匹配的结果,即替换命令引用前面的式样时,并不引用式样中的字元,而是直接引用前面式样中字元匹配的结果。反之亦然。这样做的原因是,替换命令两边所使用的是不同的字元,字元在两种类型的式样中的含义不同,字元在原来的环境中有意义在新的环境中则未必,因而,只能是引用结果而不是字元。

不过当一个被替换式样被重用于另一个被替换式样中时,或者当一个替换式样被重新用于另一个替换式样中时,重用命令只是简单地将原来的全部字元带到新在式样中,这些字元将在新的位置中重新对文本进行匹配。因此在这种情况下,重用命令的匹配的结果,并不一定与原来匹配的文本一样,因为它引用的只是字元而不是结果(译注:同样字元在新的位置可能会匹配新的值,如'/^./'匹配的是每行的第一个字,但不同行的第一个字可能是不一样的。而上一段所述情形中因为引用的是结果,假设'/^./'在上一次使用时匹配的是“在”,那重用时引用的是就是“在”。)。

现在我们再来做一个练习。有一个文件,我们想将其第237至289行中的每个单词的首字母大写,其余的字母小写。但我们事先不知文件中的大小写情况,有些行可能整行都是大写,或者都是小写,甚至是大小写交杂。我们假设词与词之间用空格隔开。有没有办法简单地用一条行模式替换命令来达到我们的目的呢?做这个练习需要的一些东西,我在文中并未直接提及。所以如果我的答案比你的要简单得多也不要觉得沮丧。




下一个: ,
上一个: sect3,
上层: Top

替换命令的其他用法

虽然“:substitute”是名称是替换命令,但它并非一定要从行里取出取出一些东西,然后再放一别些东西到原来的位置上。下面这个例子中,替换命令将一些文本放到指定范围内的所有行开头――它只是加入了一些内容,并未替换掉什么。

     537 , 542 substitute /^/WARNING:  /

下面在命令执行前的文本:

     The primary output line carries very high voltage,
     which does not immediately dissipate when power to
     the system is turned off.  Therefore, after turning
     off the system and disconnecting the power cord,
     discharge the primary output line to ground before
     servicing the output section.

现在变成了这样:

     WARNING:  The primary output line carries very high voltage,
     WARNING:  which does not immediately dissipate when power to
     WARNING:  the system is turned off.  Therefore, after turning
     WARNING:  off the system and disconnecting the power cord,
     WARNING:  discharge the primary output line to ground before
     WARNING:  servicing the output section.

使用替换命令来移除一部分的文本也是比较实用的技巧。下面的两个命令就是这方面的例子:

     % substitute / uh,//g
     . , $ substitute / *$

后一个命令将行末多余的空格删除。最后的两个斜杠可以省略,原因我们并不在原来空格的位置上置入新的内容。

上面提到的两种用法你可能会经常用到,那你一定没这样用过替换命令――既不增加文本也不删除文本。听起来这样做没多大意义对吧?这里就有一个这种用法的例子,在我写这一系列的教程的过程中我有时会使用这个命令。

     % substitute /^$

这里我帮你准备了一个不一样的练习:我已经给过你一条命令了――就是上面那一条命令。很明显的那条命令并未对文件进行任何的修改,那我为什么要使用这样的一条命令呢?要回答这个问题你需要有一些想像力,所以如果你需要“参考”答案的话也不要觉得不好意思。




下一个: ,
上一个: sect4,
上层: Top

脚本入门

现在你对这个编辑器已经有了不少的认识了,是时候尝试一些复杂点的编辑任务了。下面是一个简短的介绍,通过介绍你可以一窥写编辑脚本的这门艺术。

自下而上的编程方法。通常这是构建复杂编辑命令或命令脚本的最好方法。这里使用的是一个编程的术语,意思是先独立地处理每个细节的问题再将它们放到一个统一的架构中。而不是由一个整体的架构开始,再去强迫细节来适应这样一个架构。

例如:来自加洲旧金山的读者R.T.问道“如何能让vi编辑器自动地在每一段文本前后加入HTML的段落标签?”也就是说要在每一段第一行之前加上段落的开始标签“<p>”,而最后一行后加上“</p>”标签。在这些文本中,段落与段落之间将由一完全空白的行(行里不能有空格,甚至不能有不可打印字符)分隔。

这看起来相当容易。我们只需要找到先空行,先后在每个空行处向上移动一次来插入结束标签,再移到空行下以插入开始标签。但下面这个相当直白的命令还是有些瑕疵:

     global /^$/ - substitute :$:</P>: | ++ substitute /^/<P>/

第一个问题是:许多文件开头处会留一空行,而当编辑器找到这一行时它会试图上移一行,但因为没有上一行所以它无法完成第一个替换操作并且还停留在原来的位置上。这时当它下移两行时,它变成在段落开头的第二行而不是第一行――很显然我们并不希望将开结标签放在这里。不过我们有一些办法来解决这个问题:

让编辑器标识(:mark)空行,再去进行第一个替换,然后回到已标识的行,下移一行并进行第二个替换操作。

将第二个替换操作的地址由“++”改为“/./”,使之移至下一非空白行再进行操作。这样不管当前行是在一空行上或在在空行上面一行,“/./”都能将替换命令带到下一个段落的第一行。

将上面的命令分成两条。仍使用:global来查找空行,但每条命令只执行一种操作(加入开始标签或结束标签)。

第二个问题是:在两个段落之间也许有不只一行空行,这些空行并不会影响HTML页面的显示。如果编辑器依前面的命令中找到了空行,而这一空行是在连续两行甚至多行空行中的第一行时,我们在开头给出的命令中的第二个替换命令会应用在第二行空行上。然后:global回到第一行空行,然后移到下一“空行”――而这一行正是刚进行完替换的那一行(当然,此时这一行因为刚增加了一个开始标签已经不是空行了。但是记得吗,在执行后面的替换命令之前:global已经先对所有的行进行标识了),并对其运行第一个替换命令。也就是说一段像下面这样的文本:

     at this meeting, so be sure to be there!
	
     At next month's meeting we'll hear from the new

在修改完后应该像这样子:

     at this meeting, so be sure to be there!</P>
	
     <P>At next month's meeting we'll hear from the new

但实际上修改后的结果却是:

     at this meeting, so be sure to be there!</P>
     </P>
     <P>
     <P>At next month's meeting we'll hear from the new

当然这个“灾难”似乎可以通过将上面针对第一个问题的第二个解决办法做点修改来避免。也就是前两个替换命令前面的地址改为搜索式样形式的地址,分别向上和向下找寻第一行非空行再执行替换操作。当对连续的空行中的第一行命令时,这的确能行。但从第二行开始,替换命令会对已经添加标签的行重复添加标签。于是示例文本现在看起来像这样子:

     at this meeting, so be sure to be there!</P></P>
	
     <P><P>At next month's meeting we'll hear from the new

多重执行条件。其实,这里需要的是双重的执行条件。即替换命令执行前要先同时满足两个条件:

  • 被替换行紧邻当前所在的空行
  • 被替换行本身不是空行

编辑器能应付这种情况。当限定替换命令中的地址只能上移或下移一行时,命令中的 :global 部分就能满足第一个条件的要求。(在前述第一个问题的第一个和第三个解决办法中都能很好地满足第一个条件。)要满足第二个条件可以让替换命令从现有行中移除一个字符,然后再将它放回去(即替换文本与被替换文本是一样的)。这可以确定一个行是否空行,空行的话则替换操作失败。

第一和第三个解决办法经简单修改都能满足第二个条件。下面的示例命令中我用了第三种解法,因为它所使用的技巧比第一个更好理解一点:

     global /^$/ + substitute /^./<P>&/
     global /^$/ - substitute :.$:&</P>:

再给个更进一步的例子,就能对自下而上的技巧有更深入的了解。读者R.T.可能将大标题和副标题放在一起了,并且可能已经在大标和副标的前后都加上相应的标签了。作为练习你可以思考一下如何修改之前的命令,使之在加入段落标签时能跳过前面或后面已经有HTML标签的行?提示――一个HTML标签总是以“<”开始,以“>”结束。只需要对之前的命令作少量的修改就能完成这个练习,所以你可能不需要看解答除非你想再确认一下答案。

小技巧。要充分地发挥命令的威力我们需要灵活地使用替换命令。有时是“非常规”地使用替换命令,但――有什么关系呢?管用就行。这种使用方式可能是当初这个编辑器的作者所未料想到的。下面是其中一些可能会对你有用的技巧。

你没法对跨行的内容进行替换――至少不能直接替换。如果你以为在替换式样和被替换式样两边多放一个换行符的方法可行的话,那你就错了。但如果你将全局命令与替换命令结合使用的话,你通常能够得到与跨行替换相似的结果。

假设这样一个情形:你需要对一份很长的文档进行修改。所有的“Acme Distributors”都要改成“Barrett and Sons”。一个简单的替换命令能够完成大部分的修改工作,只是会漏掉“Acme”出现在行末而“Distributors”出现在下一行开头的情形。再补充两条替换命令分别对行头的“Distributors”和行末的“Acme”进行替换的话可能会带来破坏性的结果――这篇文档中也有“Acme Supply Co.”的纪录,并且还有其他三间公司的名字是以“Distributors”结尾的。

但下面的两条命令能很好地解决我们所遇到的困难:

     global /Acme$/ + substitute /^Distributors/and Sons
     global /^and Sons/ - substitute /Acme$/Barrett

第一条命令找到所有以“Acme”结尾的行,然后下移一行,只有下一行以“Distributors”开头时才将“Distributors”替换为“and Sons”。第二条命令是第一条命令的逆操作,确保只将符合条件的“Acme”替换为“Barrett”。(注意:第二条命令中全局命令查找的是“and Sons”而不是“Distributors”,因为在第一命令执行过后被换行符分隔的“Acme Distributors”已经被替换为“Acme and Sons”了。)

分步骤地对内容进行修改是一个好的编辑策略。通过对需要修改的部分逐步进行修改来获得想要的结果。想象一下,你现在是一位技术专栏的写手你刚为相当数量的相片写完标题――都是些类似“右边上方光源”或“左边边缘暗色调”的标题。随后你就得到了艺术总监将在排版时以水平翻转的方式使用相片的消息。突然间在“右边”的变成了在“左边”,而在左边的变到了右边。

现在你需要将标题中的“左边”和“右边”,互换。但除了在文档中逐个搜索替换外,还有更好的办法吗?下面的看似直截了当的两条替换命令可不怎么管用:

     % substitute /左边/右边/g
     % substitute /右边/左边/g

第二条命令并不能达到将原来的“右边”改为“左边”的目的。在进行完第一条命令后标题中只剩下“右边”,而第二条命令又将所有的“右边”换成了“左边”。我们需要使用一条过渡用的替换命令来达到我们的目的:

     % substitute /左边/QQQQ/g
     % substitute /右边/左边/g
     % substitute /QQQQ/右边/g

第一条命令将“左边”暂时地更换为“QQQQ”(当然也可以是任何未在文档中使用的字串),这样你就可以放心地使用第二条命令了。然后在运行第二条命令后,第三条命令再将“QQQQ”换成你将要的字串。

有时故意输入错误的文字,再使用替换命令将它们改回来并不全然是找罪受的举动。当我在写纯文本格式的文档时,我经常使用一些文本“线”段来分隔主要的段落。有些人简单地用一串的短杠(或其他单一字符)来做分隔线,但我喜欢使用有多字符组成简单图案的分隔符。下面是这种分隔符的几个例子:

     -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
	
     -+--+--+--+--+--+--+--+--+--+-
	
     *~*~*~*~*~*~*~*~*~*~*~*~*~*~*~
	
     [][][][][][][][][][][][][][][]

其实我没那么大的耐心去输入一整行的不同字符――尤其是当我必须不停地按Shift键时。我只要按住任意一个键让它占满一行――不管这是哪个字符再后都将被改为我想要的图案。对于上面的四个图案,我只要先分别输入下面四行:

     ------------------------------
	
     ------------------------------
	
     ******************************
	
     [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[

然后我只要运行几条相似的替换命令就能得到我想要的分隔符了。这里是我对上面四行分别使用的替换命令:

     substitute /--/-=/g
     substitute /---/-+-/g
     substitute /\*\*/*\~/g
     substitute /\[\[/[]/g

半自动替换。在文本编辑中有一些替换操作相当依赖于人的判断和观察,无法完全使用自动的替换操作来达到我们的修改目的。但在有些情形下我们仍可以通过以下的两种手段让编辑器完成部分的修改工作。

第一种是使用替换命令的修饰符使用之在进行替换时提示――这时用户可以选择进行或不进行替换。你只需要把“c”放在替换命令后面就可以以这种方式进行替换。如果你同时还使用了“g”就可以对行中所有所有符合被替换条件的字串进行操作,请参考下面的例子:

     % substitute /^someth
     ing/something else/c
     % substitute /something/something else/gc

vi编辑器会在屏幕上逐一显示即将进行替换的行。在被替换文本的下面会有“^”的标志,像这样:

something in the air. The consensus is that
^^^^^^^^^

如果在即将进行替换的行中有两处或两处以上符合条件的被替换文本,该行会被多次显示。并且每次“^”标志将指向准备操作的被替换文本,等待并接受用户输入直到用户输入了回车。如果你的输入以“y”开始不管你要输入什么,替换都会马上进行。如果输入的是其他的内容,则不对当前文本进行任何操作。

不过这种形式的替换操作对你来说可能还不够灵活。你可能需要通过观察更多行才能决定是否该进行替换;又或者在不同的位置你需要使用不同的文本来代替原来的文本。在这些情况下,我们可以借助:global命令的一个特性来解决问题。这是我们的程序员同志小何用过的技巧的一个简化版本(见本教程的第一篇)。

和以前一样,如果你在可视模式下的话你要先输入大写的“Q”进入行模式。在行模式下的冒号提示符后,输入下面的命令(假设你要进行与上一个例子一样的替换):

     global /something/ visual

这条命令依序将你带到包含字串“something”的行, 并自动转入屏幕编辑模式。在你观察并做完替换后(如果需要的话),只要输入大写的“Q”就可以离开屏幕编辑模式回到行模式,这时global命令会继续执行并将你带到下一符合条件的行并重新进入屏幕编辑模式。

不过,在离开屏幕编辑模式的时候要当心一些意外的状况. 那可能使我们失去所有的修改,甚至是丢失整个文件。这听起来很糟,因而我们接下来将对如何保护劳动果实方面的议题进行探讨。




下一个: ,
上一个: sect5,
上层: Top

看好你的文件

vi/ex编辑器在预防用户误操作进而引起灾难性后果的能力稍嫌不足。当然这也是当用户被赋于充分编辑自由的必然后果。不过如果我们编辑了老半天,修改的成果却没被保存下来;或者不小心丢失了原本的文档时,我们当然希望编辑器能够在我们灵巧的手犯错之前能给点提示。幸运的是用户还是有很多方法可以用来保护自己免受灾难“眷顾”的,我将在下面提供一些方法:

在紧急情况下。这个编辑器有一个基于保护目的的功能可能会偶然地造成灾难性的后果。你可能已经知道当你用vi编辑时你只是在编辑文档的一个幅本,而不是原本。在你使用写入命令(:write , 缩写是:w)或以正常方式退出编辑器之前并不会影响原文档。这本是很好的保护机制用来防止这个威力过于强大的编辑器偶然地“损坏”你的文件――更糟的是你可能事先没有备份。

你正在编辑修改的文件幅本处在一个相当不确定的环境中:操作系统可能崩溃或断电,你会因此丢失文件副本――而你做的修改都在文件副本中。副本丢失后你之前所做的工作就打水漂了。阻止灾难侵袭的第一道防线就是经常地使用:write命令――每使用一次写入命令,你所做的最新的修改就会被保存到稳定的磁盘空间中。

那如果不想动到原来的文档呢?如果想把编辑的结果保存为新文件,不更动原来的文档呢?这样的话你需要对写入命令进行一些“加工”。输入:“:write nufile”。看你想要新文档起什么名字,将“nufile”改为你想要的名字。(如果你不希望保存到当前文件夹下的话,那你还要在文件名前加上路径。)这条命令会将修改后的文档保存到新的文件中――原文档则保存不变。

但上面那种保留原有文件的方法还是有风险的。因为只要有一次你在使用:write命令时,在输入文件名之前“不小心”按了回车,那你原来的文件就会被覆盖。所以上面提供的技巧还是在你既想改变原来的文档又想另存为新文件的时候使用吧。当你进行完一阶段的修改编辑工作后运行下面的两条命令:

     write nufile
     write

然后再继续对文档进行编辑。

避免误修改原文档的更稳妥的办法是用这条命令开始你的编辑工作:“:file nufile”――也可以写成这样:“:f nufile”。在运行该命令后编辑器就会把“nufile”当成是当前修改中的文件的名称,再使用写入命令时就会自动写入到“nufile”中。(如果你一时忘了是否已经改了一个新的名字或忘了新的名字是什么时,直接输入“:nufile”不用带文件名,编辑器就会显示当前的文件名――还有一些其他的信息。)

天有不测风去。不管怎样做,我们还是有时会不巧地??上一些突发事件。后果是我们没还来得及保存的劳动成果就附之流水了。为了预防这样情况出现,编辑器会试图在崩溃的过程中保存你的文档。在一些情况下,用户会有一些时间来运行保存命令,比如你因为超出文件空间配额无法写入。这时可以输入:preserve命令(或者它的缩写形式:pre)这样当前的文档就能得以保留了。但进行这一操作还是有一些地方需要留意的。

preserve命令将当前文档保存到一特定的目录,如果目录不存在或不具可写权限就会操作失败。(这个目录的路径因不同的编辑器版本而有所不同。在大多数现代的Unix系统中这一目录通常是“/var/preserve”。)为了测试是否可以写入指定目录,可以在内容较少的文档中运行:preserve命令作为一种测试方法。如果结果是类似下面的消息:

     Can't open /var/preserve
     Preserve failed!

那你就得和系统管理员沟通沟通了。(你可以附上不能打开的目录及其路径以期尽快得到管理员的回应。)如果是类似下面的消息:

     File preserved.

目前为止还算顺利。下一个问题是使用:preserve命令时编辑器是否已经完整地将文档保留(:preserve)下来了,还是只保留了一部分――有些版本的vi编辑器就会在这里出状况。为了检查编辑器是否完整地保留了文档,将刚保留的文件恢复看看。

抢救保留下来的文件。在经历崩溃等突发事件或者使用了:preserve过后要试着恢复文档可以使用下面的两种方法。通常情况下你是在命令行下通过运行带“-r”参数的“vi”来恢复:

     vi -r novel.sect3
     vi -r

第一条命令打开你那经过“抢救”的文件“novel.sect3”,并将之置于编辑环境中。第二条命令并不打球编辑界面,而是显示所有经“紧急保存”的文件列表(即可恢复的文件列表),然后返回命令行。如果系统崩溃时你正在编辑的文件尚未取名,这个列表就很有帮助了。(没错,你的确可以在赋予一文件文件名之前就进行编辑。此时编辑器会先打开一新的工作区并等你稍后为之命名。)在这种情况下,编辑器在保存你的文件的过程中会为它命令,而你必须知道到底哪一个才是它的名字以进行恢复。

就如前面说的第一条命令根据你给的文件名,打开该文件的最近一份经抢救成功副本。如果系统崩溃已经不是一次两次了,那因你或由于系统原因让编辑器自动保存当前文件也可能不是一次两次了。如果最新的副本并不是最好的版本,那你可以选择不从最新的保存记录中恢复。而是改用编辑器保存的次新的副本。这个操作可以直接在编辑器中进行,只要输入“:recover”命令(或用简写的形式“:rec”)。这样最近的一个副本将被换成次新的版本。(因为你也经在编辑器中了,所以输入命令时不需要再给出文件名。编辑器在缺省的情况下以当前文件名为参数,否则编辑器会试图恢复你给的文件名的经保存的副本。)如果这还不是你要的那个版本,那你可以继续使用“:recover”命令。

不管用什么方式恢复,在恢复完后先浏览一遍。如果你所用的vi版本的preservation功能有缺陷,那你可能只得到一些散乱的字符,或是一些像这样子的行:

     LOST
     LOST
     LOST
     LOST
     LOST

如果是这样的话,那你之前用:preserve命令抢救下来的文件已经丢失并且这很可能是无法挽回的结果了――你只能找系统管理员建议他将这个编辑器升级到更好的版本。话说回来如果你看到的正是你要的内容的话,那要赶紧将它保存起来――自动保留的副本是你使用了恢复命令(:rec)后就会被自动删除,所以别指望下次还能用恢复命令找到同一份文件副本。

还有一个需要当心的地方。你可能相信当你使用“ZZ”“:x”“:wq”这三条命令的任何一条退出编辑器时,编辑器会先检查当前编辑的文档是否被修改过,如果已经修改则先保存修改再退出。实际上这三条命令中只有最后一条:wq总是将当前文档写入文件(不管当前文档有没有被改动。),所以你应该只使用这一条命令保存并退出vi以策万全。

前两条命令会做一些检查,但只是简单的检查。具体来说前两条命令和“:quit”命令仅仅通过内部的一个修改标志来判断当前文档是否经过修改。这个标志在当前文档被修改时被设置,而当修改被写入文件后这个标志被清空。当你在使用了“ZZ”或“:x”命令或者是不小心用了“:quit”命令时你可能需要吞咽自已带来的苦果了。

之所以这样说是因为对于编辑器来说你打开一个新文件或是恢复一份经紧急保存的文档到工作区(buffer)都是一样的――都是视作未修改的文件。如果你使用的vi版本中ZZ与:x命令的检查机制比较松散那它会认为文件打开后并没有进行过修改而不予保存。这样一来编辑器会自作聪明的选择直接退出而你恢复文件的努力在临近完工之际功败垂成。所以所以记得一定要使用“:wq”来退出vi。当然你也可以选择分两步完成,先用“:write”写入修改,再用“:quit”退出,结束编辑。

其他状况和应对方法。恶运还是可能随时降临。你可能会偶然地丢失自已所做的修改同时还破坏了当前编辑文件的原本。

想象一下如果你可能因误用全局命令而将当前工作区的文件弄乱,但碰巧全局命令修改的部分没在屏幕所显示的区域中。这时你如果使用了写入命令……惨!除了一些较小的文件以外,我们根本不可能在每次使用:write命令前先仔细检查一下当前编辑文件的内容。

也许你的确在写入之前及时发现了这个问题。并且意识到要撤消操作不太实际(因为你不知道是什么时候哪个操作造成的),这时你可以用“:edit”命令来放弃上一次写入操作(或打开文件)后的所有修改。也可以使用“:quit!”来放弃所有修改并退出编辑器。这两条命令都命令末尾使用了“!”以表示忽略还没写入的所有更改。

不过因为你并不是在写编辑器脚本所以你可能输入上面两条命令的简写形式:e!和:q!来代替完整的命令。但是你要小心别打字打错了――在标准的键盘上,“w”键被放在了“e”和“q”之间因而存在着打错字的“危险”。一旦你不小心打字打成了:w!,那就自求多福了――这条命令让编辑器强制将有问题的文件版本写入文件正本――不管有没有写保护。

如果你一直都在屏幕模式下进行编辑的话,那你还有最后一根救命??草。不管何时你都可以输入短短的命令系列来将文件退回到文件刚打开的状态(不管中间有没有使用写入命令保存修改,都能恢复到刚打开文件进行这一次编辑时的状态)。这时只要再用写入命令就可以将当前工作区的文件保存起来替换刚保存的有问题的文件。

输入“Q”离开屏幕模式回到行模式下。在行模式中有一条“undo”命令作用与屏幕模式下的“u”命令相仿。这条命令撤消在上一个在行模式中对当前文档所做的更改。因此,行模式下使用“visual”后对文档所做的“所有”修改也被视为是行模式下的“一次”修改。然后,当你在命令提示符中使用“vi novel.sect3”命令运行vi并打开相应文件后,命令解释器(shell)实际上运行的是ex(vi只是ex的一个别名),而编辑器一运行就给自己下了一条“visual”命令使文档在打开后自动处于屏幕模式下。

所以从头至尾,编辑器都有文件原本的一份完整的拷贝。这是它的职责,因为用户可能随时回到行模式下撤消初始的“visual”命令。(这也是编辑器使用的暂存空间比一般的交换空间要多的原因。)如果你想看看撤回到文件的初始状态并重新回到可视模式下所用的命令系列的话,这里是使用简写形式的版本:

     Qu
     w
     vi

最后一点提醒。对一些有经验的Unix用户而言下面的这样情况出现有些搞笑,但许多刚从单用户系统迁移过来的用户的确会碰到这种情况:除非你在少数有文件锁定功能的Unix版本中工作,不然你的的确确没办法预防其他系统用户在同一时间与你打开同一份文件进行编辑。

你们将在各自的工作区中进行编辑,你们完全没法获悉些时有人在编辑同一份文件。每次当你将修改写入文件时,另一位用户先前写入的修改将会丢失,反之亦然。在这场互不知情的较量中,获得最终“胜利”的将是进行最后一次写入操作的人。而另一位用户在一个多小时后再打开文件时会发现里面完全没有自己留下的编辑痕迹。

没有什么技术上的措施可以真正预防这种情况发生。你只能通过与其他可能需要对该文件进行编辑的用户沟通的办法来解决这一个问题。




下一个: ,
上一个: sect6,
上层: Top

读者来信

我们的一位读者对这一技巧提出了自己的问题。由于这个问题的重要性,我觉得有必要将回复放到这篇文章中。

Walter,您好…

你在教程中提到可以用下面这条命令

          global/XXX/visual
     

搜索式样“XXX”,然后对其进行其他编辑操作(记得吗,小何用这条命令来编辑他的意式面条代码……)这就产生了一个疑问:如果文章中有100个“XXX”,而我只需要对前10个进行编辑,因此不需要再找其余的90个“XXX”了。上面的命令只要我一输入“Q”就会继续找到下一个“XXX”。但我处理完前10个“XXX”处的代码了,我现在想要查看/编辑代码中有“illegal”字样的地方。所以我输入“Q”然后使用命令global/illegal/visual。

问题就出在这里:输入Q并不会出现提示符等待用户输入命令而是直接找到第11个“XXX”出现的行。我想知道的是有没有方法可以在我输入Q时vi不再继续执行global命令呢?

致礼!

Chris…

如同Chris所意识到的,如果我们不想再搜索后面的90个“XXX”我们可以简单地忽略它们(只要我们不再进入行模式)。每次命令执行时会将用户带到可视模式下,但用户没有被限制一次只能对一个地方进行修改。你可以像平时一样随意地上下页翻动,对需要的地方进行修改――只要你愿意你可以一直待在可视模式下。而当你在可视模式下进行完所有编辑工作后,你完全可以像平时一样保存文件然后再退出编辑器。而那条在后台静静地等待你再回到行模式下(以执行下一步操作)的global命令,在你退出编辑器后也跟着结束了。不过如果我们想用同样的方法找到第二个字串时――正如Chris想做的那样,我们就需要用些迂回的方法。

走出困境的最好方式是先保存修改(使用写入命令)。然后,输入:edit命令――先别急着用Q。这个命令会重新从磁盘上重新加载当前文件到工作区中。因为你才刚进行写入操作所以重新加载的版本与你原来在修改的版本的内容是其实是一样的。同时因为你还没有离开编辑器,各种设置如有名工作区,键盘映射及缩略设置还有一些设置项还保留着。只有少数项目发生了变化如未命名工作区(缓存区)被清空了――而global命令也因此中断运行。现在你可以用Q键进入行模式下并运行第二条global命令了。




下一个: ,
上一个: sect7,
上层: Top

下一篇

这系列教程进行到现在,你肯定会对这个编辑器的某些方面不太适应。好消息是这其中有些东西是可以依用户意愿更改的――并不需要用修改源代码之类的方式来更改。在这系列教程的第5篇中,我会详细说明用以建立属于自已的编辑环境的一些vi/ex的内建功能及许多可以通过这些功能进行修改的元素。



Appendix A 答案

begin 644 viex4_ans
M5FDO17BQX+RMQO<*"K7:R\35PL&WS["RSK^\M/"PN`H*"@JAL&LS-Z&QSLK,
MX@H*)2!S=6)S=&ET=71E("]<*"XJ7"EK,S=<*"XJ:S,W7"DO7#%Q-3-<,@H*
MT\/)S\/FU>+,]</\P>Z^S=#0P<NAH[6QR]'+]\J]T?FCJ+&[S.:[N\J]T?FC
MJ=;0T]#!O;CVN_+2U,G/M<3-J,7DM_O*L:.LL>"\K<;WN^'4VM?<S.7*O='Y
MQ-S&I<7DM<3'L,SAS\*CK,JYM=K2N[CVS:C%Y+?[QJ7%Y+ZAO\G$W+.DM<2S
MI+;(H:,*"@H*"@JAL+3ST*'0M*&QSLK,X@H*,C,W("P@,C@Y('-U8G-T:71U
M=&4@.EM>(%TJ.EQ,7'4F.F<*"M+RSJJAL%QUH;&ZS:&P7&RAL=?6U*JUQ-.P
MS^RUQ+>VSJ?3T,_>HZS+_,/'UKNVU,;DNO.UQ-*[N/;7UL2XT]#3L,_LH:/+
M^=+4LKN[X=;5UKFAL%Q,H;&\L*&P7%6AL;7$U_?3PZ&C"@K7]]7?S.'*OJ.Z
MU-K0M,WJU>*X]K3PL+BY_;KSHZS.TK+%MZ+/UM3:T.VVX+7$=FFPYK&^UM#5
MXM;6M[VWJ-#0LKO-J*&CTO*TR\[2T];4]KS3P<O/PL/FU>+6UKWBMZBAH\OD
MR+N[X<_@MM2XM-33M:O4VK*[S:BPYK&^M<1V:<G/MKS$W,JYT\.AHPH*,C,W
M("P@,C@Y('-U8G-T:71U=&4@.EPH6T$M6F$M>EU<*5PH6T$M6F$M>BU=*EPI
M.EQU7#%<3%PR.F<*"KRTRKG4VK7:TKO6UK#LMZC$W-#0M</-J+7$=FFPYK&^
MUM"CK+7:MO[6UKWBMZC2LM/0TKO0J;K#M*:CNK6QTKNX]K6EM,K6KL>PO?3!
MVM?%L>JUX[?[NL7*L:.LM=K2N];6L.RWJ,.[MZB]J[7:TKNX]M?6Q+C7JLZJ
MM//0M*.LMOBUVK;^UM;$W*&CQ.._R=+4U]2\NK[VMJC*Q[?QO:O3R<&LU]:W
M^Z.H+:.IP:R]T[7$N+2ZS[3*HZC(YZ.Z1G)A;FMU<G0M86TM36%I;J.IRM/7
M]]*[N/:UI;3*N_+*Q[;@N/:UI;3*H:/(Y[G[Q.//Z[VKUJ[*T\ZJMN"X]K6E
MM,JRHKVKQN3/X-.FM<3*U]?6Q+BT\]"TM<2[L*.LUKO2JKVKM=JV_M;6O>*W
MJ-;0SN6X]L&LU]:W^[7$U^ZZ\]*[N/;(I;7TOLW0T,'+H:,*"@H*"@JAL+*[
MMJ_7]Z&QM<3,YKN[P_S![@H*UKO+T<OWRKW1^<3<L\FYIK78QJ7%Y+*BQ]+#
MN]/0L_;/UK3MSO.UQ->TO_:CK+'@O*W&][[-N^'(S\ZJS.:[N[+9U_>SR;FF
MH:JAJKRTRKG+_-:[RL>]J[_5U]:TKJ.HLKO*Q[_5N/&CJ<SFN[O.JK_5U]:T
MKJ&CM;&QX+RMQO?-ZK/)LMG7][*BN/BS]J&PS.:[NZ&QM,[*_<JQHZS+_+CX
MM<3*M;S*R<^^S<K'SL2\_M;0O]70T+7$ROW$OZ&CMOC-J+G]O]70T+7$ROW$
MO\[2OLV_R=+4UJJUP,[$U<+6T-/0MN#)V;CVML["Y*.HP:RQZLSBO+#!T+'M
MS^ZCJ:&CSM*[N<3<S:BY_;VKT-#*_;/]TM2_U=#0ROW`M,70ML_*Q[?QMLZ[
MX<RKLZ2AHPH*"@H*"DA434RQZL>ISLK,X@H*SM+#Q]2MP+2UQ+>]MZC2JM3:
MTKNX]K;.PN36KL>PO-/)S['JQZFRR=/#M<2WO;>HRL>]J[7:TKNX]M?6Q+C2
MQK/]U-FPT=#"S.V\T[7$L>K'J<&LS:RXU=+&L_VUQ+7:TKNX]M?6Q+BWQ;6]
MML["Y+^JS;>TIJ&CS];4VL[2P\>[N<K'U>+1^=?VL_W!R[6QTKO0T-+4H;`\
MH;'*L:&CSM+#Q[_)TM30WKC$P_S![LJYM<.UL;7:TKNX]M?6M_O*QZ&P/*&Q
MRK&CK+&[S.:[N\J]T?G&I<7DRJ>PW*.LU>+1^<SFN[O#_,'NOLVRN[OAUK30
MT*&CTJK7]K6]U>*UX]:[TJJ]J\SFN[O*O='YUM"UQ+7CHZ@NHZG7UM2JN,3.
MJM*[N/;7UK?[H;`\H;&UQ+*YO*^CJ*&P6UX\7:&QHZG0SLJ]HZS5XM'YOLW$
MW,:EQ>3(SKK.M\>_U=#0M<2_JLVWHZRS_;?'N,/0T-+42%1-3+'JQZF_JLVW
MH:,*"LVLP.VCK+_)TM2]J\SFN[O*O='YUM"Q[<J^T-#$J;7$U]:W^[7$M>/7
MUM2JN,3.JK'MRKY(5$U,O>'*^+7$H;`^H;'7UK?[M<2RN;ROT,[*O:&CS\+#
MYLK'T-ZXQ+KSM<3#_,'NH[H*"F=L;V)A;"`O7B0O("L@<W5B<W1I='5T92`O
M7EM>/%TO/%`^)B\*9VQO8F%L("]>)"\@+2!S=6)S=&ET=71E(#I;7CY=)#HF
'/"]0/CH*"@``
`
end

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