mirror of
https://github.com/TombEngine/TombEngine.git
synced 2025-04-29 00:07:58 +03:00
573 lines
16 KiB
Lua
573 lines
16 KiB
Lua
![]() |
--- 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)
|
||
|
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
|