fix(path): normalize path slashes on Windows #37729

Problem:
On Windows, path separators may become inconsistent for various reasons,
which makes normalization quite painful.

Solution:
Normalize paths to `/` at the entry boundaries and always use it
internally, converting back only in rare cases where `\` is really
needed (e.g. cmd.exe/bat scripts?).

This is the first commit in a series of incremental steps.

Note:
* some funcs won't respect shellslash. e.g. `expand/fnamemodify`
* some funcs still respect shellslash, but will be updated in a follow
  PR. e.g. `ex_pwd/f_chdir/f_getcwd`
* uv's built-in funcs always return `\`. e.g. `uv.cwd/uv.exepath`

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
tao
2026-04-25 01:20:25 +08:00
committed by GitHub
parent 27191e0f4f
commit f130922744
41 changed files with 203 additions and 216 deletions

View File

@@ -6,7 +6,6 @@ local clear = n.clear
local fn = n.fn
local api = n.api
local command = n.command
local get_pathsep = n.get_pathsep
local rmdir = n.rmdir
local pcall_err = t.pcall_err
local mkdir = t.mkdir
@@ -81,8 +80,8 @@ describe('bufname() function', function()
it('returns expected buffer name', function()
eq('', fn.bufname('%')) -- Buffer has no name yet
command('file ' .. fname)
local wd = vim.uv.cwd()
local sep = get_pathsep()
local wd = assert(t.fix_slashes(assert(vim.uv.cwd())))
local sep = '/'
local curdirname = fn.fnamemodify(wd, ':t')
for _, arg in ipairs({ '%', 1, 'X', wd }) do
eq(fname, fn.bufname(arg))

View File

@@ -85,7 +85,7 @@ describe('backtick expansion', function()
eq({ 'file2' }, eval('argv()'))
if t.is_os('win') then
command(':silent args `dir /s/b *4`')
eq({ 'subdir\\file4' }, eval('map(argv(), \'fnamemodify(v:val, ":.")\')'))
eq({ 'subdir/file4' }, eval('map(argv(), \'fnamemodify(v:val, ":.")\')'))
else
command(':silent args `echo */*4`')
eq({ 'subdir/file4' }, eval('argv()'))

View File

@@ -19,13 +19,13 @@ describe('executable()', function()
end)
if is_os('win') then
it('exepath respects shellslash', function()
it('exepath returns consistent slashes #13787', function()
-- test/ cannot be a symlink in this test.
n.api.nvim_set_current_dir(t.paths.test_source_path)
command('let $PATH = fnamemodify("./test/functional/fixtures/bin", ":p")')
eq(
[[test\functional\fixtures\bin\null.CMD]],
[[test/functional/fixtures/bin/null.CMD]],
call('fnamemodify', call('exepath', 'null'), ':.')
)
command('set shellslash')
@@ -35,12 +35,12 @@ describe('executable()', function()
)
end)
it('stdpath respects shellslash', function()
it('stdpath returns consistent slashes #13787', function()
-- Needs to check paths relative to repo root dir.
n.api.nvim_set_current_dir(t.paths.test_source_path)
t.matches(
[[build\Xtest_xdg[%w_]*\share\nvim%-data]],
[[build/Xtest_xdg[%w_]*/share/nvim%-data]],
call('fnamemodify', call('stdpath', 'data'), ':.')
)
command('set shellslash')

View File

@@ -29,21 +29,20 @@ describe('fnamemodify()', function()
end)
it('handles the root path', function()
local root = n.pathroot()
local root = assert(t.fix_slashes(n.pathroot()))
eq(root, fnamemodify([[/]], ':p:h'))
eq(root, fnamemodify([[/]], ':p'))
if is_os('win') then
eq(root, fnamemodify([[\]], ':p:h'))
eq(root, fnamemodify([[\]], ':p'))
command('set shellslash')
root = string.sub(root, 1, -2) .. '/'
eq(root, fnamemodify([[\]], ':p:h'))
eq(root, fnamemodify([[\]], ':p'))
eq(root, fnamemodify([[/]], ':p:h'))
eq(root, fnamemodify([[/]], ':p'))
local letter_colon = root:sub(1, 2)
local old_dir = getcwd() .. '/'
local old_dir = t.fix_slashes(getcwd()) .. '/'
local foo_dir = old_dir .. 'foo/'
eq(old_dir, fnamemodify(letter_colon, ':p'))
eq(old_dir, fnamemodify(letter_colon .. '.', ':p'))