You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
19 KiB
378 lines
19 KiB
2 years ago
|
# 第六章 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` 编码获取编码值与字符的对应,但可传入额外参数按其他编码系统对应。例如:
|
||
|
```vim
|
||
|
: 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() 也是字符索引转为字节索引,组合字符单独处理
|
||
|
|
||
|
字符串可像列表一样用中括号索引,那是按字节索引的。当字符串中存在宽字符时,字符
|
||
|
数与字节数不一致,这就需要处理字符索引与字节索引的不同。请仔细观察以下示例:
|
||
|
|
||
|
```vim
|
||
|
: 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]`)就是匹配到的整个子串,其后是按顺序的分组子串。其中的关系可
|
||
|
用如下伪代码表示:
|
||
|
|
||
|
```vim
|
||
|
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`,函数不可以缩写。如以下两个语句功能类似:
|
||
|
```vim
|
||
|
: 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()` 意义一样是截断取整。当涉及负数,取整可能不太直观,请看示例:
|
||
|
|
||
|
```vim
|
||
|
: 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 那个整数,类似:
|
||
|
```vim
|
||
|
: round(+float) == trunc(+float + 0.5) |" --> 正数取整
|
||
|
: round(-float) == trunc(-float - 0.5) |" --> 负数取整
|
||
|
```
|
||
|
|
||
|
* fmod() 取余数
|
||
|
* pow() 取幂
|
||
|
* sqrt() 开平方
|
||
|
|
||
|
整数取余数可直接用操作符 `%` ,该操作符不能用于浮点数。`fmod()` 可用于浮点数的
|
||
|
取余,即使整数也当作浮点处理。VimL 并没有整数取幂的操作符(其他语言有用 `^` 或
|
||
|
`**` 作幂运算的),须用 `pow()` 函数求幂,结果也总是浮点数;
|
||
|
```vim
|
||
|
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()`
|
||
|
会将每个元素的索引与值传给该函数(字典元素的索引即是键名)。整个流程可如下模拟:
|
||
|
|
||
|
```vim
|
||
|
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` 作为函数形参,这使整个代码的可读性更佳:
|
||
|
|
||
|
```vim
|
||
|
function! MapHandle(key, val) abort
|
||
|
let l:result = deal with a:key and a:val
|
||
|
return l:result
|
||
|
endfunction
|
||
|
```
|
||
|
|
||
|
如果处理函数很简单,也可不必预定义函数,即时定义 lambda 也可以,因为 lambda 表
|
||
|
达式的值也正是一个函数引用。例如:
|
||
|
|
||
|
```vim
|
||
|
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()` 创建副本。低版本中等效的用字符串调用方式如下:
|
||
|
|
||
|
```vim
|
||
|
echo map([1, 2, 3], 'v:val * 2')
|
||
|
```
|
||
|
|
||
|
像这样简单的功能,也许字符串方式写来更简洁,但稍为复杂的功能,可执行字符串的表
|
||
|
示法就可能比较费解了。比如要将原列表中每个元素加上尖括号 `<>` 括起来,以下三种
|
||
|
调用方式都能实现:
|
||
|
|
||
|
```vim
|
||
|
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),则删除相应的元素。模拟流程如下:
|
||
|
|
||
|
```vim
|
||
|
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` 按数字排序,非数字类型的元素认为是 0
|
||
|
* `N` 按数字排序,字符串会转为数字
|
||
|
* `f` 按数字排序,列表元素限定仅是数字或浮点数
|
||
|
|
||
|
VimL 的 `sort()` 是稳定排序算法,即如果两个元素相等(按 `{fun}` 返回 `0`),排
|
||
|
序后它们也保持原来的相对顺序。如果 `{fun}` 参数是含 `dict` 属性的函数,则要提
|
||
|
供第三参数 `{self}` ,一个作为 `self` 的字典变量。
|
||
|
|
||
|
`uniq()` 函数的参数用法与 `sort()` 相同。且一般应该对已排序的列表调用 `uniq()`
|
||
|
,因为它只比较相邻元素而去重。
|
||
|
|
||
|
### 小结
|
||
|
|
||
|
VimL 的标量主要就是字符串与数字,集合也就列表与字典。所以为这些数据类型提供了
|
||
|
大量的库函数 api。用 `:h type()` 查看支持的所有变量类型。但其他类型需要支持的
|
||
|
操作非常有限,故无必要有什么专门函数处理。
|