You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

638 lines
16KB

  1. local doc = require "wslua.doc"
  2. local fndef = require "wslua.fndef"
  3. local ty = require "wslua.ty"
  4. local list = require "wslua.list"
  5. local misc = require "wslua.misc"
  6. local proc = require "wslua.proc"
  7. local string = require "wslua.string"
  8. local lfs = require "lfs"
  9. doc.module{
  10. name = "wslua.path",
  11. what = "Unix path manipulation",
  12. }
  13. local path = require "wslua.class" {name = "wslua.path", what = "Path representation."}
  14. local root = setmetatable({}, {__tostring = function() return "" end})
  15. doc.section "Creating paths"
  16. doc.var{
  17. name = "path.abs",
  18. what = "Absolute path root (= `/`).",
  19. type = ty.class(path),
  20. tests = {
  21. function() return '{{}}', path.abs:structure() end,
  22. function() return '{{}, "etc"}', (path.abs /"etc"):structure() end,
  23. },
  24. }
  25. path.rel = fndef({
  26. name = "path.rel",
  27. what = "Create a relative path.",
  28. args = {
  29. {name = "base", type = ty.stringish, what = "First path component."},
  30. },
  31. ret = {
  32. {type = ty.class(path)},
  33. },
  34. tests = {
  35. function() return '{"."}', (path.rel "."):structure() end,
  36. function() return '{"test", "foo"}', (path.rel "test"/"foo"):structure() end,
  37. function() return '{".", "test", "foo"}', (path.rel "."/"test"/"foo"):structure() end,
  38. },
  39. },
  40. function(base)
  41. local ret = path()
  42. ret[1] = base
  43. return ret
  44. end)
  45. path.url = fndef({
  46. name = "path.url",
  47. what = "Create a URL.",
  48. args = {
  49. {name = "protocol", type = ty.stringish, what = "Protocol."},
  50. {name = "base", type = ty.stringish, what = "First path component."},
  51. },
  52. ret = {
  53. {type = ty.class(path)},
  54. },
  55. ex = [[
  56. local path = require "wslua.path"
  57. print(path.url("https", "//lua.org") / "manual" / "5.4" / "contents.html")
  58. ]],
  59. tests = {
  60. function() return '{"https://lua.org"}', path.url("https", "//lua.org"):structure() end,
  61. function() return '{"https://lua.org", "manual", "5.4", "contents.html"}', (path.url("https", "//lua.org")/"manual"/"5.4"/"contents.html"):structure() end,
  62. },
  63. },
  64. function(proto, base)
  65. return path.rel(proto .. ":" .. base)
  66. end)
  67. path.parse = fndef({
  68. name = "path.parse",
  69. what = "Parse a path from a string.",
  70. args = {
  71. {name = "s", type = ty.stringish},
  72. },
  73. ret = {
  74. {type = ty.class(path)},
  75. },
  76. tests = {
  77. function() return '{{}}', path.parse("/"):structure() end,
  78. function() return '{{}, "etc"}', path.parse("/etc"):structure() end,
  79. function() return '{"test", "foo"}', path.parse("test/foo"):structure() end,
  80. function() return '{"test", "foo"}', path.parse("test//foo"):structure() end,
  81. function() return '{"test", "foo"}', path.parse("test/foo/"):structure() end,
  82. },
  83. },
  84. function(s)
  85. s = s:gsub("//+", "/")
  86. local ret = path()
  87. if s:find("^/") then ret[1] = root end
  88. for p in s:gmatch("([^/]*)%f[/\0]") do table.insert(ret, p) end
  89. return ret
  90. end)
  91. path.curr = fndef({
  92. name = "path.dir",
  93. what = "Get the current working directory.",
  94. args = {},
  95. ret = {
  96. {type = ty.class(path)},
  97. },
  98. },
  99. function()
  100. return path.parse(assert(lfs.currentdir()))
  101. end)
  102. doc.section "Path object manipulation"
  103. path.__div = fndef({
  104. name = "path:__div",
  105. what = "Append the path component `s` to `base`.",
  106. descr = "For convenience, `s` can be `nil`. In that case, the function returns the unmodified `self`.",
  107. args = {
  108. {name = "self", type = ty.class(path)},
  109. {name = "s", type = #ty.stringish},
  110. },
  111. ret = {
  112. {type = ty.class(path), what = "Changed copy of `self`."},
  113. },
  114. tests = {
  115. function() return '{{}, "etc"}', (path.abs /"etc"):structure() end,
  116. function() return '{{}, "etc", "timezone"}', (path.abs /"etc"/"timezone"):structure() end,
  117. },
  118. },
  119. function(self, s)
  120. if s == nil then return self end
  121. local ret = self:dup()
  122. if type(s) == "table" and s.instanceof and s:instanceof(path) then
  123. if s[1] == root then error("Cannot append absolute path behind other path (try :sub(2)): " .. tostring(self) .. " / " .. tostring(s), 2) end
  124. for _, v in ipairs(s) do
  125. table.insert(ret, v)
  126. end
  127. else
  128. table.insert(ret, tostring(s))
  129. end
  130. return ret
  131. end)
  132. path.__concat = fndef({
  133. name = "path:__concat",
  134. what = "Append a string to the last path component.",
  135. args = {
  136. {name = "self", type = ty.class(path) & ty.list(ty.stringish, 1)},
  137. {name = "s", type = ty.stringish},
  138. },
  139. ret = {
  140. {type = ty.class(path), what = "Changed copy of `self`."},
  141. },
  142. tests = {
  143. function() return '{{}, "etc"}', ((path.abs /"et") .. "c"):structure() end,
  144. }
  145. },
  146. function(self, s)
  147. if self[#self] == root then error("Attempt to append to the root path!", 2) end
  148. local ret = self:dup()
  149. ret[#ret] = ret[#ret] .. s
  150. return ret
  151. end)
  152. path.sub = fndef({
  153. name = "path:sub",
  154. what = "Retrieve a sub-path from a path.",
  155. descr = "An index of `-i` corresponds to the `i`-last element. Note that the first element of the absolute path `/etc` is is the root itself, not `etc`.",
  156. args = {
  157. {name = "self", type = ty.class(path)},
  158. {name = "first", type = #ty.number, what = "First path component to use, defaults to `1`."},
  159. {name = "last", type = #ty.number, what = "Last path component to use, defaults to `-1`."},
  160. },
  161. tests = {
  162. function() return '{"etc", "timezone"}', (path.abs /"etc"/"timezone"):sub(2):structure() end,
  163. function() return '{}', (path.abs /"etc"):sub(3):structure() end,
  164. function() return '{"share", "dict"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(3, 4):structure() end,
  165. function() return '{"share", "dict"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(3, -2):structure() end,
  166. function() return '{"share", "dict"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(-3, -2):structure() end,
  167. function() return '{{}, "usr", "share"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(nil, 3):structure() end,
  168. },
  169. },
  170. function(self, first, last)
  171. if not first then first = 1 end
  172. if not last then last = -1 end
  173. if first < 0 then first = first + #self + 1 end
  174. if last < 0 then last = last + #self + 1 end
  175. local ret = path()
  176. for n = first, last do if 1 <= n and n <= #self then table.insert(ret, self[n]) end end
  177. return ret
  178. end)
  179. doc.section "I/O operations"
  180. path.open = fndef({
  181. name = "path:open",
  182. what = "Open the given file using `io.open`.",
  183. args = {
  184. {name = "self", type = ty.class(path)},
  185. {name = "mode", type = #ty.string, what = "Open mode."},
  186. },
  187. ret = {
  188. {type = #ty.file, what = "`nil` if opening failed."},
  189. {type = #ty.string, what = "Error message if opening failed."},
  190. },
  191. see = {
  192. {std = true, module = "io", name = "io.open"},
  193. },
  194. },
  195. function(self, mode)
  196. return io.open(tostring(self), mode)
  197. end)
  198. path.lines = fndef({
  199. name = "path:lines",
  200. what = "Return a file contents iterator using `io.lines`.",
  201. descr = "Note that `io.lines` calls `error` if it cannot open the file.",
  202. args = {
  203. {name = "self", type = ty.class(path)},
  204. {name = "...", what = "Read specifications to pass to `io.lines`."},
  205. },
  206. ret = {
  207. {type = ty.Function, what = "Iterator function."},
  208. },
  209. },
  210. function(p, ...)
  211. return io.lines(tostring(p), ...)
  212. end)
  213. path.read = fndef({
  214. name = "path:read",
  215. what = "Fully read a file and return its contents as a string, removing a trailing line break.",
  216. descr = "Calls `error` on failure.",
  217. args = {
  218. {name = "self", type = ty.class(path)},
  219. },
  220. ret = {
  221. {type = ty.string},
  222. },
  223. },
  224. function(self)
  225. local h = assert(io.open(tostring(self)))
  226. local ret = h:read("a")
  227. h:close()
  228. return ret:gsub("\n$", "")
  229. end)
  230. path.write = fndef({
  231. name = "path:write",
  232. what = "Write a string to a file, adding a trailing newline.",
  233. descr = "Calls `error` on failure.",
  234. args = {
  235. {name = "self", type = ty.class(path)},
  236. {name = "data", type = ty.stringish, what = "Data to set the contents of the file to."},
  237. },
  238. },
  239. function(self, data)
  240. local h = assert(self:open("w"))
  241. h:write(tostring(data), "\n")
  242. h:close()
  243. end)
  244. path.writetemplate = fndef({
  245. name = "path:writetemplate",
  246. what = "Write a `wslua.string.template`-processed string (plus a newline) to a file.",
  247. descr = "Calls `error` on failure.",
  248. args = {
  249. {name = "self", type = ty.class(path)},
  250. {name = "env", type = ty.table, what = "Execution environment for the snippets."},
  251. },
  252. ret = {
  253. {type = ty.Function, what = "Function that processes additional data.", descr = "Appends the processed data (plus a newline) to the file, and returns itself."},
  254. },
  255. ex = [=[
  256. require "wslua" ();
  257. (path.abs /"tmp"/"foo"):writetemplate(_ENV)
  258. "Lua version:"
  259. "%[[_VERSION]]"
  260. ]=],
  261. },
  262. function(self, env)
  263. local template = require("wslua.misc").template
  264. return function(data)
  265. local function ret(data)
  266. self:append(string.template(data, misc.here(3), env))
  267. return ret
  268. end
  269. self:write(string.template(data, misc.here(3), env))
  270. return ret
  271. end
  272. end)
  273. path.append = fndef({
  274. name = "path:append",
  275. what = "Append data to a file, adding a trailing newline.",
  276. descr = "Calls `error` on failure.",
  277. args = {
  278. {name = "self", type = ty.class(path)},
  279. {name = "data", type = ty.stringish},
  280. },
  281. },
  282. function(self, data)
  283. local h = assert(self:open("a"))
  284. h:write(data, "\n")
  285. h:close()
  286. end)
  287. doc.subsection "Interacting with Lua files"
  288. path.loadfile = fndef({
  289. name = "path:loadfile",
  290. what = "Load a lua file as a function.",
  291. args = {
  292. {name = "self", type = ty.class(path)},
  293. {name = "mode", type = #ty.string, what = "Load mode."},
  294. {name = "env", type = #ty.table, what = "Environment that the function should run in, defaults to `_G`."},
  295. },
  296. ret = {
  297. {type = #ty.Function, what = "File contents wrapped in a function, or`nil` on error."},
  298. {type = #ty.string, what = "Error message on error."},
  299. },
  300. see = {
  301. {std = true, name = "load"},
  302. },
  303. },
  304. function(self, ...)
  305. return loadfile(tostring(self), ...)
  306. end)
  307. path.dofile = fndef({
  308. name = "path:dofile",
  309. what = "Run a lua file.",
  310. args = {
  311. {name = "self", type = ty.class(path)},
  312. {name = "mode", type = #ty.string, what = "Load mode."},
  313. {name = "env", type = #ty.table, what = "Environment to run the code in, defaults to `_G`."},
  314. },
  315. ret = {
  316. {name = "...", type = ty.any, what = "Return values of the code (if any)."},
  317. },
  318. see = {
  319. {std = true, name = "load"},
  320. },
  321. },
  322. function(self, ...)
  323. return assert(self:loadfile(...))()
  324. end)
  325. path.store = fndef({
  326. name = "path:store",
  327. what = "Serialize data to a file.",
  328. descr = "Discards functions and metatable information.",
  329. args = {
  330. {name = "self", type = ty.class(path)},
  331. {name = "data", type = ty.any},
  332. },
  333. },
  334. function(self, data)
  335. self:write(misc.show(data, {ascii = true, mt = false, ref = true}))
  336. end)
  337. path.fetch = fndef({
  338. name = "path:fetch",
  339. what = "Deserialize data from a file.",
  340. args = {
  341. {name = "self", type = ty.class(path)},
  342. {name = "env", type = #ty.table, what = "Execution environment, defaults to `{}`."},
  343. },
  344. ret = {
  345. {type = ty.any},
  346. },
  347. },
  348. function(self, env)
  349. return assert(load("return " .. self:read(), self:__tostring(), "t", env or {}))()
  350. end)
  351. doc.section "File system information"
  352. path.stat = fndef({
  353. name = "path:stat",
  354. what = "Wrapper for `lfs.attributes`.",
  355. args = {
  356. {name = "self", type = ty.class(path)},
  357. {name = "what", type = #ty.stringish},
  358. },
  359. ret = {
  360. {type = ty.any},
  361. },
  362. see = {
  363. {module = "lfs", name = "lfs.attributes"},
  364. },
  365. },
  366. function(self, what)
  367. return lfs.attributes(tostring(self), tostring(what))
  368. end)
  369. path.lstat = fndef({
  370. name = "path:lstat",
  371. what = "Wrapper for `lfs.symlinkattributes`.",
  372. args = {
  373. {name = "self", type = ty.class(path)},
  374. {name = "what", type = #ty.stringish},
  375. },
  376. ret = {
  377. {type = ty.any},
  378. },
  379. see = {
  380. {module = "lfs", name = "lfs.symlinkattributes"},
  381. },
  382. },
  383. function(self, what)
  384. return lfs.symlinkattributes(tostring(self), what)
  385. end)
  386. path.ls = fndef({
  387. name = "path:ls",
  388. what = "Iterate through the contents of a directory. Wrapper for `lfs.dir`.",
  389. descr = [[
  390. Each iteration, the iterator returns a string containing the name of the entry, skipping `.` and `..`. The order of
  391. elements is unspecified.
  392. ]],
  393. args = {
  394. {name = "self", type = ty.class(path)},
  395. },
  396. ret = {
  397. {type = ty.Function, what = "Iterator function."},
  398. {type = ty.userdata, what = "Directory descriptor."},
  399. {type = ty.Nil},
  400. {type = ty.userdata, what = "Directory descriptor (to-be-closed variable)."},
  401. },
  402. see = {
  403. {name = "path:contents"}
  404. },
  405. },
  406. function(self)
  407. local f, s, i, c = lfs.dir(tostring(self))
  408. local f_ = function(i, s)
  409. local ret
  410. repeat ret = f(i, s) until ret ~= "." and ret ~= ".."
  411. return ret
  412. end
  413. return f_, s, i ,c
  414. end)
  415. path.contents = fndef({
  416. name = "path:contents",
  417. what = "Obtain the contents of a directory in alphabetical order (skipping `.` and `..`).",
  418. args = {
  419. {name = "self", type = ty.class(path)},
  420. },
  421. ret = {
  422. {type = ty.class(list) & ty.list(ty.string)},
  423. },
  424. },
  425. function(p)
  426. local ret = list()
  427. for c in p:ls() do ret:insert(c) end
  428. ret:sort()
  429. return ret
  430. end)
  431. path.realpath = fndef({
  432. name = "path:realpath",
  433. what = "Return an absolute path, with all encountered symlinks and occurrences of `.` and `..` resolved.",
  434. args = {
  435. {name = "self", type = ty.class(path)},
  436. },
  437. ret = {
  438. {type = ty.class(path)},
  439. },
  440. tests = {
  441. function() return '{{}, "usr"}', (path.abs /"usr"/"."/"share"/".."):realpath():structure() end,
  442. },
  443. },
  444. function(self)
  445. return path.parse(proc "realpath" "--canonicalize-missing" (self) :read("l"))
  446. end)
  447. doc.section "Other functionality"
  448. path.__tostring = fndef({
  449. name = "path:__tostring",
  450. what = "Convert the path to a string.",
  451. args = {
  452. {name = "self", type = ty.class(path)},
  453. },
  454. ret = {
  455. {type = ty.string},
  456. },
  457. tests = {
  458. function() return "/", path.abs:__tostring() end,
  459. function() return "/etc", (path.abs /"etc"):__tostring() end,
  460. function() return "usr/share/dict", (path.rel "usr"/"share"/"dict"):__tostring() end,
  461. },
  462. },
  463. function(self)
  464. if #self == 1 and self[1] == root then return "/" end
  465. return list(self):map(tostring):concat("/")
  466. end)
  467. path.structure = function(self)
  468. return misc.show(self, {mt = false, oneline = true})
  469. end
  470. path.short = fndef({
  471. name = "path:short",
  472. what = "Return a shortened version of a long path to keep long messages shorter.",
  473. args = {
  474. {name = "self", type = ty.class(path)},
  475. {name = "len", type = #ty.number, what = "Maximum length of one path segment, defaults to `3`.", descr = "Longer path segments will be cut to length `len-1` plus a `~` character."},
  476. },
  477. ret = {
  478. {type = ty.string},
  479. },
  480. tests = {
  481. function() return "/", path.abs:short(3) end,
  482. function() return "sh~/dict", (path.rel "share"/"dict"):short(3) end,
  483. function() return "/media", (path.abs /"media"):short(3) end,
  484. function() return "/usr/sh~/di~/words", (path.abs /"usr"/"share"/"dict"/"words"):short(3) end,
  485. },
  486. },
  487. function(self, len)
  488. len = len or 3
  489. if #self == 1 and self[1] == root then return "/" end
  490. local ret = {}
  491. for i, v in ipairs(self) do
  492. local s = tostring(v)
  493. if #s > len and i < #self then s = s:sub(1, len - 1) .. "~" end
  494. table.insert(ret, s)
  495. end
  496. return table.concat(ret, "/")
  497. end)
  498. path.contains = fndef({
  499. name = "path:contains",
  500. what = "Check if `self` is a prefix of `other`.",
  501. descr = "Converts both paths to their canonical absolute representation using `:realpath()`.",
  502. args = {
  503. {name = "self", type = ty.class(path)},
  504. {name = "other", type = ty.class(path)},
  505. },
  506. ret = {
  507. {type = ty.boolean},
  508. },
  509. tests = {
  510. function() return true , (path.abs /"usr"/"local"):contains(path.abs /"usr"/"local"/"bin") end,
  511. function() return true , (path.abs /"usr"/"local"):contains(path.abs /"usr"/"local" ) end,
  512. function() return false, (path.abs /"usr"/"local"):contains(path.abs /"usr" ) end,
  513. },
  514. },
  515. function(self, other)
  516. self = self :realpath()
  517. other = other:realpath()
  518. for k, v in ipairs(self) do
  519. if other[k] ~= v then return false end
  520. end
  521. return true
  522. end)
  523. path.inside = fndef({
  524. name = "path:inside",
  525. what = "Check if `other` is a prefix of `self`.",
  526. args = {
  527. {name = "self", type = ty.class(path)},
  528. {name = "other", type = ty.class(path)},
  529. },
  530. ret = {
  531. {type = ty.boolean},
  532. },
  533. tests = {
  534. function() return false, (path.abs /"usr"/"local"):inside(path.abs /"usr"/"local"/"bin") end,
  535. function() return true , (path.abs /"usr"/"local"):inside(path.abs /"usr"/"local" ) end,
  536. function() return true , (path.abs /"usr"/"local"):inside(path.abs /"usr" ) end,
  537. },
  538. },
  539. function(self, other)
  540. return other:contains(self)
  541. end)
  542. path.abs = path()
  543. path.abs[1] = root
  544. return path