mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00
feat(vim.fs): find(), dir() can "follow" symlinks #31551
Problem: vim.fs.dir(), vim.fs.find() do not follow symlinks. Solution: - Add "follow" flag. - Enable it by default.
This commit is contained in:
@@ -2983,6 +2983,7 @@ vim.fs.dir({path}, {opts}) *vim.fs.dir()*
|
|||||||
• skip: (fun(dir_name: string): boolean)|nil Predicate to
|
• skip: (fun(dir_name: string): boolean)|nil Predicate to
|
||||||
control traversal. Return false to stop searching the
|
control traversal. Return false to stop searching the
|
||||||
current directory. Only useful when depth > 1
|
current directory. Only useful when depth > 1
|
||||||
|
• follow: boolean|nil Follow symbolic links. (default: true)
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`Iterator`) over items in {path}. Each iteration yields two values:
|
(`Iterator`) over items in {path}. Each iteration yields two values:
|
||||||
@@ -3024,7 +3025,7 @@ vim.fs.find({names}, {opts}) *vim.fs.find()*
|
|||||||
|
|
||||||
-- get all files ending with .cpp or .hpp inside lib/
|
-- get all files ending with .cpp or .hpp inside lib/
|
||||||
local cpp_hpp = vim.fs.find(function(name, path)
|
local cpp_hpp = vim.fs.find(function(name, path)
|
||||||
return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$')
|
return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$')
|
||||||
end, {limit = math.huge, type = 'file'})
|
end, {limit = math.huge, type = 'file'})
|
||||||
<
|
<
|
||||||
|
|
||||||
@@ -3038,8 +3039,10 @@ vim.fs.find({names}, {opts}) *vim.fs.find()*
|
|||||||
If {names} is a function, it is called for each traversed
|
If {names} is a function, it is called for each traversed
|
||||||
item with args:
|
item with args:
|
||||||
• name: base name of the current item
|
• name: base name of the current item
|
||||||
• path: full path of the current item The function should
|
• path: full path of the current item
|
||||||
return `true` if the given item is considered a match.
|
|
||||||
|
The function should return `true` if the given item is
|
||||||
|
considered a match.
|
||||||
• {opts} (`table`) Optional keyword arguments:
|
• {opts} (`table`) Optional keyword arguments:
|
||||||
• {path}? (`string`) Path to begin searching from. If
|
• {path}? (`string`) Path to begin searching from. If
|
||||||
omitted, the |current-directory| is used.
|
omitted, the |current-directory| is used.
|
||||||
@@ -3053,6 +3056,8 @@ vim.fs.find({names}, {opts}) *vim.fs.find()*
|
|||||||
• {limit}? (`number`, default: `1`) Stop the search after
|
• {limit}? (`number`, default: `1`) Stop the search after
|
||||||
finding this many matches. Use `math.huge` to place no
|
finding this many matches. Use `math.huge` to place no
|
||||||
limit on the number of matches.
|
limit on the number of matches.
|
||||||
|
• {follow}? (`boolean`, default: `true`) Follow symbolic
|
||||||
|
links.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`string[]`) Normalized paths |vim.fs.normalize()| of all matching
|
(`string[]`) Normalized paths |vim.fs.normalize()| of all matching
|
||||||
|
@@ -286,6 +286,8 @@ LUA
|
|||||||
• |vim.json.encode()| has an option to enable forward slash escaping
|
• |vim.json.encode()| has an option to enable forward slash escaping
|
||||||
• |vim.fs.abspath()| converts paths to absolute paths.
|
• |vim.fs.abspath()| converts paths to absolute paths.
|
||||||
• |vim.fs.relpath()| gets relative path compared to base path.
|
• |vim.fs.relpath()| gets relative path compared to base path.
|
||||||
|
• |vim.fs.dir()| and |vim.fs.find()| now follow symbolic links by default,
|
||||||
|
the behavior can be turn off using the new `follow` option.
|
||||||
|
|
||||||
OPTIONS
|
OPTIONS
|
||||||
|
|
||||||
|
@@ -136,6 +136,7 @@ end
|
|||||||
--- - skip: (fun(dir_name: string): boolean)|nil Predicate
|
--- - skip: (fun(dir_name: string): boolean)|nil Predicate
|
||||||
--- to control traversal. Return false to stop searching the current directory.
|
--- to control traversal. Return false to stop searching the current directory.
|
||||||
--- Only useful when depth > 1
|
--- Only useful when depth > 1
|
||||||
|
--- - follow: boolean|nil Follow symbolic links. (default: true)
|
||||||
---
|
---
|
||||||
---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
|
---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
|
||||||
--- "name" is the basename of the item relative to {path}.
|
--- "name" is the basename of the item relative to {path}.
|
||||||
@@ -147,6 +148,7 @@ function M.dir(path, opts)
|
|||||||
vim.validate('path', path, 'string')
|
vim.validate('path', path, 'string')
|
||||||
vim.validate('depth', opts.depth, 'number', true)
|
vim.validate('depth', opts.depth, 'number', true)
|
||||||
vim.validate('skip', opts.skip, 'function', true)
|
vim.validate('skip', opts.skip, 'function', true)
|
||||||
|
vim.validate('follow', opts.follow, 'boolean', true)
|
||||||
|
|
||||||
path = M.normalize(path)
|
path = M.normalize(path)
|
||||||
if not opts.depth or opts.depth == 1 then
|
if not opts.depth or opts.depth == 1 then
|
||||||
@@ -177,7 +179,9 @@ function M.dir(path, opts)
|
|||||||
if
|
if
|
||||||
opts.depth
|
opts.depth
|
||||||
and level < opts.depth
|
and level < opts.depth
|
||||||
and t == 'directory'
|
and (t == 'directory' or (t == 'link' and opts.follow ~= false and (vim.uv.fs_stat(
|
||||||
|
M.joinpath(path, f)
|
||||||
|
) or {}).type == 'directory'))
|
||||||
and (not opts.skip or opts.skip(f) ~= false)
|
and (not opts.skip or opts.skip(f) ~= false)
|
||||||
then
|
then
|
||||||
dirs[#dirs + 1] = { f, level + 1 }
|
dirs[#dirs + 1] = { f, level + 1 }
|
||||||
@@ -211,6 +215,10 @@ end
|
|||||||
--- Use `math.huge` to place no limit on the number of matches.
|
--- Use `math.huge` to place no limit on the number of matches.
|
||||||
--- (default: `1`)
|
--- (default: `1`)
|
||||||
--- @field limit? number
|
--- @field limit? number
|
||||||
|
---
|
||||||
|
--- Follow symbolic links.
|
||||||
|
--- (default: `true`)
|
||||||
|
--- @field follow? boolean
|
||||||
|
|
||||||
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
|
--- Find files or directories (or other items as specified by `opts.type`) in the given path.
|
||||||
---
|
---
|
||||||
@@ -234,7 +242,7 @@ end
|
|||||||
---
|
---
|
||||||
--- -- get all files ending with .cpp or .hpp inside lib/
|
--- -- get all files ending with .cpp or .hpp inside lib/
|
||||||
--- local cpp_hpp = vim.fs.find(function(name, path)
|
--- local cpp_hpp = vim.fs.find(function(name, path)
|
||||||
--- return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$')
|
--- return name:match('.*%.[ch]pp$') and path:match('[/\\]lib$')
|
||||||
--- end, {limit = math.huge, type = 'file'})
|
--- end, {limit = math.huge, type = 'file'})
|
||||||
--- ```
|
--- ```
|
||||||
---
|
---
|
||||||
@@ -244,6 +252,7 @@ end
|
|||||||
--- If {names} is a function, it is called for each traversed item with args:
|
--- If {names} is a function, it is called for each traversed item with args:
|
||||||
--- - name: base name of the current item
|
--- - name: base name of the current item
|
||||||
--- - path: full path of the current item
|
--- - path: full path of the current item
|
||||||
|
---
|
||||||
--- The function should return `true` if the given item is considered a match.
|
--- The function should return `true` if the given item is considered a match.
|
||||||
---
|
---
|
||||||
---@param opts vim.fs.find.Opts Optional keyword arguments:
|
---@param opts vim.fs.find.Opts Optional keyword arguments:
|
||||||
@@ -256,6 +265,7 @@ function M.find(names, opts)
|
|||||||
vim.validate('stop', opts.stop, 'string', true)
|
vim.validate('stop', opts.stop, 'string', true)
|
||||||
vim.validate('type', opts.type, 'string', true)
|
vim.validate('type', opts.type, 'string', true)
|
||||||
vim.validate('limit', opts.limit, 'number', true)
|
vim.validate('limit', opts.limit, 'number', true)
|
||||||
|
vim.validate('follow', opts.follow, 'boolean', true)
|
||||||
|
|
||||||
if type(names) == 'string' then
|
if type(names) == 'string' then
|
||||||
names = { names }
|
names = { names }
|
||||||
@@ -345,7 +355,14 @@ function M.find(names, opts)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if type_ == 'directory' then
|
if
|
||||||
|
type_ == 'directory'
|
||||||
|
or (
|
||||||
|
type_ == 'link'
|
||||||
|
and opts.follow ~= false
|
||||||
|
and (vim.uv.fs_stat(f) or {}).type == 'directory'
|
||||||
|
)
|
||||||
|
then
|
||||||
dirs[#dirs + 1] = f
|
dirs[#dirs + 1] = f
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@@ -17,6 +17,8 @@ local mkdir = t.mkdir
|
|||||||
|
|
||||||
local nvim_prog_basename = is_os('win') and 'nvim.exe' or 'nvim'
|
local nvim_prog_basename = is_os('win') and 'nvim.exe' or 'nvim'
|
||||||
|
|
||||||
|
local link_limit = is_os('win') and 64 or (is_os('mac') or is_os('bsd')) and 33 or 41
|
||||||
|
|
||||||
local test_basename_dirname_eq = {
|
local test_basename_dirname_eq = {
|
||||||
'~/foo/',
|
'~/foo/',
|
||||||
'~/foo',
|
'~/foo',
|
||||||
@@ -152,7 +154,7 @@ describe('vim.fs', function()
|
|||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('works with opts.depth and opts.skip', function()
|
it('works with opts.depth, opts.skip and opts.follow', function()
|
||||||
io.open('testd/a1', 'w'):close()
|
io.open('testd/a1', 'w'):close()
|
||||||
io.open('testd/b1', 'w'):close()
|
io.open('testd/b1', 'w'):close()
|
||||||
io.open('testd/c1', 'w'):close()
|
io.open('testd/c1', 'w'):close()
|
||||||
@@ -166,8 +168,8 @@ describe('vim.fs', function()
|
|||||||
io.open('testd/a/b/c/b4', 'w'):close()
|
io.open('testd/a/b/c/b4', 'w'):close()
|
||||||
io.open('testd/a/b/c/c4', 'w'):close()
|
io.open('testd/a/b/c/c4', 'w'):close()
|
||||||
|
|
||||||
local function run(dir, depth, skip)
|
local function run(dir, depth, skip, follow)
|
||||||
return exec_lua(function()
|
return exec_lua(function(follow_)
|
||||||
local r = {} --- @type table<string, string>
|
local r = {} --- @type table<string, string>
|
||||||
local skip_f --- @type function
|
local skip_f --- @type function
|
||||||
if skip then
|
if skip then
|
||||||
@@ -177,11 +179,11 @@ describe('vim.fs', function()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for name, type_ in vim.fs.dir(dir, { depth = depth, skip = skip_f }) do
|
for name, type_ in vim.fs.dir(dir, { depth = depth, skip = skip_f, follow = follow_ }) do
|
||||||
r[name] = type_
|
r[name] = type_
|
||||||
end
|
end
|
||||||
return r
|
return r
|
||||||
end)
|
end, follow)
|
||||||
end
|
end
|
||||||
|
|
||||||
local exp = {}
|
local exp = {}
|
||||||
@@ -197,6 +199,7 @@ describe('vim.fs', function()
|
|||||||
exp['a/b2'] = 'file'
|
exp['a/b2'] = 'file'
|
||||||
exp['a/c2'] = 'file'
|
exp['a/c2'] = 'file'
|
||||||
exp['a/b'] = 'directory'
|
exp['a/b'] = 'directory'
|
||||||
|
local lexp = vim.deepcopy(exp)
|
||||||
|
|
||||||
eq(exp, run('testd', 2))
|
eq(exp, run('testd', 2))
|
||||||
|
|
||||||
@@ -213,6 +216,29 @@ describe('vim.fs', function()
|
|||||||
exp['a/b/c/c4'] = 'file'
|
exp['a/b/c/c4'] = 'file'
|
||||||
|
|
||||||
eq(exp, run('testd', 999))
|
eq(exp, run('testd', 999))
|
||||||
|
|
||||||
|
vim.uv.fs_symlink(vim.uv.fs_realpath('testd/a'), 'testd/l', { junction = true, dir = true })
|
||||||
|
lexp['l'] = 'link'
|
||||||
|
eq(lexp, run('testd', 2, nil, false))
|
||||||
|
|
||||||
|
lexp['l/a2'] = 'file'
|
||||||
|
lexp['l/b2'] = 'file'
|
||||||
|
lexp['l/c2'] = 'file'
|
||||||
|
lexp['l/b'] = 'directory'
|
||||||
|
eq(lexp, run('testd', 2, nil, true))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('follow=true handles symlink loop', function()
|
||||||
|
local cwd = 'testd/a/b/c'
|
||||||
|
local symlink = cwd .. '/link_loop' ---@type string
|
||||||
|
vim.uv.fs_symlink(vim.uv.fs_realpath(cwd), symlink, { junction = true, dir = true })
|
||||||
|
|
||||||
|
eq(
|
||||||
|
link_limit,
|
||||||
|
exec_lua(function()
|
||||||
|
return #vim.iter(vim.fs.dir(cwd, { depth = math.huge, follow = true })):totable()
|
||||||
|
end)
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -228,6 +254,53 @@ describe('vim.fs', function()
|
|||||||
eq({ nvim_dir }, vim.fs.find(name, { path = parent, upward = true, type = 'directory' }))
|
eq({ nvim_dir }, vim.fs.find(name, { path = parent, upward = true, type = 'directory' }))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('follows symlinks', function()
|
||||||
|
local build_dir = test_source_path .. '/build' ---@type string
|
||||||
|
local symlink = test_source_path .. '/build_link' ---@type string
|
||||||
|
vim.uv.fs_symlink(build_dir, symlink, { junction = true, dir = true })
|
||||||
|
|
||||||
|
finally(function()
|
||||||
|
vim.uv.fs_unlink(symlink)
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq(
|
||||||
|
{ nvim_prog, symlink .. '/bin/' .. nvim_prog_basename },
|
||||||
|
vim.fs.find(nvim_prog_basename, {
|
||||||
|
path = test_source_path,
|
||||||
|
type = 'file',
|
||||||
|
limit = 2,
|
||||||
|
follow = true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
eq(
|
||||||
|
{ nvim_prog },
|
||||||
|
vim.fs.find(nvim_prog_basename, {
|
||||||
|
path = test_source_path,
|
||||||
|
type = 'file',
|
||||||
|
limit = 2,
|
||||||
|
follow = false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('follow=true handles symlink loop', function()
|
||||||
|
local cwd = test_source_path ---@type string
|
||||||
|
local symlink = test_source_path .. '/loop_link' ---@type string
|
||||||
|
vim.uv.fs_symlink(cwd, symlink, { junction = true, dir = true })
|
||||||
|
|
||||||
|
finally(function()
|
||||||
|
vim.uv.fs_unlink(symlink)
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq(link_limit, #vim.fs.find(nvim_prog_basename, {
|
||||||
|
path = test_source_path,
|
||||||
|
type = 'file',
|
||||||
|
limit = math.huge,
|
||||||
|
follow = true,
|
||||||
|
}))
|
||||||
|
end)
|
||||||
|
|
||||||
it('accepts predicate as names', function()
|
it('accepts predicate as names', function()
|
||||||
local opts = { path = nvim_dir, upward = true, type = 'directory' }
|
local opts = { path = nvim_dir, upward = true, type = 'directory' }
|
||||||
eq(
|
eq(
|
||||||
|
@@ -388,15 +388,18 @@ end
|
|||||||
|
|
||||||
local sysname = uv.os_uname().sysname:lower()
|
local sysname = uv.os_uname().sysname:lower()
|
||||||
|
|
||||||
--- @param s 'win'|'mac'|'freebsd'|'openbsd'|'bsd'
|
--- @param s 'win'|'mac'|'linux'|'freebsd'|'openbsd'|'bsd'
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
function M.is_os(s)
|
function M.is_os(s)
|
||||||
if not (s == 'win' or s == 'mac' or s == 'freebsd' or s == 'openbsd' or s == 'bsd') then
|
if
|
||||||
|
not (s == 'win' or s == 'mac' or s == 'linux' or s == 'freebsd' or s == 'openbsd' or s == 'bsd')
|
||||||
|
then
|
||||||
error('unknown platform: ' .. tostring(s))
|
error('unknown platform: ' .. tostring(s))
|
||||||
end
|
end
|
||||||
return not not (
|
return not not (
|
||||||
(s == 'win' and (sysname:find('windows') or sysname:find('mingw')))
|
(s == 'win' and (sysname:find('windows') or sysname:find('mingw')))
|
||||||
or (s == 'mac' and sysname == 'darwin')
|
or (s == 'mac' and sysname == 'darwin')
|
||||||
|
or (s == 'linux' and sysname == 'linux')
|
||||||
or (s == 'freebsd' and sysname == 'freebsd')
|
or (s == 'freebsd' and sysname == 'freebsd')
|
||||||
or (s == 'openbsd' and sysname == 'openbsd')
|
or (s == 'openbsd' and sysname == 'openbsd')
|
||||||
or (s == 'bsd' and sysname:find('bsd'))
|
or (s == 'bsd' and sysname:find('bsd'))
|
||||||
|
Reference in New Issue
Block a user