Skip to content

Commit 826613f

Browse files
committed
add 'touch', 'rm', & read methods
1 parent 324e52b commit 826613f

File tree

2 files changed

+238
-47
lines changed

2 files changed

+238
-47
lines changed

lua/plenary/path2.lua

Lines changed: 178 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,9 @@ end
524524
--- Will throw error if path doesn't exist.
525525
---@return uv.aliases.fs_stat_table
526526
function Path:stat()
527-
local res, _, err_msg = uv.fs_stat(self:absolute())
527+
local res, err = uv.fs_stat(self:absolute())
528528
if res == nil then
529-
error(err_msg)
529+
error(err)
530530
end
531531
return res
532532
end
@@ -541,9 +541,9 @@ end
541541
--- Will throw error if path doesn't exist.
542542
---@return uv.aliases.fs_stat_table
543543
function Path:lstat()
544-
local res, _, err_msg = uv.fs_lstat(self:absolute())
544+
local res, err = uv.fs_lstat(self:absolute())
545545
if res == nil then
546-
error(err_msg)
546+
error(err)
547547
end
548548
return res
549549
end
@@ -797,7 +797,6 @@ end
797797

798798
--- Create directory
799799
---@param opts plenary.Path2.mkdirOpts?
800-
---@return boolean success
801800
function Path:mkdir(opts)
802801
opts = opts or {}
803802
opts.mode = vim.F.if_nil(opts.mode, 511)
@@ -812,18 +811,18 @@ function Path:mkdir(opts)
812811

813812
local ok, err_msg, err_code = uv.fs_mkdir(abs_path, opts.mode)
814813
if ok then
815-
return true
814+
return
816815
end
817816
if err_code == "EEXIST" then
818-
return true
817+
return
819818
end
820819
if err_code == "ENOENT" then
821820
if not opts.parents or self.parent == self then
822821
error(err_msg)
823822
end
824823
self:parent():mkdir { mode = opts.mode }
825824
uv.fs_mkdir(abs_path, opts.mode)
826-
return true
825+
return
827826
end
828827

829828
error(err_msg)
@@ -850,7 +849,6 @@ end
850849
--- 'touch' file.
851850
--- If it doesn't exist, creates it including optionally, the parent directories
852851
---@param opts plenary.Path2.touchOpts?
853-
---@return boolean success
854852
function Path:touch(opts)
855853
opts = opts or {}
856854
opts.mode = vim.F.if_nil(opts.mode, 438)
@@ -861,29 +859,192 @@ function Path:touch(opts)
861859
if self:exists() then
862860
local new_time = os.time()
863861
uv.fs_utime(abs_path, new_time, new_time)
864-
return true
862+
return
865863
end
866864

867865
if not not opts.parents then
868-
local mode = type(opts.parents) == "number" and opts.parents ---@cast mode number?
866+
local mode = type(opts.parents) == "number" and opts.parents or nil ---@cast mode number?
869867
_ = Path:new(self:parent()):mkdir { mode = mode, parents = true }
870868
end
871869

872-
local fd, _, err_msg = uv.fs_open(self:absolute(), "w", opts.mode)
870+
local fd, err = uv.fs_open(self:absolute(), "w", opts.mode)
873871
if fd == nil then
874-
error(err_msg)
872+
error(err)
875873
end
876874

877875
local ok
878-
ok, _, err_msg = uv.fs_close(fd)
876+
ok, err = uv.fs_close(fd)
879877
if not ok then
880-
error(err_msg)
878+
error(err)
881879
end
882-
883-
return true
884880
end
885881

882+
---@class plenary.Path2.rmOpts
883+
---@field recursive boolean? remove directories and their content recursively (defaul: `false`)
884+
885+
--- rm file or optional recursively remove directories and their content recursively
886+
---@param opts plenary.Path2.rmOpts?
886887
function Path:rm(opts)
888+
opts = opts or {}
889+
opts.recursive = vim.F.if_nil(opts.recursive, false)
890+
891+
if not opts.recursive or not self:is_dir() then
892+
local ok, err = uv.fs_unlink(self:absolute())
893+
if ok then
894+
return
895+
end
896+
if self:is_dir() then
897+
error(string.format("Cannnot rm director '%s'.", self:absolute()))
898+
end
899+
error(err)
900+
end
901+
902+
for p, dirs, files in self:walk(false) do
903+
for _, file in ipairs(files) do
904+
print("delete file", file, (p / file):absolute())
905+
local _, err = uv.fs_unlink((p / file):absolute())
906+
if err then
907+
error(err)
908+
end
909+
end
910+
911+
for _, dir in ipairs(dirs) do
912+
print("delete dir", dir, (p / dir):absolute())
913+
local _, err = uv.fs_rmdir((p / dir):absolute())
914+
if err then
915+
error(err)
916+
end
917+
end
918+
end
919+
920+
self:rmdir()
921+
end
922+
923+
--- read file synchronously or asynchronously
924+
---@param callback fun(data: string)? callback to use for async version, nil for default
925+
---@return string? data
926+
function Path:read(callback)
927+
if not self:is_file() then
928+
error(string.format("'%s' is not a file", self:absolute()))
929+
end
930+
931+
if callback == nil then
932+
return self:_read_sync()
933+
end
934+
return self:_read_async(callback)
935+
end
936+
937+
---@private
938+
---@return string
939+
function Path:_read_sync()
940+
local fd, err = uv.fs_open(self:absolute(), "r", 438)
941+
if fd == nil then
942+
error(err)
943+
end
944+
945+
local stat = self:stat()
946+
local data
947+
data, err = uv.fs_read(fd, stat.size, 0)
948+
if data == nil then
949+
error(err)
950+
end
951+
return data
952+
end
953+
954+
---@private
955+
---@param callback fun(data: string) callback to use for async version, nil for default
956+
function Path:_read_async(callback)
957+
uv.fs_open(self:absolute(), "r", 438, function(err_open, fd)
958+
if err_open then
959+
error(err_open)
960+
end
961+
962+
uv.fs_fstat(fd, function(err_stat, stat)
963+
if err_stat or stat == nil then
964+
error(err_stat)
965+
end
966+
967+
uv.fs_read(fd, stat.size, 0, function(err_read, data)
968+
if err_read or data == nil then
969+
error(err_read)
970+
end
971+
callback(data)
972+
end)
973+
end)
974+
end)
975+
end
976+
977+
--- read lines of a file into a list
978+
---@return string[]
979+
function Path:readlines()
980+
local data = assert(self:read())
981+
return vim.split(data, "\r?\n")
982+
end
983+
984+
--- get an iterator for lines text in a file
985+
---@return fun(): string?
986+
function Path:iter_lines()
987+
local data = assert(self:read())
988+
return vim.gsplit(data, "\r?\n")
989+
end
990+
991+
---@param top_down boolean? walk from current path down (default: `true`)
992+
---@return fun(): plenary.Path2?, string[]?, string[]? # iterator which yields (dirpath, dirnames, filenames)
993+
function Path:walk(top_down)
994+
top_down = vim.F.if_nil(top_down, true)
995+
996+
local queue = { self } ---@type plenary.Path2[]
997+
local curr_fs = nil ---@type uv_fs_t?
998+
local curr_path = nil ---@type plenary.Path2
999+
1000+
local rev_res = {} ---@type [plenary.Path2, string[], string[]]
1001+
1002+
return function()
1003+
while #queue > 0 or curr_fs do
1004+
if curr_fs == nil then
1005+
local p = table.remove(queue, 1)
1006+
local fs, err = uv.fs_scandir(p:absolute())
1007+
1008+
if fs == nil then
1009+
error(err)
1010+
end
1011+
curr_path = p
1012+
curr_fs = fs
1013+
end
1014+
1015+
if curr_fs then
1016+
local dirs = {}
1017+
local files = {}
1018+
while true do
1019+
local name, ty = uv.fs_scandir_next(curr_fs)
1020+
if name == nil then
1021+
curr_fs = nil
1022+
break
1023+
end
1024+
1025+
if ty == "directory" then
1026+
table.insert(queue, Path:new { curr_path, name })
1027+
table.insert(dirs, name)
1028+
else
1029+
table.insert(files, name)
1030+
end
1031+
end
1032+
1033+
if top_down then
1034+
return curr_path, dirs, files
1035+
else
1036+
table.insert(rev_res, { curr_path, dirs, files })
1037+
end
1038+
end
1039+
end
1040+
1041+
if not top_down and #rev_res > 0 then
1042+
local res = table.remove(rev_res)
1043+
return res[1], res[2], res[3]
1044+
end
1045+
1046+
return nil
1047+
end
8871048
end
8881049

8891050
return Path

tests/plenary/path2_spec.lua

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
local Path = require "plenary.path2"
22
local path = Path.path
33
local compat = require "plenary.compat"
4-
local iswin = vim.loop.os_uname().sysname == "Windows_NT"
4+
local uv = vim.loop
5+
local iswin = uv.os_uname().sysname == "Windows_NT"
56

67
local hasshellslash = vim.fn.exists "+shellslash" == 1
78

@@ -411,6 +412,12 @@ describe("Path2", function()
411412
end
412413

413414
describe("mkdir / rmdir", function()
415+
after_each(function()
416+
uv.fs_rmdir "_dir_not_exist"
417+
uv.fs_rmdir "impossible/dir"
418+
uv.fs_rmdir "impossible"
419+
end)
420+
414421
it_cross_plat("can create and delete directories", function()
415422
local p = Path:new "_dir_not_exist"
416423

@@ -467,52 +474,75 @@ describe("Path2", function()
467474
end)
468475

469476
describe("touch/rm", function()
470-
it("can create and delete new files", function()
477+
after_each(function()
478+
uv.fs_unlink "test_file.lua"
479+
uv.fs_unlink "nested/nested2/test_file.lua"
480+
uv.fs_rmdir "nested/nested2"
481+
uv.fs_unlink "nested/asdf/.hidden"
482+
uv.fs_rmdir "nested/asdf"
483+
uv.fs_unlink "nested/dir/.hidden"
484+
uv.fs_rmdir "nested/dir"
485+
uv.fs_rmdir "nested"
486+
end)
487+
488+
it_cross_plat("can create and delete new files", function()
471489
local p = Path:new "test_file.lua"
472-
assert(pcall(p.touch, p))
473-
assert(p:exists())
490+
assert.not_error(function()
491+
p:touch()
492+
end)
493+
assert.is_true(p:exists())
474494

475-
p:rm()
476-
assert(not p:exists())
495+
assert.not_error(function()
496+
p:rm()
497+
end)
498+
assert.is_true(not p:exists())
477499
end)
478500

479-
it("does not effect already created files but updates last access", function()
501+
it_cross_plat("does not effect already created files but updates last access", function()
480502
local p = Path:new "README.md"
481-
local last_atime = p:stat().atime.sec
482-
local last_mtime = p:stat().mtime.sec
483-
484503
local lines = p:readlines()
485504

486-
assert(pcall(p.touch, p))
487-
print(p:stat().atime.sec > last_atime)
488-
print(p:stat().mtime.sec > last_mtime)
489-
assert(p:exists())
505+
assert.no_error(function()
506+
p:touch()
507+
end)
490508

509+
assert.is_true(p:exists())
491510
assert.are.same(lines, p:readlines())
492511
end)
493512

494-
it("does not create dirs if nested in none existing dirs and parents not set", function()
513+
it_cross_plat("does not create dirs if nested in none existing dirs and parents not set", function()
495514
local p = Path:new { "nested", "nested2", "test_file.lua" }
496-
assert(not pcall(p.touch, p, { parents = false }))
497-
assert(not p:exists())
515+
assert.has_error(function()
516+
p:touch { parents = false }
517+
end)
518+
assert.is_false(p:exists())
498519
end)
499520

500-
it("does create dirs if nested in none existing dirs", function()
521+
it_cross_plat("does create dirs if nested in none existing dirs", function()
501522
local p1 = Path:new { "nested", "nested2", "test_file.lua" }
502523
local p2 = Path:new { "nested", "asdf", ".hidden" }
503524
local d1 = Path:new { "nested", "dir", ".hidden" }
504-
assert(pcall(p1.touch, p1, { parents = true }))
505-
assert(pcall(p2.touch, p2, { parents = true }))
506-
assert(pcall(d1.mkdir, d1, { parents = true }))
507-
assert(p1:exists())
508-
assert(p2:exists())
509-
assert(d1:exists())
510-
511-
Path:new({ "nested" }):rm { recursive = true }
512-
assert(not p1:exists())
513-
assert(not p2:exists())
514-
assert(not d1:exists())
515-
assert(not Path:new({ "nested" }):exists())
525+
526+
assert.no_error(function()
527+
p1:touch { parents = true }
528+
end)
529+
assert.no_error(function()
530+
p2:touch { parents = true }
531+
end)
532+
assert.no_error(function()
533+
d1:mkdir { parents = true }
534+
end)
535+
assert.is_true(p1:exists())
536+
assert.is_true(p2:exists())
537+
assert.is_true(d1:exists())
538+
539+
assert.no_error(function()
540+
Path:new({ "nested" }):rm { recursive = true }
541+
end)
542+
assert.is_false(p1:exists())
543+
assert.is_false(p2:exists())
544+
assert.is_false(d1:exists())
545+
assert.is_false(Path:new({ "nested" }):exists())
516546
end)
517547
end)
518548

0 commit comments

Comments
 (0)