diff options
author | Mike <4576770+mike325@users.noreply.github.com> | 2025-01-15 01:39:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-14 16:39:17 -0800 |
commit | 611ef354919f1c6564efd2ff8074545941458ccc (patch) | |
tree | e4fe577188f7e05f8c618c3a9f271a48b0ef2f7c | |
parent | e8a6c1b02122852da83dc52184e78369598d8240 (diff) | |
download | rneovim-611ef354919f1c6564efd2ff8074545941458ccc.tar.gz rneovim-611ef354919f1c6564efd2ff8074545941458ccc.tar.bz2 rneovim-611ef354919f1c6564efd2ff8074545941458ccc.zip |
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.
-rw-r--r-- | runtime/doc/lua.txt | 11 | ||||
-rw-r--r-- | runtime/doc/news.txt | 2 | ||||
-rw-r--r-- | runtime/lua/vim/fs.lua | 23 | ||||
-rw-r--r-- | test/functional/lua/fs_spec.lua | 83 | ||||
-rw-r--r-- | test/testutil.lua | 7 |
5 files changed, 113 insertions, 13 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 44cbf238cf..6e5a77ff7a 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2983,6 +2983,7 @@ vim.fs.dir({path}, {opts}) *vim.fs.dir()* • skip: (fun(dir_name: string): boolean)|nil Predicate to control traversal. Return false to stop searching the current directory. Only useful when depth > 1 + • follow: boolean|nil Follow symbolic links. (default: true) Return: ~ (`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/ 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'}) < @@ -3038,8 +3039,10 @@ vim.fs.find({names}, {opts}) *vim.fs.find()* If {names} is a function, it is called for each traversed item with args: • name: base name of the current item - • path: full path of the current item The function should - return `true` if the given item is considered a match. + • path: full path of the current item + + The function should return `true` if the given item is + considered a match. • {opts} (`table`) Optional keyword arguments: • {path}? (`string`) Path to begin searching from. If 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 finding this many matches. Use `math.huge` to place no limit on the number of matches. + • {follow}? (`boolean`, default: `true`) Follow symbolic + links. Return: ~ (`string[]`) Normalized paths |vim.fs.normalize()| of all matching diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 23266d536f..8aa0a8d57b 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -286,6 +286,8 @@ LUA • |vim.json.encode()| has an option to enable forward slash escaping • |vim.fs.abspath()| converts paths to absolute paths. • |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 diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 91e06688b3..5940fa4386 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -136,6 +136,7 @@ end --- - skip: (fun(dir_name: string): boolean)|nil Predicate --- to control traversal. Return false to stop searching the current directory. --- 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". --- "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('depth', opts.depth, 'number', true) vim.validate('skip', opts.skip, 'function', true) + vim.validate('follow', opts.follow, 'boolean', true) path = M.normalize(path) if not opts.depth or opts.depth == 1 then @@ -177,7 +179,9 @@ function M.dir(path, opts) if 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) then dirs[#dirs + 1] = { f, level + 1 } @@ -211,6 +215,10 @@ end --- Use `math.huge` to place no limit on the number of matches. --- (default: `1`) --- @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. --- @@ -234,7 +242,7 @@ end --- --- -- get all files ending with .cpp or .hpp inside lib/ --- 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'}) --- ``` --- @@ -244,6 +252,7 @@ end --- If {names} is a function, it is called for each traversed item with args: --- - name: base name of the current item --- - path: full path of the current item +--- --- The function should return `true` if the given item is considered a match. --- ---@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('type', opts.type, 'string', true) vim.validate('limit', opts.limit, 'number', true) + vim.validate('follow', opts.follow, 'boolean', true) if type(names) == 'string' then names = { names } @@ -345,7 +355,14 @@ function M.find(names, opts) 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 end end diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index 0ba0948eee..33af25f629 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -17,6 +17,8 @@ local mkdir = t.mkdir 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 = { '~/foo/', '~/foo', @@ -152,7 +154,7 @@ describe('vim.fs', function() ) 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/b1', '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/c4', 'w'):close() - local function run(dir, depth, skip) - return exec_lua(function() + local function run(dir, depth, skip, follow) + return exec_lua(function(follow_) local r = {} --- @type table<string, string> local skip_f --- @type function if skip then @@ -177,11 +179,11 @@ describe('vim.fs', function() 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_ end return r - end) + end, follow) end local exp = {} @@ -197,6 +199,7 @@ describe('vim.fs', function() exp['a/b2'] = 'file' exp['a/c2'] = 'file' exp['a/b'] = 'directory' + local lexp = vim.deepcopy(exp) eq(exp, run('testd', 2)) @@ -213,6 +216,29 @@ describe('vim.fs', function() exp['a/b/c/c4'] = 'file' 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) @@ -228,6 +254,53 @@ describe('vim.fs', function() eq({ nvim_dir }, vim.fs.find(name, { path = parent, upward = true, type = 'directory' })) 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() local opts = { path = nvim_dir, upward = true, type = 'directory' } eq( diff --git a/test/testutil.lua b/test/testutil.lua index 3226bfeb1e..e69dcae120 100644 --- a/test/testutil.lua +++ b/test/testutil.lua @@ -388,15 +388,18 @@ end local sysname = uv.os_uname().sysname:lower() ---- @param s 'win'|'mac'|'freebsd'|'openbsd'|'bsd' +--- @param s 'win'|'mac'|'linux'|'freebsd'|'openbsd'|'bsd' --- @return boolean 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)) end return not not ( (s == 'win' and (sysname:find('windows') or sysname:find('mingw'))) or (s == 'mac' and sysname == 'darwin') + or (s == 'linux' and sysname == 'linux') or (s == 'freebsd' and sysname == 'freebsd') or (s == 'openbsd' and sysname == 'openbsd') or (s == 'bsd' and sysname:find('bsd')) |