VimScript 脚本编程怎么写?从变量作用域到插件结构全讲清楚
为什么要学 VimScript
Vim 的真正威力不在于快捷键多,而在于你可以用脚本把编辑器改造成自己想要的样子。自动格式化、批量重命名、项目专属配置——这些全靠 VimScript 驱动。即使你现在用 Neovim + Lua,读懂已有插件的 VimScript 源码依然是刚需。
变量与作用域
VimScript 的变量用 let 声明,用 unlet 删除。关键在于作用域前缀——每个前缀决定了变量的可见范围和生命周期:
vimlet g:global_var = 1 " 全局变量,任何地方都能访问 let s:script_var = 2 " 脚本局部变量,只在当前 .vim 文件内可见 let l:local_var = 3 " 函数局部变量,只在当前函数内可见(函数内默认) let b:buffer_var = 4 " 缓冲区局部变量,绑定到当前文件缓冲区 let w:window_var = 5 " 窗口局部变量,绑定到当前窗口 let t:tab_var = 6 " 标签页局部变量,绑定到当前标签页 let a:arg_var = 7 " 函数参数,只在函数体内可用
几个容易踩的坑:
- 函数体内不加前缀的变量默认是
l:,不是全局的。 - 脚本级的
s:变量在文件多次 source 时不会重置,生命周期跟 Vim 进程一致。 v:是 Vim 内置变量(如v:count、v:errmsg),只读居多,不要覆盖。- 还可以用
&option访问选项值(如&tabstop),@r访问寄存器,$ENV访问环境变量。
vimecho &tabstop " 读取选项 let &tabstop = 4 " 设置选项 echo @a " 读取寄存器 a 的内容 echo $HOME " 读取环境变量
字符串操作
VimScript 的字符串有两种引号,行为不同:
vimlet s1 = "hello world" " 双引号:支持 \ 等转义 let s2 = 'hello world' " 单引号:原样输出, 就是两个字符
常用字符串函数:
vimecho strlen("hello") " => 5 echo strpart("hello", 1, 3) " => ell(从索引1取3个字符) echo substitute("hello", "l", "L", "g") " => heLLo echo tolower("Hello") " => hello echo toupper("hello") " => HELLO echo stridx("hello", "ll") " => 2(查找子串位置) echo split("a,b,c", ",") " => ['a', 'b', 'c'] echo join(['a', 'b'], "-") " => a-b
拼接字符串推荐用 . 运算符:
vimlet msg = "file: " . expand("%") . " line: " . line(".")
列表与字典
列表(List)就是数组,字典(Dict)就是哈希表:
vim" 列表 let fruits = ["apple", "banana", "cherry"] echo fruits[0] " => apple echo fruits[-1] " => cherry(负索引从末尾取) call add(fruits, "date") " 追加元素 echo len(fruits) " => 4 " 列表推导 let squares = map(range(5), 'v:val * v:val') echo squares " => [0, 1, 4, 9, 16] " 字典 let user = {"name": "vim", "version": 9} echo user.name " => vim echo user["version"] " => 9 let user.lang = "VimScript" " 添加键 call remove(user, "lang") " 删除键 echo keys(user) " => ['name', 'version'] echo values(user) " => ['vim', 9]
控制流
if / else / endif
vimif &filetype ==# "python" echo "Python file" elseif &filetype ==# "javascript" echo "JS file" else echo "Other file" endif
注意 ==# 是大小写敏感比较,==? 是忽略大小写。裸写 == 受用户 ignorecase 设置影响,不推荐。
for 循环
vimfor item in ["a", "b", "c"] echo item endfor for i in range(1, 10) echo i endfor
while 循环
vimlet i = 0 while i < 5 echo i let i += 1 endwhile
VimScript 没有 break/continue 的等价物(Vim9script 有了),传统做法是用条件变量控制循环。
函数定义
vimfunction! s:Greet(name) echo "Hello, " . a:name return a:name endfunction
关键规则:
- 函数名如果不加
s:前缀,必须以大写字母开头。 function!加!表示如果函数已存在则覆盖,插件开发必加。- 函数参数用
a:前缀访问,如a:name、a:1(可变参数)。 - 函数默认返回 0,除非显式
return。 abort关键字让函数在出错时立即中止,而不是继续执行。
vimfunction! s:Min(num1, num2) abort return a:num1 < a:num2 ? a:num1 : a:num2 endfunction
可变参数用 ...:
vimfunction! s:Varargs(name, ...) echo a:name echo a:0 " 可变参数个数 echo a:1 " 第一个可变参数 echo a:000 " 可变参数列表 endfunction
autocmd 编程
autocmd 是 Vim 事件驱动的核心——在特定事件发生时自动执行命令:
vimaugroup AutoFormat autocmd! autocmd BufWritePre *.py call s:AutoFormatPython() autocmd BufWritePre *.js call s:AutoFormatJS() augroup END
要点:
- 必须用 augroup 包裹,否则每次 source 文件都会追加重复的 autocmd。
autocmd!先清空同组旧命令。 - 常用事件:
BufWritePre(保存前)、BufRead(打开文件)、FileType(设置文件类型)、InsertEnter(进入插入模式)。 - autocmd 体尽量短,复杂逻辑抽成函数调用。
vimfunction! s:AutoFormatPython() " 复杂格式化逻辑 silent! %!autopep8 - endfunction
自定义命令:command 与 、
用 :command 定义用户命令,比函数调用方便得多:
vimcommand! -nargs=1 -complete=file MyEdit :edit <args>
<f-args> 将命令参数拆分为函数参数列表,是最常用的参数传递方式:
vimcommand! -nargs=* Grep call s:Grep(<f-args>) function! s:Grep(...) abort let pattern = join(a:000, ' ') silent! grep! pattern cwindow endfunction
<range> 让命令支持行范围:
vimcommand! -range Align call s:Align(<line1>, <line2>) function! s:Align(line1, line2) abort execute a:line1 . ',' . a:line2 . '!column -t' endfunction
常用 command 参数:
| 参数 | 含义 |
|---|---|
-nargs=0 | 无参数(默认) |
-nargs=1 | 恰好一个参数 |
-nargs=* | 零或多个参数 |
-nargs=? | 零或一个参数 |
-nargs=+ | 至少一个参数 |
-range | 允许行范围 |
-bang | 允许 ! 修饰 |
-complete=file | 文件名补全 |
-bar | 允许 |
插件结构:plugin 与 autoload
一个规范的 Vim 插件目录结构:
shellmy-plugin/ ├── plugin/ │ └── my-plugin.vim " 启动时加载,定义命令和映射 ├── autoload/ │ └── my-plugin.vim " 按需加载,放函数实现 ├── doc/ │ └── my-plugin.txt " 帮助文档 └── after/ └── ftplugin/ └── python.vim " 文件类型专用配置
plugin/ 在 Vim 启动时执行,只放命令定义和映射,不放重逻辑。
autoload/ 是懒加载机制。文件 autoload/my-plugin.vim 里的函数必须命名为 my-plugin#FuncName,只有第一次调用时才会加载:
vim" plugin/my-plugin.vim(启动时执行) command! MyCommand call my-plugin#Run() " autoload/my-plugin.vim(首次调用 MyCommand 时才加载) function! my-plugin#Run() echo "Hello from autoload" endfunction
这种分离让插件启动时零开销,用到才加载。
VimScript vs Lua(Neovim)
如果你用 Neovim,可能已经在纠结该学哪个。核心区别:
性能:Lua(LuaJIT)在循环和密集计算上比 VimScript 快约 56 倍。Neovim 用 Lua 加载 30 个插件只需 94ms,VimScript 配置动辄数百毫秒。
语言设计:VimScript 有 30 年历史包袱——隐式类型转换令人困惑、作用域规则不一致、缺少标准数据结构。Lua 是正经的编程语言,有模块、闭包、协程和成熟的生态。
生态趋势:2022 年之后,主流 Neovim 插件(telescope、lazy.nvim、nvim-cmp、nvim-treesitter)全部用 Lua 编写,不再接受 VimScript 贡献。
兼容性:Neovim 仍然支持 VimScript,init.vim 和 init.lua 可以共存。用 vim.cmd() 在 Lua 中执行 VimScript,用 luaeval() 在 VimScript 中调用 Lua。
选择建议:
- 用 Vim → 只能写 VimScript(或 Vim9script)。
- 用 Neovim + 新插件 → 学 Lua,用
init.lua。 - 需要维护老插件 → 必须读懂 VimScript。
从哪里开始实践
打开 Vim,输入 :help usr_41 阅读 Vim 官方脚本教程。把你的 .vimrc 里重复的配置抽成函数,给常用操作绑定 command!,再用 augroup 挂上自动命令——这就是你第一个"插件"了。不需要一步到位写 autoload 结构,先让东西跑起来,再重构不迟。