|
- local doc = require "wslua.doc"
- local fndef = require "wslua.fndef"
- local ty = require "wslua.ty"
-
- local list = require "wslua.list"
- local misc = require "wslua.misc"
- local proc = require "wslua.proc"
- local string = require "wslua.string"
- local lfs = require "lfs"
-
-
-
- doc.module{
- name = "wslua.path",
- what = "Unix path manipulation",
- }
-
-
-
- local path = require "wslua.class" {name = "wslua.path", what = "Path representation."}
-
-
-
- local root = setmetatable({}, {__tostring = function() return "" end})
-
-
-
- doc.section "Creating paths"
-
-
- doc.var{
- name = "path.abs",
- what = "Absolute path root (= `/`).",
- type = ty.class(path),
- tests = {
- function() return '{{}}', path.abs:structure() end,
- function() return '{{}, "etc"}', (path.abs /"etc"):structure() end,
- },
- }
-
-
- path.rel = fndef({
- name = "path.rel",
- what = "Create a relative path.",
- args = {
- {name = "base", type = ty.stringish, what = "First path component."},
- },
- ret = {
- {type = ty.class(path)},
- },
- tests = {
- function() return '{"."}', (path.rel "."):structure() end,
- function() return '{"test", "foo"}', (path.rel "test"/"foo"):structure() end,
- function() return '{".", "test", "foo"}', (path.rel "."/"test"/"foo"):structure() end,
- },
- },
- function(base)
- local ret = path()
- ret[1] = base
- return ret
- end)
-
-
- path.url = fndef({
- name = "path.url",
- what = "Create a URL.",
- args = {
- {name = "protocol", type = ty.stringish, what = "Protocol."},
- {name = "base", type = ty.stringish, what = "First path component."},
- },
- ret = {
- {type = ty.class(path)},
- },
- ex = [[
- local path = require "wslua.path"
- print(path.url("https", "//lua.org") / "manual" / "5.4" / "contents.html")
- ]],
- tests = {
- function() return '{"https://lua.org"}', path.url("https", "//lua.org"):structure() end,
- function() return '{"https://lua.org", "manual", "5.4", "contents.html"}', (path.url("https", "//lua.org")/"manual"/"5.4"/"contents.html"):structure() end,
- },
- },
- function(proto, base)
- return path.rel(proto .. ":" .. base)
- end)
-
-
- path.parse = fndef({
- name = "path.parse",
- what = "Parse a path from a string.",
- args = {
- {name = "s", type = ty.stringish},
- },
- ret = {
- {type = ty.class(path)},
- },
- tests = {
- function() return '{{}}', path.parse("/"):structure() end,
- function() return '{{}, "etc"}', path.parse("/etc"):structure() end,
- function() return '{"test", "foo"}', path.parse("test/foo"):structure() end,
- function() return '{"test", "foo"}', path.parse("test//foo"):structure() end,
- function() return '{"test", "foo"}', path.parse("test/foo/"):structure() end,
- },
- },
- function(s)
- s = s:gsub("//+", "/")
-
- local ret = path()
- if s:find("^/") then ret[1] = root end
- for p in s:gmatch("([^/]*)%f[/\0]") do table.insert(ret, p) end
- return ret
- end)
-
-
- path.curr = fndef({
- name = "path.dir",
- what = "Get the current working directory.",
- args = {},
- ret = {
- {type = ty.class(path)},
- },
- },
- function()
- return path.parse(assert(lfs.currentdir()))
- end)
-
-
-
- doc.section "Path object manipulation"
-
-
- path.__div = fndef({
- name = "path:__div",
- what = "Append the path component `s` to `base`.",
- descr = "For convenience, `s` can be `nil`. In that case, the function returns the unmodified `self`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "s", type = #ty.stringish},
- },
- ret = {
- {type = ty.class(path), what = "Changed copy of `self`."},
- },
- tests = {
- function() return '{{}, "etc"}', (path.abs /"etc"):structure() end,
- function() return '{{}, "etc", "timezone"}', (path.abs /"etc"/"timezone"):structure() end,
- },
- },
- function(self, s)
- if s == nil then return self end
- local ret = self:dup()
-
- if type(s) == "table" and s.instanceof and s:instanceof(path) then
- if s[1] == root then error("Cannot append absolute path behind other path (try :sub(2)): " .. tostring(self) .. " / " .. tostring(s), 2) end
- for _, v in ipairs(s) do
- table.insert(ret, v)
- end
- else
- table.insert(ret, tostring(s))
- end
-
- return ret
- end)
-
-
- path.__concat = fndef({
- name = "path:__concat",
- what = "Append a string to the last path component.",
- args = {
- {name = "self", type = ty.class(path) & ty.list(ty.stringish, 1)},
- {name = "s", type = ty.stringish},
- },
- ret = {
- {type = ty.class(path), what = "Changed copy of `self`."},
- },
- tests = {
- function() return '{{}, "etc"}', ((path.abs /"et") .. "c"):structure() end,
- }
- },
- function(self, s)
- if self[#self] == root then error("Attempt to append to the root path!", 2) end
-
- local ret = self:dup()
- ret[#ret] = ret[#ret] .. s
- return ret
- end)
-
-
- path.sub = fndef({
- name = "path:sub",
- what = "Retrieve a sub-path from a path.",
- 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`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "first", type = #ty.number, what = "First path component to use, defaults to `1`."},
- {name = "last", type = #ty.number, what = "Last path component to use, defaults to `-1`."},
- },
- tests = {
- function() return '{"etc", "timezone"}', (path.abs /"etc"/"timezone"):sub(2):structure() end,
- function() return '{}', (path.abs /"etc"):sub(3):structure() end,
- function() return '{"share", "dict"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(3, 4):structure() end,
- function() return '{"share", "dict"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(3, -2):structure() end,
- function() return '{"share", "dict"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(-3, -2):structure() end,
- function() return '{{}, "usr", "share"}', (path.abs /"usr"/"share"/"dict"/"words"):sub(nil, 3):structure() end,
- },
- },
- function(self, first, last)
- if not first then first = 1 end
- if not last then last = -1 end
-
- if first < 0 then first = first + #self + 1 end
- if last < 0 then last = last + #self + 1 end
-
- local ret = path()
- for n = first, last do if 1 <= n and n <= #self then table.insert(ret, self[n]) end end
- return ret
- end)
-
-
-
- doc.section "I/O operations"
-
-
- path.open = fndef({
- name = "path:open",
- what = "Open the given file using `io.open`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "mode", type = #ty.string, what = "Open mode."},
- },
- ret = {
- {type = #ty.file, what = "`nil` if opening failed."},
- {type = #ty.string, what = "Error message if opening failed."},
- },
- see = {
- {std = true, module = "io", name = "io.open"},
- },
- },
- function(self, mode)
- return io.open(tostring(self), mode)
- end)
-
-
- path.lines = fndef({
- name = "path:lines",
- what = "Return a file contents iterator using `io.lines`.",
- descr = "Note that `io.lines` calls `error` if it cannot open the file.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "...", what = "Read specifications to pass to `io.lines`."},
- },
- ret = {
- {type = ty.Function, what = "Iterator function."},
- },
- },
- function(p, ...)
- return io.lines(tostring(p), ...)
- end)
-
-
- path.read = fndef({
- name = "path:read",
- what = "Fully read a file and return its contents as a string, removing a trailing line break.",
- descr = "Calls `error` on failure.",
- args = {
- {name = "self", type = ty.class(path)},
- },
- ret = {
- {type = ty.string},
- },
- },
- function(self)
- local h = assert(io.open(tostring(self)))
- local ret = h:read("a")
- h:close()
- return ret:gsub("\n$", "")
- end)
-
-
- path.write = fndef({
- name = "path:write",
- what = "Write a string to a file, adding a trailing newline.",
- descr = "Calls `error` on failure.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "data", type = ty.stringish, what = "Data to set the contents of the file to."},
- },
- },
- function(self, data)
- local h = assert(self:open("w"))
- h:write(tostring(data), "\n")
- h:close()
- end)
-
-
- path.writetemplate = fndef({
- name = "path:writetemplate",
- what = "Write a `wslua.string.template`-processed string (plus a newline) to a file.",
- descr = "Calls `error` on failure.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "env", type = ty.table, what = "Execution environment for the snippets."},
- },
- ret = {
- {type = ty.Function, what = "Function that processes additional data.", descr = "Appends the processed data (plus a newline) to the file, and returns itself."},
- },
- ex = [=[
- require "wslua" ();
- (path.abs /"tmp"/"foo"):writetemplate(_ENV)
- "Lua version:"
- "%[[_VERSION]]"
- ]=],
- },
- function(self, env)
- local template = require("wslua.misc").template
- return function(data)
- local function ret(data)
- self:append(string.template(data, misc.here(3), env))
- return ret
- end
- self:write(string.template(data, misc.here(3), env))
- return ret
- end
- end)
-
-
- path.append = fndef({
- name = "path:append",
- what = "Append data to a file, adding a trailing newline.",
- descr = "Calls `error` on failure.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "data", type = ty.stringish},
- },
- },
- function(self, data)
- local h = assert(self:open("a"))
- h:write(data, "\n")
- h:close()
- end)
-
-
- doc.subsection "Interacting with Lua files"
-
-
- path.loadfile = fndef({
- name = "path:loadfile",
- what = "Load a lua file as a function.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "mode", type = #ty.string, what = "Load mode."},
- {name = "env", type = #ty.table, what = "Environment that the function should run in, defaults to `_G`."},
- },
- ret = {
- {type = #ty.Function, what = "File contents wrapped in a function, or`nil` on error."},
- {type = #ty.string, what = "Error message on error."},
- },
- see = {
- {std = true, name = "load"},
- },
- },
- function(self, ...)
- return loadfile(tostring(self), ...)
- end)
-
-
- path.dofile = fndef({
- name = "path:dofile",
- what = "Run a lua file.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "mode", type = #ty.string, what = "Load mode."},
- {name = "env", type = #ty.table, what = "Environment to run the code in, defaults to `_G`."},
- },
- ret = {
- {name = "...", type = ty.any, what = "Return values of the code (if any)."},
- },
- see = {
- {std = true, name = "load"},
- },
- },
- function(self, ...)
- return assert(self:loadfile(...))()
- end)
-
-
- path.store = fndef({
- name = "path:store",
- what = "Serialize data to a file.",
- descr = "Discards functions and metatable information.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "data", type = ty.any},
- },
- },
- function(self, data)
- self:write(misc.show(data, {ascii = true, mt = false, ref = true}))
- end)
-
-
- path.fetch = fndef({
- name = "path:fetch",
- what = "Deserialize data from a file.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "env", type = #ty.table, what = "Execution environment, defaults to `{}`."},
- },
- ret = {
- {type = ty.any},
- },
- },
- function(self, env)
- return assert(load("return " .. self:read(), self:__tostring(), "t", env or {}))()
- end)
-
-
-
- doc.section "File system information"
-
-
- path.stat = fndef({
- name = "path:stat",
- what = "Wrapper for `lfs.attributes`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "what", type = #ty.stringish},
- },
- ret = {
- {type = ty.any},
- },
- see = {
- {module = "lfs", name = "lfs.attributes"},
- },
- },
- function(self, what)
- return lfs.attributes(tostring(self), tostring(what))
- end)
-
-
- path.lstat = fndef({
- name = "path:lstat",
- what = "Wrapper for `lfs.symlinkattributes`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "what", type = #ty.stringish},
- },
- ret = {
- {type = ty.any},
- },
- see = {
- {module = "lfs", name = "lfs.symlinkattributes"},
- },
- },
- function(self, what)
- return lfs.symlinkattributes(tostring(self), what)
- end)
-
-
- path.ls = fndef({
- name = "path:ls",
- what = "Iterate through the contents of a directory. Wrapper for `lfs.dir`.",
- descr = [[
- Each iteration, the iterator returns a string containing the name of the entry, skipping `.` and `..`. The order of
- elements is unspecified.
- ]],
- args = {
- {name = "self", type = ty.class(path)},
- },
- ret = {
- {type = ty.Function, what = "Iterator function."},
- {type = ty.userdata, what = "Directory descriptor."},
- {type = ty.Nil},
- {type = ty.userdata, what = "Directory descriptor (to-be-closed variable)."},
- },
- see = {
- {name = "path:contents"}
- },
- },
- function(self)
- local f, s, i, c = lfs.dir(tostring(self))
- local f_ = function(i, s)
- local ret
- repeat ret = f(i, s) until ret ~= "." and ret ~= ".."
- return ret
- end
- return f_, s, i ,c
- end)
-
-
- path.contents = fndef({
- name = "path:contents",
- what = "Obtain the contents of a directory in alphabetical order (skipping `.` and `..`).",
- args = {
- {name = "self", type = ty.class(path)},
- },
- ret = {
- {type = ty.class(list) & ty.list(ty.string)},
- },
- },
- function(p)
- local ret = list()
- for c in p:ls() do ret:insert(c) end
- ret:sort()
- return ret
- end)
-
-
- path.realpath = fndef({
- name = "path:realpath",
- what = "Return an absolute path, with all encountered symlinks and occurrences of `.` and `..` resolved.",
- args = {
- {name = "self", type = ty.class(path)},
- },
- ret = {
- {type = ty.class(path)},
- },
- tests = {
- function() return '{{}, "usr"}', (path.abs /"usr"/"."/"share"/".."):realpath():structure() end,
- },
- },
- function(self)
- return path.parse(proc "realpath" "--canonicalize-missing" (self) :read("l"))
- end)
-
-
-
- doc.section "Other functionality"
-
-
- path.__tostring = fndef({
- name = "path:__tostring",
- what = "Convert the path to a string.",
- args = {
- {name = "self", type = ty.class(path)},
- },
- ret = {
- {type = ty.string},
- },
- tests = {
- function() return "/", path.abs:__tostring() end,
- function() return "/etc", (path.abs /"etc"):__tostring() end,
- function() return "usr/share/dict", (path.rel "usr"/"share"/"dict"):__tostring() end,
- },
- },
- function(self)
- if #self == 1 and self[1] == root then return "/" end
- return list(self):map(tostring):concat("/")
- end)
-
-
- path.structure = function(self)
- return misc.show(self, {mt = false, oneline = true})
- end
-
-
- path.short = fndef({
- name = "path:short",
- what = "Return a shortened version of a long path to keep long messages shorter.",
- args = {
- {name = "self", type = ty.class(path)},
- {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."},
- },
- ret = {
- {type = ty.string},
- },
- tests = {
- function() return "/", path.abs:short(3) end,
- function() return "sh~/dict", (path.rel "share"/"dict"):short(3) end,
- function() return "/media", (path.abs /"media"):short(3) end,
- function() return "/usr/sh~/di~/words", (path.abs /"usr"/"share"/"dict"/"words"):short(3) end,
- },
- },
- function(self, len)
- len = len or 3
- if #self == 1 and self[1] == root then return "/" end
- local ret = {}
- for i, v in ipairs(self) do
- local s = tostring(v)
- if #s > len and i < #self then s = s:sub(1, len - 1) .. "~" end
- table.insert(ret, s)
- end
- return table.concat(ret, "/")
- end)
-
-
- path.contains = fndef({
- name = "path:contains",
- what = "Check if `self` is a prefix of `other`.",
- descr = "Converts both paths to their canonical absolute representation using `:realpath()`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "other", type = ty.class(path)},
- },
- ret = {
- {type = ty.boolean},
- },
- tests = {
- function() return true , (path.abs /"usr"/"local"):contains(path.abs /"usr"/"local"/"bin") end,
- function() return true , (path.abs /"usr"/"local"):contains(path.abs /"usr"/"local" ) end,
- function() return false, (path.abs /"usr"/"local"):contains(path.abs /"usr" ) end,
- },
- },
- function(self, other)
- self = self :realpath()
- other = other:realpath()
- for k, v in ipairs(self) do
- if other[k] ~= v then return false end
- end
- return true
- end)
-
-
- path.inside = fndef({
- name = "path:inside",
- what = "Check if `other` is a prefix of `self`.",
- args = {
- {name = "self", type = ty.class(path)},
- {name = "other", type = ty.class(path)},
- },
- ret = {
- {type = ty.boolean},
- },
- tests = {
- function() return false, (path.abs /"usr"/"local"):inside(path.abs /"usr"/"local"/"bin") end,
- function() return true , (path.abs /"usr"/"local"):inside(path.abs /"usr"/"local" ) end,
- function() return true , (path.abs /"usr"/"local"):inside(path.abs /"usr" ) end,
- },
- },
- function(self, other)
- return other:contains(self)
- end)
-
-
- path.abs = path()
- path.abs[1] = root
-
-
- return path
|