https://blog.csdn.net/yz457694/article/details/77316421
这篇文章是手册的中文译版整理而来(英文看着太慢了,感谢前人铺路Orz…),vim的markdown插件和实时预览用着也挺方便,顺便还练习了基本操作(一路:help
、百度)。前半部分是边看边整理,感觉太慢,后面就看完之后直接正则处理了(毕竟神器)…附上两张效果图:
目录
1 简介
2 变量
3 表达式
4 条件语句
5 执行一个表达式
6 使用函数
7 定义一个函数
8 列表和字典
9 例外
10 其它的讨论
11 编写插件
12 编写文件类型插件
13 编写编译器插件
14 编写快速载入的插件
15 编写库脚本
16 发布 Vim 脚本
一个简单的例子:
:let i = 1
:while i < 5
: echo "count is" i
: let i+=1
:endwhile
注::
字符并非必须,在vim脚本中可以去掉;可以拷贝示例文本,然后用:@"
执行
输出结果:
count is 1
count is 2
count is 3
count is 4
更简洁的表达:
for i in range(1,4)
echo "count is" i
endfor
赋值:
:let {变量} = {表达式}
一般定义的变量都是全局的,可以用:let
列出当前定义的所有变量。
为了防止不同脚本之间公用全局变量导致混乱,可以在变量名后面加上s:
使其变为脚本文件的局部变量。例如:一个脚本包含如下代码:
:let s:count = 1
:while s:count < 5
: source other.vim
: let s:count += 1
:endwhile
由于s:count
是局部变量,可以确信调用(source) "other.vim"
时不会改变它的值。
还有很多其他类型的变量,参阅internal-variables
,最常见的有:
name | description |
---|---|
b:name |
缓冲区的局部变量 |
w:name |
窗口的局部变量 |
g:name |
全局变量(也用于函数中) |
v:name |
Vim预定义的变量 |
删除变量:
:unlet {变量}
如:unlet s:count
,这将删除count
局部变量并释放其占用的内存。如果不确定这个变量是否存在,又不希望看到系统在它不存在时报错,可以在命令后面加!
,即:unlet! s:count
。
字符串变量和常量:
除了数值之外,字符串也能作为变量的值。变量的类型是动态的。每当我们通过 :let
语句为变量赋值时,变量的类型才被确定。
字符串常量有两种。第一种是由双引号括起来的:
:let name = "peter"
:echo name
peter
如果你想在这样的字符串内使用双引号,在之前加上反斜杠即可:
:let name = "\"peter\""
:echo name
"peter"
如果你不想使用反斜杠,也可以用单引号括起字符串:
:let name = '"peter"'
:echo name
"peter"
所有的字符在单引号内都保持其本来面目(因此反斜杠也不能实现转义)。只有单引号本身例外: 输入两个你会得到一个单引号。
在双引号括起来的字符串中可以使用特殊字符。这里有一些有用的例子:
script | meaning |
---|---|
\t |
|
\n |
,换行 |
\r |
,
|
\e |
|
\b |
,退格 |
\" |
" |
|\,反斜杠\
|\
|CTRL-W
最后两个只是用来举例子的。\
的形式可以被用来表示特殊的键 name
。
已经提到的数值,字符串常量和变量都属于表达式,其他基本的表达式有:
expr | meaning |
---|---|
$NMAE |
环境变量 |
&name |
选项 |
@r |
寄存器 |
算术运算
operation | meaning |
---|---|
a+b |
加 |
a-b |
减 |
a*b |
乘 |
a/b |
除 |
a%b |
余 |
用 “.” 可以把两个字符串连接起来。例如:
:echo "foo"."bar"
foobar
一般的,当 :echo
命令遇到多个参数时,会在它们之间加入空格。但上例中参数是一个表达式,所以不会有空格。
下面的条件表达式很显然是从 C 语言里借来的:
a?b:c
如果 a
为真用 b
,否则用 c
。例如:
:let i = 4
:echo i>5 ? "i is big":"i is small"
:if
命令在条件满足的前提下,执行其后直到 :endif
的所有语句。常用的形式为:
:if {conditon}
{statements}
:endif
语句 {statements} 仅当表达式 {condition} 为真 (非零) 时才被执行。这些语句还必
须是有效的。否则 Vim 无法找到相应的 :endif
。
也可以使用 :else
。常用形式为:
:if {condition}
{statements}
:else
{statements}
:endif
还有 :elseif
:
:if {condition}
{statements}
:elseif {condition}
{statements}
:endif
这种形式就像 :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
这里 v:version
是 Vim 定义的变量,用来存放 Vim 的版本号。600 意为 6.0 版。6.1 版的值为 601。这对编写可以在不同版本的 Vim 上运行的脚本很有用。
对数值和字符串都可以做逻辑操作。两个字符串的算术差被用来比较它们的值。这个结果是通过字节值来计算的,对于某些语言,这样做的结果未必正确。
在比较一个字符串和一个数值时,该字符串将先被转换成一个数值。这容易出错,因为当一个字符串看起来不像数值时,它会被当作 0 对待。例如:
:if 0 == "one"
: echo "yes"
:endif
上面的例子将显示 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
注意 在匹配模式中用单引号是很有用的。因为匹配模式中通常有很多反斜杠,而反斜杠在双引号字符串中必须双写才有效。
在做字符串比较时用到 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
:sleep
命令使 Vim 小憩一下。50m
表示休息 50 毫秒。再举一个例子,:sleep 4
休息 4 秒。
用 :execute
命令可以执行一个表达式的结果。这是一个创建并执行命令的非常有效的方法。
例如要跳转到一个由变量表示的标签:
:execute "tag" . tag_name
.
被用来链接字符串"tag"
和变量tag_name
的值。:execute
命令只能用来执行冒号命令。:normal
命令可以用来执行普通模式命令。然而,它的参数只能是按表面意义解释的命令字符,不能是表达式。例如:
:normal gg=G
这个命令将跳转到第一行并以=
操作符排版所有行。
为了使:normal
命令也可以带表达式,可以把:execute
与其连起来使用,例如:
:execute "normal" . normal_commands
变量normal_commands
必须包含要执行的普通模式命令。
必须确保 :normal
的参数是一个完整的命令。否则,Vim 碰到参数的结尾就会中止其运行。例如,如果你开始了插入模式,你必须也退出插入模式。这样没问题:
:execute "normal Inew test\"
这将在当前行插入 new text
。注意 这里使用了特殊键 \
。这样就避免了在你的脚本当中键入真正的
字符。
如果你不想执行字符串,而想执行它作为表达式计算的结果,可以用eval()
函数:
:let optname = "path"
:let optval = eval('&'.optname)
&
被加到 path
前面,这样传给eval()
的参数成为 &path
。这时得到的返回值就是 ‘path’ 选项的值。
相同的操作可以这样完成:
:exe 'let optval = &'.optname
Vim 定义了大量的函数并通过这些函数提供了丰富的功能。可参考|functions
|找到一个完整的列表。
一个函数可以被:call
命令调用。参数列表要用括号括起来,并用逗号分割。例如:
:call search("Date:","W")
这将以Date:
和W
作为参数调用search()
函数。search()
函数的第一个参数时一个查找模式,第二个是一个标志。标志W
表示查找操作遇到文件尾时不折返。
在一个表达式内也可以调用函数,例如:
:let line = getline(".")
:let repl = substitute(line,'\a',"*","g")
:call setline(".",repl)
getline()
函数从当前缓冲区获取一行文本 ,其参数时行号。.
代表光标所在行。substitute()
函数的功能和:substitute
命令相似。它的第一个参数是要执行替换操作的源字符串,第二个参数时一个匹配模式,第三个参数时替换字符串,最后一个参数是一个标志位。setline()
函数将第一个参数表示的行的文本置为第二个参数表示的字符串。本例中光标所在行被substitute()
函数的结果所替换。因此这三条语句的效果等同于:
:substitute/\a/*/g
如果你在调用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() |
取得 :s 和substitute() 匹配中指定的某个匹配 |
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 表达式 |
Vim允许你定义自己的函数,基本的函数声明如下:
:function {name}({var1},{var2},...)
: {body}
:endfunction
注意 函数名必须以大写字母开始。
下面来定义一个返回两数中较小者的函数。从下面这行开始:
function Min(num1,num2)
这将告诉 Vim 这个函数名叫 Min
并且带两个参数: num1
和 num2
。
要做的第一件事就是看看哪个数值小一些:
: if a:num1 < a:num2
特殊前缀a:
告诉Vim该变量是一个函数参数。我们把最小的数值赋给smaller
变量:
: if a:num1 < a:num2
: let smaller = a:num1
: else
: let smaller = a:num2
: endif
smaller
是一个局部变量。一个在函数内部使用的变量,除非被加上类似g:
、a:
、s:
的前缀,都是局部变量。
备注 为了从一个函数内部访问一个全局变量你必须在前面加上 g:
。因此在一个函数内 g:today
表示全局变量 today
,而 today
是另外一个仅用于该函数内的局部变量。
现在你可以使用 :return
语句来把最小的数值返回给调用者了。最后,你需要结束这个函数:
: return smaller
:endfunction
下面时这个函数的完整定义:
:funciton Min(num1,num2)
: if a:num1
调用用户自定义函数的方式和调用内置函数完全一致。仅仅是函数名不同而已。上面的Min
函数可以这样来使用:
:echo Min(5,8)
只有这时函数才被 Vim 解释并执行。如果函数中有类似未定义的变量之类的错误,你将得到一个错误信息。这些错误在定义函数时是不会被检测到的。
当一个函数执行到 :endfunction
或 :return
语句没有带参数时,该函数返回零。
如果要重定义一个已经存在的函数,在 function
命令后加上!
:
:function! Min(num1,num2,num3)
范围的使用
:call
命令可以带一个行表示的范围。这可以分成两种情况。当一个函数定义时给出了range
关键字时,表示它会自行处理该范围。
Vim 在调用这样一个函数时给它传递两个参数: a:firstline
和 a: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
你可以这样调用上面的函数:
:10,30call Count_words()
这个函数将被调用一次并显示字数。
另一种使用范围的方式是在定义函数时不给出 range
关键字。Vim 将把光标移动到范围内的每一行,并分别对该行调用此函数。例如:
:function Number()
: echo "line".line(".")."contains".getline(".")
:endfunction
如果你用下面的方式调用该函数:
:10,15call Number()
它将被执行六次。
可 变 参 数
Vim 允许你定义参数个数可变的函数。下面的例子给出一个至少有一个参数 (start),但可以多达 20 个附加参数的函数:
:function Show(start, ...)
变量 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
上例中 :echohl
命令被用来给出接下来的 :echo
命令如何高亮输出。:echohl None
终止高亮。:echon
命令除了不输出换行符外,和 :echo
一样。
你可以用a:000
变量,它是所有 ...
参数的列表。见 a:000
。
函 数 清 单
:function
命令列出所有用户自定义的函数及其参数:
:function
function Show(start, ...)
function GetVimIndent()
function SetSyn(name)
如果要查看该函数具体做什么,用该函数名作为 :function
命令的参数即可:
:function SetSyn
1 if &syntax == ''
2 let &syntax = a:name
3 endif
endfunction
调 试
调试或者遇到错误信息时,行号是很有用的。有关调试模式请参阅 debug-scripts
。
你也可以通过将 verbose
选项设为 12 以上来察看所有函数调用。将该参数设为15 或以上可以查看所有被执行的行。
删 除 函 数
为了删除Show()
函数:
:delfunction Show
如果该函数不存在,你会得到一个错误信息。
函 数 引 用
有时使变量指向一个或另一个函数可能有用。要这么做,用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!
注意 保存函数引用的变量名必须用大写字母开头,不然和内建函数的名字会引起混淆。
调用变量指向的函数可以用call()
函数。它的第一个参数是函数引用,第二个参数是参数构成的列表。
和字典组合使用函数引用是最常用的,下一节解释。
到目前为止,我们用了基本类型字符串和数值。Vim 也支持两种复合类型: 列表和字典。
列表是事物的有序序列。这里的事物包括各种类型的值。所以你可以建立数值列表、列表列表甚至混合项目的列表。要建立包含三个字符串的列表:
:let alist = ['aap', 'mies', 'noot']
列表项目用方括号包围,逗号分割。要建立空列表:
:let alist = []
用add()
函数可以为列表加入项目:
:let alist = []
:call add(alist, 'foo')
:call add(alist, 'bar')
:echo alist
['foo', 'bar']
列表的连接用+
完成:
:echo alist + ['foo', 'bar']
['foo', 'bar', 'foo', 'bar']
或者,你可以直接用extend()
函数扩展一个列表:
:let alist = ['one']
:call extend(alist, ['two', 'three'])
:echo alist
['one', 'two', 'three']
注意 这里如果用add()
,效果不一样:
:let alist = ['one']
:call add(alist, ['two', 'three'])
:echo alist
['one', ['two', 'three']]
add()
的第二个参数作为单个项目被加入。
FOR 循 环
使用列表的一个好处是可以在上面进行叠代:
:let alist = ['one', 'two', 'three']
:for n in alist
: echo n
:endfor
one
two
three
这段代码循环遍历列表 alist
的每个项目,分别把它们的值赋给变量 n
。for 循环通用的形式是:
:for {varname} in {listexpression}
: {commands}
:endfor
要循环若干次,你需要长度为给定次数的列表。range()
函数建立这样的列表:
:for a in range(3)
: echo a
:endfor
0
1
2
注意range()
产生的列表的第一个项目为零,而最后一个项目比列表的长度小一。
你也可以指定最大值、步进,反向也可以:
:for a in range(8, 4, -2)
: echo a
:endfor
8
6
4
更有用的示例,循环遍历缓冲区的所有行:
:for line in getline(1, 20)
: if line =~ "Date: "
: echo matchstr(line, 'Date: \zs.*')
: endif
:endfor
察看行 1 到 20 (包含),并回显那里找到的任何日期。
字 典
字典保存键-值
组对。如果知道键,你可以快速查找值。字典用花括号形式建立:
:let uk2nl = {'one': 'een', 'two': 'twee', 'three': 'drie'}
现在你可以把键放在方括号里以查找单词:
:echo uk2nl['two']
twee
字典定义的通用形式是:
{ : , ...}
空字典是不包含任何键的字典:
{}
字典的用途很多。它可用的函数也不少。例如,你可以得到它的键列表并在其上循环:
:for key in keys(uk2nl)
: echo key
:endfor
three
one
two
注意 这些键没有排序。你自己可以对返回列表按照特定顺序进行排序:
:for key in sort(keys(uk2nl))
: echo key
:endfor
one
three
two
但你永远不能得到项目定义时的顺序。为此目的,只能用列表。列表里的项目被作为有序序列保存。
字 典 函 数
字典项目通常可以用方括号里的索引得到:
:echo uk2nl['one']
een
完成同样操作且无需那么多标点符号的方法:
:echo uk2nl.one
een
这只能用于由 ASCII 字母、数位和下划线组成的键。此方式也可以用于赋值:
:let uk2nl.four = 'vier'
:echo uk2nl
{'three': 'drie', 'four': 'vier', 'one': 'een', 'two': 'twee'}
现在来一些特别的: 你可以直接定义函数并把它的引用放在字典里:
:function uk2nl.translate(line) dict
: return join(map(split(a:line), 'get(self, v:val, "???")'))
:endfunction
让我们先试试:
:echo uk2nl.translate('three two five one')
drie twee ??? een
你注意到的第一个特殊之处是 :function
一行最后的 dict
。这标记该函数为某个字典使用。self
局部变量这时可以引用该字典。
现在把这个复杂的return
命令拆开:
split(a:line)
split()
函数接受字符串,把它分成空白分隔的多个单词,并返回这些单词组成的列表。所以下例返回的是:
:echo split('three two five one')
['three', 'two', 'five', 'one']
map()
函数的第一个参数是上面这个列表。它然后遍历列表,用它的第二个参数来进行计算,过程中 v:val
设为每个项目的值。这相当于 for 循环的快捷方式。命令:
:let alist = map(split(a:line), 'get(self, v:val, "???")')
等价于:
:let alist = split(a:line)
:for idx in range(len(alist))
: let alist[idx] = get(self, alist[idx], "???")
:endfor
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
和上面的函数稍有不同,这里用 self.words
来查找单词的翻译,但我们还没有self.words
。所以你可以把这叫做抽象类。
让我们现在实例化一个荷兰语的翻译对象:
:let uk2nl = copy(transdict)
:let uk2nl.words = {'one': 'een', 'two': 'twee', 'three': 'drie'}
:echo uk2nl.translate('three one')
drie een
然后来一个德语的翻译器:
:let uk2de = copy(transdict)
:let uk2de.words = {'one': 'ein', 'two': 'zwei', 'three': 'drei'}
:echo uk2de.translate('three one')
drei ein
你看到copy()
函数被用来建立 transdict
字典的备份,然后修改此备份以加入单词表。当然,原来的字典还是保持原样。
现在你可以再进一步,使用你偏好的翻译器:
:if $LANG =~ "de"
: let trans = uk2de
:else
: let trans = uk2nl
:endif
:echo trans.translate('one two three')
een twee drie
这里 trans
指向两个对象 (字典) 之一,并不涉及到备份的建立。关于列表和字典同一性的更多说明可见 list-identity
和 dict-identity
。
你使用的语言现在可能还不支持。你可以覆盖translate()
函数,让它什么都不做:
:let uk2uk = copy(transdict)
:function! uk2uk.translate(line)
: return a:line
:endfunction
:echo uk2uk.translate('three one wladiwostok')
three one wladiwostok
注意 使用!
会覆盖已有的函数引用。现在,在没找到能够识别的语言的时候,让我们用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
进一步的阅读可见 Lists
和 Dictionaries
。
让我们从一个例子开始:
:try
: read ~/templates/pascal.tmpl
:catch /E484:/
: echo "Sorry, the Pascal template file cannot be found."
:endtry
如果该文件不存在的话,:read
命令就会失败。这段代码可以捕捉到该错误并向用户给出一个友好的信息,而不是一个一般的出错信息。
在 :try
和 :endtry
之间的命令产生的错误将被转变成为例外。例外以字符串的形式出现。当例外是错误时该字符串就是出错信息。而每一个出错信息都有一个对应的错误码。在上面的例子中,我们捕捉到的错误包括 E484
。Vim 确保这个错误码始终不变(文字可能会变,例如被翻译)。
当 :read
命令引起其它错误时,模式 E484:
不会被匹配。因此该例外不会被捕获,结果是一个一般的出错信息。
你可能想这样做:
:try
: read ~/templates/pascal.tmpl
:catch
: echo "Sorry, the Pascal template file cannot be found."
:endtry
这意味着所有的错误都将被捕获。然而这样你就无法得到那些有用的错误信息,比如说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
这个例子将自光标处到文件尾的所有行通过过滤器 filter
。该程序的参数是文件名。无论在 :try
和 :finally
之间发生了什么,call delete(tmp)
命令始终被执行。这可以确保你不会留下一个临时文件。
关于例外处理更多的讨论可以阅读参考手册: |exception-handling|
。
这里集中了一些和 Vim 脚本相关的讨论。别的地方其实也提到过,这里算做一个整理。
行结束符取决于所在的系统。Unix 系统使用单个的
字符。MS-DOS、Windows、OS/2系列的系统使用
。对于那些使用
的映射而言,这一点很重要。参阅|:source_crnl|
。
空 白 字 符
可以使用空白行,但没有作用。
行首的空白字符 (空格和制表符) 总被忽略。参数间的 (例如像下面命令中set
和cpoptions
之间的) 空白字符被归约为单个,仅用作分隔符。而最后一个 (可见) 字符之后的空白字符可能会被忽略也可能不会,视情况而定。见下。
对于一个带有等号 =
的 :set
命令,如下:
:set cpoptions =aABceFst
紧接着等号之前的空白字符会被忽略。然而其后的空白字符是不允许的!
为了在一个选项值内使用空格,必须像下面例子那样使用反斜杠:
:set tags=my\ nice\ file
如果写成这样:
:set tags=my nice file
Vim 会给出错误信息,因为它被解释成:
:set tags=my
:set nice
:set file
注 释
双引号字符"
标记注释的开始。除了那些不接受注释的命令外 (见下例),从双引号起的直到行末的所有字符都将被忽略。注释可以从一行的任意位置开始。
对于某些命令来说,这里有一个小小的 “陷阱”。例如:
:abbrev dev development " shorthand
:map o#include " insert include
:execute cmd " do it
:!ls *.c " list C files
缩写 dev
会被展开成 development " shorthand
;
的键盘映射会是包括" insert include
在内的那一整行;execute
命令会给出错误;!
命令会将其后的所有字符传给 shell,从而引起一个不匹配 "
的错误。
结论是,:map
,:abbreviate
,:execute
和 !
命令之后不能有注释。(另外还有几个命令也是如此)。不过,对于这些命令有一个小窍门:
:abbrev dev development|" shorthand
:map o#include|" insert include
:execute cmd |" do it
|
字符被用来将两个命令分隔开。后一个命令仅仅是一个注释。最后一个命令里,你需
要做两件事: |:execute|
和用 |
:
:exe '!ls *.c' |" list C files
注意 在缩写和映射后的 |
之前没有空格。这是因为对于这些命令,直到行尾或者 |
字符为止的内容都是有效的。此行为的后果之一,是你没法总看到这些命令后面包括的空白字符:
:map o#include
要发现这个问题,你可以在你的 vimrc 文件内置位 list
选项。
Unix 上有一个特殊的办法给一行加注释,从而使得 Vim 脚本可执行:
#!/usr/bin/env vim -S
echo "this is a Vim script"
quit
#
命令本身列出一行并带行号。加上感叹号后使得它什么也不做。从而,你可以在后面加上 shell 命令来执行其余的文件。|:#!|
|-S|
陷 阱
下面的例子的问题就更大了:
:map ,ab o#include
:unmap ,ab
这里,unmap
命令是行不通的,因为它试着 unmap ,ab
。而这个映射根本就不存在。因为 unmap ,ab
的末尾的那个空白字符是不可见的,这个错误很难被找出。
在下面这个类似的例子里,unmap
后面带有注释:
:unmap ,ab " comment
注释将被忽略。然而,Vim 会尝试unmap
不存在的 ,ab
。可以重写成:
:unmap ,ab|" comment
恢 复 一 个 视 窗 位 置
有时有你想做一些改动然后回到光标原来的位置。如果能恢复相对位置,把和改动前同样的行置于窗口顶端就更好了。
这里的例子拷贝当前行,粘贴到文件的第一行,然后恢复视窗位置:
map ,p ma"aYHmbgg"aP`bzt`a
解析:
ma"aYHmbgg"aP`bzt`a
ma 在当前位置做标记 a
"aY 将当前行拷贝至寄存器 a
Hmb 移动到窗口的顶行并做标记 b
gg 移动到文件首行
"aP 粘贴拷贝的行到上方
`b 移动到刚才的顶行
zt 使窗口出现的文本恢复旧观
`a 回到保存的光标位置
封 装
为了避免你的函数名同其它的函数名发生冲突,使用这样的方法:
- 在函数名前加上独特的字符串。我通常使用一个缩写。例如,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
用约定方式编写的脚本能够被除作者外的很多人使用。这样的脚本叫做插件。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 " Vim global plugin for correcting typing mistakes
2 " Last Change: 2000 Oct 15
3 " Maintainer: Bram Moolenaar
关于版权和许可: 由于插件很有用,而且几乎不值得限制其发行,请考虑对你的插件使用公共领域 (public domain) 或 Vim 许可 |license|
。在文件顶部放上说明就行了。例如:
4 " License: This file is placed in the public domain.
续 行,避 免 副 效 应
在上面的第 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
我们先将 cpoptions
的旧值存在s:save_cpo
变量中。在插件的最后该值将被恢复。
注意 上面使用了脚本局部变量 |s:var|
。因为可能已经使用了同名的全局变量。对于仅在脚本内用到的变量总应该使用脚本局部变量。
禁 止 加 载
有可能一个用户并不总希望加载这个插件。或者系统管理员在系统的插件目录中已经把这个插件删除了,而用户希望使用它自己安装的插件。用户应该有机会选择不加载指定的插件。下面的一段代码就是用来实现这个目的的:
6 if exists("g:loaded_typecorr")
7 finish
8 endif
9 let g:loaded_typecorr = 1
这同时也避免了同一个脚本被加载两次以上。因为那样用户会得到各种各样的错误信息。比如函数被重新定义,自动命令被多次加入等等。
建议使用的名字以 loaded_
开头,然后是插件的文件名,按原义输入。之前加上 g:
以免错误地在函数中使用该变量 (没有 g:
可以是局部于函数的变量)。
finish
阻止 Vim 继续读入文件的其余部分,这比用if-endif
包围整个文件要快得多。
映 射
现在让我们把这个插件变得更有趣些: 我们将加入一个映射用来校正当前光标下的单词。
我们当然可以任意选一个键组合,但是用户可能已经将其定义为其它的什么功能了。为了使用户能够自己定义在插件中的键盘映射使用的键,我们可以使用
标识:
22 map a TypecorrAdd
那个
会做实际的工作,后面我们还会做更多解释。
用户可以将 mapleader
变量设为他所希望的开始映射的键组合。比如假设用户这样做:
let mapleader = "_"
映射将定义为 _a
。如果用户没有这样做,Vim 将使用缺省值反斜杠。这样就会定义一个映射 - \a
。
注意 其中用到了
,这会使得 Vim 在映射已经存在时给出错误信息。|:map-
但是如果用户希望定义自己的键操作呢?我们可以用下面的方法来解决:
21 if !hasmapto('TypecorrAdd')
22 map a TypecorrAdd
23 endif
我们先检查对
的映射是否存在。仅当不存在时我们才定义映射
。这样用户就可以在他自己的 vimrc 文件中加入:
map ,c TypecorrAdd
那么键序列就会是 ,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
这样我们就可以在这个脚本之内调用函数s:Add()
。如果另一个脚本也定义s:Add()
,该函数将只能在其所定义的脚本内部被调用。独立于这两个函数的全局的 Add() 函数 (不带 s:
) 也可以存在。
映射则可用
。它产生一个脚本ID
。在我们的错误更正插件中我们可以做以下的定义:
24 noremap