Skip to content

Commit 72c8734

Browse files
committed
head and tail
1 parent 4e164d3 commit 72c8734

File tree

2 files changed

+226
-3
lines changed

2 files changed

+226
-3
lines changed

lua/plenary/path2.lua

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
--- - `Path.new` no longer supported (think it's more confusing that helpful
99
--- and not really used as far as I can tell)
1010
---
11-
--- - drop `__concat` metamethod? it was untested, not sure how functional it is
11+
--- - drop `__concat` metamethod? it was untested and had some todo comment,
12+
--- not sure how functional it is
1213
---
1314
--- - `Path` objects are now "read-only", I don't think people were ever doing
1415
--- things like `path.filename = 'foo'` but now explicitly adding some barrier
@@ -27,6 +28,9 @@
2728
--- eg. `Path:new("foo/bar_baz"):make_relative("foo/bar", true)` => returns
2829
--- "../bar_baz"
2930
---
31+
--- - error handling is generally more loud, ie. emit errors from libuv rather
32+
--- than swallowing it
33+
---
3034
--- - remove `Path:normalize`. It doesn't make any sense. eg. this test case
3135
--- ```lua
3236
--- it("can normalize ~ when file is within home directory (trailing slash)", function()
@@ -55,6 +59,9 @@
5559
--- - drops interactive mode
5660
--- - return value table is pre-flattened
5761
--- - return value table value is `{success: boolean, err: string?}` rather than just `boolean`
62+
---
63+
--- - drops `check_self` mechanism (ie. doing `Path.read("some/file/path")`)
64+
--- seems unnecessary... just do `Path:new("some/file/path"):read()`
5865

5966
local bit = require "plenary.bit"
6067
local uv = vim.loop
@@ -1029,15 +1036,26 @@ function Path:read(callback)
10291036
return self:_read_async(callback)
10301037
end
10311038

1039+
---@private
1040+
---@return uv.aliases.fs_stat_table
1041+
function Path:_get_readable_stat()
1042+
local stat = self:stat()
1043+
if stat.type ~= "file" then
1044+
error(string.format("Cannot read non-file '%s'.", self:absolute()))
1045+
end
1046+
return stat
1047+
end
1048+
10321049
---@private
10331050
---@return string
10341051
function Path:_read_sync()
1052+
local stat = self:_get_readable_stat()
1053+
10351054
local fd, err = uv.fs_open(self:absolute(), "r", 438)
10361055
if fd == nil then
10371056
error(err)
10381057
end
10391058

1040-
local stat = self:stat()
10411059
local data
10421060
data, err = uv.fs_read(fd, stat.size, 0)
10431061
if data == nil then
@@ -1083,6 +1101,106 @@ function Path:iter_lines()
10831101
return vim.gsplit(data, "\r?\n")
10841102
end
10851103

1104+
--- read the first few lines of a file
1105+
---@param lines integer? number of lines to read from the head of the file (default: `10`)
1106+
---@return string data
1107+
function Path:head(lines)
1108+
local stat = self:_get_readable_stat()
1109+
1110+
lines = vim.F.if_nil(lines, 10)
1111+
local chunk_size = 256
1112+
1113+
local fd, err = uv.fs_open(self:absolute(), "r", 438)
1114+
if fd == nil then
1115+
error(err)
1116+
end
1117+
1118+
local data = {}
1119+
local read_chunk ---@type string?
1120+
local index, count = 0, 0
1121+
while count < lines and index < stat.size do
1122+
read_chunk, err = uv.fs_read(fd, chunk_size, index)
1123+
if read_chunk == nil then
1124+
error(err)
1125+
end
1126+
1127+
local i = 1
1128+
while i <= #read_chunk do
1129+
local ch = read_chunk:byte(i)
1130+
if ch == 10 then -- `\n`
1131+
count = count + 1
1132+
if count >= lines then
1133+
break
1134+
end
1135+
end
1136+
index = index + 1
1137+
i = i + 1
1138+
end
1139+
1140+
table.insert(data, read_chunk:sub(1, i))
1141+
end
1142+
1143+
_, err = uv.fs_close(fd)
1144+
if err ~= nil then
1145+
error(err)
1146+
end
1147+
1148+
return (table.concat(data):gsub("\n$", ""))
1149+
end
1150+
1151+
--- read the last few lines of a file
1152+
---@param lines integer? number of lines to read from the tail of the file (default: `10`)
1153+
---@return string data
1154+
function Path:tail(lines)
1155+
local stat = self:_get_readable_stat()
1156+
1157+
lines = vim.F.if_nil(lines, 10)
1158+
local chunk_size = 256
1159+
1160+
local fd, err = uv.fs_open(self:absolute(), "r", 438)
1161+
if fd == nil then
1162+
error(err)
1163+
end
1164+
1165+
local data = {}
1166+
local read_chunk ---@type string?
1167+
local index, count = stat.size - 1, 0
1168+
while count < lines and index > 0 do
1169+
local real_index = index - chunk_size
1170+
if real_index < 0 then
1171+
chunk_size = chunk_size + real_index
1172+
real_index = 0
1173+
end
1174+
1175+
read_chunk, err = uv.fs_read(fd, chunk_size, real_index)
1176+
if read_chunk == nil then
1177+
error(err)
1178+
end
1179+
1180+
local i = #read_chunk
1181+
while i > 0 do
1182+
local ch = read_chunk:byte(i)
1183+
if ch == 10 then -- `\n`
1184+
count = count + 1
1185+
if count >= lines then
1186+
break
1187+
end
1188+
end
1189+
index = index - 1
1190+
i = i - 1
1191+
end
1192+
1193+
table.insert(data, 1, read_chunk:sub(i + 1, #read_chunk))
1194+
end
1195+
1196+
_, err = uv.fs_close(fd)
1197+
if err ~= nil then
1198+
error(err)
1199+
end
1200+
1201+
return (table.concat(data):gsub("\n$", ""))
1202+
end
1203+
10861204
---@param top_down boolean? walk from current path down (default: `true`)
10871205
---@return fun(): plenary.Path2?, string[]?, string[]? # iterator which yields (dirpath, dirnames, filenames)
10881206
function Path:walk(top_down)

tests/plenary/path2_spec.lua

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,112 @@ describe("Path2", function()
845845
it_cross_plat("should return itself if it corresponds to path.root", function()
846846
local p = Path:new(Path.path.root(vim.fn.getcwd()))
847847
assert.are.same(p:absolute(), p:parent():absolute())
848-
-- assert.are.same(p, p:parent())
848+
assert.are.same(p, p:parent())
849+
end)
850+
end)
851+
852+
describe("head", function()
853+
it_cross_plat("should read head of file", function()
854+
local p = Path:new "LICENSE"
855+
local data = p:head()
856+
local should = [[MIT License
857+
858+
Copyright (c) 2020 TJ DeVries
859+
860+
Permission is hereby granted, free of charge, to any person obtaining a copy
861+
of this software and associated documentation files (the "Software"), to deal
862+
in the Software without restriction, including without limitation the rights
863+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
864+
copies of the Software, and to permit persons to whom the Software is
865+
furnished to do so, subject to the following conditions:]]
866+
867+
assert.are.same(should, data)
868+
end)
869+
870+
it_cross_plat("should read the first line of file", function()
871+
local p = Path:new "LICENSE"
872+
local data = p:head(1)
873+
local should = [[MIT License]]
874+
assert.are.same(should, data)
875+
end)
876+
877+
it_cross_plat("head should max read whole file", function()
878+
local p = Path:new "LICENSE"
879+
local data = p:head(1000)
880+
local should = [[MIT License
881+
882+
Copyright (c) 2020 TJ DeVries
883+
884+
Permission is hereby granted, free of charge, to any person obtaining a copy
885+
of this software and associated documentation files (the "Software"), to deal
886+
in the Software without restriction, including without limitation the rights
887+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
888+
copies of the Software, and to permit persons to whom the Software is
889+
furnished to do so, subject to the following conditions:
890+
891+
The above copyright notice and this permission notice shall be included in all
892+
copies or substantial portions of the Software.
893+
894+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
895+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
896+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
897+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
898+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
899+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
900+
SOFTWARE.]]
901+
assert.are.same(should, data)
902+
end)
903+
end)
904+
905+
describe("tail", function()
906+
it_cross_plat("should read tail of file", function()
907+
local p = Path:new "LICENSE"
908+
local data = p:tail()
909+
local should = [[The above copyright notice and this permission notice shall be included in all
910+
copies or substantial portions of the Software.
911+
912+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
913+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
914+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
915+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
916+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
917+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
918+
SOFTWARE.]]
919+
assert.are.same(should, data)
920+
end)
921+
922+
it_cross_plat("should read the last line of file", function()
923+
local p = Path:new "LICENSE"
924+
local data = p:tail(1)
925+
local should = [[SOFTWARE.]]
926+
assert.are.same(should, data)
927+
end)
928+
929+
it_cross_plat("tail should max read whole file", function()
930+
local p = Path:new "LICENSE"
931+
local data = p:tail(1000)
932+
local should = [[MIT License
933+
934+
Copyright (c) 2020 TJ DeVries
935+
936+
Permission is hereby granted, free of charge, to any person obtaining a copy
937+
of this software and associated documentation files (the "Software"), to deal
938+
in the Software without restriction, including without limitation the rights
939+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
940+
copies of the Software, and to permit persons to whom the Software is
941+
furnished to do so, subject to the following conditions:
942+
943+
The above copyright notice and this permission notice shall be included in all
944+
copies or substantial portions of the Software.
945+
946+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
947+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
948+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
949+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
950+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
951+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
952+
SOFTWARE.]]
953+
assert.are.same(should, data)
849954
end)
850955
end)
851956
end)

0 commit comments

Comments
 (0)