第六章 VimL 内建函数使用
一般实用的语言包括语法与标准库,毕竟写程序不能完全从零开始,须站在他人的基石之 上。而要开发更有产品价值的程序,更要站在巨人的肩膀上,比如社区提供的第三方库。
细思起来,VimL 语言的“标准库”包括两大类:内建命令与内建函数。用户在此基础上可 自定义命令与自定义函数,再合乎语法地组成起来,以达成所需的功能。第三章简要地介 绍了部分基础命令,其实那更倾向于 Vim 编辑器的功能。本章要介绍的内建函数,则更 倾向于 VimL 语言的功能。
不过本章将会是比较无聊的一章。帮助文档 :help function-list
会按类别列出内置 函数,:help functions
则会按字母序列出内置函数,可供参考。中文用户可找一份帮 助文档的中译本,虽然可能不是最新版本的,不过绝大部分内置函数都应该是稳定向下兼 容的。
所以本章不会(也没必要)罗列所有内建函数,只择要讲些内建函数的使用经验技法。要 查看某个函数的解释,请直接 :help func_name()
,请注意加一对括号,限定查函数 的文档,否则有可能查到的是同名的命令或选项等。
6.1 操作数据类型
字符串运算
Vim 是文本编辑器,所以处理文本字符串是一重点任务。每个字符在计算机内部都用一个 整数表示(即编码值,与用数字字符组成表示的可读整数不同概念),具体如何对应取决 于编码系统(有时简称编码)。目前计算机界的趋势是用 utf-8
编码表示 Unicode 字 符集,因为它与最早的 ASCII 编码兼容。一个汉字在此编码下用 3 个字节表示,英文字 符仍用一个字节表示。
- nr2char() 将编码值转为字符
- char2nr() 将字符转为编码值
- str2nr() 将字符串转为整数
- str2float() 将字符串转为浮点数
由于 VimL 没有字符串类型,char2nr()
其实是将字符串首字符将为编码值的。默认按 utf-8
编码获取编码值与字符的对应,但可传入额外参数按其他编码系统对应。例如:
: echo char2nr('中国') |" --> 20013
: echo nr2char(20013) |" --> 中
整数类型与字符串一般可自动转换,一般用不上 str2nr()
。但如果从安全考虑习惯主 动判断类型的话,要注意从命令行输入的参数都是字符串,不是整数。此外,字符串不会 自动转为浮点数,而是截断为整数,所以确实要处理浮点数是,用 str2float()
转换 。
- printf() 格式化字符串
简单的字符串连接用连接操作符 .
即可。要组装复杂字符串时可用 printf()
函数 ,通过字符串模式与 %
占位符,插入变量。其用法与 C 语言的 sprintf()
类似, 因为该函数会返回结果字符串,“打印”字符串用 :echo
命令。
- escape() 将字符串中指定的字符用反斜杠
\
转义 - shellescape() 转义特殊字符以适于 shell 命令
- fnameescape() 转义特殊字符以短于 Vim 命令,主要用于转义文件名参数
当组装字符串用于当作命令执行时,为安全起见,应先调用 shellescape()
或 fnameescape()
进行转义。这两个函数自有其转义策略,适用大部分情况。当有特殊需 求时,可用 escape()
指定要转义哪些字符(传入第二参数的字符串中出现的所有字符 都表示要转义的)。
- tolower() 将字符中转为小写
- toupper() 将字符串转为大写
- tr() 按一一对应的方式转换字符串
- strtrans() 将字符串转换为可打印字符串
tr()
函数进行简单的字符串转换(不是正则替换),效果如同 unix 工具 tr
。大小 写转换是其一种特例策略,如转大写相当于 tr(str, 'abcdefg...', 'ABCDEFG...')
。 而 strtrans()
是按 vim 的自定策略将不可打印字符转换为可视字符(组合,一般以 ^
开关)表示。
- strlen() 按字节数获取字符串长度
- strchars() 按字符数获取字符串长度,当含宽字符(如汉字时)与字节长度有差异
- strwidth() 字符串宽度,显示在屏幕上时将占用的列宽度,但未处理制表符
- strdisplaywidth() 字符串实际显示宽度,并按设置处理制表符宽度
- byteidx() 第几个字符的字节索引,不单独处理组合字符
- byteidxcomp() 也是字符索引转为字节索引,组合字符单独处理
字符串可像列表一样用中括号索引,那是按字节索引的。当字符串中存在宽字符时,字符 数与字节数不一致,这就需要处理字符索引与字节索引的不同。请仔细观察以下示例:
: let str = 'vim 中国'
: echo strlen(str) |" --> 10
: echo strchars(str) |" --> 6(vim 加空格加两汉字)
: echo len(str) |" --> 10
: echo strwidth(str) |" --> 8(每个汉字三字节但两宽度)
: echo str[0:2] |" --> vim
: echo str[4] |" --> <e4>(中字的第一个字节)
: echo str[4:5] |" --> <e4><b8>
: echo str[4:6] |" --> 中
组合字符(有的书籍叫重音字符),中国人一般不必关注,欧洲人才用得到。比如 é
是通过一个正常的 e
字母加上重音符组成而成的('e' . nr2char(0x301)
),显示 上像是一个字符,但计算机要用两个字符表示。至于算一个字符还是两个字符,似乎都有 理有用,所以就提供了不同的函数或可选参数来处理这种情况。这与汉字宽字符的情况不 一样。汉字的三个字节是不可分的,取第一字节是无效字符。但组合字符的第一字节仍是 个有效字符(字母)。
字符与编码看似简单,如同空气与水一样简单,但深入细处还挺复杂。所以建议初学者不 必深究,始终用英文文本示例测试学习即可。当实际工作中遇到中文问题时再回头查阅。 此外,据说早期的中国程序员常要念经“一个汉字等于两个字节”,那是用 GB 编码的原因 ,现在请升级经文“一个汉字等于三个字节”。
- stridx() 查找一个短字符串在另一个长字符串第一次出现的起始索引
- strridx() 查找一个短字符串在另一个长字符串最后一次出现的起始索引
- strpart() 截取字符串从某个索引开始的定长子串
查找简单子串存在情况可用 stridx()
,要求精确匹配,且大小写敏感。返回的结果索 引是字节索引,索引从 0 开始,若不存在子串返回 -1。截取子串可用中括号索引切片方 式,如上例 str[4:6]
,参数是起始索引与终止索引(含双端)。而 strpart()
的参 数是起始索引与长度,如上例等效于 strpart(str, 4, 3)
。
- match() 查找一个正则表达式在字符串出现的起始索引,不匹配时返回 -1
- matchend() 查找一个正则表达式在字符串出现的终止索引
- matchstr() 返回字符串中匹配正则表达式的部分,不匹配时返回空串
- matchlist() 将正则匹配结果按分组返回至列表中,不匹配时返回空列表
- substitute() 正则表达式替换,
:s
命令的函数式 - submatch() 获取正则匹配的分组子串,只可用于
:s
命令的替换部分
这几个函数用于处理正式表达式的匹配查找与替换。如果仅是要判断是否匹配,可直接用 操作符 if str =~# pattern
。match()
函数主要是还能返回匹配成功的起始索引, 相应地 matchend()
返回的是终止索引。matchstr()
返回的是匹配到的整个子串。 如果正则表达式中有括号分组 \(\)
,最好用 matchlist()
函数,它返回的列表中, 第一个元素([0]
)就是匹配到的整个子串,其后是按顺序的分组子串。其中的关系可 用如下伪代码表示:
let s = some_string
let p = search_pattern
if some_string =~# search_pattern
let sidx = match(s, p)
let eidx = match(s, p)
sidx != -1; eidx != -1
s[sidx:eidx] == mathcstr(s, p)
let slist = matchlist(s, p)
slist[0] == matchstr(s, p) == & == submatch(0)
slist[1] == \1 == submatch(1)
slist[2] == \2 == submatch(2)
...
endif
替换函数 substitute()
的参数及意义与 :substitute
命令的几个部分完全一样。 不过命令可以缩写为 :s
,函数不可以缩写。如以下两个语句功能类似:
: s/pat/sub/flag |" 对当前行替换 pat 为 sub
: call substitute(line('.'), pat, sub, flag)
在替换部分 {sub}
可用表达式,以 \=
开始即可,如此 submatch()
表示前面 {pat}
部分的分组子串。而在以常规字面字符串表示 {sub}
部分时,则用 \1
表 示分组子串。
- string() 将其他任意变量或表达式转为字符串表达,类似
:echo
的显示 - expand() 将具有特殊意义的标记(如
% # <cword>
等)展开 - iconv() 转换字符串编码
- repeat() 将字符串重复串接多次生成长字符串
- eval() 将字符串当作表达式来执行,并返回结果
- execute() 将字符串当作命令来执行,将结果返回为字符串
注意,eval()
与 execute()
很灵活,但比较低效,也可能有一定风险。如有其他更 优雅的实现写法,尽量用替代方案。
浮点数学运算
用 VimL 做数学运算并不常见,但如果啥时想到需要她,她也在那儿。一般整数运算直接 用操作符,浮点运算才需要调用函数,且这些内置函数的结果一般也是浮点数,即使参数 都是整数。
- float2nr() 将浮点数转为整数类型
- trunc() 截断取整
- round() 四舍五入取整
- floor() 向下取整
- ceil() 向上取整
这几个函数都是取整运算,但实际上只有 float2nr()
的结果是整数类型( v:t_number
),其他函数取整后仍为浮点数(v:t_float
)。float2nr()
与 trunc()
意义一样是截断取整。当涉及负数,取整可能不太直观,请看示例:
: echo float2nr(4.56) float2nr(-4.56) |" --> 4 -4
: echo trunc(4.56) trunc(-4.56) |" --> 4.0 -4.0
: echo round(4.56) round(-4.56) |" --> 5.0 -5.0
: echo floor(4.56) floor(-4.56) |" --> 4.0 -5.0
: echo ceil(4.56) ceil(-4.56) |" --> 5.0 -4.0
当四舍五入正好在中值时(如小数部分是 0.5),取远离 0 那个整数,类似:
: round(+float) == trunc(+float + 0.5) |" --> 正数取整
: round(-float) == trunc(-float - 0.5) |" --> 负数取整
- fmod() 取余数
- pow() 取幂
- sqrt() 开平方
整数取余数可直接用操作符 %
,该操作符不能用于浮点数。fmod()
可用于浮点数的 取余,即使整数也当作浮点处理。VimL 并没有整数取幂的操作符(其他语言有用 ^
或 **
作幂运算的),须用 pow()
函数求幂,结果也总是浮点数;
echo 10 % 3 |" --> 1
echo fmod(10, 3) |" --> 1.0
echo pow(2, 10) |" --> 1024.0
echo pow(4, 1/2) |" --> 1.0 (先计算 1/2 = 0)
echo pow(4, 1/2.0) |" --> 2.0
echo sqrt(4) |" --> 2.0
- exp() 自然指数
- log() 自然对数,以
e = 2.718282
为底 - log10() 常用对数,以
10
为底 - 三角函数与反三角函数:sin() cos() asin() acos() 等
log()
是 exp()
的反函数,在数学上的记号是 ln
;而数学上记为 lg
的对数, 程序上是 log10()
。这个命令习惯应该是源自 C 语言的标准库函数。同样,一众三角 函数也是类似 C 语言的,参数是以弧度单位表示的角度。不过很难想像需要在 VimL 中 用到这些略为高深的数学计算的场景。
- isnan() 判断是否为非数
自 Vim8 引入非数的判断。像 0/0
这样的计算结果叫非数,在其他一些计算机语言与 文档中习惯用 NaN
来表示。可能为了更好地与其他数据文件交互,Vim 也增加这个函 数来处理非数。
列表与字典运算
在第四章介绍数据结构时,已经顺便介绍了操作列表与字典的函数。这里不再重复,只作 些补充说明。
首先,很多函数可同时作用于列表与字典,甚至字符串。因为脚本语言弱类型的缘故,没 法限定传入函数的参数,只能根据参数类型作出不同的合理反馈。例如:
- len() 取列表或字典集合中元素个数,也取字符串的(字节)长度
- empty() 可判断是否空列表、空字典或空字符串,整数 0 也认为是空的
- match() 还能匹配字符串列表,返回能匹配成功的元素索引
其次,列表与字典都是集合,有一类高阶函数,可接收另一个函数(引用)作为参数,用 于处理集合内的每一个元素。比如 map()
与 filter()
函数。
前面提及,VimL 有许多与命令同名的函数,都是实现类似的功能。但 map()
是例外, 它与定义键映射的 :map
命令没有语义关系,完全是不同的概念。
- map({expr1}, {expr2}) 修改集合的每个元素
其中参数一 {expr1}
可以是列表如字典,参数二 {expr2}
是函数引用。参数一集合 的每个元素,传给参数二所代表的函数,将结果值替换原来的元素。最终会原位修改列表 或字典。关键是参数二所引用的函数定义要遵循一定的规范,它应接收两个参数,map()
会将每个元素的索引与值传给该函数(字典元素的索引即是键名)。整个流程可如下模拟:
function! MapDict(dict, fun)
for [l:key, l:val] in items(a:dict)
let l:val_new = a:fun(l:key, l:val)
let a:dict[l:key] = l:val_new
endfor
endfunction
function! MapList(list, fun)
for l:idx in range(len(a:list))
let l:val = a:list[l:idx]
let l:val_new = a:fun(l:idx, l:val)
let a:list[l:idx] = l:val_new
endfor
endfunction
如果处理每个元素的函数很简单,则可不必创建函数再传入函数引用。可用一个字符串代 替,该字符串调用 eval()
执行后,将结果替换原元素。在字符串用,用内置变量 v:val
代表迭代的每个元素值,v:key
代表元素索引(键名)。相当于传入函数版本 的两个参数。用这种方法得注意字符串的转义,建议用单引号括起字面字符串。可用其他 字符串函数或操作符组装,最终结果的字符串再调用 eval()
计算结果新值。
事实上,在低版本的 vim 中, map()
函数的参数二只能用字符串。这才需要 v:key
与 v:val
这两个特殊变量标记置于可执行字符串中。自 vim8 后,强烈建议使用函数 引用参数。这就无须理解 v:key
与 v:val
的即时意义。不过在定义处理元素的函数 时,建议也用 key
与 val
作为函数形参,这使整个代码的可读性更佳:
function! MapHandle(key, val) abort
let l:result = deal with a:key and a:val
return l:result
endfunction
如果处理函数很简单,也可不必预定义函数,即时定义 lambda 也可以,因为 lambda 表 达式的值也正是一个函数引用。例如:
let list1 = [1, 2, 3]
let list2 = map(list2, {idx, val -> val * 2})
echo list1
echo list2
let list3 = map(copy(list2), {idx, val -> val * 3})
echo list2
echo list3
以上示例也说明了 map()
函数是原位修改的,如果不想修改原集合,可先调用 copy()
创建副本。低版本中等效的用字符串调用方式如下:
echo map([1, 2, 3], 'v:val * 2')
像这样简单的功能,也许字符串方式写来更简洁,但稍为复杂的功能,可执行字符串的表 示法就可能比较费解了。比如要将原列表中每个元素加上尖括号 <>
括起来,以下三种 调用方式都能实现:
echo map([1, 2, 3], '"<" . v:val . ">"')
echo map([1, 2, 3], 'printf("<%s>", v:val)')
echo map([1, 2, 3], {idx, val -> printf('<%s>', val)})
用 lambda 表达式可避免多重引号的理解困难。而且 lambda 表达式或函数若预先定义的 话,在其他地方也是可用的。而含 v:val
的特征字符串,放在其他地方几乎是没什么 意义了。
- filter({expr1}, {expr2}) 过滤集合内的元素
filter()
与 map()
函数类似。参数二所代表的处理函数,也接收索引与值两个参数 ,但是要求返回布而逻辑值。如果返回的是真(数字 1),则保留不处理,如果返回的是 假(数字 0),则删除相应的元素。模拟流程如下:
function! FilterDict(dict, fun)
for [l:key, l:val] in items(a:dict)
let l:bKeep = a:fun(l:key, l:val)
if empty(l:bKeep)
unlet a:dict[l:key]
endif
endfor
endfunction
自己模拟过滤列表可能略有麻烦,因为如果正向迭代,删除元素后,索引可能会变化。当 然了,你不必真的自己写或用这样的模拟函数,请用内置的库函数!
同样地,可以用字符串或 lambda 表达式。且在支持 lambda 表达式的 vim 中,尽量用 lambda 表达式。
- sort({list} [, {fun}, {self}]) 为列表排序,从小到大
- uniq({list} [, {fun}, {self}]) 删除相邻重复元素
几乎在任一本算法教科书,排序都是重点。但是几乎在任一个语言中,排序都有已优化实 现的库函数,不必自己写的,自己需要做的只是提供比较函数,说明要如何排序的需求。 VimL 要求的比较函数能接收两个参数,返回值意义如下:
0
两个参数视为相等1
第一个参数视为比第二个参数大-1
第一个参数视为比第二个参数小
sort()
函数只能为列表排序,因为字典是无序的。第二参数 {fun}
一般是函数引用 ,可用 lambda 表达式,但不支持像 map()
那样的可执行字符串。然而,可以是普通 字符用于表示 vim 预设的几种排序策略(常用需求):
- 空串或省略,按字符串排序,类似
:sort
命令为当前 buffer 的排序行为。 l
或i
,忽略大小写的排序n
按数字排序,非数字类型的元素认为是 0N
按数字排序,字符串会转为数字f
按数字排序,列表元素限定仅是数字或浮点数
VimL 的 sort()
是稳定排序算法,即如果两个元素相等(按 {fun}
返回 0
),排 序后它们也保持原来的相对顺序。如果 {fun}
参数是含 dict
属性的函数,则要提 供第三参数 {self}
,一个作为 self
的字典变量。
uniq()
函数的参数用法与 sort()
相同。且一般应该对已排序的列表调用 uniq()
,因为它只比较相邻元素而去重。
小结
VimL 的标量主要就是字符串与数字,集合也就列表与字典。所以为这些数据类型提供了 大量的库函数 api。用 :h type()
查看支持的所有变量类型。但其他类型需要支持的 操作非常有限,故无必要有什么专门函数处理。