|
|
|
--- Common functions for strings.
|
|
|
|
--
|
|
|
|
-- @module gear.strings
|
|
|
|
-- @copyright 2022 The DoubleFourteen Code Forge
|
|
|
|
-- @author Lorenzo Cogotti
|
|
|
|
|
|
|
|
local strings = {}
|
|
|
|
|
|
|
|
-- Platform preferred path separator
|
|
|
|
local SEP = package.config:sub(1,1)
|
|
|
|
local DOT = string.byte('.')
|
|
|
|
local SLASH = string.byte('/')
|
|
|
|
local BACKSLASH = string.byte('\\')
|
|
|
|
local COLON = string.byte(':')
|
|
|
|
|
|
|
|
--- Find file extension within path.
|
|
|
|
--
|
|
|
|
-- @string path a file path
|
|
|
|
-- @string[opt] sep path separator pattern, any combination of '/', '\\' or ':' to support various OSes, defaults to all
|
|
|
|
-- @treturn number extension position within path if an extension is found, nil otherwise
|
|
|
|
function strings.findpathext(path, sep)
|
|
|
|
sep = sep or '/\\:'
|
|
|
|
for i = 1,#sep do
|
|
|
|
local byt = sep:byte(i)
|
|
|
|
if byt ~= SLASH and byt ~= BACKSLASH and byt ~= COLON then
|
|
|
|
error(("Unsupported path separator pattern: %q"):format(sep))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function issep(byt)
|
|
|
|
for i = 1,#sep do
|
|
|
|
if byt == sep:byte(i) then return true end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local pos = nil
|
|
|
|
|
|
|
|
for i = #path,2,-1 do
|
|
|
|
local byt = path:byte(i)
|
|
|
|
|
|
|
|
if byt == DOT and not issep(path:byte(i-1)) then
|
|
|
|
-- Update extension position
|
|
|
|
pos = i
|
|
|
|
elseif issep(byt) then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return pos
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Set default file extension.
|
|
|
|
--
|
|
|
|
-- If path contains an extension, returns the path unaltered,
|
|
|
|
-- otherwise set extension to the specified one.
|
|
|
|
--
|
|
|
|
-- @string path a file path
|
|
|
|
-- @string ext default extension to be set
|
|
|
|
-- @string[opt] sep path separator pattern, any combination of '/', '\\' or ':' to support various OSes, defaults to all
|
|
|
|
-- @treturn string updated path, and separator position
|
|
|
|
function strings.setdefpathext(path, ext, sep)
|
|
|
|
if ext:byte(1) ~= DOT then
|
|
|
|
error(("Bad extension %q: must be a string starting with '.'"):format(ext))
|
|
|
|
end
|
|
|
|
|
|
|
|
local pos = strings.findpathext(path, sep)
|
|
|
|
if not pos then
|
|
|
|
-- Append default extension
|
|
|
|
pos = #path
|
|
|
|
path = path..ext
|
|
|
|
end
|
|
|
|
|
|
|
|
return path, pos
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Set file extension.
|
|
|
|
--
|
|
|
|
-- @string path a file path
|
|
|
|
-- @string ext extension to be set (including '.')
|
|
|
|
-- @string[opt] sep path separator pattern, any combination of '/', '\\' or ':' to support various OSes, defaults to all
|
|
|
|
-- @treturn string updated path and separator position
|
|
|
|
function strings.setpathext(path, ext, sep)
|
|
|
|
if ext:byte(1) ~= DOT then
|
|
|
|
error(("Bad extension %q: must be a string starting with '.'"):format(ext))
|
|
|
|
end
|
|
|
|
|
|
|
|
local pos = strings.findpathext(path, sep)
|
|
|
|
if pos then
|
|
|
|
-- Trim existing extension
|
|
|
|
path = path:sub(1, pos-1)
|
|
|
|
end
|
|
|
|
|
|
|
|
return path..ext, pos
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Get file extension.
|
|
|
|
--
|
|
|
|
-- @string path a file path
|
|
|
|
-- @string[opt] sep path separator pattern, any combination of '/', '\\' or ':' to support various OSes, defaults to all
|
|
|
|
-- @treturn string file extension and position, if any is found, nil otherwise
|
|
|
|
function strings.getpathext(path, sep)
|
|
|
|
local pos = strings.findpathext(path, sep)
|
|
|
|
if pos then return path:sub(pos), pos end
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Remove redundant slashes and resolve dot and dot-dots in path.
|
|
|
|
--
|
|
|
|
-- @string path a file path
|
|
|
|
-- @string[opt] sep separator pattern, '/' for Unix, '\\' for Windows (default)
|
|
|
|
-- @string[optchain] osep target separator pattern, '/' for Unix, '\\' for Windows, uses the platform preferred path separator by default.
|
|
|
|
-- @treturn string cleared path
|
|
|
|
function strings.clearpath(path, sep, osep)
|
|
|
|
sep = sep or '\\' -- conservative, both / and \ as seps
|
|
|
|
osep = osep or SEP
|
|
|
|
|
|
|
|
local dot, dotdot, sepsub, esub
|
|
|
|
if sep == '\\' then
|
|
|
|
-- Windows style separator pattern
|
|
|
|
dot, dotdot = '[\\/]+%.?[\\/]', '[^\\/]+[\\/]%.%.[\\/]?'
|
|
|
|
sepsub, esub = '[\\/]', '[\\/]$'
|
|
|
|
elseif sep == '/' then
|
|
|
|
-- Unix like separators only
|
|
|
|
dot, dotdot = '/+%.?/', '[^/]+/%.%./?'
|
|
|
|
sepsub, esub = '/', '/$'
|
|
|
|
else
|
|
|
|
error("Unsupported separator pattern: "..tostring(sep))
|
|
|
|
end
|
|
|
|
if osep ~= '\\' and osep ~= '/' then
|
|
|
|
error("Unsupported target separator pattern: "..tostring(osep))
|
|
|
|
end
|
|
|
|
|
|
|
|
local k
|
|
|
|
|
|
|
|
repeat -- /./ -> /
|
|
|
|
path,k = path:gsub(dot, osep, 1)
|
|
|
|
until k == 0
|
|
|
|
|
|
|
|
repeat -- A/../ -> (empty)
|
|
|
|
path,k = path:gsub(dotdot, '', 1)
|
|
|
|
until k == 0
|
|
|
|
|
|
|
|
-- Make separators consistent
|
|
|
|
path = path:gsub(sepsub, osep)
|
|
|
|
path = path:gsub(esub, '') -- never leave trailing separator
|
|
|
|
return path == '' and '.' or path
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Split path into components: directory, basename, extension.
|
|
|
|
--
|
|
|
|
-- @string path path to be split
|
|
|
|
-- @treturn string directory name (including separator), '' if none was found in path
|
|
|
|
-- @treturn string file name without extension, '' if none was found in path
|
|
|
|
-- @treturn string file extension including '.', '' if none was found in path
|
|
|
|
function strings.splitpath(path)
|
|
|
|
return path:match("(.-)([^\\/]-)(%.?[^%.\\/]*)$")
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Test whether the path is absolute.
|
|
|
|
--
|
|
|
|
-- @string path a path to be tested
|
|
|
|
-- @string[opt] sep separator pattern, '/' for Unix, '\\' for Windows, if none is provided, path is tested for both
|
|
|
|
-- @treturn boolean true if path is absolute, false otherwise
|
|
|
|
function strings.isabspath(path, sep)
|
|
|
|
if sep == nil then
|
|
|
|
-- Conservative test, both Unix-style and Windows-style
|
|
|
|
return strings.isabspath(path, '/') or strings.isabspath(path, '\\')
|
|
|
|
elseif sep == '/' then
|
|
|
|
-- Unix-style
|
|
|
|
return #path >= 1 and path:byte(1) == SLASH
|
|
|
|
elseif sep == '\\' then
|
|
|
|
-- Windows-style
|
|
|
|
return
|
|
|
|
(#path >= 1 and path:byte(1) == BACKSLASH) or
|
|
|
|
(#path >= 3 and path:byte(2) == COLON and path:byte(3) == BACKSLASH)
|
|
|
|
else
|
|
|
|
error(("Unsupported separator pattern: %q"):format(sep))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Test whether a string starts with a prefix.
|
|
|
|
--
|
|
|
|
-- This is an optimized version of: return s:sub(1, #prefix) == prefix.
|
|
|
|
--
|
|
|
|
-- @string s string to be tested
|
|
|
|
-- @string prefix prefix to test for
|
|
|
|
-- @treturn bool true if prefix is found, false otherwise
|
|
|
|
function strings.startswith(s, prefix)
|
|
|
|
for i = 1,#prefix do
|
|
|
|
if s:byte(i) ~= prefix:byte(i) then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Test whether a string ends with a trailing suffix.
|
|
|
|
--
|
|
|
|
-- This is an optimized version of: return trailing == ""
|
|
|
|
-- or s:sub(-#trailing) == trailing.
|
|
|
|
--
|
|
|
|
-- @string s string to be tested
|
|
|
|
-- @string trailing suffix to test for
|
|
|
|
-- @treturn bool true if suffix is found, false otherwise
|
|
|
|
function strings.endswith(s, trailing)
|
|
|
|
local n1,n2 = #s,#trailing
|
|
|
|
|
|
|
|
for i = 0,n2-1 do
|
|
|
|
if s:byte(n1-i) ~= trailing:byte(n2-i) then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
return strings
|