5月27日 14:52

VimScript 脚本编程怎么写?从变量作用域到插件结构全讲清楚

为什么要学 VimScript

Vim 的真正威力不在于快捷键多,而在于你可以用脚本把编辑器改造成自己想要的样子。自动格式化、批量重命名、项目专属配置——这些全靠 VimScript 驱动。即使你现在用 Neovim + Lua,读懂已有插件的 VimScript 源码依然是刚需。

变量与作用域

VimScript 的变量用 let 声明,用 unlet 删除。关键在于作用域前缀——每个前缀决定了变量的可见范围和生命周期:

vim
let 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:countv:errmsg),只读居多,不要覆盖。
  • 还可以用 &option 访问选项值(如 &tabstop),@r 访问寄存器,$ENV 访问环境变量。
vim
echo &tabstop " 读取选项 let &tabstop = 4 " 设置选项 echo @a " 读取寄存器 a 的内容 echo $HOME " 读取环境变量

字符串操作

VimScript 的字符串有两种引号,行为不同:

vim
let s1 = "hello world" " 双引号:支持 \ 等转义 let s2 = 'hello world' " 单引号:原样输出, 就是两个字符

常用字符串函数:

vim
echo 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

拼接字符串推荐用 . 运算符:

vim
let 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

vim
if &filetype ==# "python" echo "Python file" elseif &filetype ==# "javascript" echo "JS file" else echo "Other file" endif

注意 ==# 是大小写敏感比较,==? 是忽略大小写。裸写 == 受用户 ignorecase 设置影响,不推荐。

for 循环

vim
for item in ["a", "b", "c"] echo item endfor for i in range(1, 10) echo i endfor

while 循环

vim
let i = 0 while i < 5 echo i let i += 1 endwhile

VimScript 没有 break/continue 的等价物(Vim9script 有了),传统做法是用条件变量控制循环。

函数定义

vim
function! s:Greet(name) echo "Hello, " . a:name return a:name endfunction

关键规则:

  • 函数名如果不加 s: 前缀,必须以大写字母开头。
  • function!! 表示如果函数已存在则覆盖,插件开发必加。
  • 函数参数用 a: 前缀访问,如 a:namea:1(可变参数)。
  • 函数默认返回 0,除非显式 return
  • abort 关键字让函数在出错时立即中止,而不是继续执行。
vim
function! s:Min(num1, num2) abort return a:num1 < a:num2 ? a:num1 : a:num2 endfunction

可变参数用 ...

vim
function! s:Varargs(name, ...) echo a:name echo a:0 " 可变参数个数 echo a:1 " 第一个可变参数 echo a:000 " 可变参数列表 endfunction

autocmd 编程

autocmd 是 Vim 事件驱动的核心——在特定事件发生时自动执行命令:

vim
augroup 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 体尽量短,复杂逻辑抽成函数调用。
vim
function! s:AutoFormatPython() " 复杂格式化逻辑 silent! %!autopep8 - endfunction

自定义命令:command 与

:command 定义用户命令,比函数调用方便得多:

vim
command! -nargs=1 -complete=file MyEdit :edit <args>

<f-args> 将命令参数拆分为函数参数列表,是最常用的参数传递方式:

vim
command! -nargs=* Grep call s:Grep(<f-args>) function! s:Grep(...) abort let pattern = join(a:000, ' ') silent! grep! pattern cwindow endfunction

<range> 让命令支持行范围:

vim
command! -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 插件目录结构:

shell
my-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.viminit.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 结构,先让东西跑起来,再重构不迟。

标签:Vim