Compare commits

...

2 Commits

Author SHA1 Message Date
James Trew
0af91129ed refactor(fs.normalize): ~ expansion 2024-09-09 21:04:59 -04:00
James Trew
6948948fa2 fix(fs.normalize): improve environment variable expansion
Adds Windows support.
2024-09-09 21:04:59 -04:00
4 changed files with 204 additions and 38 deletions

View File

@ -23,7 +23,7 @@ env:
NVIM_LOG_FILE: ${{ github.workspace }}/build/.nvimlog
TSAN_OPTIONS: log_path=${{ github.workspace }}/build/log/tsan
VALGRIND_LOG: ${{ github.workspace }}/build/log/valgrind-%p.log
# TEST_FILE: test/functional/core/startup_spec.lua
TEST_FILE: test/functional/lua/fs_spec.lua
# TEST_FILTER: foo
jobs:

View File

@ -2962,17 +2962,17 @@ vim.fs.normalize({path}, {opts}) *vim.fs.normalize()*
On Windows, backslash (\) characters are converted to forward slashes (/).
Examples: >lua
[[C:\Users\jdoe]] => "C:/Users/jdoe"
"~/src/neovim" => "/home/jdoe/src/neovim"
"$XDG_CONFIG_HOME/nvim/init.vim" => "/Users/jdoe/.config/nvim/init.vim"
"~/src/nvim/api/../tui/./tui.c" => "/home/jdoe/src/nvim/tui/tui.c"
"./foo/bar" => "foo/bar"
"foo/../../../bar" => "../../bar"
"/home/jdoe/../../../bar" => "/bar"
"C:foo/../../baz" => "C:../baz"
"C:/foo/../../baz" => "C:/baz"
[[\\?\UNC\server\share\foo\..\..\..\bar]] => "//?/UNC/server/share/bar"
Examples: >
C:\Users\jdoe => C:/Users/jdoe
~/src/neovim => /home/jdoe/src/neovim
$XDG_CONFIG_HOME/nvim/init.vim => /Users/jdoe/.config/nvim/init.vim
~/src/nvim/api/../tui/./tui.c => /home/jdoe/src/nvim/tui/tui.c
./foo/bar => foo/bar
foo/../../../bar => ../../bar
/home/jdoe/../../../bar => /bar
C:foo/../../baz => C:../baz
C:/foo/../../baz => C:/baz
\\?\UNC\server\share\foo\..\..\..\bar => //?/UNC/server/share/bar
<
Parameters: ~

View File

@ -486,10 +486,139 @@ local function path_resolve_dot(path)
return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
end
---@param path string
---@return string
local function windows_env_expand(path)
local res = {}
local i = 1
---@param var string
---@param end_ number
local function add_expand(var, end_)
local val = vim.uv.os_getenv(var)
if val then
table.insert(res, (val:gsub('\\', '/')))
else
table.insert(res, path:sub(i, end_))
end
end
while i <= #path do
local ch = path:sub(i, i)
if ch == "'" then -- no expansion inside single quotes
local end_ = path:find("'", i + 1, true)
if end_ then
table.insert(res, path:sub(i, end_))
i = end_
else
table.insert(res, ch)
end
elseif ch == '%' then
local end_ = path:find('%', i + 1, true)
if end_ then
local var = path:sub(i + 1, end_ - 1)
add_expand(var, end_)
i = end_
else
table.insert(res, ch)
end
elseif ch == '$' then
local nextch = path:sub(i + 1, i + 1)
if nextch == '$' then
i = i + 1
table.insert(res, ch)
elseif nextch == '{' then
local end_ = path:find('}', i + 2, true)
if end_ then
local var = path:sub(i + 2, end_ - 1)
add_expand(var, end_)
i = end_
else
table.insert(res, ch)
end
else
local end_ = path:find('[^%w_]', i + 1, false) or #path + 1
local var = path:sub(i + 1, end_ - 1)
add_expand(var, end_ - 1)
i = end_ - 1
end
else
table.insert(res, ch)
end
i = i + 1
end
return table.concat(res)
end
---@param path string
---@return string
local function posix_env_expand(path)
local res = {}
local i = 1
local function add_expand(var, end_)
local val = vim.uv.os_getenv(var)
if val then
table.insert(res, val)
else
table.insert(res, path:sub(i, end_))
end
end
while i <= #path do
local ch = path:sub(i, i)
if ch == '$' then
if path:sub(i + 1, i + 1) == '{' then
local end_ = path:find('}', i + 2, true)
if end_ then
local var = path:sub(i + 2, end_ - 1)
add_expand(var, end_)
i = end_
else
table.insert(res, ch)
end
else
local end_ = path:find('[^%w_]', i + 1, false) or #path + 1
local var = path:sub(i + 1, end_ - 1)
add_expand(var, end_ - 1)
i = end_ - 1
end
else
table.insert(res, ch)
end
i = i + 1
end
return table.concat(res)
end
--- expand ~/ in a path
--- does not handle ~user/ constructs and instead returns the path as-is
---@param path string
---@return string
local function expand_home(path)
if not vim.startswith(path, '~') then
return path
end
local home = vim.uv.os_homedir():gsub('\\', '/') or '~'
home = home:gsub('/$', '')
if path:sub(2, 2) == '/' then
return home .. path:sub(2)
end
return path
end
--- @class vim.fs.normalize.Opts
--- @inlinedoc
---
--- Expand environment variables.
--- Expand environment variables. Substrings in the form of `$VAR` or `${VAR}` are replaced with the
--- value of the environment variable `VAR`. If the environment variable is not set, the substring
--- is left unchanged.
--- On Windows, substrings in the form of `%VAR%` are also replaced unless they are inside single
--- quotes (eg. `'%VAR%'`).
--- (default: `true`)
--- @field expand_env? boolean
---
@ -515,17 +644,17 @@ end
--- On Windows, backslash (\) characters are converted to forward slashes (/).
---
--- Examples:
--- ```lua
--- [[C:\Users\jdoe]] => "C:/Users/jdoe"
--- "~/src/neovim" => "/home/jdoe/src/neovim"
--- "$XDG_CONFIG_HOME/nvim/init.vim" => "/Users/jdoe/.config/nvim/init.vim"
--- "~/src/nvim/api/../tui/./tui.c" => "/home/jdoe/src/nvim/tui/tui.c"
--- "./foo/bar" => "foo/bar"
--- "foo/../../../bar" => "../../bar"
--- "/home/jdoe/../../../bar" => "/bar"
--- "C:foo/../../baz" => "C:../baz"
--- "C:/foo/../../baz" => "C:/baz"
--- [[\\?\UNC\server\share\foo\..\..\..\bar]] => "//?/UNC/server/share/bar"
--- ```
--- C:\Users\jdoe => C:/Users/jdoe
--- ~/src/neovim => /home/jdoe/src/neovim
--- $XDG_CONFIG_HOME/nvim/init.vim => /Users/jdoe/.config/nvim/init.vim
--- ~/src/nvim/api/../tui/./tui.c => /home/jdoe/src/nvim/tui/tui.c
--- ./foo/bar => foo/bar
--- foo/../../../bar => ../../bar
--- /home/jdoe/../../../bar => /bar
--- C:foo/../../baz => C:../baz
--- C:/foo/../../baz => C:/baz
--- \\?\UNC\server\share\foo\..\..\..\bar => //?/UNC/server/share/bar
--- ```
---
---@param path (string) Path to normalize
@ -550,25 +679,19 @@ function M.normalize(path, opts)
return ''
end
-- Expand ~ to users home directory
if vim.startswith(path, '~') then
local home = vim.uv.os_homedir() or '~'
if home:sub(-1) == os_sep_local then
home = home:sub(1, -2)
end
path = home .. path:sub(2)
end
-- Expand environment variables if `opts.expand_env` isn't `false`
if opts.expand_env == nil or opts.expand_env then
path = path:gsub('%$([%w_]+)', vim.uv.os_getenv)
end
if win then
-- Convert path separator to `/`
path = path:gsub(os_sep_local, '/')
end
path = expand_home(path)
-- Expand environment variables if `opts.expand_env` isn't `false`
if opts.expand_env == nil or opts.expand_env then
local expand = win and windows_env_expand or posix_env_expand
path = expand(path)
end
-- Check for double slashes at the start of the path because they have special meaning
local double_slash = false
if not opts._fast then

View File

@ -332,6 +332,7 @@ describe('vim.fs', function()
end)
it('works with ~', function()
eq(vim.fs.normalize(assert(vim.uv.os_homedir())) .. '/src/foo', vim.fs.normalize('~/src/foo'))
eq(vim.fs.normalize('~user/src/foo'), vim.fs.normalize('~user/src/foo'))
end)
it('works with environment variables', function()
local xdg_config_home = test_build_dir .. '/.config'
@ -451,5 +452,47 @@ describe('vim.fs', function()
eq('/foo', vim.fs.normalize('/foo/../../foo', posix_opts))
end)
end)
describe('expands env vars', function()
before_each(function()
vim.uv.os_setenv('FOOVAR', 'foo')
vim.uv.os_setenv('{foovar', 'foo1')
vim.uv.os_setenv('{foovar}', 'foo\\2')
end)
after_each(function()
vim.uv.os_unsetenv('FOOVAR')
vim.uv.os_unsetenv('{foovar')
vim.uv.os_unsetenv('{foovar}')
end)
it('', function()
eq('foo', vim.fs.normalize('$FOOVAR'))
eq('foo/foo', vim.fs.normalize('$FOOVAR/$FOOVAR'))
eq('foo foo', vim.fs.normalize('$FOOVAR $FOOVAR'))
eq('foo$', vim.fs.normalize('$FOOVAR$'))
eq('foo', vim.fs.normalize('${FOOVAR}'))
eq('foo$', vim.fs.normalize('${FOOVAR}$'))
eq('foo/foo', vim.fs.normalize('${FOOVAR}/${FOOVAR}'))
eq('foo foo', vim.fs.normalize('${FOOVAR} ${FOOVAR}'))
eq('${FOO$VAR}', vim.fs.normalize('${FOO$VAR}'))
eq('foo/$/bar$', vim.fs.normalize('foo/$/bar$'))
eq('foo/${}/bar${}', vim.fs.normalize('foo/${}/bar${}'))
eq('$UNSET_ENV_VAR/baz', vim.fs.normalize('$UNSET_ENV_VAR/baz'))
if is_os('win') then
eq('foo', vim.fs.normalize('%FOOVAR%'))
eq('foo', vim.fs.normalize('%foovar%'))
eq('foo1', vim.fs.normalize('%{foovar%'))
eq('foo1', vim.fs.normalize('${{foovar}'))
eq('foo1}', vim.fs.normalize('${{foovar}}'))
eq("'%FOOVAR%'", vim.fs.normalize("'%FOOVAR%'"))
eq("'$FOOVAR'", vim.fs.normalize("'$FOOVAR'"))
eq("'${FOOVAR}'", vim.fs.normalize("'${FOOVAR}'"))
eq('foo/2', vim.fs.normalize('%{foovar}%'))
eq('foo/%%/bar%%', vim.fs.normalize('foo/%%/bar%%'))
end
end)
end)
end)
end)