commit 14c708634efec514463bb495d9648c78828ee198
parent c3defb4b458fc398e3b0bed2bcb737285d909970
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date: Thu, 5 Feb 2026 12:02:54 +0200
fix(vim.fs): make `rm()` work with symlink to a directory
Diffstat:
3 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
@@ -2696,7 +2696,9 @@ vim.fs.rm({path}, {opts}) *vim.fs.rm()*
Since: 0.11.0
Parameters: ~
- • {path} (`string`) Path to remove
+ • {path} (`string`) Path to remove. Removes symlinks without touching
+ the origin. To remove the origin, resolve explicitly with
+ |uv.fs_realpath()|.
• {opts} (`table?`) A table with the following fields:
• {recursive}? (`boolean`) Remove directories and their
contents recursively
diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua
@@ -735,12 +735,13 @@ end
--- Remove files or directories
--- @since 13
---- @param path string Path to remove
+--- @param path string Path to remove. Removes symlinks without touching the origin.
+---To remove the origin, resolve explicitly with |uv.fs_realpath()|.
--- @param opts? vim.fs.rm.Opts
function M.rm(path, opts)
opts = opts or {}
- local stat, err, errnm = uv.fs_stat(path)
+ local stat, err, errnm = uv.fs_lstat(path)
if stat then
rm(path, stat.type, opts.recursive, opts.force)
elseif not opts.force or errnm ~= 'ENOENT' then
diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua
@@ -9,6 +9,7 @@ local rmdir = n.rmdir
local nvim_dir = n.nvim_dir
local command = n.command
local api = n.api
+local fn = n.fn
local test_build_dir = t.paths.test_build_dir
local test_source_path = t.paths.test_source_path
local nvim_prog = n.nvim_prog
@@ -714,4 +715,41 @@ describe('vim.fs', function()
end
end)
end)
+
+ describe('rm()', function()
+ before_each(function()
+ t.mkdir('Xtest_fs-rm')
+ t.write_file('Xtest_fs-rm/file-to-link', 'File to link')
+ t.mkdir('Xtest_fs-rm/dir-to-link')
+ t.write_file('Xtest_fs-rm/dir-to-link/file', 'File in dir to link')
+ end)
+
+ after_each(function()
+ vim.uv.fs_unlink('Xtest_fs-rm/dir-to-link/file')
+ vim.uv.fs_rmdir('Xtest_fs-rm/dir-to-link')
+ vim.uv.fs_unlink('Xtest_fs-rm/file-to-link')
+ vim.uv.fs_rmdir('Xtest_fs-rm')
+ end)
+
+ it('works with symlink', function()
+ -- File
+ vim.uv.fs_symlink('Xtest_fs-rm/file-to-link', 'Xtest_fs-rm/file-as-link')
+ vim.fs.rm('Xtest_fs-rm/file-as-link')
+ eq(vim.uv.fs_stat('Xtest_fs-rm/file-as-link'), nil)
+ eq({ 'File to link' }, fn.readfile('Xtest_fs-rm/file-to-link'))
+
+ -- Directory
+ local function assert_rm_symlinked_dir(opts)
+ vim.uv.fs_symlink('Xtest_fs-rm/dir-to-link', 'Xtest_fs-rm/dir-as-link')
+ vim.fs.rm('Xtest_fs-rm/dir-as-link', opts)
+ eq(vim.uv.fs_stat('Xtest_fs-rm/dir-as-link'), nil)
+ eq({ 'File in dir to link' }, fn.readfile('Xtest_fs-rm/dir-to-link/file'))
+ end
+
+ assert_rm_symlinked_dir({})
+ assert_rm_symlinked_dir({ force = true })
+ assert_rm_symlinked_dir({ recursive = true })
+ assert_rm_symlinked_dir({ recursive = true, force = true })
+ end)
+ end)
end)