TombEngine/Documentation/compiler/pl/app.lua

310 lines
11 KiB
Lua
Raw Permalink Normal View History

--- Application support functions.
-- See @{01-introduction.md.Application_Support|the Guide}
--
-- Dependencies: `pl.utils`, `pl.path`
-- @module pl.app
local io,package,require = _G.io, _G.package, _G.require
local utils = require 'pl.utils'
local path = require 'pl.path'
local app = {}
--- return the name of the current script running.
-- The name will be the name as passed on the command line
-- @return string filename
function app.script_name()
if _G.arg and _G.arg[0] then
return _G.arg[0]
end
return utils.raise("No script name found")
end
--- prefixes the current script's path to the Lua module path.
-- Applies to both the source and the binary module paths. It makes it easy for
-- the main file of a multi-file program to access its modules in the same directory.
-- `base` allows these modules to be put in a specified subdirectory, to allow for
-- cleaner deployment and resolve potential conflicts between a script name and its
-- library directory.
--
-- Note: the path is prefixed, so it is searched first when requiring modules.
-- @string base optional base directory (absolute, or relative path).
-- @bool nofollow always use the invocation's directory, even if the invoked file is a symlink
-- @treturn string the current script's path with a trailing slash
function app.require_here (base, nofollow)
local p = app.script_name()
if not path.isabs(p) then
p = path.join(path.currentdir(),p)
end
if not nofollow then
local t = path.link_attrib(p)
if t and t.mode == 'link' then
t = t.target
if not path.isabs(t) then
t = path.join(path.dirname(p), t)
end
p = t
end
end
p = path.normpath(path.dirname(p))
if p:sub(-1,-1) ~= path.sep then
p = p..path.sep
end
if base then
if path.is_windows then
base = base:gsub('/','\\')
end
if path.isabs(base) then
p = base .. path.sep
else
p = p..base..path.sep
end
end
local so_ext = path.is_windows and 'dll' or 'so'
local lsep = package.path:find '^;' and '' or ';'
local csep = package.cpath:find '^;' and '' or ';'
package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
return p
end
--- return a suitable path for files private to this application.
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
-- SNAME is the name of the script without .lua extension.
-- If the directory does not exist, it will be created.
-- @string file a filename (w/out path)
-- @return a full pathname, or nil
-- @return cannot create directory error
-- @usage
-- -- when run from a script called 'testapp' (on Windows):
-- local app = require 'pl.app'
-- print(app.appfile 'test.txt')
-- -- C:\Documents and Settings\steve\.testapp\test.txt
function app.appfile(file)
local sfullname, err = app.script_name()
if not sfullname then return utils.raise(err) end
local sname = path.basename(sfullname)
local name = path.splitext(sname)
local dir = path.join(path.expanduser('~'),'.'..name)
if not path.isdir(dir) then
local ret = path.mkdir(dir)
if not ret then return utils.raise('cannot create '..dir) end
end
return path.join(dir,file)
end
--- return string indicating operating system.
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
function app.platform()
if path.is_windows then
return 'Windows'
else
local f = io.popen('uname')
local res = f:read()
if res == 'Darwin' then res = 'OSX' end
f:close()
return res
end
end
--- return the full command-line used to invoke this script.
-- It will not include the scriptname itself, see `app.script_name`.
-- @return command-line
-- @return name of Lua program used
-- @usage
-- -- execute: lua -lluacov -e 'print(_VERSION)' myscript.lua
--
-- -- myscript.lua
-- print(require("pl.app").lua()) --> "lua -lluacov -e 'print(_VERSION)'", "lua"
function app.lua()
local args = _G.arg
if not args then
return utils.raise "not in a main program"
end
local cmd = {}
local i = -1
while true do
table.insert(cmd, 1, args[i])
if not args[i-1] then
return utils.quote_arg(cmd), args[i]
end
i = i - 1
end
end
--- parse command-line arguments into flags and parameters.
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
--
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`),
-- a number value can be given without a space. If the flag is marked
-- as having a value, then a space-separated value is also accepted (`-i hello`),
-- see the `flags_with_values` argument).
--
-- Multiple short args can be combined like so: ( `-abcd`).
--
-- When specifying the `flags_valid` parameter, its contents can also contain
-- aliasses, to convert short/long flags to the same output name. See the
-- example below.
--
-- Note: if a flag is repeated, the last value wins.
-- @tparam {string} args an array of strings (default is the global `arg`)
-- @tab flags_with_values any flags that take values, either list or hash
-- table e.g. `{ out=true }` or `{ "out" }`.
-- @tab flags_valid (optional) flags that are valid, either list or hashtable.
-- If not given, everything
-- will be accepted(everything in `flags_with_values` will automatically be allowed)
-- @return a table of flags (flag=value pairs)
-- @return an array of parameters
-- @raise if args is nil, then the global `args` must be available!
-- @usage
-- -- Simple form:
-- local flags, params = app.parse_args(nil,
-- { "hello", "world" }, -- list of flags taking values
-- { "l", "a", "b"}) -- list of allowed flags (value ones will be added)
--
-- -- More complex example using aliasses:
-- local valid = {
-- long = "l", -- if 'l' is specified, it is reported as 'long'
-- new = { "n", "old" }, -- here both 'n' and 'old' will go into 'new'
-- }
-- local values = {
-- "value", -- will automatically be added to the allowed set of flags
-- "new", -- will mark 'n' and 'old' as requiring a value as well
-- }
-- local flags, params = app.parse_args(nil, values, valid)
--
-- -- command: myapp.lua -l --old:hello --value world param1 param2
-- -- will yield:
-- flags = {
-- long = true, -- input from 'l'
-- new = "hello", -- input from 'old'
-- value = "world", -- allowed because it was in 'values', note: space separated!
-- }
-- params = {
-- [1] = "param1"
-- [2] = "param2"
-- }
function app.parse_args (args,flags_with_values, flags_valid)
if not args then
args = _G.arg
if not args then utils.raise "Not in a main program: 'arg' not found" end
end
local with_values = {}
for k,v in pairs(flags_with_values or {}) do
if type(k) == "number" then
k = v
end
with_values[k] = true
end
local valid
if not flags_valid then
-- if no allowed flags provided, we create a table that always returns
-- the keyname, no matter what you look up
valid = setmetatable({},{ __index = function(_, key) return key end })
else
valid = {}
for k,aliasses in pairs(flags_valid) do
if type(k) == "number" then -- array/list entry
k = aliasses
end
if type(aliasses) == "string" then -- single alias
aliasses = { aliasses }
end
if type(aliasses) == "table" then -- list of aliasses
-- it's the alternate name, so add the proper mappings
for i, alias in ipairs(aliasses) do
valid[alias] = k
end
end
valid[k] = k
end
do
local new_with_values = {} -- needed to prevent "invalid key to 'next'" error
for k,v in pairs(with_values) do
if not valid[k] then
valid[k] = k -- add the with_value entry as a valid one
new_with_values[k] = true
else
new_with_values[valid[k]] = true --set, but by its alias
end
end
with_values = new_with_values
end
end
-- now check that all flags with values are reported as such under all
-- of their aliasses
for k, main_alias in pairs(valid) do
if with_values[main_alias] then
with_values[k] = true
end
end
local _args = {}
local flags = {}
local i = 1
while i <= #args do
local a = args[i]
local v = a:match('^-(.+)')
local is_long
if not v then
-- we have a parameter
_args[#_args+1] = a
else
-- it's a flag
if v:find '^-' then
is_long = true
v = v:sub(2)
end
if with_values[v] then
if i == #args or args[i+1]:find '^-' then
return utils.raise ("no value for '"..v.."'")
end
flags[valid[v]] = args[i+1]
i = i + 1
else
-- a value can also be indicated with = or :
local var,val = utils.splitv (v,'[=:]', false, 2)
var = var or v
val = val or true
if not is_long then
if #var > 1 then
if var:find '.%d+' then -- short flag, number value
val = var:sub(2)
var = var:sub(1,1)
else -- multiple short flags
for i = 1,#var do
local f = var:sub(i,i)
if not valid[f] then
return utils.raise("unknown flag '"..f.."'")
else
f = valid[f]
end
flags[f] = true
end
val = nil -- prevents use of var as a flag below
end
else -- single short flag (can have value, defaults to true)
val = val or true
end
end
if val then
if not valid[var] then
return utils.raise("unknown flag '"..var.."'")
else
var = valid[var]
end
flags[var] = val
end
end
end
i = i + 1
end
return flags,_args
end
return app