碎碎念

2007, October 13

分栏显示文本

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

在网上看到这个,一时手痒写一下

: 不知能否完成这个操作:
:
: 1-10行长短不一,11-20行也是,现在想把11-20行整体搬移到1-10行
: 的后面,行与行对应。1-10行最长有20字符,11-20行就从第30字符处开始

有写过shell的想必都会想到paste(paste需要2个输入文件,需要有额外的导出的步骤)。awk当然也没有问题。但最合适的工具是pr。如果你用Linux的话你已经有这些工具了,Windows的话可以下载gnuwin32工具

那Vim呢?Vim如何完成上面的工作?
答案是Vim也是使用上面的工具:

:1,20!pr -2t

上面这条命令将1至20行,分成两栏显示。如果要分成4栏:将-2改成-4就可以了。

那单纯用Vim有没有办法呢?有。

:1,10g/^/+10t.|-1j
:11,20d

只需要两行的ex命令。而且用到的4个命令:g,:t,:j和:d都是最基本的ex命令:)

简单地解释如下:

":1,10g/^/"        对前10行进行处理
"+10t."            相对于当前行下移10行并复制到当前行下
"-1j"              合并两行
":11,20d"          删除不需要的行

关于:g命令的用法可以见这里
这个ex脚本有个小问题,就是不怎么整齐。改进的版本如下:

:1,10g/^/s/.*/\=printf("%-30s",submatch(0))/|+10t.|-1j
:11,20d

加入的命令就是:

s/.*/\=printf("%-30s",submatch(0))

这条命令确保第一栏的宽度为30。

让我们实际操作一遍。现在有如下文本:

 1 aaaaaaaaaaaaaaaaa
 2 aaaaaaaaaaaa
 3 aaaaaaa
 4 aaaaaaaaaaaaaaa
 5 aaaaaaaaaa
 6 aaaaaaaaaaa
 7 aaaaaaaaaaaaaa
 8 aaaaaaaaaa
 9 aaaaaaaaaaaaa
10 aaaaaaaaaaaaaa
11 aaaaaaaaaaaaa
12 aaaaaaaaaaaaacccc

在Vim中输入如下命令:

:1,6g/^/s/.*/\=printf("%-30s",submatch(0))/|+6t.|-1j
:7,$d

结果如下:

 1 aaaaaaaaaaaaaaaaa          7 aaaaaaaaaaaaaa
 2 aaaaaaaaaaaa               8 aaaaaaaaaa
 3 aaaaaaa                    9 aaaaaaaaaaaaa
 4 aaaaaaaaaaaaaaa            10 aaaaaaaaaaaaaa
 5 aaaaaaaaaa                 11 aaaaaaaaaaaaa
 6 aaaaaaaaaaa                12 aaaaaaaaaaaaacccc

显然这种任务Vim自己就能轻松应对。不过如果有pr的话还是用pr。Vim的力量来源于对Unix文化的传承,Vim随时做好了与外部程序协作的准备!记住,Vi界并不鼓励重新发明轮子。

在这个问题的来源处可以看到另一种解法。我想这里的解法会灵活一点,想想如果你要操作的不是20行是200行,手工操作并不方便。而这里的解法只要将范围改一下就可以了:

:1,200!pr -2t

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 27

用SED打印斐波那契序列

Filed under: sed

整理邮件时看到了这个SED脚本(2006年1月),好像是我写的最后一篇关于SED的文章……

# 脚本开始
# SED打印斐波那契序列

# 调用:seq 10 | sed -f fib.sed # 打印前10个数字(运行较慢,不要打印太多)
# yes | sed -f fib.sed # 打印序列直到溢出

# 这个脚本用模拟的方式进行数字运算。有关这种方法的更
# 多细节可以参考Greg Ubben写的教程:
# http://sed.sourceforge.net/grabbag/tutorials/

1{
# 下面字母串的长度决定了可打印的序列长度
# 可以修改下面的字串使之支持更多的序列
# s/.*/zyxwvutsrqponmlkjihgfedcba/
s/.*/kjihgfedcba/
# 初始化序列为“1, 1”
s/.*/ &a &a /; h; d
}

# 从hs取最后两组数字串进行运算
g
s/.* \(.*\) \(.*\) $/\1\2/

# 进行进位操作前先对两组数字加总(其实是排序)
s/\(\(.\)\2*\)/\1 /g
:lpsort
s/\(\([^ ]\)\2\{0,\}\)\( .*\)\2/\1\2\3/
tlpsort
s/ //g

# 调整加总的结果
# 加总时我们进行如下运算:
# edcbaa + edcbaaa = edcbaaaa
# 1 + 2 = 3
# 但实际在加总(排序)后我们等到的结果是:
# edcbaa + edcbaaa = eeddccbbaaaaa
# 我们需要将eeddccbbaaaaa转换为edcbaaaa

s/\(.\)\(\1\{1,19\}\)/\2/g

# 进行进位操作。
# baaaaaaaaaaa进位后成为bba——10

:doCarry
s/\(.\)\(\(.\)\3\{9\}\)\3/\1\1\3/
t doCarry

# 简单(粗糙)的溢出检验
# 因为第一个字母如果超过9个说明没有正常进位——即溢出
/^\(.\)\1\{9\}/{g;s/$/ ++OVERFLOW++/; b2digi}

# 将这一轮的结果保存到hold space。记得在最后加上一个空格。
G
s/\(.*\)\n\(.*\)/\2\1 /
$!{h;d}
g

# 运算完后将模拟结果转为数字表示的结果
:2digi
s/\([a-z]\)\1\{9\}/9/g
s/\([a-z]\)\1\{8\}/8/g
s/\([a-z]\)\1\{7\}/7/g
s/\([a-z]\)\1\{6\}/6/g
s/\([a-z]\)\1\{5\}/5/g
s/\([a-z]\)\1\{4\}/4/g
s/\([a-z]\)\1\{3\}/3/g
s/\([a-z]\)\1\1/2/g
s/\([a-z]\)\1/1/g
s/\([a-z]\)/0/g
s/ 0*/ /g
# 溢出检验需要q命令
q
# 脚本结束

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

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