vimscript:编写Vim脚本

编写Vim脚本

https://blog.csdn.net/yz457694/article/details/77316421

 

这篇文章是手册的中文译版整理而来(英文看着太慢了,感谢前人铺路Orz…),vim的markdown插件和实时预览用着也挺方便,顺便还练习了基本操作(一路:help、百度)。前半部分是边看边整理,感觉太慢,后面就看完之后直接正则处理了(毕竟神器)…附上两张效果图:
vimscript:编写Vim脚本_第1张图片

vimscript:编写Vim脚本_第2张图片


目录
1 简介
2 变量
3 表达式
4 条件语句
5 执行一个表达式
6 使用函数
7 定义一个函数
8 列表和字典
9 例外
10 其它的讨论
11 编写插件
12 编写文件类型插件
13 编写编译器插件
14 编写快速载入的插件
15 编写库脚本
16 发布 Vim 脚本


1 简介

一个简单的例子:

:let i = 1
:while i < 5
:    echo "count is" i
:    let i+=1
:endwhile
  • 1
  • 2
  • 3
  • 4
  • 5

注::字符并非必须,在vim脚本中可以去掉;可以拷贝示例文本,然后用:@"执行
输出结果:

count is 1
count is 2
count is 3
count is 4
  • 1
  • 2
  • 3
  • 4

更简洁的表达:

for i in range(1,4)
    echo "count is" i
endfor
  • 1
  • 2
  • 3

2 变量

赋值:

:let {变量} = {表达式}
  • 1

一般定义的变量都是全局的,可以用:let列出当前定义的所有变量。
为了防止不同脚本之间公用全局变量导致混乱,可以在变量名后面加上s:使其变为脚本文件的局部变量。例如:一个脚本包含如下代码:

:let s:count = 1
:while s:count < 5
:    source other.vim
:    let s:count += 1
:endwhile
  • 1
  • 2
  • 3
  • 4
  • 5

由于s:count是局部变量,可以确信调用(source) "other.vim"时不会改变它的值。
还有很多其他类型的变量,参阅internal-variables,最常见的有:

name description
b:name 缓冲区的局部变量
w:name 窗口的局部变量
g:name 全局变量(也用于函数中)
v:name Vim预定义的变量

删除变量:

:unlet {变量}
  • 1

:unlet s:count,这将删除count局部变量并释放其占用的内存。如果不确定这个变量是否存在,又不希望看到系统在它不存在时报错,可以在命令后面加!,即:unlet! s:count

字符串变量和常量:

除了数值之外,字符串也能作为变量的值。变量的类型是动态的。每当我们通过 :let 语句为变量赋值时,变量的类型才被确定。
字符串常量有两种。第一种是由双引号括起来的:

:let name = "peter"
:echo name
peter
  • 1
  • 2
  • 3

如果你想在这样的字符串内使用双引号,在之前加上反斜杠即可:

:let name = "\"peter\""
:echo name
"peter"
  • 1
  • 2
  • 3

如果你不想使用反斜杠,也可以用单引号括起字符串:

:let name = '"peter"'
:echo name
"peter"
  • 1
  • 2
  • 3

所有的字符在单引号内都保持其本来面目(因此反斜杠也不能实现转义)。只有单引号本身例外: 输入两个你会得到一个单引号。
在双引号括起来的字符串中可以使用特殊字符。这里有一些有用的例子:

script meaning
\t
\n ,换行
\r
\e
\b ,退格
\" "

|\,反斜杠
\|
\|CTRL-W

最后两个只是用来举例子的。\ 的形式可以被用来表示特殊的键 name

3 表达式

已经提到的数值,字符串常量和变量都属于表达式,其他基本的表达式有:

expr meaning
$NMAE 环境变量
&name 选项
@r 寄存器

算术运算

operation meaning
a+b
a-b
a*b
a/b
a%b

用 “.” 可以把两个字符串连接起来。例如:

:echo "foo"."bar"
foobar
  • 1
  • 2

一般的,当 :echo 命令遇到多个参数时,会在它们之间加入空格。但上例中参数是一个表达式,所以不会有空格。
下面的条件表达式很显然是从 C 语言里借来的:

a?b:c
  • 1

如果 a 为真用 b,否则用 c。例如:

:let i = 4
:echo i>5 ? "i is big":"i is small"
  • 1
  • 2

4 条件语句

:if 命令在条件满足的前提下,执行其后直到 :endif 的所有语句。常用的形式为:

:if {conditon}
    {statements}
:endif
  • 1
  • 2
  • 3

语句 {statements} 仅当表达式 {condition} 为真 (非零) 时才被执行。这些语句还必
须是有效的。否则 Vim 无法找到相应的 :endif
也可以使用 :else。常用形式为:

:if {condition}
    {statements}
:else
    {statements}
:endif
  • 1
  • 2
  • 3
  • 4
  • 5

还有 :elseif:

:if {condition}
    {statements}
:elseif {condition}
    {statements}
:endif
  • 1
  • 2
  • 3
  • 4
  • 5

这种形式就像 :else 接着 if 一样,但是少出现一个 :endif

逻辑运算

operation meaning
a == b 等于
a != b 不等于
a > b 大于
a >= b 大于等于
a < b 小于
a <= b 小于等于

如果条件满足,结果为 1,否则为 0。例如:

:if v:version >=700
:    echo "congratulations"
:else
:    echo "your version is old,please update it"
:endif
  • 1
  • 2
  • 3
  • 4
  • 5

这里 v:version 是 Vim 定义的变量,用来存放 Vim 的版本号。600 意为 6.0 版。6.1 版的值为 601。这对编写可以在不同版本的 Vim 上运行的脚本很有用。
对数值和字符串都可以做逻辑操作。两个字符串的算术差被用来比较它们的值。这个结果是通过字节值来计算的,对于某些语言,这样做的结果未必正确。
在比较一个字符串和一个数值时,该字符串将先被转换成一个数值。这容易出错,因为当一个字符串看起来不像数值时,它会被当作 0 对待。例如:

:if 0 == "one"
:    echo "yes"
:endif
  • 1
  • 2
  • 3

上面的例子将显示 yes,因为 one 看起来不像一个数值,所以被转换为 0 了。
对于字符串来说还有两种操作:

operation meaning
a =~ b 匹配
a !~ b 不匹配

左边的 a 被当作一个字符串。右边的 b 被当作一个匹配模式,正如做查找操作一样。例如:

if str =~ " "
    echo "exist space"
endif
if str !~ '\.$'
    echo "not end up with dot"
endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意 在匹配模式中用单引号是很有用的。因为匹配模式中通常有很多反斜杠,而反斜杠在双引号字符串中必须双写才有效。
在做字符串比较时用到 ignorecase 选项。如果你不希望使用该选项,可以在比较时加上 #?# 表示大小写敏感;? 表示忽略大小写。因此 ==? 比较两字符串是否相等,不计大小写。!~# 检查一个模式是否被匹配,同时也考虑大小写。expr-== 有一个完整的字符串比较/匹配操作列表。

循环详述

:while 命令已经在前面提到了。还有另外两条语句可以在 :while:endwhile之间使用。

command function
:continue 跳回while循环的开始;继续循环
break 跳至:endwhile;循环结束

例:

:while counter < 40
:    call do_something()
:    if skip_flag
:        contunue
:    endif
:    if finished_flag
:        break
:    endif
:    sleep 50m
:endwhile
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

:sleep 命令使 Vim 小憩一下。50m 表示休息 50 毫秒。再举一个例子,:sleep 4休息 4 秒。

5 执行一个表达式

:execute 命令可以执行一个表达式的结果。这是一个创建并执行命令的非常有效的方法。
例如要跳转到一个由变量表示的标签:

:execute "tag" . tag_name
  • 1

.被用来链接字符串"tag"和变量tag_name的值。
:execute命令只能用来执行冒号命令。:normal命令可以用来执行普通模式命令。然而,它的参数只能是按表面意义解释的命令字符,不能是表达式。例如:

:normal gg=G
  • 1

这个命令将跳转到第一行并以=操作符排版所有行。
为了使:normal命令也可以带表达式,可以把:execute与其连起来使用,例如:

:execute "normal" . normal_commands
  • 1

变量normal_commands必须包含要执行的普通模式命令。
必须确保 :normal 的参数是一个完整的命令。否则,Vim 碰到参数的结尾就会中止其运行。例如,如果你开始了插入模式,你必须也退出插入模式。这样没问题:

:execute "normal Inew test\"
  • 1

这将在当前行插入 new text。注意 这里使用了特殊键 \。这样就避免了在你的脚本当中键入真正的 字符。
如果你不想执行字符串,而想执行它作为表达式计算的结果,可以用eval()函数:

:let optname = "path"
:let optval = eval('&'.optname)
  • 1
  • 2

& 被加到 path 前面,这样传给eval()的参数成为 &path。这时得到的返回值就是 ‘path’ 选项的值。
相同的操作可以这样完成:

:exe 'let optval = &'.optname
  • 1

6 使用函数

Vim 定义了大量的函数并通过这些函数提供了丰富的功能。可参考|functions|找到一个完整的列表。
一个函数可以被:call命令调用。参数列表要用括号括起来,并用逗号分割。例如:

:call search("Date:","W")
  • 1

这将以Date:W作为参数调用search()函数。search()函数的第一个参数时一个查找模式,第二个是一个标志。标志W表示查找操作遇到文件尾时不折返。
在一个表达式内也可以调用函数,例如:

:let line = getline(".")
:let repl = substitute(line,'\a',"*","g")
:call setline(".",repl)
  • 1
  • 2
  • 3

getline()函数从当前缓冲区获取一行文本 ,其参数时行号。.代表光标所在行。
substitute()函数的功能和:substitute命令相似。它的第一个参数是要执行替换操作的源字符串,第二个参数时一个匹配模式,第三个参数时替换字符串,最后一个参数是一个标志位。
setline()函数将第一个参数表示的行的文本置为第二个参数表示的字符串。本例中光标所在行被substitute()函数的结果所替换。因此这三条语句的效果等同于:

:substitute/\a/*/g
  • 1

如果你在调用substitute()之前或之后有更多的事情要做的话,用函数的方式就会更有 趣了。

函数

Vim 提供的函数很多。这里我们以它们的用途分类列出。你可以在 functions 找到一个以字母顺序排列的列表。在函数名上使用CTRL-] 可以跳转至该函数的详细说明。

字符串操作:

name func
nr2char() 通过 ASCII 码值取得一个字符
char2nr() 取得字符的 ASCII 码值
str2nr() 把字符串转换为数值
str2float() 把字符串转换为浮点数
printf() 根据 % 项目格式化字符串
escape() 将字符串通过 ‘\’ 转义
shellescape() 转义字符串用于外壳命令
fnameescape() 转义 Vim 命令使用的文件名
tr() 把一组字符翻译成另一组
strtrans() 将一个字符串变成可显示的格式
tolower() 将一个字符串转换为小写
toupper() 将一个字符串转换为大写
match() 字符串中的模式匹配处
matchend() 字符串中的模式匹配结束处
matchstr() 在一个字符串中匹配一个模式
matchlist() 类似matchstr(),同时返回子匹配
stridx() 子串在母串中第一次出现的地方
strridx() 子串在母串中最后一次出现的地方
strlen() 字符串长度
substitute() 用一个字符串替换一个匹配的模式
submatch() 取得 :ssubstitute() 匹配中指定的某个匹配
strpart() 取得字符串的一部分
expand() 展开特殊的关键字
iconv() 转换文本编码格式
byteidx() 字符串里字符的字节位置
repeat() 重复字符串多次
eval() 计算字符串表达式

列表处理:

name func
get() 得到项目,错误索引不报错
len() 列表的项目总数
empty() 检查列表是否为空
insert() 在列表某处插入项目
add() 在列表后附加项目
extend() 在列表后附加另一个列表
remove() 删除列表里一或多个项目
copy() 建立列表的浅备份
deepcopy() 建立列表的完整备份
filter() 删除列表的选定项目
map() 改变每个列表项目
sort() 给列表排序
reverse() 反转列表项目的顺序
split() 分割字符串成为列表
join() 合并列表项目成为字符串
range() 返回数值序列的列表
string() 列表的字符串表示形式
call() 调用函数,参数以列表形式提供
index() 列表里某值的索引
max() 列表项目的最大值
min() 列表项目的最小值
count() 计算列表里某值的出现次数
repeat() 重复列表多次

字典处理:

name func
get() 得到项目,错误的键不报错
len() 字典项目的总数
has_key() 检查某键是否出现在字典里
empty() 检查字典是否为空
remove() 删除字典的项目
extend() 从一个字典增加项目到另一个字典
filter() 删除字典的选定项目
map() 改变每个字典项目
keys() 得到字典的键列表
values() 得到字典的值列表
items() 得到字典的键-值组对的列表
copy() 建立字典的浅备份
deepcopy() 建立字典的完整备份
string() 字典的字符串表示形式
max() 字典项目的最大值
min() 字典项目的最小值
count() 计算字典里某值的出现次数

浮点数计算:

name func
float2nr() 把浮点数转换为数值
abs() 绝对值 (也适用于数值)
round() 四舍五入
ceil() 向上取整
floor() 向下取整
trunc() 删除小数点后的值
log10() 以 10 为底的对数
pow() x 的 y 次方
sqrt() 平方根
sin() 正弦
cos() 余弦
tan() 正切
asin() 反正弦
acos() 反余弦
atan() 反正切
atan2() 反正切
sinh() 双曲正弦
cosh() 双曲余弦
tanh() 双曲正切

其它计算:

name func
and() 按位与
invert() 按位取反
or() 按位或
xor() 按位异或

变量:

name func
type() 变量的类型
islocked() 检查变量是否加锁
function() 得到函数名对应的函数引用
getbufvar() 取得指定缓冲区中的变量值
setbufvar() 设定指定缓冲区中的变量值
getwinvar() 取得指定窗口的变量值
gettabvar() 取得指定标签页的变量值
gettabwinvar() 取得指定窗口和标签页的变量值
setwinvar() 设定指定窗口的变量值
settabvar() 设定指定标签页的变量值
settabwinvar() 设定指定窗口和标签页的变量值
garbagecollect() 可能情况下释放内存

光标和位置标记位置:

name func
col() 光标或位置标记所在的列
virtcol() 光标或位置标记所在的屏幕列
line() 光标或位置标记所在行
wincol() 光标所在窗口列
winline() 光标所在窗口行
cursor() 置光标于 行/列 处
getpos() 得到光标、位置标记等的位置
setpos() 设置光标、位置标记等的位置
byte2line() 取得某字节位置所在行号
line2byte() 取得某行之前的字节数
diff_filler() 得到一行之上的填充行数目

操作当前缓冲区的文本:

name func
getline() 从缓冲区中取一行
setline() 替换缓冲区中的一行
append() 附加行或行的列表到缓冲区
indent() 某行的缩进
cindent() 根据C 缩进法则的某行的缩进
lispindent() 根据Lisp 缩进法则的某行的缩进
nextnonblank() 查找下一个非空白行
prevnonblank() 查找前一个非空白行
search() 查找模式的匹配
searchpos() 寻找模式的匹配
searchpair() 查找start/skip/end 配对的另一端
searchpairpos() 查找start/skip/end 配对的另一端
searchdecl() 查找名字的声明

系统调用及文件操作:

name func
glob() 展开通配符
globpath() 在几个路径中展开通配符
findfile() 在目录列表里查找文件
finddir() 在目录列表里查找目录
resolve() 找到一个快捷方式所指
fnamemodify() 改变文件名
pathshorten() 缩短路径里的目录名
simplify() 简化路径,不改变其含义
executable() 检查一个可执行程序是否存在
filereadable() 检查一个文件可读与否
filewritable() 检查一个文件可写与否
getfperm() 得到文件权限
getftype() 得到文件类型
isdirectory() 检查一个目录是否存在
getfsize() 取得文件大小
getcwd() 取得当前工作路径
haslocaldir() 检查当前窗口是否使用过 :lcd
tempname() 取得一个临时文件的名称
mkdir() 建立新目录
delete() 删除文件
rename() 重命名文件
system() 取得一个 shell 命令的结果
hostname() 系统的名称
readfile() 读入文件到一个行列表
writefile() 把一个行列表写到文件里

日期和时间:

name func
getftime() 得到文件的最近修改时间
localtime() 得到以秒计的当前时间
strftime() 把时间转换为字符串
reltime() 得到准确的当前或者已经经过的时间
reltimestr() reltime() 的结果转换为字符串

缓冲区,窗口及参数列表:

name func
argc() 参数列表项数
argidx() 参数列表中的当前位置
argv() 从参数列表中取得一项
bufexists() 检查缓冲区是否存在
buflisted() 检查缓冲区是否存在并在列表内
bufloaded() 检查缓冲区是否存在并已加载
bufname() 取得某缓冲区名
bufnr() 取得某缓冲区号
tabpagebuflist() 得到标签页里的缓冲区列表
tabpagenr() 得到标签页号
tabpagewinnr() 类似于特定标签页里的winnr()
winnr() 取得当前窗口的窗口号
bufwinnr() 取得某缓冲区的窗口号
winbufnr() 取得某窗口的缓冲区号
getbufline() 得到指定缓冲区的行列表

命令行:

name func
getcmdline() 得到当前命令行
getcmdpos() 得到命令行里的光标位置
setcmdpos() 设置命令行里的光标位置
getcmdtype() 得到当前命令行的类型

quickfix和位置列表:

name func
getqflist() quickfix错误的列表
setqflist() 修改quickfix列表
getloclist() 位置列表项目的列表
setloclist() 修改位置列表

插入模式补全:

name func
complete() 设定要寻找的匹配
complete_add() 加入要寻找的匹配
complete_check() 检查补全是否被中止
pumvisible() 检查弹出菜单是否显示

折叠:

name func
foldclosed() 检查某一行是否被折叠起来
foldclosedend() 类似foldclosed() 但同时返回最后一行
foldlevel() 检查某行的折叠级别
foldtext() 产生折叠关闭时所显示的行
foldtextresult() 得到关闭折叠显示的文本

语法和高亮:

name func
clearmatches() 清除matchadd():match 诸命令定义的所有匹配
getmatches() 得到matchadd():match 诸命令定义的所有匹配
hlexists() 检查高亮组是否存在
hlID() 取得高亮组标示
synID() 取得某位置的语法标示
synIDattr() 取得某语法标示的特定属性
synIDtrans() 取得翻译后的语法标示
synstack() 取得指定位置的语法标示的列表
synconcealed() 取得和隐藏 (conceal) 相关的信息
diff_hlID() 得到diff 模式某个位置的高亮标示
matchadd() 定义要高亮的模式 (一个 “匹配”)
matcharg() 得到 :match 参数的相关信息
matchdelete() 删除matchadd():match 诸命令定义的匹配
setmatches() 恢复getmatches() 保存的匹配列表

拼写:

name func
spellbadword() 定位光标所在或之后的错误拼写的单词
spellsuggest() 返回建议的拼写校正列表
soundfold() 返回 “发音相似” 的单词等价形式

历史记录:

name func
histadd() 在历史记录中加入一项
histdel() 从历史记录中删除一项
histget() 从历史记录中提取一项
histnr() 取得某历史记录的最大索引号

交互:

name func
browse() 显示文件查找器
browsedir() 显示目录查找器
confirm() 让用户作出选择
getchar() 从用户那里取得一个字符输入
getcharmod() 取得最近键入字符的修饰符
feedkeys() 把字符放到预输入队列中
input() 从用户那里取得一行输入
inputlist() 让用户从列表里选择一个项目
inputsecret() 从用户那里取得一行输入,不回显
inputdialog() 从用户那里取得一行输入,使用对话框
inputsave() 保存和清除预输入 (typeahead)
inputrestore() 恢复预输入 (译注: 参阅input())

GUI:

name func
getfontname() 得到当前使用的字体名
getwinposx() GUI Vim 窗口的 X 位置
getwinposy() GUI Vim 窗口的 Y 位置

Vim 服务器:

name func
serverlist() 返回服务器列表
remote_send() 向 Vim 服务器发送字符命令
remote_expr() 在 Vim 服务器内对一个表达式求值
server2client() 向一个服务器客户发送应答
remote_peek() 检查一个服务器是否已经应答
remote_read() 从一个服务器读取应答
foreground() 将一个 Vim 窗口移至前台
remote_foreground() 将一个 Vim 服务器窗口移至前台

窗口大小和位置:

name func
winheight() 取得某窗口的高度
winwidth() 取得某窗口的宽度
winrestcmd() 恢复窗口大小的返回命令
winsaveview() 得到当前窗口的视图
winrestview() 恢复保存的当前窗口的视图

映射:

name func
hasmapto() 检查映射是否存在
mapcheck() 检查匹配的映射是否存在
maparg() 取得映射的右部 (rhs)
wildmenumode() 检查 wildmode 是否激活

杂项:

name func
mode() 取得当前编辑状态
visualmode() 最近一次使用过的可视模式
exists() 检查变量,函数等是否存在
has() 检查 Vim 是否支持某特性
changenr() 返回最近的改变号
cscope_connection() 检查有无与cscope的连接
did_filetype() 检查某文件类型自动命令是否已经使用
eventhandler() 检查是否在一个事件处理程序内
getpid() 得到 Vim 的进程号
libcall() 调用一个外部库函数
libcallnr() 同上,但返回一个数值
getreg() 取得寄存器内容
getregtype() 取得寄存器类型
setreg() 设定寄存器内容及类型
taglist() 得到匹配标签的列表
mzeval() 计算MzScheme 表达式

7 定义一个函数

Vim允许你定义自己的函数,基本的函数声明如下:

:function {name}({var1},{var2},...)
:    {body}
:endfunction
  • 1
  • 2
  • 3

注意 函数名必须以大写字母开始。
下面来定义一个返回两数中较小者的函数。从下面这行开始:

function Min(num1,num2)
  • 1

这将告诉 Vim 这个函数名叫 Min 并且带两个参数: num1num2
要做的第一件事就是看看哪个数值小一些:

:    if a:num1 < a:num2
  • 1

特殊前缀a:告诉Vim该变量是一个函数参数。我们把最小的数值赋给smaller变量:

:    if a:num1 < a:num2
:        let smaller = a:num1
:    else
:        let smaller = a:num2
:    endif
  • 1
  • 2
  • 3
  • 4
  • 5

smaller是一个局部变量。一个在函数内部使用的变量,除非被加上类似g:a:s:的前缀,都是局部变量。
备注 为了从一个函数内部访问一个全局变量你必须在前面加上 g:。因此在一个函数内 g:today 表示全局变量 today,而 today 是另外一个仅用于该函数内的局部变量。
现在你可以使用 :return 语句来把最小的数值返回给调用者了。最后,你需要结束这个函数:

:    return smaller
:endfunction
  • 1
  • 2

下面时这个函数的完整定义:

:funciton Min(num1,num2)
:    if a:num1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

调用用户自定义函数的方式和调用内置函数完全一致。仅仅是函数名不同而已。上面的Min函数可以这样来使用:

:echo Min(5,8)
  • 1

只有这时函数才被 Vim 解释并执行。如果函数中有类似未定义的变量之类的错误,你将得到一个错误信息。这些错误在定义函数时是不会被检测到的。
当一个函数执行到 :endfunction:return 语句没有带参数时,该函数返回零。
如果要重定义一个已经存在的函数,在 function 命令后加上!:

:function! Min(num1,num2,num3)
  • 1

范围的使用

:call 命令可以带一个行表示的范围。这可以分成两种情况。当一个函数定义时给出了range 关键字时,表示它会自行处理该范围。
Vim 在调用这样一个函数时给它传递两个参数: a:firstlinea:lastline,用来表示该范围所包括的第一行和最后一行。例如:

:funtion Count_words() range
:    let lnum = a:firstline
:    let n = 0
:    while lnum <= a:lastline
:        let n = n + len(split(getline(lnum)))
:        let lnum = lnum + 1
:    endwhile
:    echo "found" . n . "words"
:endfunction
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

你可以这样调用上面的函数:

:10,30call Count_words()
  • 1

这个函数将被调用一次并显示字数。
另一种使用范围的方式是在定义函数时不给出 range 关键字。Vim 将把光标移动到范围内的每一行,并分别对该行调用此函数。例如:

:function Number()
:    echo "line".line(".")."contains".getline(".")
:endfunction
  • 1
  • 2
  • 3

如果你用下面的方式调用该函数:

:10,15call Number()
  • 1

它将被执行六次。

可 变 参 数

Vim 允许你定义参数个数可变的函数。下面的例子给出一个至少有一个参数 (start),但可以多达 20 个附加参数的函数:

    :function Show(start, ...)
  • 1

变量 a:1 表示第一个可选的参数,a:2 表示第二个,如此类推。变量 a:0 表示这些参数的个数。例如:

    :function Show(start, ...)
    :  echohl Title
    :  echo "start is " . a:start
    :  echohl None
    :  let index = 1
    :  while index <= a:0
    :    echo "  Arg " . index . " is " . a:{index}
    :    let index = index + 1
    :  endwhile
    :  echo ""
    :endfunction
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上例中 :echohl 命令被用来给出接下来的 :echo 命令如何高亮输出。:echohl None 终止高亮。:echon 命令除了不输出换行符外,和 :echo 一样。
你可以用a:000变量,它是所有 ... 参数的列表。见 a:000

函 数 清 单

:function 命令列出所有用户自定义的函数及其参数:

    :function
    function Show(start, ...)
    function GetVimIndent()
    function SetSyn(name)
  • 1
  • 2
  • 3
  • 4

如果要查看该函数具体做什么,用该函数名作为 :function 命令的参数即可:

    :function SetSyn
    1     if &syntax == ''
    2       let &syntax = a:name
    3     endif
       endfunction
  • 1
  • 2
  • 3
  • 4
  • 5

调 试

调试或者遇到错误信息时,行号是很有用的。有关调试模式请参阅 debug-scripts
你也可以通过将 verbose 选项设为 12 以上来察看所有函数调用。将该参数设为15 或以上可以查看所有被执行的行。

删 除 函 数

为了删除Show()函数:

    :delfunction Show
  • 1

如果该函数不存在,你会得到一个错误信息。

函 数 引 用

有时使变量指向一个或另一个函数可能有用。要这么做,用function()函数。它把函数名转换为引用:

    :let result = 0     " 或 1
    :function! Right()
    :  return 'Right!'
    :endfunc
    :function! Wrong()
    :  return 'Wrong!'
    :endfunc
    :
    :if result == 1
    :  let Afunc = function('Right')
    :else
    :  let Afunc = function('Wrong')
    :endif
    :echo call(Afunc, [])
    Wrong!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注意 保存函数引用的变量名必须用大写字母开头,不然和内建函数的名字会引起混淆。
调用变量指向的函数可以用call()函数。它的第一个参数是函数引用,第二个参数是参数构成的列表。
和字典组合使用函数引用是最常用的,下一节解释。

8 列表和字典

到目前为止,我们用了基本类型字符串和数值。Vim 也支持两种复合类型: 列表和字典。
列表是事物的有序序列。这里的事物包括各种类型的值。所以你可以建立数值列表、列表列表甚至混合项目的列表。要建立包含三个字符串的列表:

    :let alist = ['aap', 'mies', 'noot']
  • 1

列表项目用方括号包围,逗号分割。要建立空列表:

    :let alist = []
  • 1

add()函数可以为列表加入项目:

    :let alist = []
    :call add(alist, 'foo')
    :call add(alist, 'bar')
    :echo alist
    ['foo', 'bar']
  • 1
  • 2
  • 3
  • 4
  • 5

列表的连接用+完成:

    :echo alist + ['foo', 'bar']
    ['foo', 'bar', 'foo', 'bar']
  • 1
  • 2

或者,你可以直接用extend()函数扩展一个列表:

    :let alist = ['one']
    :call extend(alist, ['two', 'three'])
    :echo alist
    ['one', 'two', 'three']
  • 1
  • 2
  • 3
  • 4

注意 这里如果用add(),效果不一样:

    :let alist = ['one']
    :call add(alist, ['two', 'three'])
    :echo alist
    ['one', ['two', 'three']]
  • 1
  • 2
  • 3
  • 4
  • 5

add()的第二个参数作为单个项目被加入。

FOR 循 环

使用列表的一个好处是可以在上面进行叠代:

    :let alist = ['one', 'two', 'three']
    :for n in alist
    :  echo n
    :endfor
    one
    two
    three
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这段代码循环遍历列表 alist 的每个项目,分别把它们的值赋给变量 n。for 循环通用的形式是:

    :for {varname} in {listexpression}
    :  {commands}
    :endfor
  • 1
  • 2
  • 3

要循环若干次,你需要长度为给定次数的列表。range()函数建立这样的列表:

    :for a in range(3)
    :  echo a
    :endfor
    0
    1
    2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意range()产生的列表的第一个项目为零,而最后一个项目比列表的长度小一。
你也可以指定最大值、步进,反向也可以:

    :for a in range(8, 4, -2)
    :  echo a
    :endfor
    8
    6
    4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

更有用的示例,循环遍历缓冲区的所有行:

    :for line in getline(1, 20)
    :  if line =~ "Date: "
    :    echo matchstr(line, 'Date: \zs.*')
    :  endif
    :endfor
  • 1
  • 2
  • 3
  • 4
  • 5

察看行 1 到 20 (包含),并回显那里找到的任何日期。

字 典

字典保存键-值组对。如果知道键,你可以快速查找值。字典用花括号形式建立:

    :let uk2nl = {'one': 'een', 'two': 'twee', 'three': 'drie'}
  • 1

现在你可以把键放在方括号里以查找单词:

    :echo uk2nl['two']
    twee
  • 1
  • 2

字典定义的通用形式是:

    { : , ...}
  • 1

空字典是不包含任何键的字典:

    {}
  • 1

字典的用途很多。它可用的函数也不少。例如,你可以得到它的键列表并在其上循环:

    :for key in keys(uk2nl)
    :  echo key
    :endfor
    three
    one
    two
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意 这些键没有排序。你自己可以对返回列表按照特定顺序进行排序:

    :for key in sort(keys(uk2nl))
    :  echo key
    :endfor
    one
    three
    two
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但你永远不能得到项目定义时的顺序。为此目的,只能用列表。列表里的项目被作为有序序列保存。

字 典 函 数

字典项目通常可以用方括号里的索引得到:

    :echo uk2nl['one']
    een
  • 1
  • 2

完成同样操作且无需那么多标点符号的方法:

    :echo uk2nl.one
    een
  • 1
  • 2

这只能用于由 ASCII 字母、数位和下划线组成的键。此方式也可以用于赋值:

    :let uk2nl.four = 'vier'
    :echo uk2nl
    {'three': 'drie', 'four': 'vier', 'one': 'een', 'two': 'twee'}
  • 1
  • 2
  • 3

现在来一些特别的: 你可以直接定义函数并把它的引用放在字典里:

    :function uk2nl.translate(line) dict
    :  return join(map(split(a:line), 'get(self, v:val, "???")'))
    :endfunction
  • 1
  • 2
  • 3

让我们先试试:

    :echo uk2nl.translate('three two five one')
    drie twee ??? een
  • 1
  • 2

你注意到的第一个特殊之处是 :function 一行最后的 dict。这标记该函数为某个字典使用。self 局部变量这时可以引用该字典。
现在把这个复杂的return命令拆开:

    split(a:line)
  • 1

split()函数接受字符串,把它分成空白分隔的多个单词,并返回这些单词组成的列表。所以下例返回的是:

    :echo split('three two five one')
    ['three', 'two', 'five', 'one']
  • 1
  • 2

map() 函数的第一个参数是上面这个列表。它然后遍历列表,用它的第二个参数来进行计算,过程中 v:val 设为每个项目的值。这相当于 for 循环的快捷方式。命令:

    :let alist = map(split(a:line), 'get(self, v:val, "???")')
  • 1

等价于:

    :let alist = split(a:line)
    :for idx in range(len(alist))
    :  let alist[idx] = get(self, alist[idx], "???")
    :endfor
  • 1
  • 2
  • 3
  • 4

get()函数检查某键是否在字典里存在。如果是,提取它对应的键。如果不是,返回缺省值,此例中缺省值是???。此函数可以很方便地处理键不一定存在而你不想要错误信息的情形。
join()函数和split()刚好相反: 它合并列表里的单词,中间放上空格。split()map()join()的组合非常简洁地对单词组成的行进行过滤。

面 向 对 象 编 程

现在你可以把值和函数都放进字典里,实际上,字典已经可以作为对象来使用。
上面我们用了一个字典来把荷兰语翻译为英语。我们可能也想为其他的语言作同样的事。让我们先建立一个对象 (也就是字典),它支持translate函数,但没有要翻译的单词表:

    :let transdict = {}
    :function transdict.translate(line) dict
    :  return join(map(split(a:line), 'get(self.words, v:val, "???")'))
    :endfunction
  • 1
  • 2
  • 3
  • 4

和上面的函数稍有不同,这里用 self.words 来查找单词的翻译,但我们还没有self.words。所以你可以把这叫做抽象类。
让我们现在实例化一个荷兰语的翻译对象:

    :let uk2nl = copy(transdict)
    :let uk2nl.words = {'one': 'een', 'two': 'twee', 'three': 'drie'}
    :echo uk2nl.translate('three one')
    drie een
  • 1
  • 2
  • 3
  • 4

然后来一个德语的翻译器:

    :let uk2de = copy(transdict)
    :let uk2de.words = {'one': 'ein', 'two': 'zwei', 'three': 'drei'}
    :echo uk2de.translate('three one')
    drei ein
  • 1
  • 2
  • 3
  • 4

你看到copy()函数被用来建立 transdict 字典的备份,然后修改此备份以加入单词表。当然,原来的字典还是保持原样。

现在你可以再进一步,使用你偏好的翻译器:

    :if $LANG =~ "de"
    :  let trans = uk2de
    :else
    :  let trans = uk2nl
    :endif
    :echo trans.translate('one two three')
    een twee drie
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里 trans 指向两个对象 (字典) 之一,并不涉及到备份的建立。关于列表和字典同一性的更多说明可见 list-identitydict-identity

你使用的语言现在可能还不支持。你可以覆盖translate()函数,让它什么都不做:

    :let uk2uk = copy(transdict)
    :function! uk2uk.translate(line)
    :  return a:line
    :endfunction
    :echo uk2uk.translate('three one wladiwostok')
    three one wladiwostok
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意 使用!会覆盖已有的函数引用。现在,在没找到能够识别的语言的时候,让我们用uk2uk:

    :if $LANG =~ "de"
    :  let trans = uk2de
    :elseif $LANG =~ "nl"
    :  let trans = uk2nl
    :else
    :  let trans = uk2uk
    :endif
    :echo trans.translate('one two three')
    one two three
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

进一步的阅读可见 ListsDictionaries

9 例外

让我们从一个例子开始:

    :try
    :   read ~/templates/pascal.tmpl
    :catch /E484:/
    :   echo "Sorry, the Pascal template file cannot be found."
    :endtry
  • 1
  • 2
  • 3
  • 4
  • 5

如果该文件不存在的话,:read 命令就会失败。这段代码可以捕捉到该错误并向用户给出一个友好的信息,而不是一个一般的出错信息。

:try:endtry 之间的命令产生的错误将被转变成为例外。例外以字符串的形式出现。当例外是错误时该字符串就是出错信息。而每一个出错信息都有一个对应的错误码。在上面的例子中,我们捕捉到的错误包括 E484。Vim 确保这个错误码始终不变(文字可能会变,例如被翻译)。

:read 命令引起其它错误时,模式 E484: 不会被匹配。因此该例外不会被捕获,结果是一个一般的出错信息。
你可能想这样做:

    :try
    :   read ~/templates/pascal.tmpl
    :catch
    :   echo "Sorry, the Pascal template file cannot be found."
    :endtry
  • 1
  • 2
  • 3
  • 4
  • 5

这意味着所有的错误都将被捕获。然而这样你就无法得到那些有用的错误信息,比如说E21: Cannot make changes, 'modifiable' is off

另一个有用的机制是 :finally 命令:

    :let tmp = tempname()
    :try
    :   exe ".,$write " . tmp
    :   exe "!filter " . tmp
    :   .,$delete
    :   exe "$read " . tmp
    :finally
    :   call delete(tmp)
    :endtry
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这个例子将自光标处到文件尾的所有行通过过滤器 filter。该程序的参数是文件名。无论在 :try:finally 之间发生了什么,call delete(tmp) 命令始终被执行。这可以确保你不会留下一个临时文件。

关于例外处理更多的讨论可以阅读参考手册: |exception-handling|

10 其它的讨论

这里集中了一些和 Vim 脚本相关的讨论。别的地方其实也提到过,这里算做一个整理。
行结束符取决于所在的系统。Unix 系统使用单个的字符。MS-DOS、Windows、OS/2系列的系统使用。对于那些使用的映射而言,这一点很重要。参阅|:source_crnl|

空 白 字 符

可以使用空白行,但没有作用。

行首的空白字符 (空格和制表符) 总被忽略。参数间的 (例如像下面命令中setcpoptions 之间的) 空白字符被归约为单个,仅用作分隔符。而最后一个 (可见) 字符之后的空白字符可能会被忽略也可能不会,视情况而定。见下。

对于一个带有等号 =:set 命令,如下:

    :set cpoptions    =aABceFst
  • 1

紧接着等号之前的空白字符会被忽略。然而其后的空白字符是不允许的!
为了在一个选项值内使用空格,必须像下面例子那样使用反斜杠:

    :set tags=my\ nice\ file
  • 1

如果写成这样:

    :set tags=my nice file
  • 1

Vim 会给出错误信息,因为它被解释成:

    :set tags=my
    :set nice
    :set file
  • 1
  • 2
  • 3

注 释

双引号字符"标记注释的开始。除了那些不接受注释的命令外 (见下例),从双引号起的直到行末的所有字符都将被忽略。注释可以从一行的任意位置开始。
对于某些命令来说,这里有一个小小的 “陷阱”。例如:

    :abbrev dev development     " shorthand
    :map  o#include     " insert include
    :execute cmd            " do it
    :!ls *.c            " list C files
  • 1
  • 2
  • 3
  • 4

缩写 dev 会被展开成 development " shorthand 的键盘映射会是包括" insert include 在内的那一整行;execute 命令会给出错误;! 命令会将其后的所有字符传给 shell,从而引起一个不匹配 " 的错误。
结论是,:map:abbreviate:execute! 命令之后不能有注释。(另外还有几个命令也是如此)。不过,对于这些命令有一个小窍门:

    :abbrev dev development|" shorthand
    :map  o#include|" insert include
    :execute cmd            |" do it
  • 1
  • 2
  • 3

| 字符被用来将两个命令分隔开。后一个命令仅仅是一个注释。最后一个命令里,你需
要做两件事: |:execute| 和用 |:

    :exe '!ls *.c'          |" list C files
  • 1

注意 在缩写和映射后的 | 之前没有空格。这是因为对于这些命令,直到行尾或者 |字符为止的内容都是有效的。此行为的后果之一,是你没法总看到这些命令后面包括的空白字符:

    :map  o#include
  • 1

要发现这个问题,你可以在你的 vimrc 文件内置位 list 选项。
Unix 上有一个特殊的办法给一行加注释,从而使得 Vim 脚本可执行:

    #!/usr/bin/env vim -S
    echo "this is a Vim script"
    quit
  • 1
  • 2
  • 3

# 命令本身列出一行并带行号。加上感叹号后使得它什么也不做。从而,你可以在后面加上 shell 命令来执行其余的文件。|:#!| |-S|

陷 阱

下面的例子的问题就更大了:

    :map ,ab o#include
    :unmap ,ab
  • 1
  • 2

这里,unmap命令是行不通的,因为它试着 unmap ,ab。而这个映射根本就不存在。因为 unmap ,ab 的末尾的那个空白字符是不可见的,这个错误很难被找出。

在下面这个类似的例子里,unmap 后面带有注释:

    :unmap ,ab     " comment
  • 1

注释将被忽略。然而,Vim 会尝试unmap不存在的 ,ab。可以重写成:

    :unmap ,ab|" comment
  • 1

恢 复 一 个 视 窗 位 置

有时有你想做一些改动然后回到光标原来的位置。如果能恢复相对位置,把和改动前同样的行置于窗口顶端就更好了。
这里的例子拷贝当前行,粘贴到文件的第一行,然后恢复视窗位置:

    map ,p ma"aYHmbgg"aP`bzt`a
  • 1

解析:

    ma"aYHmbgg"aP`bzt`a
    ma                      在当前位置做标记 a
      "aY                   将当前行拷贝至寄存器 a
         Hmb                移动到窗口的顶行并做标记 b
            gg              移动到文件首行
              "aP           粘贴拷贝的行到上方
                 `b         移动到刚才的顶行
                    zt      使窗口出现的文本恢复旧观
                      `a    回到保存的光标位置
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

封 装

为了避免你的函数名同其它的函数名发生冲突,使用这样的方法:
- 在函数名前加上独特的字符串。我通常使用一个缩写。例如,OW_ 被用在option window函数上。
- 将你的函数定义放在一个文件内。设置一个全局变量用来表示这些函数是否已经被加载了。当再次source这个文件的时候,先将这些函数卸载。
例如:

    " This is the XXX package

    if exists("XXX_loaded")
      delfun XXX_one
      delfun XXX_two
    endif

    function XXX_one(a)
        ... body of function ...
    endfun

    function XXX_two(b)
        ... body of function ...
    endfun

    let XXX_loaded = 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

11 编写插件

用约定方式编写的脚本能够被除作者外的很多人使用。这样的脚本叫做插件。Vim 用户只要把你写的脚本放在 plugin 目录下就可以立即使用了: |add-plugin|

实际上有两种插件:

name description
全局插件 适用于所有类型的文件。
文件类型插件 仅适用于某种类型的文件。

这一节将介绍第一种。很多的东西也同样适用于编写文件类型插件。仅适用于编写文件类型插件的知识将在下一节 |write-filetype-plugin| 做介绍。

插 件 名

首先你得给你的插件起个名字。这个名字应该很清楚地表示该插件的用途。同时应该避免同别的插件用同样的名字而用途不同。请将插件名限制在 8 个字符以内,这样可以使得该插件在老的 Windows 系统也能使用。

一个纠正打字错误的插件可能被命名为 typecorr.vim。我们将用这个名字来举例。

为了使一个插件能被所有人使用,要注意一些事项。下面我们将一步步的讲解。最后会给出这个插件的完整示例。

插 件 体

让我们从做实际工作的插件体开始:

 14 iabbrev teh the
 15 iabbrev otehr other
 16 iabbrev wnat want
 17 iabbrev synchronisation
 18     \ synchronization
 19 let s:count = 4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然,真正的清单会比这长的多。

上面的行号只是为了方便解释,不要把它们也加入到你的插件文件中去!

插 件 头

你很可能对这个插件做新的修改并很快就有了好几个版本。并且当你发布文件的时候,别人也想知道是谁编写了这样好的插件或者给作者提点意见。所以,在你的插件头部加上一些描述性的注释是很必要的:

  1 " Vim global plugin for correcting typing mistakes
  2 " Last Change: 2000 Oct 15
  3 " Maintainer: Bram Moolenaar 
  • 1
  • 2
  • 3

关于版权和许可: 由于插件很有用,而且几乎不值得限制其发行,请考虑对你的插件使用公共领域 (public domain) 或 Vim 许可 |license|。在文件顶部放上说明就行了。例如:

  4 " License:  This file is placed in the public domain.
  • 1

续 行,避 免 副 效 应

在上面的第 18 行中,用到了续行机制 |line-continuation|。那些置位了compatible 选项的用户可能会在这里遇到麻烦。他们会得到一个错误信息。我们不能简单的复位 compatible 选项,因为那样会带来很多的副效应。为了避免这些副效应,我们可以将 cpoptions 选项设为 Vim 缺省值并在后面恢复之。这将允许续行功能并保证对大多数用户来讲脚本是可用的。就像下面这样:

 11 let s:save_cpo = &cpo
 12 set cpo&vim
 ..
 42 let &cpo = s:save_cpo
 43 unlet s:save_cpo
  • 1
  • 2
  • 3
  • 4
  • 5

我们先将 cpoptions 的旧值存在s:save_cpo变量中。在插件的最后该值将被恢复。

注意 上面使用了脚本局部变量 |s:var|。因为可能已经使用了同名的全局变量。对于仅在脚本内用到的变量总应该使用脚本局部变量。

禁 止 加 载

有可能一个用户并不总希望加载这个插件。或者系统管理员在系统的插件目录中已经把这个插件删除了,而用户希望使用它自己安装的插件。用户应该有机会选择不加载指定的插件。下面的一段代码就是用来实现这个目的的:

  6 if exists("g:loaded_typecorr")
  7   finish
  8 endif
  9 let g:loaded_typecorr = 1
  • 1
  • 2
  • 3
  • 4

这同时也避免了同一个脚本被加载两次以上。因为那样用户会得到各种各样的错误信息。比如函数被重新定义,自动命令被多次加入等等。

建议使用的名字以 loaded_ 开头,然后是插件的文件名,按原义输入。之前加上 g:以免错误地在函数中使用该变量 (没有 g: 可以是局部于函数的变量)。

finish 阻止 Vim 继续读入文件的其余部分,这比用if-endif包围整个文件要快得多。

映 射

现在让我们把这个插件变得更有趣些: 我们将加入一个映射用来校正当前光标下的单词。
我们当然可以任意选一个键组合,但是用户可能已经将其定义为其它的什么功能了。为了使用户能够自己定义在插件中的键盘映射使用的键,我们可以使用标识:

 22   map  a  TypecorrAdd
  • 1

那个 TypecorrAdd 会做实际的工作,后面我们还会做更多解释。

用户可以将 mapleader 变量设为他所希望的开始映射的键组合。比如假设用户这样做:

    let mapleader = "_"
  • 1

映射将定义为 _a。如果用户没有这样做,Vim 将使用缺省值反斜杠。这样就会定义一个映射 - \a

注意 其中用到了,这会使得 Vim 在映射已经存在时给出错误信息。
|:map-|

但是如果用户希望定义自己的键操作呢?我们可以用下面的方法来解决:

 21 if !hasmapto('TypecorrAdd')
 22   map  a  TypecorrAdd
 23 endif
  • 1
  • 2
  • 3

我们先检查对 TypecorrAdd 的映射是否存在。仅当不存在时我们才定义映射a。这样用户就可以在他自己的 vimrc 文件中加入:

    map ,c  TypecorrAdd
  • 1

那么键序列就会是 ,c 而不是 _a 或者 \a 了。

分 割

如果一个脚本变得相当长,你通常希望将其分割成几部分。常见做法是函数或映射。但同时,你又不希望脚本之间这些函数或映射相互干扰。例如,你定义了一个函数Add(),但另一个脚本可能也试图定义同名的函数。为了避免这样的情况发生,我们可以在局部函数的前面加上 s:

我们来定义一个用来添加新的错误更正的函数:

 30 function s:Add(from, correct)
 31   let to = input("type the correction for " . a:from . ": ")
 32   exe ":iabbrev " . a:from . " " . to
 ..
 36 endfunction
  • 1
  • 2
  • 3
  • 4
  • 5

这样我们就可以在这个脚本之内调用函数s:Add()。如果另一个脚本也定义s:Add(),该函数将只能在其所定义的脚本内部被调用。独立于这两个函数的全局的 Add() 函数 (不带 s:) 也可以存在。

映射则可用。它产生一个脚本ID。在我们的错误更正插件中我们可以做以下的定义:

 24 noremap  
                    
                    

你可能感兴趣的:(vim)