碎碎念

2006, June 27

Vim7中的万能补全(Omni completion)

Filed under: vim, 编辑器, 教程




Up: (dir)

Vim7中的万能补全(Omni completion)

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

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



1 万能补全基础

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

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

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

se ofu=UCompl

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

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

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

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

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

how ar_
^^^ ^^

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

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

我再给一个例子,输入:

abcd_

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

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

运行上面的脚本并输入:

abcd_

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

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

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

补全的结果如下:

abXXX_

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



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

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

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

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



3 更多

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

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

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

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

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

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

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

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



4 使用外部文件

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

         let tl=taglist("thetag")
    

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

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

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



5 实例

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

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

下面是代码:

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

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



Appendix A 中文议题

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

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

这是示例文_

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

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

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

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

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

这是最后的函数:

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

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



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

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

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

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

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

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


附图:
vim_oc

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

Filed under: vim, 编辑器, 教程




Up: (dir)

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

Windows的用户可能会因为这一篇里面用了大量的Unix工具而倍感沮丧。但大可不必如此,这一篇里面我们更多的是讲一种使用Vim的理念──用户可以依据自身对外部工具的掌握程度适时地使用这些工具来减少工作量。常的工具都可以找到对应的Winodws版本,而且通常很小。

unix工具箱哲学的一个核心思想是每个工具只完成各自相对简单的任务,这些工具的真正威力来自于它们之间关系。你可以组合这些工具来完成复杂的工作。Vim虽然是一个交互式的编辑器,但这种思想仍在它身上得了很好的体现。它能很好地与其他工具配合来扩展功能或完成相对复杂的编辑工作。

这一篇教程我们将讨论与Vim中与外部程序有关的议题。




1 使用外部程序的Vim命令

Vim中有一些功能是通过外部程序来实现的。比如:make命令,Vim没有内置make工具。但是用户可以为:make命令指定一个外部程序,比如:gnu make或Windows下的nmake。这样我们在Vim中就可以使用:make命令了。设置make工具所用的设置项是'makeprg'。我们先看一下跟外部程序有关命令和其对应设置项。

:cscope

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

=

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

gq

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

:grep

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

:make

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

K

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

:shell

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

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

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

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

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

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

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

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

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

:grep
:make
这两条命令的真正有用的/有效的特性在于Vim可以根据运行的结果生成quickfix窗口。而且你可以用:cn, :clist, cw…等quickfix命令在不同的位置间跳转。要用好两条命令首先要了解这两个设置项:grepformat errorformat。这两个设置项的作用是捕捉并分析输出。在使用make工具时由于在使用不同编译器时输出的错误信息的格式也不尽相同,所以在设置好了grepprg或makeprg后还要教它“读懂输出”。

在缺省情况下Unix平台的Vim的'grepprg'是'grep -n'而Windows下则是'findstr /n',不过它们运行的结果都一样。假设你是命令行下(shell中)用前面的命令在当前目录查找:grep -n goes *.txtl,返回信息是以下面的格式出现的:

     a_text_file.txt:52: something goes here .........

这条信息中包括了:文件名a_text_file,行号52,该行的内容共三个部分。这三个部分是由:隔开的。
我们再看一下缺省情况下的gfm:

     :se gfm?
     grepformat=%f:%l:%m, ....

同时设置的多种不同格式的grepformat间用,隔开。其中%f、%l、%m分别表示文件名,行号和提示信息(这个例子中就是该行的内容)。中间的:是帮助对应不同部分的关键,Vim在输出的信息中查找:并将冒号分隔起来的三个部分与gfm中的三部分对应起来。这样我们是Vim中使用grep后,它就能自动解读输出的信息并带我们到不同文件的特定行去了。要了解%f、%l等更多占位符的意义及用法见*error-file-format*

提示:在Vim中通过程序的输出信息,使用cn cl cw等命令在文件中快速移动的这一特性称为quickfix。使用quickfix时,文件名是必需的——这样才能打开正确的文件,有行号的话更好——精确地定位到行。

可以看到在设置好程序的前提下,要用好这两条命令的关键是设置好合适的gfm/efm。Vim文档中提供了相当多的现成的gfm和efm,几乎含盖了各种类型的编译器。所以在你设置"合适的gfm/efm"之前先看一看有没有现成的。

这里再来看一下makeinfo的输出格式,

     somethingwrong.texi:5: Unknown command `blah'.

与我为它设置的“错误信息格式”:

     se efm=%f:%l:\ %m
     se makeprg=makeinfo

很眼熟不是?它跟grep是一样的只是刚才的查找结果现在成了“错误信息”,本质上:grep跟:make是一样的。除了命令名称和设置项名称的不同它们的其他地方是一样。在设置这些后我只要使用:make,Vim就会带我到出错的行去——如果有错的话。

提示:se makeprg=makeinfo这条命令不是必需的,你可以将命令放到makefile中,然后使用make工具。当然直接用makeinfo编译文档还是使用make工具由你自己决定。当然efm是要设置的。同样的还有tex文档,你一样可以使用make工具但efm要设置正确。

这两条命令的作用可不仅局限于使用各种make工具或grep类工具。想想下面的设置会产生什么样的效果:

     :se gfm=%f
     :se grepprg=find

上面的设置让你可以快速定位特定的文件并打开。将find改为ls就有了一个简易的Explorer插件了。

看到下面的命令,你可能会一头雾水,

     :se grepprg=cat
     :se gfm=%f
     :grep blah.txt

但如果你知道blah.txt有着下面的内容后,你就会知道了——这是个项目文件。

     farsi.c
     farsi.h
     feature.h
     glbl_ime.cpp
     proto/buffer.pro
     proto/charset.pro

再想一下如果blah.txt的内容是一些网址,上面的命令又会产生什么结果?就像这样:

     ftp://ftp.vim.org/pub/vim/
     http://sf.net/

动手实验一下,然后:h netrw。

最后提供用来调试sed/awk脚本用的'efm':

     " sed
     se efm=sed:\ file\ %f\ line\ %l:\ %m
	
     " awk 仅限于使用脚本文件时。
     se efm=awk:\ %f:%l:\ %t:\ %m

对于make和grep的设定可以在ft文件中设置或是在vimrc中使用au(:help :autocmd)进行设置。K,gq,=的功能也可以用map的方式来达到(这就是它们并非必不可少的原因),并且map也带来了灵活性。但使用keywordprg你不能指定Vim的内部命令,同时使用外部命令时map通常意味着更复杂的设置过程。有利有弊。

另外,Vim处理make和grep和cscope的方式是内建一个命令的接口(quickfix),要达到同样的效果又需要用户写相当长度的脚本。可以说这点来说使用内置的接口还是要方便些的。

尽管我们可以通过这些Vim命令灵活地使用外部程序,但Vim与外部程序的互动更多地来自:!命令。




2 !命令

Vim为用户提供了极大的可能性,让用户可以自由的操纵他们的文本。尤其是通过内置的脚本解释器,用户只要写Vim脚本就可以扩展Vim的功能。但是Vim知道大量经得住时间考验的Unix工具如果闲置起来就太可惜了。通过提供方便的接口,Vim在保持体积小巧的同时也让用户有选择的余地。

Vim的插件机制是以两种方式实现的:一是通过内置的脚本解释器。一是通过保留与外部程序协作的接口。本篇我们会看看如何在Vim中使用外部程序或shell脚本。

一般而言我们可以通过使用多个软件来满足我们的特殊要求:先用A程序处理一部分保存,再由B程序接着处理,……但是Vim提供的接口让我们在使用外部程序就象在使用内建的功能一样方便快捷,你不必在程序之间来回切换。

Vim调用外部程序主要是通过!实现的。!命令可以在行模式下也可以在一般模式中使用。带地址/范围地使用这条命令时(即作为过滤命令时),当前地址范围内的内容会被命令的输出所替代。否则只是在shell中运行该命令。详细用法见:! 和 :! 。

在你决定花点时间写Vim脚本前也许你应该先考虑以下这几个问题

  • 我需要什么样的结果?
  • 有这样的外部程序存在并且能更好或更简单地满足我的要求吗?
  • 最后就是你对哪种方式(Vim脚本或外部工具程序)更熟悉。

比如你可能在整理一份软件列表,你需要某个目录的文件列表。毫无疑问你会选择用外部程序(或命令):

:r !dir ...
:r !ls ...

这是另一种情况,你要将文本区中每一行文本复制一行在该行下面,你可以使用下面的任意一条命令:

%!sed p
g/^/norm Yp

使用两命令看上去没什么差别你只需要照自己的习惯来决定使用哪条命令。但如果你要对文本排序或是更复杂的工作呢?这时写Vim脚本会是很累人的工作。

2.1 !的两种使用风格

!命令在shell中运行命令,所有能在命令行中运行的程序/工具都可以。对Linux来说这代表了几乎所有的可执行程序都可以从vim中启动运行。而Windows中则与路径的设置有关,但通常来说在windows目录下和system32目录下的程序都可以直接在Vim中运行。此外还有命令解释器的内部命令。能在shell中运行的程序脚本或批处理文件都能在Vim中使用。但我们可以作一些简单的分类。
在Vim中使用命令不外乎有两种情况。

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

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

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

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

  2. 是为了获取程序的输出。又可细分为两种。过滤程序(filter)产生的输出;非过滤程序的输出。
    • 获取非过滤程序的输出。前面的读入目录的命令就是在Vim中使用非过滤命令的例子,这里再举几个例子:
                r !date         "当前行下插入日期
      	
                " 在当前位置插入脚本运行时间的测试结果
                .!time myscript 
      	
                " 运行用户的shell脚本或批处理程序,并获取输出
                r !myshellscript
           

      能够在命令行产生某种输出的程序(批处理/脚本)都能与Vim配合(如前面例子中的date),而其中一些从stdin读取数据处理后输出到stdout的程序,即filter程序,在Vim中的应用范围要更大一点。相对而言一个图形介面程序在这种情况几乎毫无用处可言,无法在命令行下运行也就代表着难以与其他程序配合。因为当一个程序在处理的结果是在对话框中显示的话,表示这个结果无法直接为Vim所用。我们必须至少要经过一个复制粘贴的过程:-(

    • 获取过滤程序输出。与非过滤程序不同的是过滤程序对给定范围内的文本进行“加工”,然后才产生输出。下面是一些使用过滤命令的例子。
                " gggqG
                %!fmt
      	
                " 选择打印部分“列”数据
                8,20!cut ..
      	
                " 删除选区中的重复行,改变次序
                '<,>'!sort |uniq 
      	
                " 同上,但不改变行的次序
                '<,>'!awk '{if ($0 in a) next}{a[$0]++}8'
           

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

2.2 实例

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

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

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

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

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

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

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

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

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

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

  6. 导出为CVS(这样就方便在其他数据处理分析工具中打开):
         " 选择要导出的范围,然后输入……
         :'<,'>w !tr -s ' ' , >成绩.cvs
    




3 相关议题

  • 要以某一部分的文本,如圈选区的文本作为某个外部程序的输入但又不捕捉该程序的输出要怎么做呢?我们需要用到:w命令,格式如下:
         :3,5w !cmd
    

    我们前面导出为cvs用的就是这种方法。

  • 很多情况下我们需要在Vim脚本中运行并捕捉外部程序的输出。这时我们可以用system()函数(*system()*)。
         :let files = system("ls")
    

    上面的例子是文档中带的例子。看了这个例子大家应该就知道这个函数的用用法了。




4 小结

Unix的这些工具软件有着比Vim脚本更多的应用,对于个人的使用目的来讲写Vim脚本不应做为首选。别忘了从Vi诞生之始开始工具软件就一直是扩展Vi功能的主要途径。充分利用现有的外部程序资源一直是用好Vi的关键之一,除了节省大量时间外,使用对的工具正是Unix风格的一贯体现。如果你使用Linux/Unix的话花点时间在这些工具上会是很好的投资。

最后一点补充:在写完这一篇后,我才发现关于!的部分的篇幅与其在实际应用中的广泛和重要程度完全不符(这当然是因为我前面说过的“这更多是涉及外部程序和而非Vim的使用)。所以我想我得说明一下:对于相当一部分的老手来说!是他们解决一些棘手编辑问题的第一个选择,其次才是脚本。而其中有些人(尤其是perl老手)根本不愿意写超过5行的Vim脚本。

Appendix A

myawkscript.awk的代码:

# /usr/bin/gawk -f
# 从期末成绩计算各成绩段的人数、比例及图示
	
# 计算各成绩段的人数
{$3>=90?++a[1]:$3>=80?++a[2]:$3>=70?++a[3]:$3>=60?++a[4]:++a[5]}
	
# 显示输入的数据
8888
	
END{
split("优 良 中 及 不",rank)
print "\n-- 期末考成绩分布图示 --"
# 显示统计信息
for(i=1;i<=5;i++){
    if(a[i]=="")a[i]=0
    printf "%s\t%d人\t%d%%",rank[i],a[i],a[i]*100/NR
    # 显示图示
    while(a[i]-->0)printf "■"; print ""
   }
}



Footnotes

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

2006, June 18

暂停更新

Filed under: 五四三

在接下来的一段时间内这个Blog将暂停更新

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

Vi/Ex编辑器教程[3]

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

Vi/Ex编辑器教程

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



第三章 功能强大的全局命令

你可能会因为我在上一篇的教程中没提到“:global”而觉得奇怪,不过“:global”其实不是一个地址。这事实上是一个行模式命令――全局命令,并且它的作用比大多数用户想像的要大得多。

就算是有经验的用户在想到全局命令时也会与下面的这些话联系在一起:“如果你输入了:global接着输入搜索式样,然后是行模式命令,把它们放在同一行。那么编辑器会对逐一对文件中包含匹配式样的行运行行模式命令”。也就是,在输入:

     global /^Chapter [1-9]/ delete

后,用户想到的是编辑器会查找并删除文件中所有以“Chapter ”1到9开头的行。没错上面的例子干的正是这事,这个命令的这种用法随处可见。但还是不时地出现误用,以下命令:

     global /^Chapter [1-9]/ write >> t.of.contents

就算是有一定经验的用户也可能会以为上面命令的作用是将匹配式样的行添加到名为“t.of.contents”文件中去,这当然是错的。(上面的命令更像是用来消耗磁盘空间的)





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

全局命令的操作细节

更重要的是,对全局命令的不了解让用户只能发掘到该命令一小部分的潜力,无法发挥它真正的作用。庆幸地是你再也不用受这种不了解的束缚了――在本文里我们会完整地呈现这个命令的方方面面。

在指定的位置搜索:

与其他的行模式命令一样,全局命令之前也可以放一至两个地址。它的默认搜索范围是整篇文档,但如果你使用命令“257 , 382 global ….”时搜索会从257行开始一直搜索到382行(包括382行)。全局命令前可以放置所有类型的行模式地址,因此以“?^Exercises? +++ , $ global”开始一个命令时编辑器会回搜到的第一个以“Exercises”开头的行并以该行之下的第3行为全局命令作用范围的开端,这个范围一直到文件的最后一行结束。

标识匹配或不匹配的行:

输入全局命令“global”或“g”会使之对搜索范围内的每一行包含要搜索的式样的行进行标识。但输入“global!”、“g!”或“v”的作用则相反:现在它只标识未包含搜索式样的行了。如果你正编辑一个记录错误信息的日志文件,你需要的只是以“Error 3b:”开始行,那可以用以下命令将其他行删除:

     global! /^Error 3b:/ delete

选择你自己的搜索式样分隔符:

因为这个命令总是由上至下对整篇文档(或是你选择的范围)进行搜索,几乎所有的标点符号都能用来表示搜索式样的开始与结束。也不需要用“?”或“/”来选择搜索方向。如果你想要删除所有包含三个斜杠的行,这些命令:

          global +///+ delete
          global ;///; delete
          global ]///] delete
     

都比使用斜杠作为分隔符然后为欲搜索的三个斜杠前分别加上反斜杠要来得简单。(但是使用“!”作为分隔符要当心,因为全局命令“:global”会把命令后的“!”当成指示全局命令搜索不含指定式样的行的一个开关符号。)

当然这只对紧跟在全局命令后的搜索式样有效,这个搜索式样用来指定要标识的行。如果是在全局命令前的用来指定全局命令的作用范围的搜索式样,那么还得像往常一样使用“?”和“/”作为分隔符。

看似无用的一些全局命令:

有些时候,虽然只在文件中找一行,使用“:global”或“:global !”也是明智之选。当选择性的对行执行行模式命令时这是一种基本的技能。举个简单的例子,当你想要删除最后一行当且仅当最后一行是空白行时。你可以选择每次都自己移动到文件末尾然后看一下有没有附加的空行,但让编辑器去检查并删除(如果需要删除的话)会轻松一点,这时你可以输入:

          $ global /^$/ delete
     

让全局命令标识你自己限定的范围内的所有行也是有实用价值的技巧!我们说过的程序员――小何(Hal,在这系列教程的第一章中出场过的)在倒置文件中的所有行时就使用过这一技巧。他所用的命令以完整的形式写出来就是:

          global /^/ move 0
     

标识所有有“开端”的行,其实就是标识所有行(包括空行)。接下来该命令从第一行开始并将第一行移到虚构的行――零行之下。然后它再到第二行并同样把第二行移到零行之下,原来的第一行这时成了第二行。它对第三行做同样的动作,然后第四行,第五……。就这样它逐渐地将行的次序颠倒了。

一个全局命令后能使用多个命令:

可以在“:global”命令后使用多个命令和命令所用的搜索式样。在标识了特定行后,全局命令接着逐个对标识行依次(依照你输入它们的次序)运行所有命令。这些命令通过竖线(“|”)分隔。如果你输入了:

          global /^CHAPTER/ substitute /APTER/apter/ | copy $
     

编辑器会对以章名(CHAPTER)开头的每一行执行替换,将“CHAPTER”换成“Chapter”,然后复制该行(现在是以“Chapter”而非“CHAPTER”开头了)到文件末尾。这两条命令(替换substitute和复制copy)的次序很重要,替换的命令必须在前,不然复制到文件末尾的就成了原来的全部大写的“CHAPTER”了。

并没有限制说只能在全局命令后使用两条命令。对命令数目的上限并没有规定。对整条命令序列(与全局命令一起使用的多个行模式命令组成的序列)的长度的限制则依Vi版本的不同而有所不同。话说回来我从没遇到过这一长度上限小于256个字符的。但对于命令序列的使用方式有些规定:

  • 关键字“global”和后面命令序列必须在同一行中。(指的是“物理行”,中间没有回车符分隔的行,如果该行长度超过了显示设备的宽度的话,可以折行显示――当然这没关系。)
  • 命令序列不能包括“undo”(撤消)命令和另一个“:global”命令
  • 如果序列中包括一条在shell(命令解释器)运行的命令,那它必须是序列中的最后一个命令(在同一命令序列中使用两个或以上的sell命令是无意义的)。这样该命令才能在它的shell命令行中使用管道操作(“|”,与命令序列的分隔符是一样的),而不至于让编辑器分不清那是一个全局命令的分隔符还是一个命令行下的管道操作符。

命令并非一定要在所有全局命令标识的行上运行:

使用全局命令就如同用手工移至标识行上再运行命令一般。正如同有时你运行命令并不是对输入命令时所在行进行操作一般,全局命令中的命令序列也不一定要对所有标识过的行进行操作。这里有三点要强调的:

  1. 在全局命令后的任何命令都可以有自己的地址,与那些命令在单独使用时一样。因此这一命令串:
                   global /^XX/ - copy $ | /ZZ$/ , +5 delete
              

    是完全合法的。它逐个地找到以两个大写X开头的行,上移一行,复制该行到文件末尾,然后向下搜索以“ZZ”结尾的行,并将该行及其下的五行删除。

  2. 在全局命令的命令串中即使你没有给出命令的地址,这些命令命令还是可能不会对全局命令标识的行进行操作因为它有默认的地址。这就是在本章的介绍部分中说的所说的全局命令的错误用法。因为写入(write)命令的默认地址范围是整篇文档,所以那条命令的作用是当每遇到一条全局命令标识的行就将整篇文档写进(添加到)另一个文件的末尾一次。要将全局命令标识的行写进另一个文档的正确做法是:
                   global /^Chapter [1-9]/ . write >> t.of.contents
              

    在写入命令前面的“点”(半角句号)告诉写入命令只对它所在的行进行操作。

  3. 但全局命令后的命令序列中使用有默认地址的命令时,如果该命令不是序列中的第一个命令(即跟在“:global之后的命令”)的地址即使它有默认的地址,在没有给它自己的地址的情况下它也可能并不是用全局命令标识的行作为它的地址。原因:在全局命令的命令序列中每一条命令都以上一条命令结束时的所在行为当前行。

在我的之前的一个例子中,讲了如何将以“CHAPTER”开头的行改为“Chapter”并复制到文件末尾。那个任务本身很简单因为被复制的行都是已经被更改了大小写的行。那么如果希望文件中间的“CHAPTER”改为“Chapter”,而复制到文件末尾的仍是全部大写字母的形式时要怎么做呢?这看上去似乎只要对调一下命令序列中的两个命令的位置就行了,这样就可以先执行复制命令然后再执行用来更改大小写的替换命令,就像这样:

          global /^CHAPTER/ copy $ | substitute /APTER/apter/
     

出乎很多人意料的是,这命令运行的结果与预想正好相反。这条命令会将复制到文件末尾的行的大小写更改而文件中的那些行的却仍然是全部大写的。究其原因,复制操作将行复制到文件末尾并在那里――而不是原来由全局命令标识的行结束复制操作。而替换命令以上一命令结束时所在的行(最后一行)为当前行。在没有明确给出地址的情况下替换命令的默认地址便是当前行,因而它会对文件末尾的行(复制的行)而不是原来的行(全局命令标识的行)进行大小写更改的操作。

但有一件事是无论在命令序列中的“当前行”怎样地变化也不能改变的。当全局命令从一标识行开始运行命令时,不管命令序列最后在哪一行结束运行(这一行成了新的当前行),全局命令总是到下一标识行(而不是当前行)重新开始运行命令序列。要阻止全局命令从下一标识行开始运行,只能让命令序列中的命令删除下一标识行――但那样的话,全局命令就会移到下一未被删除的行开始命令。

假设你想要对文件进行一些删减――每隔一行就删除一行。你可以用下面的命令:

          global /^/ + delete
     

全局命令首先对每一行进行标识。当它来到行1时,它就执行命令删除行2。然后移到下一未删除行――行3,并删除行4,依此类推。或者如果你想要删除每三行中的后两行,输入:

          global /^/ + , ++ delete
     





下一个: ,
上一个: 全局命令的操作细节,
上层: global

全局命令的例子

上面的那些例子除了用来说明全局命令的工作原理外,还展现了它较鲜为人知的一些技巧。但上面的例子无法涵盖它所有的重要技巧。这而补充一些有价值的技巧。

计数。有时全局命令后面的命令序列与全局命令标识的行根本毫无关连,这些命令并不对全局命令标识的行进行操作。这种情况通常出现在我们需要重复运行一个行模式命令一定的次数时。

我经常受邀到一些展会上测试系统,就在展台上测试。我不能总带着一份有着10000行的测试文件,因为我事先不知那个系统对存储介质、格式有什么样的要求。我的做法当场新建一个文件并在其中输入10行,然后让编辑器复制全部并贴到文件末尾,如此反复十次。(每次复制都使文件的大小翻倍,所以最后文件中就有10240行了。)

但那要求精确计算操作次数。如果我输入命令的次数出现的错误(就算是在前后一次的误差范围内)那我要么要得到一个只有我预想的一半大小的文件,要么有两倍大――这将毁了整个测试结果。但我不打算自己数操作次数,我让编辑器来替我数。在输完开始的十行后,我给编辑器一条命令:

     global /^/ % copy $

这条命令让编辑器通搜整篇文档,标识所有有“开端”的行(也就是所有行),然后对标识的十行逐一运行通篇拷贝的命令。这确保了命令会精确地运行十次。

注意这个技巧仅限于文件中的行数与要命令重复运行的次数一致时使用。如果我在文件中输入了20行,要将之通篇拷贝10次可以这样使用全局命令:

     1 , 10 global /^/ % copy $

自动移动。你可能不时地会需要应付文件中一系列的编辑问题,在没使用全局命令时你得一个一个地解决这些问题。但在这进行操作的点之间移动是件烦琐的事。如果有一个式样来找到这些需要编辑的点,或者你能写出一个脚本来将式样插入到这些地方的话――就你第一章中小何做的那样,那全局命令能自动带你到这些点。

你可能还记得小何用了一个脚本来对源代码进行标识,他将每行lint警告放在源文件中相应的行后,中间用“XXX”分隔以利于定位这些行。假如这个“无恶不做”的信息系统副主管又回过头来要小何仔细看一下这些行,看是否能通过重写这些行来消除“警告”作为补救的一种手段。

小何应该怎么做呢?要快速的翻阅这些代码,然后逐个找出那些用来识别问题行的“XXX”式样吗?小何知道他所面对是意大利面条式的代码,实际出现问题的地方与lint所指示的那些行可能会有的出入。在查找问题点的过程中他可能已经跳过了几个“XXX”式样,因而在文件中查找下一个“XXX”可能会将他带回他已经处理过的点,或者是漏掉了那些他在查找实际的问题点时跳过的几个“XXX”式样。此外,他在修复一个问题时频繁地使用式样搜索,这使得他无法在可视模式中使用“n”命令来快速地定位下一个“XXX”式样――他必须每次都重新输入式样。

但是小何知道怎么应付这些问题――回到行模式(在可视模式下输入大写的“Q”)然后使用一条简单的全局命令:

     global /XXX/ visual | write

这条命令让小何先回到“XXX”在文件中第一次出现时的所在的行,然后切换到可视模式等待小何编辑。在小何做完修改后,只要再输入大写的“Q”编辑器就会带他到第二个包含“XXX”的行,并回到可视模式。不论在前一次编辑中小何怎样地移动或进行何种操作,编辑器都能将他带到第二个包含“XXX的行”。然后小何只要在每次进行完修改后按“Q”就能到下一个包含“XXX”的行中。在每完成一次编辑后write命令会自动地将修改过的文件保存到磁盘上。




下一个: ,
上一个: 全局命令的例子,
上层: global

现在换你了

在你将全局命令的进阶技巧在实际工作中运用前,这儿有一些练习来让你练练手。我已经为每个练习提供了至少一种参考答案(见附录),还有一个提示是针对最后一个问题(也是最难的一个)的。

复制后改为小写。回想一下那个想把文中所有以“CHAPTER”开头的行复制到文件末尾的用户。除了复制外他还想把文中原来的那些行中的“CHAPTER”改为“Chapter”――但复制到文件末尾的那些行仍保留大写的状态。

现在我们已经知道下面的两条命令都不能完成这项任务了:

     global /^CHAPTER/ substitute /APTER/apter/ | copy $
     global /^CHAPTER/ copy $ | substitute /APTER/apter/

怎样才能用全局命令(:global)完成这项任务呢?有许多种办法,要找到一条不算太难。

准确的字串长度。一位老友在用troff1时做一些特殊的工作时需要在一行中插入连续的64个反斜杠。反斜杠的数目必须准确的为64个,不然troff不能正常地完成工作。在不费力数数的前提下,他怎样才能准确地输入64个反斜杠呢?

假设他要在第217行的字串“n(PDu”前面插入16个反斜杠。使用什么命令才能完成这一任务而免于手工数数的尴尬呢?如果知道要用哪些命令的话那答案就呼之欲出了。

为段落编号。一位文档的作者将每个章节又分成了多个段落。他是一个troff的用户所以他用将宏“.pp”单独置于一行作为段落的标记。所以段落间的是以这种方式分隔的:

     which is the only way that argon gas can be dissolved
     in this liquid.
	
     .pp
     The problem of energizing the
      argon to fluorescence while
     it is dissolved was first approached by applying a strong

这个技术作家要怎样用vi编辑器来为章节中的每一个段落编号呢?(你可能觉得这个问题有些“扯”,但就曾有一个Unix专家在电话中问我这个问题。)为了简化问题我们假设每个章节中的段落数不超过35,并且编号用罗马数字来表示。

这个问题还是有一定难度的,所以我要提供两个提示。第一个当然是使用全局命令了。在你已经准备放弃或要看答案时,可以先看一下第二个提示。




下一个: ,
上一个: try,
上层: global

下一篇

在这篇教程的下一部分中,我会讲一些其他用来处理文本和文件的行模式命令,并会涉及到这些命令较不为人知的一面。如果你被本文中的全局命令的内容压得透不过气来的话,那告诉你一个好消息:替换命令要简单得多,事实上其他的命令都要比全局命令来得简单得多。

而在讲完了那些个命令后,这篇教程的后面的部分将会集中在可视模式上――比行模式要简单也更有趣。



Appendix A 答案和提示

答案用uuencode编码过。

begin 644 viex3_ans
M5FDO17BQX+RMQO<*"K7:R/W5PL&WS["RSK^\M/"PN`K7]]7?H[I786QT97(@
M06QA;B!::6YT>@K2Z]7?H[IH<3`P92`*"@JAL+BTUL:Z\[C$SJK0H="TH;'.
MRLSB"@J]S\ZJUK&PU[7$L.RWJ,K'H[H*"F=L;V)A;"`O7D-(05!415(O(&UA
M<FL@82!\(&-O<'D@)"!\("=A('-U8G-T:71U=&4@+T%05$52+V%P=&5R+PH*
MS\BVU-2MRKRUQ-#0O?C0T-?WO,>ZQ:.HT\-M87)KP_S![J.IHZS(N[KSO:O6
MKKBTUL:UO<[$O/[$J<ZRHZS7[KKSN/F^W=?VM<2QZKS'U=*UO=2MP+2UQ-#0
MLJ*]^-#0S.:[N[+9U_>]J[3ST+37UL2XN,3.JM"AT+37UL2XH:,*"KNYT]#2
MN];6LKO$Q\.TUK&]V,'+M;&UJ\VLT?G3T-"GM<2WO;>HOLW*Q\_(O:NAL$-(
M05!415*AL<BKLK_,YKN[SJK0H="THZS(N[KSN+36QM"AT+2AL+#FL;ZAL;7$
MU>+0J=#0M;W.Q+S^Q*G.LJ.LU^ZZ\]/#S.:[N\/\P>Z]J[VKM;''L-#0M<2A
ML$-H87!T97*AL=39M,[,YKN[SJJT\]"TM<2AL$-(05!415*AL:&C"@H*"G1R
M;V9FSLK,X@H*P_S![M#0H[H*"C$@+"`Q-B!G;&]B86P@+UXO(#(Q-R!S=6)S
M=&ET=71E("]N*%!$=2]<7&XH4$1U+PH*S:BY_=3+T-`Q-K3.M<3,YKN[P_S!
M[L"TS>JSR;C#R,[.\:&CP[_2N[3.S.:[N[:\U-JAL&XH4$1UH;''L++ER.O2
MN[CVM[30L;C<H:.CJ,SFN[O*O='YUM#6KLOYTM32JL&]N/:WM-"QN-S*Q]+R
MSJJWM-"QN-RQOLGMU-K,YKN[RKW1^=;0TK+*Q]*[N/;,V,KBU]:W^Z&CHZD*
M"@H*"J&PML["Y+'@NL6AL<[*S.*UQ,SARKX*"L[2M<2P[+>HT.C2JM*[N/;6
MT+SDLKW6Z*&JH:K4VL._N/:ZZK7$NO/#YLSMO-.T\]"TU];$N*&P2:&QH:.V
M^-3:P[^X]KKJNO/7UL2XH;!)H;&UQ+CVROV^S<K'Q,>X]K;.PN2UQ+'@NL6A
MH]*ROLW*Q]3:RK7*J<'+U>*X]M;0O.2RO=;HNO.VSL+DO[3)S\BEOLW/\=7B
MT?FCN@H*+G!P24E)24D*"@H*L>"ZQ<[*S.*UQ++.O[RT\+"X"@J_R=+4T\/!
MO<SUR*N^UL/\P>[`M,WJL\G5XM*[LMG7]Z&CM=K2N\SUH[H*"F=L;V)A;"`O
M7EPN<'`O("X@+"`D('-U8G-T:71U=&4@+R\N<'!)+PH*MM32U+KJH;`N<'"A
ML;^JRKRUQ,._TKO0T*.LU,O0T,SFN[O#_,'NO:NT\]"TU];$N*&P2:&QUL/3
MVKKJNO.AH\SFN[O#_,'NM-.UVM*[N/:QZLJVT-"_JLJ\U-K+^=/0M<2ZZKKS
MO-/)SZ&P2:&QH[O4VK7:MOZX]K'JRK;0T-;0MM2S_;7:TKNX]K'JRK;0T,WB
MM<3+^=/0NNJZ\[S3R<^AL$FAL:.[R+NZ\]3:M=K(_;CVL>K*MM#0HZRVU+/]
MM=K2NZ&BMOZX]K'JRK;0T,WBM<3+^=/0H;`N<'"AL;KJNO.\T\G/H;!)H;&C
MK-+`M,O`X,W&H:/5XM'YU-K(J[[6P_S![M3+T-#-ZKKSHZS#O[CVNNJZ\[:\
MT]#7UL2XH;!)H;'7Z;/)M<37UK2NP<NAH]?6M*ZUQ+.DMLC3Z[;.PN2UQ+'@
MNL7*Q]*[T?FUQ*&CTK*^S<K'U-K#_,'NU,O0T,WJNO.CK-3:M=K(_;CVH;`N
M<'"AL;KJNO/3T-?6M*ZAL$E)2:&QH:/4VK7:SN6X]K;.PN33T-?6M*ZAL$E)
M24E)H;&CK+7(M<BAH[3+RK'$X]+1OJW3P\+>PNVCJ+ORR[72N];6NMRAL-2M
MRKRAL;7$HZG*_=?6SJJVSL+DL>"ZQ<'+H:,*"K7:MO[,]<BKOM;#_,'NO:O"
MWL+MROW7UM>JN[O.JKC\SJJYYK>VM<30SLJ]H[H*"F=L;V)A;"`O7EPN<'`O
M('-U8G-T:71U=&4@+TE)24E)+U8O(&<@7`I\('-U8G-T:71U=&4@+U96+U@O
M(&<@?"!S=6)S=&ET=71E("])24E)+TE6+PH*HZC4VL_"TKO5PM;0Q..[X=&G
MM;W4VLSFN[O#_,'NNO.\T\G/M<30H="TU];$N*&P9Z&QHZS*Q]/#P+36N,J^
ML>"\K<;WMM3#O]*[T-"]^-#0OJ&_R<3<MN"TSK7$S.:[NZ&CMOC)S\/FP_S!
M[K7$M=K2N[*_M];0T,2IM<30L;C<L>W*OL_"TKO0T+7$P_S![LK'O=/0^-3:
MU>+2N]#0NO.AHZ.I"@K2JM:JM<"UVK;^S/7(J[[6P_S![LK'U/71^;FDU_>U
MQ*.LO\F_M-*[S\*AL+7:,3FVSJ&QM<3'Z;_VH://PL/FM<3+Q-#0L>W*OK7$
MM]:Q\,K'NNK4VLSFN[O#_,'NU,O0T,>PHZRZS;W3S\+`M+7$R/VX]LSFN[O#
M_,'NUO"X]M:TT-"Z\[7$L>2[K\?IO_:CN@H*+G!P24E)24E)24E)24E)24E)
M24E)20HN<'!65E9)24E)"BYP<%A624E)20HN<'!85DE6"@K/N-#$M<2VP=7?
MO\G$W->BTN*UO<'+UKO0Z-/#TKO,]<BKOM;#_,'NOLW$W+;4U>+0J;;.PN2]
M^-#0L>"ZQ<'+H:/4VLBKOM;#_,'NTL:UO<_"TKO0T,>PHZRZZLOYU-JUQ+6Q
MQ[#0T+KSTM&^K<SMO-/!RZ&P2:&QP<NAH\OYTM3.TL/'O\G2U-3:P_S![K^J
MRKS'L+;4R*N^UL/\P>ZUQ+'JRK;0T-?WO,>ZQ:.HO[32N\_"M=K2N[CVSLK,
MXK7$LLZ_O+3PL+BCJ:.LR+NZ\\K'S.V\TZ&P2:&QM<3,YKN[P_S![J.LU^ZZ
L\[O8M;W7]\'+O,>ZQ;7$T-"RHM3+T-#7[KKSM<3(_;CVS.:[N\/\P>ZAHPH`
`
end




脚注

[1] Unix下的老牌排版软件


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