diff options
Diffstat (limited to 'macros/luatex/generic/penlight/pl/path.lua')
-rw-r--r-- | macros/luatex/generic/penlight/pl/path.lua | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/macros/luatex/generic/penlight/pl/path.lua b/macros/luatex/generic/penlight/pl/path.lua new file mode 100644 index 0000000000..a438a9df87 --- /dev/null +++ b/macros/luatex/generic/penlight/pl/path.lua @@ -0,0 +1,575 @@ +--- Path manipulation and file queries. +-- +-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}. +-- +-- NOTE: the functions assume the paths being dealt with to originate +-- from the OS the application is running on. Windows drive letters are not +-- to be used when running on a Unix system for example. The one exception +-- is Windows paths to allow both forward and backward slashes (since Lua +-- also accepts those) +-- +-- Dependencies: `pl.utils`, `lfs` +-- @module pl.path + +-- imports and locals +local _G = _G +local sub = string.sub +local getenv = os.getenv +local tmpnam = os.tmpname +local package = package +local append, concat, remove = table.insert, table.concat, table.remove +local utils = require 'pl.utils' +local assert_string,raise = utils.assert_string,utils.raise + +local res,lfs = _G.pcall(_G.require,'lfs') +if not res then + error("pl.path requires LuaFileSystem") +end + +local attrib = lfs.attributes +local currentdir = lfs.currentdir +local link_attrib = lfs.symlinkattributes + +local path = {} + +local function err_func(name, param, err, code) + local ret = ("%s failed"):format(tostring(name)) + if param ~= nil then + ret = ret .. (" for '%s'"):format(tostring(param)) + end + ret = ret .. (": %s"):format(tostring(err)) + if code ~= nil then + ret = ret .. (" (code %s)"):format(tostring(code)) + end + return ret +end + +--- Lua iterator over the entries of a given directory. +-- Implicit link to [`luafilesystem.dir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function dir +path.dir = lfs.dir + +--- Creates a directory. +-- Implicit link to [`luafilesystem.mkdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function mkdir +path.mkdir = function(d) + local ok, err, code = lfs.mkdir(d) + if not ok then + return ok, err_func("mkdir", d, err, code), code + end + return ok, err, code +end + +--- Removes a directory. +-- Implicit link to [`luafilesystem.rmdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function rmdir +path.rmdir = function(d) + local ok, err, code = lfs.rmdir(d) + if not ok then + return ok, err_func("rmdir", d, err, code), code + end + return ok, err, code +end + +--- Gets attributes. +-- Implicit link to [`luafilesystem.attributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function attrib +path.attrib = function(d, r) + local ok, err, code = attrib(d, r) + if not ok then + return ok, err_func("attrib", d, err, code), code + end + return ok, err, code +end + +--- Get the working directory. +-- Implicit link to [`luafilesystem.currentdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function currentdir +path.currentdir = function() + local ok, err, code = currentdir() + if not ok then + return ok, err_func("currentdir", nil, err, code), code + end + return ok, err, code +end + +--- Gets symlink attributes. +-- Implicit link to [`luafilesystem.symlinkattributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function link_attrib +path.link_attrib = function(d, r) + local ok, err, code = link_attrib(d, r) + if not ok then + return ok, err_func("link_attrib", d, err, code), code + end + return ok, err, code +end + +--- Changes the working directory. +-- On Windows, if a drive is specified, it also changes the current drive. If +-- only specifying the drive, it will only switch drive, but not modify the path. +-- Implicit link to [`luafilesystem.chdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference) +-- @function chdir +path.chdir = function(d) + local ok, err, code = lfs.chdir(d) + if not ok then + return ok, err_func("chdir", d, err, code), code + end + return ok, err, code +end + +--- is this a directory? +-- @string P A file path +function path.isdir(P) + assert_string(1,P) + if P:match("\\$") then + P = P:sub(1,-2) + end + return attrib(P,'mode') == 'directory' +end + +--- is this a file? +-- @string P A file path +function path.isfile(P) + assert_string(1,P) + return attrib(P,'mode') == 'file' +end + +-- is this a symbolic link? +-- @string P A file path +function path.islink(P) + assert_string(1,P) + if link_attrib then + return link_attrib(P,'mode')=='link' + else + return false + end +end + +--- return size of a file. +-- @string P A file path +function path.getsize(P) + assert_string(1,P) + return attrib(P,'size') +end + +--- does a path exist? +-- @string P A file path +-- @return the file path if it exists (either as file, directory, socket, etc), nil otherwise +function path.exists(P) + assert_string(1,P) + return attrib(P,'mode') ~= nil and P +end + +--- Return the time of last access as the number of seconds since the epoch. +-- @string P A file path +function path.getatime(P) + assert_string(1,P) + return attrib(P,'access') +end + +--- Return the time of last modification as the number of seconds since the epoch. +-- @string P A file path +function path.getmtime(P) + assert_string(1,P) + return attrib(P,'modification') +end + +---Return the system's ctime as the number of seconds since the epoch. +-- @string P A file path +function path.getctime(P) + assert_string(1,P) + return path.attrib(P,'change') +end + + +local function at(s,i) + return sub(s,i,i) +end + +path.is_windows = utils.is_windows + +local sep, other_sep, seps +-- constant sep is the directory separator for this platform. +-- constant dirsep is the separator in the PATH environment variable +if path.is_windows then + path.sep = '\\'; other_sep = '/' + path.dirsep = ';' + seps = { ['/'] = true, ['\\'] = true } +else + path.sep = '/' + path.dirsep = ':' + seps = { ['/'] = true } +end +sep = path.sep + +--- are we running Windows? +-- @class field +-- @name path.is_windows + +--- path separator for this platform. +-- @class field +-- @name path.sep + +--- separator for PATH for this platform +-- @class field +-- @name path.dirsep + +--- given a path, return the directory part and a file part. +-- if there's no directory part, the first value will be empty +-- @string P A file path +-- @return directory part +-- @return file part +-- @usage +-- local dir, file = path.splitpath("some/dir/myfile.txt") +-- assert(dir == "some/dir") +-- assert(file == "myfile.txt") +-- +-- local dir, file = path.splitpath("some/dir/") +-- assert(dir == "some/dir") +-- assert(file == "") +-- +-- local dir, file = path.splitpath("some_dir") +-- assert(dir == "") +-- assert(file == "some_dir") +function path.splitpath(P) + assert_string(1,P) + local i = #P + local ch = at(P,i) + while i > 0 and ch ~= sep and ch ~= other_sep do + i = i - 1 + ch = at(P,i) + end + if i == 0 then + return '',P + else + return sub(P,1,i-1), sub(P,i+1) + end +end + +--- return an absolute path. +-- @string P A file path +-- @string[opt] pwd optional start path to use (default is current dir) +function path.abspath(P,pwd) + assert_string(1,P) + if pwd then assert_string(2,pwd) end + local use_pwd = pwd ~= nil + if not use_pwd and not currentdir() then return P end + P = P:gsub('[\\/]$','') + pwd = pwd or currentdir() + if not path.isabs(P) then + P = path.join(pwd,P) + elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then + P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt' + end + return path.normpath(P) +end + +--- given a path, return the root part and the extension part. +-- if there's no extension part, the second value will be empty +-- @string P A file path +-- @treturn string root part (everything upto the "."", maybe empty) +-- @treturn string extension part (including the ".", maybe empty) +-- @usage +-- local file_path, ext = path.splitext("/bonzo/dog_stuff/cat.txt") +-- assert(file_path == "/bonzo/dog_stuff/cat") +-- assert(ext == ".txt") +-- +-- local file_path, ext = path.splitext("") +-- assert(file_path == "") +-- assert(ext == "") +function path.splitext(P) + assert_string(1,P) + local i = #P + local ch = at(P,i) + while i > 0 and ch ~= '.' do + if seps[ch] then + return P,'' + end + i = i - 1 + ch = at(P,i) + end + if i == 0 then + return P,'' + else + return sub(P,1,i-1),sub(P,i) + end +end + +--- return the directory part of a path +-- @string P A file path +-- @treturn string everything before the last dir-separator +-- @see splitpath +-- @usage +-- path.dirname("/some/path/file.txt") -- "/some/path" +-- path.dirname("file.txt") -- "" (empty string) +function path.dirname(P) + assert_string(1,P) + local p1 = path.splitpath(P) + return p1 +end + +--- return the file part of a path +-- @string P A file path +-- @treturn string +-- @see splitpath +-- @usage +-- path.basename("/some/path/file.txt") -- "file.txt" +-- path.basename("/some/path/file/") -- "" (empty string) +function path.basename(P) + assert_string(1,P) + local _,p2 = path.splitpath(P) + return p2 +end + +--- get the extension part of a path. +-- @string P A file path +-- @treturn string +-- @see splitext +-- @usage +-- path.extension("/some/path/file.txt") -- ".txt" +-- path.extension("/some/path/file_txt") -- "" (empty string) +function path.extension(P) + assert_string(1,P) + local _,p2 = path.splitext(P) + return p2 +end + +--- is this an absolute path? +-- @string P A file path +-- @usage +-- path.isabs("hello/path") -- false +-- path.isabs("/hello/path") -- true +-- -- Windows; +-- path.isabs("hello\path") -- false +-- path.isabs("\hello\path") -- true +-- path.isabs("C:\hello\path") -- true +-- path.isabs("C:hello\path") -- false +function path.isabs(P) + assert_string(1,P) + if path.is_windows and at(P,2) == ":" then + return seps[at(P,3)] ~= nil + end + return seps[at(P,1)] ~= nil +end + +--- return the path resulting from combining the individual paths. +-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following). +-- empty elements (except the last) will be ignored. +-- @string p1 A file path +-- @string p2 A file path +-- @string ... more file paths +-- @treturn string the combined path +-- @usage +-- path.join("/first","second","third") -- "/first/second/third" +-- path.join("first","second/third") -- "first/second/third" +-- path.join("/first","/second","third") -- "/second/third" +function path.join(p1,p2,...) + assert_string(1,p1) + assert_string(2,p2) + if select('#',...) > 0 then + local p = path.join(p1,p2) + local args = {...} + for i = 1,#args do + assert_string(i,args[i]) + p = path.join(p,args[i]) + end + return p + end + if path.isabs(p2) then return p2 end + local endc = at(p1,#p1) + if endc ~= path.sep and endc ~= other_sep and endc ~= "" then + p1 = p1..path.sep + end + return p1..p2 +end + +--- normalize the case of a pathname. On Unix, this returns the path unchanged, +-- for Windows it converts; +-- +-- * the path to lowercase +-- * forward slashes to backward slashes +-- @string P A file path +-- @usage path.normcase("/Some/Path/File.txt") +-- -- Windows: "\some\path\file.txt" +-- -- Others : "/Some/Path/File.txt" +function path.normcase(P) + assert_string(1,P) + if path.is_windows then + return P:gsub('/','\\'):lower() + else + return P + end +end + +--- normalize a path name. +-- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`. +-- +-- An empty path results in '.'. +-- @string P a file path +function path.normpath(P) + assert_string(1,P) + -- Split path into anchor and relative path. + local anchor = '' + if path.is_windows then + if P:match '^\\\\' then -- UNC + anchor = '\\\\' + P = P:sub(3) + elseif seps[at(P, 1)] then + anchor = '\\' + P = P:sub(2) + elseif at(P, 2) == ':' then + anchor = P:sub(1, 2) + P = P:sub(3) + if seps[at(P, 1)] then + anchor = anchor..'\\' + P = P:sub(2) + end + end + P = P:gsub('/','\\') + else + -- According to POSIX, in path start '//' and '/' are distinct, + -- but '///+' is equivalent to '/'. + if P:match '^//' and at(P, 3) ~= '/' then + anchor = '//' + P = P:sub(3) + elseif at(P, 1) == '/' then + anchor = '/' + P = P:match '^/*(.*)$' + end + end + local parts = {} + for part in P:gmatch('[^'..sep..']+') do + if part == '..' then + if #parts ~= 0 and parts[#parts] ~= '..' then + remove(parts) + else + append(parts, part) + end + elseif part ~= '.' then + append(parts, part) + end + end + P = anchor..concat(parts, sep) + if P == '' then P = '.' end + return P +end + +--- relative path from current directory or optional start point +-- @string P a path +-- @string[opt] start optional start point (default current directory) +function path.relpath (P,start) + assert_string(1,P) + if start then assert_string(2,start) end + local split,min,append = utils.split, math.min, table.insert + P = path.abspath(P,start) + start = start or currentdir() + local compare + if path.is_windows then + P = P:gsub("/","\\") + start = start:gsub("/","\\") + compare = function(v) return v:lower() end + else + compare = function(v) return v end + end + local startl, Pl = split(start,sep), split(P,sep) + local n = min(#startl,#Pl) + if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then + return P + end + local k = n+1 -- default value if this loop doesn't bail out! + for i = 1,n do + if compare(startl[i]) ~= compare(Pl[i]) then + k = i + break + end + end + local rell = {} + for i = 1, #startl-k+1 do rell[i] = '..' end + if k <= #Pl then + for i = k,#Pl do append(rell,Pl[i]) end + end + return table.concat(rell,sep) +end + + +--- Replace a starting '~' with the user's home directory. +-- In windows, if HOME isn't set, then USERPROFILE is used in preference to +-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows. +-- @string P A file path +function path.expanduser(P) + assert_string(1,P) + if at(P,1) == '~' then + local home = getenv('HOME') + if not home then -- has to be Windows + home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH') + end + return home..sub(P,2) + else + return P + end +end + + +---Return a suitable full path to a new temporary file name. +-- unlike os.tmpname(), it always gives you a writeable path (uses TEMP environment variable on Windows) +function path.tmpname () + local res = tmpnam() + -- On Windows if Lua is compiled using MSVC14 os.tmpname + -- already returns an absolute path within TEMP env variable directory, + -- no need to prepend it. + if path.is_windows and not res:find(':') then + res = getenv('TEMP')..res + end + return res +end + +--- return the largest common prefix path of two paths. +-- @string path1 a file path +-- @string path2 a file path +-- @return the common prefix (Windows: separators will be normalized, casing will be original) +function path.common_prefix (path1,path2) + assert_string(1,path1) + assert_string(2,path2) + -- get them in order! + if #path1 > #path2 then path2,path1 = path1,path2 end + local compare + if path.is_windows then + path1 = path1:gsub("/", "\\") + path2 = path2:gsub("/", "\\") + compare = function(v) return v:lower() end + else + compare = function(v) return v end + end + for i = 1,#path1 do + if compare(at(path1,i)) ~= compare(at(path2,i)) then + local cp = path1:sub(1,i-1) + if at(path1,i-1) ~= sep then + cp = path.dirname(cp) + end + return cp + end + end + if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end + return path1 + --return '' +end + +--- return the full path where a particular Lua module would be found. +-- Both package.path and package.cpath is searched, so the result may +-- either be a Lua file or a shared library. +-- @string mod name of the module +-- @return on success: path of module, lua or binary +-- @return on error: nil, error string listing paths tried +function path.package_path(mod) + assert_string(1,mod) + local res, err1, err2 + res, err1 = package.searchpath(mod,package.path) + if res then return res,true end + res, err2 = package.searchpath(mod,package.cpath) + if res then return res,false end + return raise ('cannot find module on path\n' .. err1 .. "\n" .. err2) +end + + +---- finis ----- +return path |