Skip to content

Commit 21afd79

Browse files
committed
improve head/tail line endings compat
1 parent f3c0169 commit 21afd79

File tree

2 files changed

+120
-26
lines changed

2 files changed

+120
-26
lines changed

lua/plenary/path2.lua

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
---
6363
--- - drops `check_self` mechanism (ie. doing `Path.read("some/file/path")`)
6464
--- seems unnecessary... just do `Path:new("some/file/path"):read()`
65+
---
66+
--- - renamed `iter` into `iter_lines` for more clarity
6567

6668
local bit = require "plenary.bit"
6769
local uv = vim.loop
@@ -1115,6 +1117,10 @@ end
11151117
---@param lines integer? number of lines to read from the head of the file (default: `10`)
11161118
---@return string data
11171119
function Path:head(lines)
1120+
vim.validate {
1121+
lines = { lines, "n", true },
1122+
}
1123+
11181124
local stat = self:_get_readable_stat()
11191125

11201126
lines = vim.F.if_nil(lines, 10)
@@ -1138,11 +1144,17 @@ function Path:head(lines)
11381144
while i <= #read_chunk do
11391145
local ch = read_chunk:byte(i)
11401146
if ch == 10 then -- `\n`
1141-
count = count + 1
1142-
if count >= lines then
1143-
break
1147+
if read_chunk:byte(i - 1) ~= 13 then
1148+
count = count + 1
11441149
end
1150+
elseif ch == 13 then
1151+
count = count + 1
11451152
end
1153+
1154+
if count >= lines then
1155+
break
1156+
end
1157+
11461158
index = index + 1
11471159
i = i + 1
11481160
end
@@ -1155,13 +1167,17 @@ function Path:head(lines)
11551167
error(err)
11561168
end
11571169

1158-
return (table.concat(data):gsub("\n$", ""))
1170+
return (table.concat(data):gsub("[\r\n]$", ""))
11591171
end
11601172

11611173
--- read the last few lines of a file
11621174
---@param lines integer? number of lines to read from the tail of the file (default: `10`)
11631175
---@return string data
11641176
function Path:tail(lines)
1177+
vim.validate {
1178+
lines = { lines, "n", true },
1179+
}
1180+
11651181
local stat = self:_get_readable_stat()
11661182

11671183
lines = vim.F.if_nil(lines, 10)
@@ -1174,7 +1190,7 @@ function Path:tail(lines)
11741190

11751191
local data = {}
11761192
local read_chunk ---@type string?
1177-
local index, count = stat.size - 1, 0
1193+
local index, count = stat.size, -1
11781194
while count < lines and index > 0 do
11791195
local real_index = index - chunk_size
11801196
if real_index < 0 then
@@ -1190,12 +1206,18 @@ function Path:tail(lines)
11901206
local i = #read_chunk
11911207
while i > 0 do
11921208
local ch = read_chunk:byte(i)
1193-
if ch == 10 then -- `\n`
1194-
count = count + 1
1195-
if count >= lines then
1196-
break
1209+
if ch == 13 then
1210+
if read_chunk:byte(i + 1) ~= 10 then
1211+
count = count + 1
11971212
end
1213+
elseif ch == 10 then
1214+
count = count + 1
1215+
end
1216+
1217+
if count >= lines then
1218+
break
11981219
end
1220+
11991221
index = index - 1
12001222
i = i - 1
12011223
end
@@ -1208,7 +1230,7 @@ function Path:tail(lines)
12081230
error(err)
12091231
end
12101232

1211-
return (table.concat(data):gsub("\n$", ""))
1233+
return (table.concat(data):gsub("[\r\n]$", ""))
12121234
end
12131235

12141236
--- write to file
@@ -1302,18 +1324,4 @@ function Path:walk(top_down)
13021324
end
13031325
end
13041326

1305-
--[[
1306-
Fail || Path2 write write string - windows (noshellslash)
1307-
./lua/plenary/path2.lua:896: EPERM: operation not permitted: C:\Users\jtrew\neovim\plenary.nvim\foobar
1308-
1309-
stack traceback:
1310-
...s/jtrew/neovim/plenary.nvim/tests/plenary/path2_spec.lua:859: in function <...s/jtrew/neovim/plenary.nvim/tests/plenary/path2_spec.lua:857>
1311-
1312-
]]
1313-
1314-
vim.o.shellslash = false
1315-
local p = Path:new "foobar"
1316-
p:touch()
1317-
vim.o.shellslash = true
1318-
13191327
return Path

tests/plenary/path2_spec.lua

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,18 @@ describe("Path2", function()
895895
end)
896896
end)
897897

898+
local function diff_str(a, b)
899+
a = a:gsub("\n", "\\n"):gsub("\r", "\\r"):gsub("\t", "\\t")
900+
b = b:gsub("\n", "\\n"):gsub("\r", "\\r"):gsub("\t", "\\t")
901+
---@diagnostic disable-next-line: missing-fields
902+
return vim.diff(a, b, {})
903+
end
904+
898905
describe("head", function()
906+
after_each(function()
907+
uv.fs_unlink "foobar.txt"
908+
end)
909+
899910
it_cross_plat("should read head of file", function()
900911
local p = Path:new "LICENSE"
901912
local data = p:head()
@@ -920,7 +931,7 @@ furnished to do so, subject to the following conditions:]]
920931
assert.are.same(should, data)
921932
end)
922933

923-
it_cross_plat("head should max read whole file", function()
934+
it_cross_plat("should max read whole file", function()
924935
local p = Path:new "LICENSE"
925936
local data = p:head(1000)
926937
local should = [[MIT License
@@ -946,9 +957,43 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
946957
SOFTWARE.]]
947958
assert.are.same(should, data)
948959
end)
960+
961+
it_cross_plat("handles unix lf line endings", function()
962+
local p = Path:new "foobar.txt"
963+
p:touch()
964+
965+
local txt = "foo\nbar\nbaz"
966+
p:write(txt, "w")
967+
local data = p:head()
968+
assert.are.same(txt, data, diff_str(txt, data))
969+
end)
970+
971+
it_cross_plat("handles windows crlf line endings", function()
972+
local p = Path:new "foobar.txt"
973+
p:touch()
974+
975+
local txt = "foo\r\nbar\r\nbaz"
976+
p:write(txt, "w")
977+
local data = p:head()
978+
assert.are.same(txt, data, diff_str(txt, data))
979+
end)
980+
981+
it_cross_plat("handles mac cr line endings", function()
982+
local p = Path:new "foobar.txt"
983+
p:touch()
984+
985+
local txt = "foo\rbar\rbaz"
986+
p:write(txt, "w")
987+
local data = p:head()
988+
assert.are.same(txt, data, diff_str(txt, data))
989+
end)
949990
end)
950991

951992
describe("tail", function()
993+
after_each(function()
994+
uv.fs_unlink "foobar.txt"
995+
end)
996+
952997
it_cross_plat("should read tail of file", function()
953998
local p = Path:new "LICENSE"
954999
local data = p:tail()
@@ -972,7 +1017,7 @@ SOFTWARE.]]
9721017
assert.are.same(should, data)
9731018
end)
9741019

975-
it_cross_plat("tail should max read whole file", function()
1020+
it_cross_plat("should max read whole file", function()
9761021
local p = Path:new "LICENSE"
9771022
local data = p:tail(1000)
9781023
local should = [[MIT License
@@ -998,5 +1043,46 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
9981043
SOFTWARE.]]
9991044
assert.are.same(should, data)
10001045
end)
1046+
1047+
it_cross_plat("handles unix lf line endings", function()
1048+
local p = Path:new "foobar.txt"
1049+
p:touch()
1050+
1051+
local txt = "foo\nbar\nbaz"
1052+
p:write(txt, "w")
1053+
local data = p:tail()
1054+
assert.are.same(txt, data, diff_str(txt, data))
1055+
end)
1056+
1057+
it_cross_plat("handles windows crlf line endings", function()
1058+
local p = Path:new "foobar.txt"
1059+
p:touch()
1060+
1061+
local txt = "foo\r\nbar\r\nbaz"
1062+
p:write(txt, "w")
1063+
local data = p:tail()
1064+
assert.are.same(txt, data, diff_str(txt, data))
1065+
end)
1066+
1067+
it_cross_plat("handles mac cr line endings", function()
1068+
local p = Path:new "foobar.txt"
1069+
p:touch()
1070+
1071+
local txt = "foo\rbar\rbaz"
1072+
p:write(txt, "w")
1073+
local data = p:tail()
1074+
assert.are.same(txt, data, diff_str(txt, data))
1075+
end)
1076+
1077+
it_cross_plat("handles extra newlines", function()
1078+
local p = Path:new "foobar.txt"
1079+
p:touch()
1080+
1081+
local txt = "foo\nbar\nbaz\n\n\n"
1082+
local expect = "foo\nbar\nbaz\n\n"
1083+
p:write(txt, "w")
1084+
local data = p:tail()
1085+
assert.are.same(expect, data, diff_str(expect, data))
1086+
end)
10011087
end)
10021088
end)

0 commit comments

Comments
 (0)