碎碎念

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 23

Vim中输入法与编码设置的FAQ

Filed under: vim, vi, 编辑器, Python




Up: (dir)

输入法与编码设置的FAQ

从去年到现在一直陆陆续续的都有人在问起中文输入法的设置和编码的设置这方面的问题。我将这些问题整理了一下,也许有些用处。

1 中文输入法

如果每次进出insert模式都要切换一下输入法是很恼人的,我自己就有过这样经历。应该也有不少人在初次尝试Vim后放弃的原因就是因为无法让中文输入法与Vim无间配合。其实这个问题是可以解决地(不过只对图形界面的Vim有效)。

1.1 Windows

:h mbyte-IME

在Windows中如果是直接下载完整的安装包的话,输入法支持已经包涵在gVim中了。如果自行编译的话记得加入以下选项:+multi_byte_ime加入这一选项后,每次退出插入模式后,Vim会依所用命令与上次退出前的输入法状态自动判断使用中文还是英文,在重新进入插入模式时回到上一次退出时的输入状态(即上一次退出时是中文输入还是英文输入的状态)。

举例来说如果你在插入模式、中文输入状态下按

<ESC>dT

在按<ESC>后Vim自动换回英文输入状态,在按到T后Vim又会自动切换至中文输入状态。

1.2 Linux

:h mbyte-XIM

Linux下要实现同样的功能需要一些额外的努力。确保所用的gVim版本包括+xim+GUI_GTK选项。下面是ubuntu下fcitx的设置方式(我不是很清楚用scim如何设置),在.gvimrc(或者.vimrc也行)加入如下设置,

se imak=C imi=2 ims=2 imc
  • imactivatekey (imak)
    imak用来设置桌面系统控制输入法开关所使用的快捷键。fcitx的激活键是Ctrl-spaceCtrl则是中英切换键。如果将imak设为Ctrl-space,则每次进入插入模式都是中文输入。将imak设为Ctrl则可以保留上一次退出插入模式时的输入状态。

  • iminsert/imsearch
    imi/ims告诉Vim在插入模式与搜索时使用输入法。
  • imcmdline (imc)
    imc告诉Vim在使用ex命令时也允许输入法。可根据自己的需要随时设置为开或关。

2 编码问题

:h mbyte.txt

但凡是需要在Windows与Linux之间传递文档的中文用户或多或少会遇到编码方面的问题——通常是utf8/gb2312文件的识别问题。但有时候情况要更复杂一点,特别是对需要处理不同语言文本的人而言。

2.1 怎么让Vim正确识别编码?

:h charset-conversion
:h fencs

通常下面的命令可以解决大部分的问题:

set fileencodings=ucs-bom,utf-8,chinese,big5,latin1

注意:上面的big5与latin1并不起作用可以省略。

问题解决了吗?很好,现在可以收工了……除非你想了解更多细节。

2.1.1 关于编码的一些基础

字符在计算机里面有用一个或多个字节表示的。因此可以直接以十六进制数字(因为2**8==16**2)来表示某个字符,某个字符对应的十六进制数字就是该字符的编码。a在ansi、utf8的编码都是0x61,但通常不同的编码系统(编码标准)对某个数字表示的字符有着不同的规定,因而同一个汉字在GBK与Big5中以不同的十六进制数字不一样。同理,同一个数字在不同的编码系统中也对应着不同的字。如,数字0xbac3中gb2312中是“疑”而在Big5中是“好”。 某个编码系统中的字符的集合就是字集charset。

'termencoding'('tenc'), 'encoding'('enc'), fileencoding'('fenc'),是Vim中三个跟编码设置有关的设置项,它们对应着三个概念:终端编码,Vim编码与文件编码。
文件编码就是文件的数据在磁盘中存储所使用的编码。
Vim编码Vim在处理文本或表示文本时所使用的编码。
终端编码终端处理文本或表示文本时所使用的编码。

(此外还有:scriptencoding命令,设定脚本文件所使用的编码)

下文中将以xterm指代所有模拟终端如xterm, konsole, rxvt还有Windows下的命令窗口(DOS窗口)。

在图形系统中每个窗口都拥有自已的绘图区,它们自已决定所使用编码,自己负责输入输出。在使用gVim时,'enc'的值就是就是gVim使用的编码——缺省使用系统的默认编码。gVim在读入文件时需要判断文件所使用的编码并转换为自己的工作编码即&enc。所以gVim只需要处理'enc'与'fenc'。

与gVim不同,Vim没有自己的窗口,它“借用”xterm的窗口。同其他桌面程序一样xterm也有自己的绘图区域,自己负责输入输出(当然也有自己的编码设置)。在xterm运行Vim时,Vim自己并不负责显示,而是提供数据由xterm将数据打印到绘图区,用户的输入也是经由xterm传给Vim。所以Vim还需要考虑到xterm的编码,即'tenc'。为了数据能正常显示Vim必须先将文本转换为xterm支持的编码再将数据提交到xterm。Vim当然不知道xterm的编码,它用猜的——根据环境变量的值。我们可以使用:set tenc=xxx命令显式地告诉Vim,xterm使用的编码是xxx。

从下面这张图可以看到它们之间的关系:

	
--------------------------------------- <- 用户
        ^       |
        |       |
        |       V
--------------------------------------- <- Xterm(tenc):输入输出
        ^       |
  转换  |       |  转换
        |       V
--------------------------------------- <- Vim(enc):处理
        ^       |
  转换  |       |  转换
        |       V
--------------------------------------- <-- 磁盘(fenc):存储
	

Xterm负责与从接收用户的输入与输出信息至屏幕。Vim负责根据xterm传来的用户指令进行文件内容的增删减。Vim以encoding项设置的编码对文本进行操作。未设置时则使用系统的默认编码。使用ga(:asc)查看的就是Vim工作时使用的编码。磁盘负责存储数据。fileencoding表示的就是文件存储所使用的编码。使用十六进制工具打开文件看到的就是文件的实际编码。1

还有一个设置项没提到,就是'fileencodings'('fencs'),fencs是一套规则帮助Vim判断文件的编码,解决乱码问题就靠它了。

se fencs=ucs-bom,utf8,chinese

这条命令就vim先尝试ucs-bom作为文件编码,如果发现文件数据不符合ucs-bom的编码规则(比如没有Byte Order Mark(BOM)),就试下一个编码直到文件数据符合某个编码规则。因为编码放置的顺序是非常重要的。Big5编码范围与GBK(Vim中chinese是cp936/GBK的别名)编码范围相似所以如果Big5放在GBK后不可能被识别。

注意:我们常见的繁简转换并不是一种编码转换而是一种“翻译”,因为这个转换过程不是在GBK中查找同样的繁体字(GBK包含繁体字编码)而是在GBK编码中查找相应的简体字替代原来的繁体字符。Big5与GBK进行编码转换的一个例子是由操作系统进行的。当你从Big5编码的网页上复制文字到时,会自动进行Big5至GBK的编码转换(在简体中文Windows操作系统上),转换后仍然是繁体字(GBK中包含繁体字的编码)但已经不是Big5编码。在复制日文字符时也会进行类似的转换。所以你看到的日文或繁体汉字也有可能是GBK编码的。看上去很头大是不是。好在大部分情况下我们不需要理会这些。大部分情况下读写都是使用默认编码。

最后一条关于Vim编码的基础知识是:fenc(fencs)可以使用的编码要多于enc可以使用的编码。fenc可以设置为(几乎)任何一个的编码,而enc能支持的编码则取决于系统。一个例子是:Vim中可以使用'iso-2022-cn'(中文)编码存储文件(即,将fenc设置为'iso-2022-cn'),但不支持使用该编码处理文件(也就是将enc设置为'iso-2022-cn')。当然此时可以使用gbk或utf8作为enc——所以这点不是大问题。

2.2 为什么有乱码?

上面图中可以看到在tenc与enc,enc与fenc都发生了编码的转换,许多乱码的出现都与编码转换有关。这里整理一下出现乱码的几个原因:

1. Vim无法识别文件编码。
这种情况下Vim会以默认编码显示文字,当文件编码不等于默认编码时就会出现乱码。
2. 错误地识别文件编码。
在一些情况下Vim错误地识别编码(常见的是将Big5字符识别为GB2312字符或是将ANSI/Latin1的8位字符识别为汉字,如.nfo文件)。这样通过encoding显示出来的就是乱码或是一堆无意义的字符。
3. fenc与enc无法转换。
并不是所有的编码之间都可以相互转换的,转换失败就会导致乱码。如果显示设备支持utf8或其他unicode编码,将enc设为这些编码中的一个可以减少这一问题出现的可能性。
4. enc与tenc无法转换。
使用gVim可不考虑tenc。但终端(如xterm或是Windows的命令行窗口)一般只支持某种特定的编码,所以使用Vim时需要设定为该终端特定的编码。
5. 使用的xterm不支持所使用的encoding。
可以看到从读取数据到提交数据到Terminal的过程中发生了两次的编码转换。在转换失败或者Vim提交至xterm的数据不能被正常显示(通常是由于错误的tenc)就会出现乱码。需要说明的是现代的GUI可以处理多种编码,所以在图形介面下输入输出的编码总是与enc一致的,tenc不起作用。因此需要考虑tenc的只有在term或xterm或者Windows的DOS窗口。
6. 没有正常显示某些字符所需要的字体。

显然要让Vim正确显示我们就要解决上面的6个问题。

上面的第一二点就是我一开始说的:se fencs命令所解决的问题。这已经可以解决大部分的问题了。但错误识别的问题仍无法完全避免,比如你有一个文件已知里面是一个双字节字0xbac3,你没办法判别(也没有工具可以做得到,只有创建者知道)这是GB2312的“好”还是Big5编码的“疑”(也可能是某个日文字符),这两个字在各自的编码中都是0xbac3。如果fencs中Vim先遇到哪个,那个就会被判别为文件的编码(fenc)而这有可能是错的。我们能做的只是通过一些方法提高识别率。

要解决第三、四点建议尽量将将它们设置为一样的值减少编码的转换。如果需要处理多种字符的话建议使用utf8。但仍有可能出现第五个问题,我们可以依tenc的设置选择合适的fenc与enc(好在现在大多模拟终端是可以设置编码的。)

如果你跟许多不同的语言打交道你可能会遇到上面的第6个问题,只能通过添加相应的等宽字体来解决。

提示:Windows上的DotumChe与GulimChe可以显示中日韩字符。

这是略微优化后的设置:

" 前面说过了big5不会被识别,可依需要调整次序
" 将chinese改为相应的CJK编码如果需要识别日韩编码
set fencs=ucs-bom,utf-8,default,chinese,big5
	
" 下面的三项需要依实际情况设置
" 设定新建文档所使用的编码
set fenc=utf8
" 设定Vim工作用的编码
set encoding=utf-8
set termencoding=utf-8

2.3 命令使用的细节

:h 'fenc'

在vimrc中指定fenc只对新建文档有用,在打开已有文档时并不起作用——Vim在每打开一个文件都要根据fencs设置(如果有vimrc中有fencs设置项的话)重新判断fenc,如果没有fencs并且Vim不能判断所用的文件编码的话则使用enc的值(&enc)如果enc也为空的话使用系统的默认编码。编辑时使用:se fenc指定一个与当前fenc不同的值并保存可以转换文件的编码。

前面说过Vim在读入文件时会先进行从&fenc至&enc的编码转换,然后将使用&enc编码内容显示在编辑区。注意这个转换只有在载入文件时进行。如果在文件载入后再使用:se enc命令更改enc,Vim并不对编辑区的文本进行编码转换而是以新的enc的方式“解读”已有的编码。而在此之后打开的文件则仍会进行由&fenc至新enc的转换。enc的设定对之后打开的文档都起作用,而:se fenc设定只对当前文档有影响,在其后每打开一个文档Vim都会重新判断编码。举例而言:

这是“好”和“疑”在GB与Big5中对应的编码(可以看到“好”euc-cn和“疑”的euc-tw编码是一样的):

好 euc-cn 0xbac3
好 euc-tw 0xa66e
疑 euc-tw 0xbac3

添加如下设置至vimrc:

se fencs=chinese enc=big5

现有一个文件内容为euc-cn(chinese是euc-cn/cp936的别名,Big5是euc-tw/cp950的别名)编码的“好”(0xbac3),用Vim打开后还是显示“好”,使用ga指令可看到编码已转换成了(0xa66e)。
将vimrc改为:

se fencs=chinese enc=chinese

现在再打开文件,使用ga可以看到编码仍时0xbac3,此时使用:se enc=big5可以看到“好”字显示成了“疑”,但编码并未发生转换——只是将文本当成了Big5的字符显示了。此时再打开新的文件时就会发生由该文件的&fenc至Big5编码的转换。

正因为编码转换在文本载入前发生,所以没办法通过modeline指定fenc或fencs。

2.4 如何实时地手工设置某个文件的fenc与enc?

如果要告诉Vim某个文档的正确编码是korea,而不是chinese,我们可以修改vimrc中fencs设置,同时设置enc为korea或utf8,然后重新运行Vim再打开该文档。然后等关掉该文档后再将设置改回来——这样做有点费力。现在我们已经知道打开文档后用:se fenc=xx设置fenc的话Vim会以为你要使用新的文件编码存储文件,要让Vim以正确的文件编码(fenc)载入文档可以使用这条命令:

" 强制Vim以korea为fenc读入文件yourfile
" 省略yourfile则重新载入当前buffer的文件
:se enc=utf8 | e ++enc=korea yourfile

设enc为utf8确保Vim能正常处理韩文字符(这里也可以设置为korea)——注意:如果需要设置enc的话,enc的设定命令一定要先于fenc的命令,而++enc用来指定所打开的文档的fenc(而不是让Vim自己判断)。为了显示韩文字符,在打开后你可能还要设置字体:

" Windows(Linux下这条命令会长得多)
se guifont=GulimChe

另外也可以使用:

" 建议Vim以korea为fenc读入文件yourfile
" 省略yourfile则重新载入当前buffer的文件
:se enc=utf8 fencs=korea| e yourfile

与上面的命令的不同之处在于,这条命令只有在文件的内容符合korea(euc-kr/cp949)的编码规则时才将fenc设为korea。而上一条命令则是无条件的将编码设置为korea。

2.5 有没有办法提高识别率?

我们可以通过一些方法提高识别率。其中之一是根据环境变量更改fencs的值——这实在是一种聊胜于无的做法。另外一种方法是使用更专业一点的工具。

2.5.1 enca

Linux下可以使用enca,确保系统中已经安装了enca(很有可能已经预装了),然后将下面的脚本加进vimrc

func! DetectFenc(fname)
  let enc=split(system("enca -Pe ". a:fname),"\n")[0]
  return enc=='unknown'?'default':enc
endfunc
	
au BufReadPre * let fenc=DetectFenc(exapnd("<afile>"))|exe "se fencs=ucs-bom,utf-8,".fenc."default"

enca主要是用以识别东欧的编码,对CJK的支持差一点。所以如果需要识别日韩编码这个工具并不适用——如果不需要打开日韩编码的文件则这个工具可以用以识别GBK与Big5编码。

2.5.2 MultiEnc.vim

在Windows下可以使用MultiEnc.vim——这是在官网找到的一个插件,里面使用了作者Yongwei Wu自己写的一个编码侦测工具tellenc。好处是不需要自己写脚本,缺点同样是不支持侦测日韩编码。

2.5.3 chardet

考虑到大多数中文用户只需要跟gb和Big5打交道上面的两个工具应该可以应付大多多数的情况了。如果你频繁出入各种编码的文件的话,那你需要更趁手的工具。这一节我们要用Universal Encoding Detector (chardet)将Vim打造成一个所有平台上编码识别能力最强的编辑器。Universal Encoding Detector是一个Python库,这意味着几乎所有平台下都可以使用——当然你的系统上要先安装Python。

关于这个包所使用的编码检测算法的描述可以参考:一种语言/编码检测的复合方法

首先下载并安装chardet至Python:

注意:这个库里面有一个Bug会导致utf-16le无法被识别。在安装前先将universaldetector.py第84行处的:

elif aBuf[:4] == '\xFF\xFE':
    # FF FE  UTF-16, little endian BOM

改为:

elif aBuf[:2] == '\xFF\xFE':
    # FF FE  UTF-16, little endian BOM

其次,在vimrc中加入这段脚本:2

if has('python')
func! SetFencs(fname)
python <<EOF
try:
  import chardet,vim
  from chardet.universaldetector import UniversalDetector
  # 'chardet' can be found at http://chardet.feedparser.org/
  chardetImported=True
except ImportError:
  chardetImported=False
	
def norm(encname):
  ''' replace encoding names with ones which can be
  recoginzed by Vim '''
	
  encgroups=[
["latin1","ascii"],
["chinese","euc-cn","gbk","gb18030","gb2312"],["iso-2022-cn"], # chinese
["korea","euc-kr"],["iso-2022-kr"],     # korean
["shift-jis"],["iso-2022-jp"],["euc-jp"], # japanese
["big5","euc-tw"],                    # big5
["cp874","tis-620"],                  # thai
["cp1250","windows-1250"],            # east europe
["cp1251","windows-1251","maccyrillic","ibm855","ibm866"],
["cp1252","windows-1252"],["cp1253","windows-1253"],
["cp1255","windows-1255"],["koi8-r"], # russia
["ucs-4be","utf-32be"],["ucs-4le","utf-32le"]
]
  for aliases in encgroups:
    if encname in aliases:return aliases[0]
  # return as is for other encodings like, utf8, iso-8859-xx
  return encname
	
def detectEnc(fname):
  ''' detect the file encoding of a file then returns
  a valid Vim *encoding-value* '''
	
  detector = UniversalDetector()
  for line in open(fname,'r'):
      detector.feed(line)
      if detector.done: break
  detector.close()
  result=detector.result['encoding']
  if result==None:return 'default'
  return norm(result.lower())
	
def detectEnc2(fname,samplesize=20000):
  ''' This is a shrunk copy of detectEnc() with the max
  sample length limited to 20K byte to accelerate a little
  bit at the cost of accuracy lose '''
	
  result=chardet.detect(open(fname,'r').read(samplesize))['encoding']
  if result==None:return 'default'
  return norm(result.lower())
	
if __name__=='__main__':
  if chardetImported:
    enc=detectEnc(vim.eval('a:fname'))
    #enc=detectEnc2(vim.eval('a:fname'))
    vim.command('se fencs='+enc)
  else:
    # use the original 'fencs' setting
    pass
EOF
	
endfunc
au BufReadPre * :call SetFencs(expand('<afile>:p'))
endif
" au BufRead * :if &fenc=='cp874'|set guifont=Courier\ MonoThai:h9:cTHAI

注意:python对空格很敏感所以格式不能弄乱。另外使用这个脚本Vim要有+python。如果没有+python可以将检测代码放在单独的python脚本中。然后在vim中使用system()调用——就像上面的enca一样。这里不再赘述。

现在可以试着打开一个文档,看编码是不是被正确识别了。这个方案的优点是支持编码多并且侦测结果准确,缺点同样明显——慢(使用detectEnc2()代替detectEnc()可以改善这个问题,但准确率也会因此下降)。当然,没有任何工具可以100%识别编码的。

稍微解释一下,

  • def norm() 这个函数将chardet侦测到的编码名称转为Vim能识别的编码名称。比如chardet侦测到的编码是"euc-jp",这个函数将返回下面列表中的第一项
    `["chinese","euc-cn","gbk","gb18030","gb2312"]‘即"chinese"。这个编码名称在windows下与linux下都能被Vim识别。注意,因为我对一些编码尤其是东欧语系的编码之间的关系不熟悉,所以上面的表可能有错误的地方。
  • def detectEnc()这个函数用以检测文件的编码。结果将通过norm()转换为Vim能识别的编码名称。在一些特殊情况下这个函数会很慢。
  • def detectEnc2()功能跟上面的是一样的。只是限制了读入数据的大小,这样可以使识别速度更快一点。当然识别率也会降低一点,可依自己的情况选择用哪一个。
  • 下面这条命令用以设置fencs的值,因为这条命令是在BufReadPre执行的,所以在读入文件时Vim就可以依据这个新的fencs值判断文件的编码。
         vim.command('se fencs=' + enc)
    
  • `au BufReadPre * :call SetFencs(expand('<afile>:p'))‘这条命令使Vim在每次打开文件时都使用这个函数侦测文件编码。
  • `au BufRead * :if &fenc=='cp874'|set guifont=Courier\ MonoThai:h9:cTHAI‘前面说过要正常显示除了编码设置正确外,还要有相应的字体。这命令让Vim在遇到泰文时自动调整为泰文字体。你可以将多条字体选择命令放在独立的脚本文件中,再使用au执行该脚本。

3 小结

  • 通过设置在Vim中可以方便地使用中文输入法
  • 编码的设定要在文件载入编辑区之前设定
  • fencs设置项决定Vim检查文件编码的次序,设置合适的fencs可以解决大部分的乱码问题
  • vimrc中的fenc设置项只对新建文件有用,除非fencs为空
  • e ++enc=xxx命令可以以指定的fenc载入文件
  • fenc支持的编码比enc要多
  • fenc, enc, tenc设转为一样的值可以减少编码转换的次数从而减少乱码
  • 将enc设置为utf8则可以实现更好的兼容性
  • 有时乱码是由于没有设置合适的字体
  • 编码侦测可以通过外部工具达到更好的结果

4 参考资料


Footnotes

[1] 而这些十六进制的字符表示也是用tenc显示出来的。

[2] 实际上与Vim编码相关的还有scriptencoding,而在python中使用汉字也需要声明编码。网页上复制粘贴可能导至编码转换,所以下面的混合脚本不使用中文注释,以免在解决问题时制造更多问题;P

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

2007, July 29

Vim的潜能

Filed under: vim, vi, 编辑器, Python

Vim潜能

虽然独特的屏幕编辑指令与方便使用的正则接口才是Vi类编辑器最重要的特点;虽然大部分用户只需要用10-20%的Vi功能;虽然Vim的定位是一个普通文本编辑器。但是大家还是想知道除了常规的编辑功能外,这个编辑器到底还可以做什么?

下面这些都是Vim扩展功能的重要手段:

  • 宏(包括了q命令,:map命令和:ab命令)
  • Vim Script(升级版的Ex命令)
  • 外部程序

宏与Vim Script能实现复杂的功能但局限于Vim提供的命令/函数中。使用外部程序的灵活性要差一点,通常只用来过滤编辑区的文本。这就是Vim的全部本事吗?当然不是。Vim还有两大武器——开放源码与编程语言接口。

1 开放源码

Vim是开放源码软件,所以像所有开放源码软件一样,你可以对它做任意的改动。Vim-ShellVimGdb是较为人所知的例子,它们使得在Vim中使用shell及在Vim中进行集成调试成为可能。

得益于开放源码,c程序员们可以在源码级别为Vim“添砖加瓦”。这是扩充Vim的功能最强大的途径——开放源码意味着可能性。一直以来,Bram Moolenaar都声称不会在Vim中集成shell或加入控制调试器的功能1。但由于Vim源码开放,用户还是有机会在Vim中使用Shell及调试程序——虽然它们没能成为Vim的正式发布的一部分,但这些补丁的存在至少让“源码上的无限可能性”不至于成为流于虚无的口号。

Bram认为Vim应该成为一个开发框架的一部分(编辑器部分)而不是成为开发框架本身。而他也身体力行,于是就有了Agide。我想正是由于他对Vim的清晰定位,Vim才不至于成为另一个自带编辑器的操作系统。但小小私心的说,在Vim中加入Shell应该还是符合Vim的自身特点的(因为有行模式的存在),也有利于Vim与其他程序的交互操作。而且只是作为Shell的表现层,不至于增加许多代码。

只是从源码上改进Vim终究是麻烦了一点——需要编译部署的周期。2于是有人为Vim增加了一些接口目的是能在Vim中使用通用脚本语言,重要的是这还成了正式发布的一部分。于是我们就有了perl和Python接口!当然,通过这些接口你还是无法改变Vim的运行机制,无法像c一样无所不能。但这样做还是换来了多方面的好处:首先,你不用为了增加一个绿豆大的功能,而在Vim源码海里打转。其次,你的开发周期中少了编译及重新部署Vim的过程。第三,现今流行的大多数脚本语言都是高级的语言,可以大幅减少开发时间。最后,你还可以选择不加入这些接口——如果你不需要的话,也没有人会强迫你安装一个你不需要用的60MB的脚本引擎。

2 Vim的编程语言接口

现在仍有许多Vim用户对Vim的程序语言接口不了解,。Vim中除了有自带的脚本引擎外还支持多种脚本语言,其中perl和python自Vim 5开始就成为了正式发布的一部分。现在可以使用的有五种脚本语言Perl, Python, Ruby, TCL和MzScheme。据不一定可靠消息称Java和Lua的接口也在开发中。

记住:只有在编译时加入了相应的选项才能使用这些程序语言接口。

??:在Vim脚本中使用这些脚本言与通过!命令使用这些脚本语言有什么不同?外部命令/程序只能修改buffer的内容,而这些接口允许在脚本语言中访问Vim的所有功能。与开发一个更强的Vim脚本引擎相比这样做有什么优势?这些脚本语言都是成熟的通用编程语言,功能上无赘言;多种脚本语言支持也为用户提供更多的选择,用户可以使用自己熟悉的语言。当然,它们都符合Unix工具箱哲学——工具专注于各自的领域,并可以组合使用。

各个脚本语言能直接访问的Vim对象不完全一样。总的来说可以直接在这些程序语言中使用的对象还不多,但Vim提供了访问Vim Script的接口确保所有的Vim功能都可以通过这些脚本语言访问。举例而言,你可以在perl中修改buffer的内容,但没法直接指定某一段文字所使用的语法高亮颜色。然而,你可以通过VIM::DoCommand()调用Vim Script设置高亮。

那利用Vim的程序语言接口到底能做些什么呢?通常使用的分工模式是由Vim负责输入输出,而处理逻辑部分由脚本负责。因为这几个都是通用编程语言,所以负责处理逻辑的脚本实际上可以完成任何编程语言能完成的工作。不过记住不要滥用Vim了!如果你只是打开Vim,运行脚本,然后关闭Vim,说明你可能不需要用到Vim而可以直接用该脚本语言完成同样的工作。想一下能否在Vim中实现下面描述的功能?

  1. 按一个功能键(比如<F2>)就自动从网上抓取最新的Vim Tips,并在一个分割窗口中显示。
  2. 将当前编辑文本发布到twitter。
  3. 按<F2>将当前编辑文本导出为Word格式,并自动进行适当的格式编排。
  4. 与GTalk用户聊天。
  5. 以MySQL(或其他数据库)做为万能补全的匹配源
  6. 写一个机器人应答程序。
  7. 一个音乐播放器。

没错,所有这些功能都可以实现。

  • 解析XML对主流脚本语言来说都只是小菜一碟,你唯一需要确定是在Vim中的显示格式。
  • 发送POST请求同样不是问题。可参考:Twitter Wiki,这里是现成的Vim script(Twitter)
  • 导出为Word格式需要机子上预先安装有Word,脚本语言可以使用自己的com接口来访问Word对象(Word.Application)。将解析过的文本传递给Word的com对象,并根据解析的结果添加适当的样式(控制Word对象或添加Word的样式比较简单,根据文本判断选择样式则相对复杂一点。因为涉及到文本的解析。)见使用Python來控制MS_Word
  • Perl、Python、Ruby都有现成的库XMPP可以与GTalk通信,为Vim添加这个功能并不困难。参考:XMPP Perl Library
  • 通过脚本的SQL接口访问数据库返回自动补全的匹配源。这当然也没问题。
  • 如果你需要的只是不断回答Yes的机器人的话,使用Vim Script足矣。如果你要写一个更智能的机器人话你就需要这些更强大的脚本语言。如果你想用现成的那就看这里,这是Python实现
  • 音乐播放器?没错,还是可以。只是在这个应用中其实不需要Vim。

如果你觉得这不像是Vim能做到什么,而更像是这些接口能做些什么的话——没错,实现“责任和义务的转移”这正是这些接口的意义所在。有了这些通用开发语言加持,Vim能做什么就看你的创意了——你唯一需要确定的是实现这些功能需不需要用到Vim

用这些接口开发Vim扩展的一个局限是,你可以同Vim交互的手段相当有限(比如没办法开一个监听Vim事件的守护进程)而且Vim没有图形方面的接口。只是当你遇到这方面的限制时,说明你很可能已经越界了——你正用Vim来做一些跟编辑/显示文本无关的事。

3 最后

c语言无疑是最强大的武器,但用c扩展Vim功能的开发成本相对较高。如果你会前面说的这几门脚本语言中的任何一种,你就可以用少量的开发成本为Vim增加非常多的可能性。啰嗦一句——如果你不会,你还是可以让Vim助你提高工作效率,要知道在这些接口出现前Vi类编辑器就已经是最受欢迎的编辑器了。

我听到很多人不用Vim的原因竟是因为他们用不了这么多功能-__-!,我写这篇文章时用到的最高级的的Vim指令是搜索,但已经足够提高编辑效率了(类似的话我这几天大概说了200次,只是效果不彰XD)。Vim有很多的高级应用,那演示的是Vim的可能性,但大多数人的大多数操作都只用到几十个最常用的Vi(m)指令及指令组合——而这几个指令才是Vi(m)最有价值的功能。

作为一个Vim的用户,应该要清楚这两点,首先,Vim的目标之一是可以方便地与其他工具协作而不是取代其他工具;其次,Vim的定位是通用文本编辑器。因此使用Vim的脚本接口扩展功能前,你应该先问一下自己:有没有现成的更好/更方便的工具?为什么这个应用中需要用到Vim?

Appendix A Vim开发原则

相信大家选择编辑器无外乎就是在功能、体积、速度、外观(还有操作方式,只是在用Vi之前大家可能没意识到编辑器是可以有不同的操作方式的)中做一个权衡。没有一个编辑器能满足所有要求。对功能,外观上的要求越高,就要牺牲点体积和速度;反之亦然。Vim试图同时满足功能、体积和速度方面的要求——这不是一个容易达成的目标。Bram Moolenaar为此定了一些Vim开发的原则。可以通过以下命令查看:

:h develop.txt

这些既是Vim的开发原则,也解释了Vim之所以成为现在的Vim。通过这些原则我们可以一窥Vim背后的理念。为了减少大家在浏览器与Vim中切换的次数,这里列出一部分:

  • Vim的目标是成为一个更好的Vi而不是一个全新的编辑器。
  • 尽可能地使用键盘,因为大数人没有第三只手握鼠标。
  • 减少使用控制键的使用,因为按控制键不方便。
  • 要能支持多种不同的终端、平台、多种编译器和库。尽可能的在多平台间保持外观和功能上的一致。除非确实是很酷的功能否则不要开发某平台独有的功能。
  • 新的功能都要带有完整的文档。
  • 体积要小,速度要快。确保Vim只用少量的系统资源,让旧的机子也能用Vim。
  • 有些用户使用低带宽的网络连接,要减少网络通讯量。
  • 用户拥有定制的自由,包括可以选择去除那些带来体积增加却又不是大多数人用得上的功能。
  • Vim的目标是方便高效地与其他程序协作,而不是代替所有其他程序。Vim的定位是系统的组件之一,而不是一个包罗所有功能的巨大程序。
  • Vim不是一个shell(交互式命令行解释器/控制台/命令提示符)也不是一个操作系统。所以你没法在Vim中开一个shell或用Vim去控制debugger。按Vim的设计理念,应该是将Vim作为其他shell或IDE中的编辑部件(而不是成为shell或IDE本身)。
  • Vim不会为了更华美的外观而牺牲其外观在不同平台的一致性。
  • 原则上欢迎添加任意功能(除非与其他原则相抵触XD)。

Bram在这篇文章里面对Vim的发展有类似的阐述:工具应专注于各自的领域,并且可以通过组合产生1+1>2的效果,而不是成为一个无所不能的臃肿程序。Vim功能上的增强会被限制在文本编辑方面。它的目标是成为最好的编辑器并且可以与方便地其他工具相配合。3




Footnotes

[1] :h desing-not

[2] 而且考虑到Unix/Linux上大把的开源软件,“可以从源码上扩展功能”这一点实在不是特别吸引人的特性。

[3] 听起来有点耳熟?就是前面提到的Unix的工具箱哲学。关于这段话可以在原文中搜索”World domination”。事实上Bram Moolenaar曾多次提及这些原则,google一下“高效文本编辑的七个习惯”。


vim_chatbot

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