fix(fs): allow backslash characters in unix paths

Backslashes are valid characters in unix style paths.

Fix the conversion of backslashes to forward slashes in several `vim.fs`
functions when not on Windows. On Windows, backslashes will still be converted
to forward slashes.
This commit is contained in:
James Trew 2024-03-29 12:23:01 -04:00 committed by GitHub
parent 36acb2a8ec
commit 38e38d1b40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 65 additions and 49 deletions

View File

@ -2954,13 +2954,14 @@ vim.fs.joinpath({...}) *vim.fs.joinpath()*
vim.fs.normalize({path}, {opts}) *vim.fs.normalize()* vim.fs.normalize({path}, {opts}) *vim.fs.normalize()*
Normalize a path to a standard format. A tilde (~) character at the Normalize a path to a standard format. A tilde (~) character at the
beginning of the path is expanded to the user's home directory and any beginning of the path is expanded to the user's home directory and
backslash (\) characters are converted to forward slashes (/). Environment environment variables are also expanded.
variables are also expanded.
On Windows, backslash (\) characters are converted to forward slashes (/).
Examples: >lua Examples: >lua
vim.fs.normalize('C:\\\\Users\\\\jdoe') vim.fs.normalize('C:\\\\Users\\\\jdoe')
-- 'C:/Users/jdoe' -- On Windows: 'C:/Users/jdoe'
vim.fs.normalize('~/src/neovim') vim.fs.normalize('~/src/neovim')
-- '/home/jdoe/src/neovim' -- '/home/jdoe/src/neovim'

View File

@ -1,6 +1,7 @@
local M = {} local M = {}
local iswin = vim.uv.os_uname().sysname == 'Windows_NT' local iswin = vim.uv.os_uname().sysname == 'Windows_NT'
local os_sep = iswin and '\\' or '/'
--- Iterate over all the parents of the given path. --- Iterate over all the parents of the given path.
--- ---
@ -47,19 +48,23 @@ function M.dirname(file)
return nil return nil
end end
vim.validate({ file = { file, 's' } }) vim.validate({ file = { file, 's' } })
if iswin and file:match('^%w:[\\/]?$') then if iswin then
return (file:gsub('\\', '/')) file = file:gsub(os_sep, '/') --[[@as string]]
elseif not file:match('[\\/]') then if file:match('^%w:/?$') then
return file
end
end
if not file:match('/') then
return '.' return '.'
elseif file == '/' or file:match('^/[^/]+$') then elseif file == '/' or file:match('^/[^/]+$') then
return '/' return '/'
end end
---@type string ---@type string
local dir = file:match('[/\\]$') and file:sub(1, #file - 1) or file:match('^([/\\]?.+)[/\\]') local dir = file:match('/$') and file:sub(1, #file - 1) or file:match('^(/?.+)/')
if iswin and dir:match('^%w:$') then if iswin and dir:match('^%w:$') then
return dir .. '/' return dir .. '/'
end end
return (dir:gsub('\\', '/')) return dir
end end
--- Return the basename of the given path --- Return the basename of the given path
@ -72,10 +77,13 @@ function M.basename(file)
return nil return nil
end end
vim.validate({ file = { file, 's' } }) vim.validate({ file = { file, 's' } })
if iswin and file:match('^%w:[\\/]?$') then if iswin then
return '' file = file:gsub(os_sep, '/') --[[@as string]]
if file:match('^%w:/?$') then
return ''
end
end end
return file:match('[/\\]$') and '' or (file:match('[^\\/]*$'):gsub('\\', '/')) return file:match('/$') and '' or (file:match('[^/]*$'))
end end
--- Concatenate directories and/or file paths into a single path with normalization --- Concatenate directories and/or file paths into a single path with normalization
@ -334,15 +342,16 @@ end
--- @field expand_env boolean --- @field expand_env boolean
--- Normalize a path to a standard format. A tilde (~) character at the --- Normalize a path to a standard format. A tilde (~) character at the
--- beginning of the path is expanded to the user's home directory and any --- beginning of the path is expanded to the user's home directory and
--- backslash (\) characters are converted to forward slashes (/). Environment --- environment variables are also expanded.
--- variables are also expanded. ---
--- On Windows, backslash (\) characters are converted to forward slashes (/).
--- ---
--- Examples: --- Examples:
--- ---
--- ```lua --- ```lua
--- vim.fs.normalize('C:\\\\Users\\\\jdoe') --- vim.fs.normalize('C:\\\\Users\\\\jdoe')
--- -- 'C:/Users/jdoe' --- -- On Windows: 'C:/Users/jdoe'
--- ---
--- vim.fs.normalize('~/src/neovim') --- vim.fs.normalize('~/src/neovim')
--- -- '/home/jdoe/src/neovim' --- -- '/home/jdoe/src/neovim'
@ -364,7 +373,7 @@ function M.normalize(path, opts)
if path:sub(1, 1) == '~' then if path:sub(1, 1) == '~' then
local home = vim.uv.os_homedir() or '~' local home = vim.uv.os_homedir() or '~'
if home:sub(-1) == '\\' or home:sub(-1) == '/' then if home:sub(-1) == os_sep then
home = home:sub(1, -2) home = home:sub(1, -2)
end end
path = home .. path:sub(2) path = home .. path:sub(2)
@ -374,7 +383,7 @@ function M.normalize(path, opts)
path = path:gsub('%$([%w_]+)', vim.uv.os_getenv) path = path:gsub('%$([%w_]+)', vim.uv.os_getenv)
end end
path = path:gsub('\\', '/'):gsub('/+', '/') path = path:gsub(os_sep, '/'):gsub('/+', '/')
if iswin and path:match('^%w:/$') then if iswin and path:match('^%w:/$') then
return path return path
end end

View File

@ -36,6 +36,7 @@ local test_basename_dirname_eq = {
'c:/users/foo', 'c:/users/foo',
'c:/users/foo/bar.lua', 'c:/users/foo/bar.lua',
'c:/users/foo/bar/../', 'c:/users/foo/bar/../',
'~/foo/bar\\baz',
} }
local tests_windows_paths = { local tests_windows_paths = {
@ -70,26 +71,26 @@ describe('vim.fs', function()
it('works', function() it('works', function()
eq(test_build_dir, vim.fs.dirname(nvim_dir)) eq(test_build_dir, vim.fs.dirname(nvim_dir))
--- @param paths string[] ---@param paths string[]
local function test_paths(paths) ---@param is_win? boolean
local function test_paths(paths, is_win)
local gsub = is_win and [[:gsub('\\', '/')]] or ''
local code = string.format(
[[
local path = ...
return vim.fn.fnamemodify(path,':h')%s
]],
gsub
)
for _, path in ipairs(paths) do for _, path in ipairs(paths) do
eq( eq(exec_lua(code, path), vim.fs.dirname(path), path)
exec_lua(
[[
local path = ...
return vim.fn.fnamemodify(path,':h'):gsub('\\', '/')
]],
path
),
vim.fs.dirname(path),
path
)
end end
end end
test_paths(test_basename_dirname_eq) test_paths(test_basename_dirname_eq)
if is_os('win') then if is_os('win') then
test_paths(tests_windows_paths) test_paths(tests_windows_paths, true)
end end
end) end)
end) end)
@ -98,26 +99,26 @@ describe('vim.fs', function()
it('works', function() it('works', function()
eq(nvim_prog_basename, vim.fs.basename(nvim_prog)) eq(nvim_prog_basename, vim.fs.basename(nvim_prog))
--- @param paths string[] ---@param paths string[]
local function test_paths(paths) ---@param is_win? boolean
local function test_paths(paths, is_win)
local gsub = is_win and [[:gsub('\\', '/')]] or ''
local code = string.format(
[[
local path = ...
return vim.fn.fnamemodify(path,':t')%s
]],
gsub
)
for _, path in ipairs(paths) do for _, path in ipairs(paths) do
eq( eq(exec_lua(code, path), vim.fs.basename(path), path)
exec_lua(
[[
local path = ...
return vim.fn.fnamemodify(path,':t'):gsub('\\', '/')
]],
path
),
vim.fs.basename(path),
path
)
end end
end end
test_paths(test_basename_dirname_eq) test_paths(test_basename_dirname_eq)
if is_os('win') then if is_os('win') then
test_paths(tests_windows_paths) test_paths(tests_windows_paths, true)
end end
end) end)
end) end)
@ -284,9 +285,6 @@ describe('vim.fs', function()
end) end)
describe('normalize()', function() describe('normalize()', function()
it('works with backward slashes', function()
eq('C:/Users/jdoe', vim.fs.normalize('C:\\Users\\jdoe'))
end)
it('removes trailing /', function() it('removes trailing /', function()
eq('/home/user', vim.fs.normalize('/home/user/')) eq('/home/user', vim.fs.normalize('/home/user/'))
end) end)
@ -309,10 +307,18 @@ describe('vim.fs', function()
) )
) )
end) end)
if is_os('win') then if is_os('win') then
it('Last slash is not truncated from root drive', function() it('Last slash is not truncated from root drive', function()
eq('C:/', vim.fs.normalize('C:/')) eq('C:/', vim.fs.normalize('C:/'))
end) end)
it('converts backward slashes', function()
eq('C:/Users/jdoe', vim.fs.normalize('C:\\Users\\jdoe'))
end)
else
it('allows backslashes on unix-based os', function()
eq('/home/user/hello\\world', vim.fs.normalize('/home/user/hello\\world'))
end)
end end
end) end)
end) end)