Eric's Blog 时光荏苒,岁月如梭

文件路径大小写敏感导致 Lua 模块重载

2025-12-28
Eric Wong

事情起因

时隔十年,再次被 Windows 系统的路劲大小写问题坑了一把。记得上一次被坑是因为写 Vim Script 的 autoload 脚本时出现的问题。 最近使用 Lua 重新写了 ChineseLinter.vim 这个插件,最开始的文件结构:

文件:plugins/chineselinter.lua

return {
  'wsdjeg/ChineseLinter.nvim',
  dev = true,
  opts = {
    ignored_errors = { 'E015', 'E013', 'E020', 'E021' },
  },
  cmds = { 'CheckChinese' },
  desc = 'Chinese Document Language Standards Checking Tool',
}

按照以上配置,无论如何 ignored_errors 配置都无法起效。

寻找原因

上述插件在载入时没有报错,说明被成功载入并且执行了 setup 函数。我试着用单独的脚本来测试,并且打入一些日志:

vim.opt.runtimepath:append("D:/wsdjeg/job.nvim")
vim.opt.runtimepath:append("D:/wsdjeg/logger.nvim")
vim.opt.runtimepath:append("D:/wsdjeg/nvim-plug")

require('plug').setup({

    bundle_dir = 'D:/bundle_dir',
    raw_plugin_dir = 'D:/bundle_dir/raw_plugin',
    -- ui = 'notify',
    http_proxy = 'http://127.0.0.1:7890',
    https_proxy = 'http://127.0.0.1:7890',
    enable_priority = false,
    enable_luarocks = true,
    max_processes = 16,
    dev_path = 'D:/wsdjeg',
})

require("plug").add({
	{
		"wsdjeg/ChineseLinter.nvim",
		dev = true,
		opts = {
			ignored_errors = { "E015", "E013", "E020", "E021" },
		},
		cmds = { "CheckChinese" },
		desc = "Chinese Document Language Standards Checking Tool",
	},
})

日志结果如下:

[ 23:35:32:449 ] [ Info  ] [ cnlint ] module is loaded
[ 23:35:32:450 ] [ Info  ] [ cnlint ] setup function is called
[ 23:35:32:450 ] [ Info  ] [   plug ] load plug: ChineseLinter.nvim in 4.3624ms
[ 23:35:32:451 ] [ Info  ] [ cnlint ] module is loaded
[ 23:35:32:451 ] [ Info  ] [ cnlint ] check function is called

不难看出 ChineseLinter 模块被载入了两次,第一次载入及setup函数是 nvim-plug 在执行,执行后计算的载入时间,第二次是执行 CheckChinese 命令时, 而这一命令是在 plugin/ChineseLinter.lua 内定义的:

vim.api.nvim_create_user_command("CheckChinese", function(opt)
	require("ChineseLinter").check()
end, { nargs = "*" })

问题就在这里,这个命令内 require('ChineseLinter') 不应该再次载入模块文件,因为前面 nvim-plug 已经执行过一次了,正常情况下 package.loaded 内会缓存模块。

看一下 nvim-plug 载入 Lua 插件的逻辑,它会给 plugSpec 自动设置一个模块名称, 以便于自动执行 require(plugSpec.module).setup(plugSpec.opts)

问题就在于这个 module 名称生成函数原先是:

local function get_default_module(name)
  return name
    :lower()
    :gsub('[%.%-]lua$', '')
    :gsub('^n?vim-', '')
    :gsub('[%.%-]n?vim', '')
end

也就是说,按照上述载入插件方式,nvim-plug 执行的是 require('chineselinter'),这在 Windows 系统下, 因为文件 lua/ChineseLinter/init.lua 已存在,那么上述 require 函数就会读取这个模块。

:CheckChinese 命令实际上调用的模块是 require('ChineseLinter')。 因为 Lua 的模块名称实际上是大小写敏感的,就会再次去寻找模块文件以载入。

如何修复?

我查阅了几个插件管理器,他们的获取模块名称的函数基本上逻辑类似,都使用了 lower() 函数:

---@param name string
---@return string
function M.normname(name)
  local ret = name:lower():gsub("^n?vim%-", ""):gsub("%.n?vim$", ""):gsub("[%.%-]lua", ""):gsub("[^a-z]+", "")
  return ret
end

实际上,最好是不要自动去将模块的名字全部小写,按照仓库的名称来最合适,去除掉前后缀,修改 nvim-plug 如下:

diff --git a/lua/plug/loader.lua b/lua/plug/loader.lua
index d0fc7b6..957fcb7 100644
--- a/lua/plug/loader.lua
+++ b/lua/plug/loader.lua
@@ -68,8 +68,7 @@ end
 --- @param name string
 --- @return string
 local function get_default_module(name)
-    return name:lower()
-        :gsub('[%.%-]lua$', '')
+    return name:gsub('[%.%-]lua$', '')
         :gsub('^n?vim-', '')
         :gsub('[%.%-]n?vim', '')
 end
@@ -94,6 +93,13 @@ function M.parser(plugSpec)
     plugSpec.name = check_name(plugSpec)
     if not plugSpec.module then
         plugSpec.module = get_default_module(plugSpec.name)
+        log.info(
+            string.format(
+                'set %s default module name to %s',
+                plugSpec.name,
+                plugSpec.module
+            )
+        )
     end
     if #plugSpec.name == 0 then
         plugSpec.enabled = false

考虑到 Windows 系统的大小写敏感,以及 Shift 键这么难按,我将插件的名称以及其内模块的名称都改成了小写,修改后插件的安装方式:

return {
  'wsdjeg/chineselinter.nvim',
  dev = true,
  opts = {
    ignored_errors = { 'E015', 'E013', 'E020', 'E021' },
  },
  cmds = { 'CheckChinese' },
  desc = 'Chinese Document Language Standards Checking Tool',
}

总结

上述核心问题在于 Lua 的 require() 函数读取模块缓存时判断的是 package.load[key],这里的 key 是大小写敏感的。 而发现缓存不存在时,依照 key 去载入文件时,在 Windows 系统下路劲又是不敏感的, 会导致同一个模块被不同的大小写模块名称多次载入。


版权声明:本文为原创文章,遵循 署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)版权协议,转载请附上原文出处链接和本声明。


延生阅读

分享到:

评论