LuaComp/build/luacomp-jit-static.lua

2045 lines
52 KiB
Lua

#!/usr/bin/env luajit
--[[
staticinit.lua - Main file of LuaComp, directly includes argparse.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local function _sv(k, v)
_G[k] = v
--os.setenv(k, tostring(v))
end
_sv("LUACOMP_V_MAJ", 1)
_sv("LUACOMP_V_MIN", 1)
_sv("LUACOMP_V_PAT", 0)
_sv("LUACOMP_VERSION", LUACOMP_V_MAJ.."."..LUACOMP_V_MIN.."."..LUACOMP_V_PAT)
_sv("LUACOMP_NAME", "LuaComp")
--[[
ast.lua - Generates a structure for use in preprocessing.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local function nextc(f, c)
c = c or 1
return f:read(c)
end
local function peek(f, c)
c = c or 1
local z = f:read(c)
f:seek("cur", -c)
return z
end
local function skip(f, c)
c = c or 1
return f:seek("cur", c)
end
local ws = {
["\t"] = true,
[" "] = true
}
local function parse_hex(f)
local lc = " "
local hex = ""
while (48 <= lc:byte() and lc:byte() <= 57) or (97 <= lc:byte() and lc:byte() <= 102) or (65 <= lc:byte() and lc:byte() <= 70) do
lc = nextc(f)
if (48 <= lc:byte() and lc:byte()) <= 57 or (97 <= lc:byte() and lc:byte() <= 102) or (65 <= lc:byte() and lc:byte() <= 70) then
hex = hex .. lc
end
end
return tonumber(hex, 16)
end
local function parse_number(f, c)
local lc = " "
local num = c
while 48 <= lc:byte() and lc:byte() <= 57 do
lc = nextc(f)
if (48 <= lc:byte() and lc:byte() <= 57) then
num = num .. lc
end
end
return tonumber(hex, 10)
end
local esct = {
["t"] = "\t",
["n"] = "\n",
["r"] = "\r",
["\\"] = "\\\\"
}
for i=0, 9 do
esct[tostring(i)] = string.char(i)
end
local function parse_dblquote(f)
local val = ""
while peek(f) ~= "\"" do
local c = nextc(f)
if (peek(f) == "\n" or peek(f) == "\r") then
return nil, "Unexpected end of line"
end
if (c == "\\") then
if (esct[peek(f)]) then
c = esct[peek(f)]
skip(f)
else
c = nextc(f)
end
end
val = val .. c
end
skip(f)
return val
end
local function parse_snglquote(f)
local val = ""
while peek(f) ~= "\'" do
local c = nextc(f)
if (peek(f) == "\n" or peek(f) == "\r") then
return nil, "Unexpected end of line"
end
if (c == "\\") then
if (esct[peek(f)]) then
c = esct[peek(f)]
skip(f)
else
c = nextc(f)
end
end
val = val .. c
end
skip(f)
return val
end
local function parse_envarg(f)
local val = ""
while peek(f) ~= ")" do
if (peek(f) == "\n" or peek(f) == "\r") then
return nil, "Unexpected end of line"
end
val = val .. nextc(f)
end
skip(f)
return val
end
local function parse_directive(f)
local lc = "_"
local name = ""
local args = {}
local carg = ""
while not ws[lc] do
lc = nextc(f)
if (lc == "\n" or lc == "\r") then
if (lc == "\r" and peek(f) == "\n") then skip(f) end
return {type="directive", name=name}
elseif not ws[lc] then
name = name .. lc
end
end
while true do
lc = nextc(f)
if (lc == "\n" or lc == "\r") then
if (lc == "\r" and peek(f) == "\n") then skip(f) end
return {type="directive", name=name, args=args}
elseif lc == "0" and peek(f) == "x" then
skip(f)
local val = parse_hex(f)
args[#args+1] = val
elseif 48 <= lc:byte() and lc:byte() <= 57 then
local val = parse_number(f, lc)
args[#args+1] = val
elseif lc == "\"" then
local val, e = parse_dblquote(f)
if not val then return val, e end
args[#args+1] = val
elseif lc == "\'" then
local val, e = parse_snglquote(f)
if not val then return val, e end
args[#args+1] = val
elseif lc == "$" and peek(f) == "(" then
skip(f)
local val = parse_envarg(f)
if not os.getenv(val) then return nil, "Enviroment variable `"..val.."' does not exist." end
args[#args+1] = os.getenv(val)
elseif not ws[lc] then
return nil, "Syntax error"
end
end
end
local function mkast(f, n)
io.stderr:write("PROC\t",n,"\n")
local lc = " "
local lpos = 1
local ilpos = 1
local tree = {}
local code = ""
local branches = {}
local function add_code()
tree[#tree+1] = {type="code", data=code, file=n, line=lpos}
code = ""
end
local function parse_error(e)
io.stderr:write("ERROR:"..n..":"..lpos..": "..e.."\n")
os.exit(1)
end
while lc and lc ~= "" do
lc = nextc(f)
if (lc == "-" and ilpos == 1) then
if (peek(f, 2) == "-#") then --Directive
add_code()
skip(f, 2)
local d, r = parse_directive(f)
if not d then
parse_error(r)
end
d.line = lpos
d.file = n
lpos = lpos+1
tree[#tree+1] = d
else
code = code .. lc
ilpos = ilpos+1
end
elseif (lc == "/" and ilpos == 1) then
if (peek(f, 2) == "/#") then --Directive
add_code()
skip(f, 2)
local d, r = parse_directive(f)
if not d then
parse_error(r)
end
d.line = lpos
d.file = n
lpos = lpos+1
tree[#tree+1] = d
else
code = code .. lc
ilpos = ilpos+1
end
elseif (lc == "$" and peek(f) == "(") then
add_code()
skip(f)
local val, e = parse_envarg(f)
if not val then
parse_error(e)
end
tree[#tree+1] = {type="envvar", var=val, file=n, line=lpos}
elseif (lc == "@" and peek(f, 2) == "[[") then
add_code()
skip(f, 2)
local val = ""
while peek(f, 2) ~= "]]" do
val = val .. nextc(f)
end
tree[#tree+1] = {type="lua", code=val, file=n, line=lpos}
skip(f, 2)
elseif (lc == "@" and peek(f, 2) == "[{") then
add_code()
skip(f, 2)
local val = ""
while peek(f, 2) ~= "}]" do
val = val .. nextc(f)
end
tree[#tree+1] = {type="lua_r", code=val, file=n, line=lpos}
skip(f, 2)
elseif (lc == "\r" or lc == "\n") then
if (lc == "\r" and peek(f) == "\n") then
skip(f)
end
lpos = lpos+1
ilpos = 1
code = code .. "\n"
else
code = code .. (lc or "")
ilpos = ilpos+1
end
end
add_code()
return tree
end
--[[
generator.lua - Generates the code.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local function lua_escape(code)
return code:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n")
end
local directives = {}
local function generate(ast)
local lua_code = ""
for i=1, #ast do
local leaf = ast[i]
if (leaf.type == "lua") then
lua_code = lua_code .. leaf.code
elseif (leaf.type == "directive") then
local stargs = {}
for i=1, #leaf.args do
local arg = leaf.args[i]
if (type(arg) == "string") then
stargs[i] = "\""..lua_escape(arg).."\""
elseif (type(arg) == "number") then
stargs[i] = tostring(arg)
end
end
lua_code = lua_code .. "call_directive(\""..leaf.file..":"..tostring(leaf.line).."\",\""..leaf.name.."\","..table.concat(stargs, ",")..")"
elseif (leaf.type == "envvar") then
lua_code = lua_code .. "put_env(\""..leaf.file..":"..tostring(leaf.line).."\",\""..leaf.var.."\")"
elseif (leaf.type == "code") then
lua_code = lua_code .. "put_code(\""..leaf.file..":"..tostring(leaf.line).."\",\"" .. lua_escape(leaf.data) .. "\")"
elseif (leaf.type == "lua_r") then
lua_code = lua_code .. "put_code(\""..leaf.file..":"..tostring(leaf.line).."\",tostring("..leaf.code.."))"
else
io.stderr:write("ERROR: Internal catastrophic failure, unknown type "..leaf.type.."\n")
os.exit(1)
end
lua_code = lua_code .. "\n"
end
local env = {code = ""}
local function run_away_screaming(fpos, err)
io.stdout:write("ERROR: "..fpos..": "..err.."\n")
os.exit(1)
end
local function call_directive(fpos, dname, ...)
if (not directives[dname]) then
run_away_screaming(fpos, "Invalid directive name `"..dname.."'")
end
local r, er = directives[dname](env, ...)
if (not r) then
run_away_screaming(fpos, er)
end
end
local function put_env(fpos, evar)
local e = os.getenv(evar)
if not e then
run_away_screaming(fpos, "Enviroment variable `"..evar.."' does not exist!")
end
env.code = env.code .. "\""..lua_escape(e).."\""
end
local function put_code(fpos, code)
env.code = env.code .. code --not much that can fail here...
end
local fenv = {}
for k, v in pairs(_G) do
fenv[k] = v
end
fenv._G = fenv
fenv._ENV = fenv
fenv.call_directive = call_directive
fenv.put_code = put_code
fenv.put_env = put_env
local func = assert(load(lua_code, "=(generated code)", "t", fenv))
func()
return env.code
end
--[[
directive_provider.lua - Provides preprocessor directives
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
---#include "directives/define.lua"
function directives.include(env, file)
if (not os.execute("stat "..file..">/dev/null")) then
return false, "File `"..file.."' does not exist!"
end
local f = io.open(file, "r")
local fast = mkast(f, file)
local code = generate(fast)
env.code = env.code .. "\n" .. code .. "\n"
return true
end
local warned = false
function directives.loadmod(env, mod)
if not warned then
io.stderr:write("Warning: loadmod is depreciated and unsafe. The API differs from luapreproc.\n")
warned = true
end
if (not os.execute("stat "..file..">/dev/null")) then
return false, "Module `"..file.."' does not exist!"
end
local modname, func = dofile(mod)
directives[modname] = func
return true
end
--[[
cfg/minifier_providers.lua - Provides minifier providers.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local providers = {}
function providers.luamin(cin)
local fn = os.tmpname()
local fh = io.open(fn, "w")
fh:write(cin)
fh:close()
local lmh = io.popen("luamin -f "..fn.." 2>&1", "r")
local dat = lmh:read("*a")
local stat, _, code = lmh:close()
if (code ~= 0) then
return false, dat
end
return dat
end
function providers.none(cin)
return cin
end
local argparse = (function()
-- The MIT License (MIT)
-- Copyright (c) 2013 - 2018 Peter Melnichenko
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-- the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function deep_update(t1, t2)
for k, v in pairs(t2) do
if type(v) == "table" then
v = deep_update({}, v)
end
t1[k] = v
end
return t1
end
-- A property is a tuple {name, callback}.
-- properties.args is number of properties that can be set as arguments
-- when calling an object.
local function class(prototype, properties, parent)
-- Class is the metatable of its instances.
local cl = {}
cl.__index = cl
if parent then
cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
else
cl.__prototype = prototype
end
if properties then
local names = {}
-- Create setter methods and fill set of property names.
for _, property in ipairs(properties) do
local name, callback = property[1], property[2]
cl[name] = function(self, value)
if not callback(self, value) then
self["_" .. name] = value
end
return self
end
names[name] = true
end
function cl.__call(self, ...)
-- When calling an object, if the first argument is a table,
-- interpret keys as property names, else delegate arguments
-- to corresponding setters in order.
if type((...)) == "table" then
for name, value in pairs((...)) do
if names[name] then
self[name](self, value)
end
end
else
local nargs = select("#", ...)
for i, property in ipairs(properties) do
if i > nargs or i > properties.args then
break
end
local arg = select(i, ...)
if arg ~= nil then
self[property[1]](self, arg)
end
end
end
return self
end
end
-- If indexing class fails, fallback to its parent.
local class_metatable = {}
class_metatable.__index = parent
function class_metatable.__call(self, ...)
-- Calling a class returns its instance.
-- Arguments are delegated to the instance.
local object = deep_update({}, self.__prototype)
setmetatable(object, self)
return object(...)
end
return setmetatable(cl, class_metatable)
end
local function typecheck(name, types, value)
for _, type_ in ipairs(types) do
if type(value) == type_ then
return true
end
end
error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value)))
end
local function typechecked(name, ...)
local types = {...}
return {name, function(_, value) typecheck(name, types, value) end}
end
local multiname = {"name", function(self, value)
typecheck("name", {"string"}, value)
for alias in value:gmatch("%S+") do
self._name = self._name or alias
table.insert(self._aliases, alias)
end
-- Do not set _name as with other properties.
return true
end}
local function parse_boundaries(str)
if tonumber(str) then
return tonumber(str), tonumber(str)
end
if str == "*" then
return 0, math.huge
end
if str == "+" then
return 1, math.huge
end
if str == "?" then
return 0, 1
end
if str:match "^%d+%-%d+$" then
local min, max = str:match "^(%d+)%-(%d+)$"
return tonumber(min), tonumber(max)
end
if str:match "^%d+%+$" then
local min = str:match "^(%d+)%+$"
return tonumber(min), math.huge
end
end
local function boundaries(name)
return {name, function(self, value)
typecheck(name, {"number", "string"}, value)
local min, max = parse_boundaries(value)
if not min then
error(("bad property '%s'"):format(name))
end
self["_min" .. name], self["_max" .. name] = min, max
end}
end
local actions = {}
local option_action = {"action", function(_, value)
typecheck("action", {"function", "string"}, value)
if type(value) == "string" and not actions[value] then
error(("unknown action '%s'"):format(value))
end
end}
local option_init = {"init", function(self)
self._has_init = true
end}
local option_default = {"default", function(self, value)
if type(value) ~= "string" then
self._init = value
self._has_init = true
return true
end
end}
local add_help = {"add_help", function(self, value)
typecheck("add_help", {"boolean", "string", "table"}, value)
if self._has_help then
table.remove(self._options)
self._has_help = false
end
if value then
local help = self:flag()
:description "Show this help message and exit."
:action(function()
print(self:get_help())
os.exit(0)
end)
if value ~= true then
help = help(value)
end
if not help._name then
help "-h" "--help"
end
self._has_help = true
end
end}
local Parser = class({
_arguments = {},
_options = {},
_commands = {},
_mutexes = {},
_groups = {},
_require_command = true,
_handle_options = true
}, {
args = 3,
typechecked("name", "string"),
typechecked("description", "string"),
typechecked("epilog", "string"),
typechecked("usage", "string"),
typechecked("help", "string"),
typechecked("require_command", "boolean"),
typechecked("handle_options", "boolean"),
typechecked("action", "function"),
typechecked("command_target", "string"),
typechecked("help_vertical_space", "number"),
typechecked("usage_margin", "number"),
typechecked("usage_max_width", "number"),
typechecked("help_usage_margin", "number"),
typechecked("help_description_margin", "number"),
typechecked("help_max_width", "number"),
add_help
})
local Command = class({
_aliases = {}
}, {
args = 3,
multiname,
typechecked("description", "string"),
typechecked("epilog", "string"),
typechecked("target", "string"),
typechecked("usage", "string"),
typechecked("help", "string"),
typechecked("require_command", "boolean"),
typechecked("handle_options", "boolean"),
typechecked("action", "function"),
typechecked("command_target", "string"),
typechecked("help_vertical_space", "number"),
typechecked("usage_margin", "number"),
typechecked("usage_max_width", "number"),
typechecked("help_usage_margin", "number"),
typechecked("help_description_margin", "number"),
typechecked("help_max_width", "number"),
typechecked("hidden", "boolean"),
add_help
}, Parser)
local Argument = class({
_minargs = 1,
_maxargs = 1,
_mincount = 1,
_maxcount = 1,
_defmode = "unused",
_show_default = true
}, {
args = 5,
typechecked("name", "string"),
typechecked("description", "string"),
option_default,
typechecked("convert", "function", "table"),
boundaries("args"),
typechecked("target", "string"),
typechecked("defmode", "string"),
typechecked("show_default", "boolean"),
typechecked("argname", "string", "table"),
typechecked("hidden", "boolean"),
option_action,
option_init
})
local Option = class({
_aliases = {},
_mincount = 0,
_overwrite = true
}, {
args = 6,
multiname,
typechecked("description", "string"),
option_default,
typechecked("convert", "function", "table"),
boundaries("args"),
boundaries("count"),
typechecked("target", "string"),
typechecked("defmode", "string"),
typechecked("show_default", "boolean"),
typechecked("overwrite", "boolean"),
typechecked("argname", "string", "table"),
typechecked("hidden", "boolean"),
option_action,
option_init
}, Argument)
function Parser:_inherit_property(name, default)
local element = self
while true do
local value = element["_" .. name]
if value ~= nil then
return value
end
if not element._parent then
return default
end
element = element._parent
end
end
function Argument:_get_argument_list()
local buf = {}
local i = 1
while i <= math.min(self._minargs, 3) do
local argname = self:_get_argname(i)
if self._default and self._defmode:find "a" then
argname = "[" .. argname .. "]"
end
table.insert(buf, argname)
i = i+1
end
while i <= math.min(self._maxargs, 3) do
table.insert(buf, "[" .. self:_get_argname(i) .. "]")
i = i+1
if self._maxargs == math.huge then
break
end
end
if i < self._maxargs then
table.insert(buf, "...")
end
return buf
end
function Argument:_get_usage()
local usage = table.concat(self:_get_argument_list(), " ")
if self._default and self._defmode:find "u" then
if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then
usage = "[" .. usage .. "]"
end
end
return usage
end
function actions.store_true(result, target)
result[target] = true
end
function actions.store_false(result, target)
result[target] = false
end
function actions.store(result, target, argument)
result[target] = argument
end
function actions.count(result, target, _, overwrite)
if not overwrite then
result[target] = result[target] + 1
end
end
function actions.append(result, target, argument, overwrite)
result[target] = result[target] or {}
table.insert(result[target], argument)
if overwrite then
table.remove(result[target], 1)
end
end
function actions.concat(result, target, arguments, overwrite)
if overwrite then
error("'concat' action can't handle too many invocations")
end
result[target] = result[target] or {}
for _, argument in ipairs(arguments) do
table.insert(result[target], argument)
end
end
function Argument:_get_action()
local action, init
if self._maxcount == 1 then
if self._maxargs == 0 then
action, init = "store_true", nil
else
action, init = "store", nil
end
else
if self._maxargs == 0 then
action, init = "count", 0
else
action, init = "append", {}
end
end
if self._action then
action = self._action
end
if self._has_init then
init = self._init
end
if type(action) == "string" then
action = actions[action]
end
return action, init
end
-- Returns placeholder for `narg`-th argument.
function Argument:_get_argname(narg)
local argname = self._argname or self:_get_default_argname()
if type(argname) == "table" then
return argname[narg]
else
return argname
end
end
function Argument:_get_default_argname()
return "<" .. self._name .. ">"
end
function Option:_get_default_argname()
return "<" .. self:_get_default_target() .. ">"
end
-- Returns labels to be shown in the help message.
function Argument:_get_label_lines()
return {self._name}
end
function Option:_get_label_lines()
local argument_list = self:_get_argument_list()
if #argument_list == 0 then
-- Don't put aliases for simple flags like `-h` on different lines.
return {table.concat(self._aliases, ", ")}
end
local longest_alias_length = -1
for _, alias in ipairs(self._aliases) do
longest_alias_length = math.max(longest_alias_length, #alias)
end
local argument_list_repr = table.concat(argument_list, " ")
local lines = {}
for i, alias in ipairs(self._aliases) do
local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr
if i ~= #self._aliases then
line = line .. ","
end
table.insert(lines, line)
end
return lines
end
function Command:_get_label_lines()
return {table.concat(self._aliases, ", ")}
end
function Argument:_get_description()
if self._default and self._show_default then
if self._description then
return ("%s (default: %s)"):format(self._description, self._default)
else
return ("default: %s"):format(self._default)
end
else
return self._description or ""
end
end
function Command:_get_description()
return self._description or ""
end
function Option:_get_usage()
local usage = self:_get_argument_list()
table.insert(usage, 1, self._name)
usage = table.concat(usage, " ")
if self._mincount == 0 or self._default then
usage = "[" .. usage .. "]"
end
return usage
end
function Argument:_get_default_target()
return self._name
end
function Option:_get_default_target()
local res
for _, alias in ipairs(self._aliases) do
if alias:sub(1, 1) == alias:sub(2, 2) then
res = alias:sub(3)
break
end
end
res = res or self._name:sub(2)
return (res:gsub("-", "_"))
end
function Option:_is_vararg()
return self._maxargs ~= self._minargs
end
function Parser:_get_fullname()
local parent = self._parent
local buf = {self._name}
while parent do
table.insert(buf, 1, parent._name)
parent = parent._parent
end
return table.concat(buf, " ")
end
function Parser:_update_charset(charset)
charset = charset or {}
for _, command in ipairs(self._commands) do
command:_update_charset(charset)
end
for _, option in ipairs(self._options) do
for _, alias in ipairs(option._aliases) do
charset[alias:sub(1, 1)] = true
end
end
return charset
end
function Parser:argument(...)
local argument = Argument(...)
table.insert(self._arguments, argument)
return argument
end
function Parser:option(...)
local option = Option(...)
if self._has_help then
table.insert(self._options, #self._options, option)
else
table.insert(self._options, option)
end
return option
end
function Parser:flag(...)
return self:option():args(0)(...)
end
function Parser:command(...)
local command = Command():add_help(true)(...)
command._parent = self
table.insert(self._commands, command)
return command
end
function Parser:mutex(...)
local elements = {...}
for i, element in ipairs(elements) do
local mt = getmetatable(element)
assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i))
end
table.insert(self._mutexes, elements)
return self
end
function Parser:group(name, ...)
assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name)))
local group = {name = name, ...}
for i, element in ipairs(group) do
local mt = getmetatable(element)
assert(mt == Option or mt == Argument or mt == Command,
("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1))
end
table.insert(self._groups, group)
return self
end
local usage_welcome = "Usage: "
function Parser:get_usage()
if self._usage then
return self._usage
end
local usage_margin = self:_inherit_property("usage_margin", #usage_welcome)
local max_usage_width = self:_inherit_property("usage_max_width", 70)
local lines = {usage_welcome .. self:_get_fullname()}
local function add(s)
if #lines[#lines]+1+#s <= max_usage_width then
lines[#lines] = lines[#lines] .. " " .. s
else
lines[#lines+1] = (" "):rep(usage_margin) .. s
end
end
-- Normally options are before positional arguments in usage messages.
-- However, vararg options should be after, because they can't be reliable used
-- before a positional argument.
-- Mutexes come into play, too, and are shown as soon as possible.
-- Overall, output usages in the following order:
-- 1. Mutexes that don't have positional arguments or vararg options.
-- 2. Options that are not in any mutexes and are not vararg.
-- 3. Positional arguments - on their own or as a part of a mutex.
-- 4. Remaining mutexes.
-- 5. Remaining options.
local elements_in_mutexes = {}
local added_elements = {}
local added_mutexes = {}
local argument_to_mutexes = {}
local function add_mutex(mutex, main_argument)
if added_mutexes[mutex] then
return
end
added_mutexes[mutex] = true
local buf = {}
for _, element in ipairs(mutex) do
if not element._hidden and not added_elements[element] then
if getmetatable(element) == Option or element == main_argument then
table.insert(buf, element:_get_usage())
added_elements[element] = true
end
end
end
if #buf == 1 then
add(buf[1])
elseif #buf > 1 then
add("(" .. table.concat(buf, " | ") .. ")")
end
end
local function add_element(element)
if not element._hidden and not added_elements[element] then
add(element:_get_usage())
added_elements[element] = true
end
end
for _, mutex in ipairs(self._mutexes) do
local is_vararg = false
local has_argument = false
for _, element in ipairs(mutex) do
if getmetatable(element) == Option then
if element:_is_vararg() then
is_vararg = true
end
else
has_argument = true
argument_to_mutexes[element] = argument_to_mutexes[element] or {}
table.insert(argument_to_mutexes[element], mutex)
end
elements_in_mutexes[element] = true
end
if not is_vararg and not has_argument then
add_mutex(mutex)
end
end
for _, option in ipairs(self._options) do
if not elements_in_mutexes[option] and not option:_is_vararg() then
add_element(option)
end
end
-- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex.
for _, argument in ipairs(self._arguments) do
-- Pick a mutex as a part of which to show this argument, take the first one that's still available.
local mutex
if elements_in_mutexes[argument] then
for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do
if not added_mutexes[argument_mutex] then
mutex = argument_mutex
end
end
end
if mutex then
add_mutex(mutex, argument)
else
add_element(argument)
end
end
for _, mutex in ipairs(self._mutexes) do
add_mutex(mutex)
end
for _, option in ipairs(self._options) do
add_element(option)
end
if #self._commands > 0 then
if self._require_command then
add("<command>")
else
add("[<command>]")
end
add("...")
end
return table.concat(lines, "\n")
end
local function split_lines(s)
if s == "" then
return {}
end
local lines = {}
if s:sub(-1) ~= "\n" then
s = s .. "\n"
end
for line in s:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
return lines
end
local function autowrap_line(line, max_length)
-- Algorithm for splitting lines is simple and greedy.
local result_lines = {}
-- Preserve original indentation of the line, put this at the beginning of each result line.
-- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts
-- of the second and the following lines vertically align with the start of the second word.
local indentation = line:match("^ *")
if line:find("^ *[%*%+%-]") then
indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)")
end
-- Parts of the last line being assembled.
local line_parts = {}
-- Length of the current line.
local line_length = 0
-- Index of the next character to consider.
local index = 1
while true do
local word_start, word_finish, word = line:find("([^ ]+)", index)
if not word_start then
-- Ignore trailing spaces, if any.
break
end
local preceding_spaces = line:sub(index, word_start - 1)
index = word_finish + 1
if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then
-- Either this is the very first word or it fits as an addition to the current line, add it.
table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation.
table.insert(line_parts, word)
line_length = line_length + #preceding_spaces + #word
else
-- Does not fit, finish current line and put the word into a new one.
table.insert(result_lines, table.concat(line_parts))
line_parts = {indentation, word}
line_length = #indentation + #word
end
end
if #line_parts > 0 then
table.insert(result_lines, table.concat(line_parts))
end
if #result_lines == 0 then
-- Preserve empty lines.
result_lines[1] = ""
end
return result_lines
end
-- Automatically wraps lines within given array,
-- attempting to limit line length to `max_length`.
-- Existing line splits are preserved.
local function autowrap(lines, max_length)
local result_lines = {}
for _, line in ipairs(lines) do
local autowrapped_lines = autowrap_line(line, max_length)
for _, autowrapped_line in ipairs(autowrapped_lines) do
table.insert(result_lines, autowrapped_line)
end
end
return result_lines
end
function Parser:_get_element_help(element)
local label_lines = element:_get_label_lines()
local description_lines = split_lines(element:_get_description())
local result_lines = {}
-- All label lines should have the same length (except the last one, it has no comma).
-- If too long, start description after all the label lines.
-- Otherwise, combine label and description lines.
local usage_margin_len = self:_inherit_property("help_usage_margin", 3)
local usage_margin = (" "):rep(usage_margin_len)
local description_margin_len = self:_inherit_property("help_description_margin", 25)
local description_margin = (" "):rep(description_margin_len)
local help_max_width = self:_inherit_property("help_max_width")
if help_max_width then
local description_max_width = math.max(help_max_width - description_margin_len, 10)
description_lines = autowrap(description_lines, description_max_width)
end
if #label_lines[1] >= (description_margin_len - usage_margin_len) then
for _, label_line in ipairs(label_lines) do
table.insert(result_lines, usage_margin .. label_line)
end
for _, description_line in ipairs(description_lines) do
table.insert(result_lines, description_margin .. description_line)
end
else
for i = 1, math.max(#label_lines, #description_lines) do
local label_line = label_lines[i]
local description_line = description_lines[i]
local line = ""
if label_line then
line = usage_margin .. label_line
end
if description_line and description_line ~= "" then
line = line .. (" "):rep(description_margin_len - #line) .. description_line
end
table.insert(result_lines, line)
end
end
return table.concat(result_lines, "\n")
end
local function get_group_types(group)
local types = {}
for _, element in ipairs(group) do
types[getmetatable(element)] = true
end
return types
end
function Parser:_add_group_help(blocks, added_elements, label, elements)
local buf = {label}
for _, element in ipairs(elements) do
if not element._hidden and not added_elements[element] then
added_elements[element] = true
table.insert(buf, self:_get_element_help(element))
end
end
if #buf > 1 then
table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1)))
end
end
function Parser:get_help()
if self._help then
return self._help
end
local blocks = {self:get_usage()}
local help_max_width = self:_inherit_property("help_max_width")
if self._description then
local description = self._description
if help_max_width then
description = table.concat(autowrap(split_lines(description), help_max_width), "\n")
end
table.insert(blocks, description)
end
-- 1. Put groups containing arguments first, then other arguments.
-- 2. Put remaining groups containing options, then other options.
-- 3. Put remaining groups containing commands, then other commands.
-- Assume that an element can't be in several groups.
local groups_by_type = {
[Argument] = {},
[Option] = {},
[Command] = {}
}
for _, group in ipairs(self._groups) do
local group_types = get_group_types(group)
for _, mt in ipairs({Argument, Option, Command}) do
if group_types[mt] then
table.insert(groups_by_type[mt], group)
break
end
end
end
local default_groups = {
{name = "Arguments", type = Argument, elements = self._arguments},
{name = "Options", type = Option, elements = self._options},
{name = "Commands", type = Command, elements = self._commands}
}
local added_elements = {}
for _, default_group in ipairs(default_groups) do
local type_groups = groups_by_type[default_group.type]
for _, group in ipairs(type_groups) do
self:_add_group_help(blocks, added_elements, group.name .. ":", group)
end
local default_label = default_group.name .. ":"
if #type_groups > 0 then
default_label = "Other " .. default_label:gsub("^.", string.lower)
end
self:_add_group_help(blocks, added_elements, default_label, default_group.elements)
end
if self._epilog then
local epilog = self._epilog
if help_max_width then
epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n")
end
table.insert(blocks, epilog)
end
return table.concat(blocks, "\n\n")
end
local function get_tip(context, wrong_name)
local context_pool = {}
local possible_name
local possible_names = {}
for name in pairs(context) do
if type(name) == "string" then
for i = 1, #name do
possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
if not context_pool[possible_name] then
context_pool[possible_name] = {}
end
table.insert(context_pool[possible_name], name)
end
end
end
for i = 1, #wrong_name + 1 do
possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1)
if context[possible_name] then
possible_names[possible_name] = true
elseif context_pool[possible_name] then
for _, name in ipairs(context_pool[possible_name]) do
possible_names[name] = true
end
end
end
local first = next(possible_names)
if first then
if next(possible_names, first) then
local possible_names_arr = {}
for name in pairs(possible_names) do
table.insert(possible_names_arr, "'" .. name .. "'")
end
table.sort(possible_names_arr)
return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?"
else
return "\nDid you mean '" .. first .. "'?"
end
else
return ""
end
end
local ElementState = class({
invocations = 0
})
function ElementState:__call(state, element)
self.state = state
self.result = state.result
self.element = element
self.target = element._target or element:_get_default_target()
self.action, self.result[self.target] = element:_get_action()
return self
end
function ElementState:error(fmt, ...)
self.state:error(fmt, ...)
end
function ElementState:convert(argument, index)
local converter = self.element._convert
if converter then
local ok, err
if type(converter) == "function" then
ok, err = converter(argument)
elseif type(converter[index]) == "function" then
ok, err = converter[index](argument)
else
ok = converter[argument]
end
if ok == nil then
self:error(err and "%s" or "malformed argument '%s'", err or argument)
end
argument = ok
end
return argument
end
function ElementState:default(mode)
return self.element._defmode:find(mode) and self.element._default
end
local function bound(noun, min, max, is_max)
local res = ""
if min ~= max then
res = "at " .. (is_max and "most" or "least") .. " "
end
local number = is_max and max or min
return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s")
end
function ElementState:set_name(alias)
self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name)
end
function ElementState:invoke()
self.open = true
self.overwrite = false
if self.invocations >= self.element._maxcount then
if self.element._overwrite then
self.overwrite = true
else
local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true)
self:error("%s must be used %s", self.name, num_times_repr)
end
else
self.invocations = self.invocations + 1
end
self.args = {}
if self.element._maxargs <= 0 then
self:close()
end
return self.open
end
function ElementState:pass(argument)
argument = self:convert(argument, #self.args + 1)
table.insert(self.args, argument)
if #self.args >= self.element._maxargs then
self:close()
end
return self.open
end
function ElementState:complete_invocation()
while #self.args < self.element._minargs do
self:pass(self.element._default)
end
end
function ElementState:close()
if self.open then
self.open = false
if #self.args < self.element._minargs then
if self:default("a") then
self:complete_invocation()
else
if #self.args == 0 then
if getmetatable(self.element) == Argument then
self:error("missing %s", self.name)
elseif self.element._maxargs == 1 then
self:error("%s requires an argument", self.name)
end
end
self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs))
end
end
local args
if self.element._maxargs == 0 then
args = self.args[1]
elseif self.element._maxargs == 1 then
if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then
args = self.args
else
args = self.args[1]
end
else
args = self.args
end
self.action(self.result, self.target, args, self.overwrite)
end
end
local ParseState = class({
result = {},
options = {},
arguments = {},
argument_i = 1,
element_to_mutexes = {},
mutex_to_element_state = {},
command_actions = {}
})
function ParseState:__call(parser, error_handler)
self.parser = parser
self.error_handler = error_handler
self.charset = parser:_update_charset()
self:switch(parser)
return self
end
function ParseState:error(fmt, ...)
self.error_handler(self.parser, fmt:format(...))
end
function ParseState:switch(parser)
self.parser = parser
if parser._action then
table.insert(self.command_actions, {action = parser._action, name = parser._name})
end
for _, option in ipairs(parser._options) do
option = ElementState(self, option)
table.insert(self.options, option)
for _, alias in ipairs(option.element._aliases) do
self.options[alias] = option
end
end
for _, mutex in ipairs(parser._mutexes) do
for _, element in ipairs(mutex) do
if not self.element_to_mutexes[element] then
self.element_to_mutexes[element] = {}
end
table.insert(self.element_to_mutexes[element], mutex)
end
end
for _, argument in ipairs(parser._arguments) do
argument = ElementState(self, argument)
table.insert(self.arguments, argument)
argument:set_name()
argument:invoke()
end
self.handle_options = parser._handle_options
self.argument = self.arguments[self.argument_i]
self.commands = parser._commands
for _, command in ipairs(self.commands) do
for _, alias in ipairs(command._aliases) do
self.commands[alias] = command
end
end
end
function ParseState:get_option(name)
local option = self.options[name]
if not option then
self:error("unknown option '%s'%s", name, get_tip(self.options, name))
else
return option
end
end
function ParseState:get_command(name)
local command = self.commands[name]
if not command then
if #self.commands > 0 then
self:error("unknown command '%s'%s", name, get_tip(self.commands, name))
else
self:error("too many arguments")
end
else
return command
end
end
function ParseState:check_mutexes(element_state)
if self.element_to_mutexes[element_state.element] then
for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do
local used_element_state = self.mutex_to_element_state[mutex]
if used_element_state and used_element_state ~= element_state then
self:error("%s can not be used together with %s", element_state.name, used_element_state.name)
else
self.mutex_to_element_state[mutex] = element_state
end
end
end
end
function ParseState:invoke(option, name)
self:close()
option:set_name(name)
self:check_mutexes(option, name)
if option:invoke() then
self.option = option
end
end
function ParseState:pass(arg)
if self.option then
if not self.option:pass(arg) then
self.option = nil
end
elseif self.argument then
self:check_mutexes(self.argument)
if not self.argument:pass(arg) then
self.argument_i = self.argument_i + 1
self.argument = self.arguments[self.argument_i]
end
else
local command = self:get_command(arg)
self.result[command._target or command._name] = true
if self.parser._command_target then
self.result[self.parser._command_target] = command._name
end
self:switch(command)
end
end
function ParseState:close()
if self.option then
self.option:close()
self.option = nil
end
end
function ParseState:finalize()
self:close()
for i = self.argument_i, #self.arguments do
local argument = self.arguments[i]
if #argument.args == 0 and argument:default("u") then
argument:complete_invocation()
else
argument:close()
end
end
if self.parser._require_command and #self.commands > 0 then
self:error("a command is required")
end
for _, option in ipairs(self.options) do
option.name = option.name or ("option '%s'"):format(option.element._name)
if option.invocations == 0 then
if option:default("u") then
option:invoke()
option:complete_invocation()
option:close()
end
end
local mincount = option.element._mincount
if option.invocations < mincount then
if option:default("a") then
while option.invocations < mincount do
option:invoke()
option:close()
end
elseif option.invocations == 0 then
self:error("missing %s", option.name)
else
self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount))
end
end
end
for i = #self.command_actions, 1, -1 do
self.command_actions[i].action(self.result, self.command_actions[i].name)
end
end
function ParseState:parse(args)
for _, arg in ipairs(args) do
local plain = true
if self.handle_options then
local first = arg:sub(1, 1)
if self.charset[first] then
if #arg > 1 then
plain = false
if arg:sub(2, 2) == first then
if #arg == 2 then
if self.options[arg] then
local option = self:get_option(arg)
self:invoke(option, arg)
else
self:close()
end
self.handle_options = false
else
local equals = arg:find "="
if equals then
local name = arg:sub(1, equals - 1)
local option = self:get_option(name)
if option.element._maxargs <= 0 then
self:error("option '%s' does not take arguments", name)
end
self:invoke(option, name)
self:pass(arg:sub(equals + 1))
else
local option = self:get_option(arg)
self:invoke(option, arg)
end
end
else
for i = 2, #arg do
local name = first .. arg:sub(i, i)
local option = self:get_option(name)
self:invoke(option, name)
if i ~= #arg and option.element._maxargs > 0 then
self:pass(arg:sub(i + 1))
break
end
end
end
end
end
end
if plain then
self:pass(arg)
end
end
self:finalize()
return self.result
end
function Parser:error(msg)
io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg))
os.exit(1)
end
-- Compatibility with strict.lua and other checkers:
local default_cmdline = rawget(_G, "arg") or {}
function Parser:_parse(args, error_handler)
return ParseState(self, error_handler):parse(args or default_cmdline)
end
function Parser:parse(args)
return self:_parse(args, self.error)
end
local function xpcall_error_handler(err)
return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2)
end
function Parser:pparse(args)
local parse_error
local ok, result = xpcall(function()
return self:_parse(args, function(_, err)
parse_error = err
error(err, 0)
end)
end, xpcall_error_handler)
if ok then
return true, result
elseif not parse_error then
error(result, 0)
else
return false, parse_error
end
end
local argparse = {}
argparse.version = "0.6.0"
setmetatable(argparse, {__call = function(_, ...)
return Parser(default_cmdline[0]):add_help(true)(...)
end})
return argparse
end)()
local parser = argparse(arg[0], "LuaComp v"..LUACOMP_VERSION.."\nA Lua preprocessor+postprocessor.")
parser:argument("input", "Input file (- for STDIN)")
parser:option("-O --output", "Output file. (- for STDOUT)", "-")
parser:option("-m --minifier", "Sets the minifier", "none")
parser:option("-x --executable", "Makes the script an executable (default: current lua version)"):args "?"
local args = parser:parse()
local file = args.input
local f
if (file ~= "-") then
f = io.open(file, "r")
if not f then
io.stderr:write("ERROR: File `"..file.."' does not exist!\n")
os.exit(1)
end
else
f = io.stdin
end
local ast = mkast(f, file)
local ocode = generate(ast)
local minifier = providers[args.minifier]
local rcode = minifier(ocode)
local of
if (args.output == "-") then
of = io.stdout
else
of = io.open(args.output, "w")
end
local ver = _VERSION:lower():gsub(" ", "")
if jit then
ver = "luajit"
end
if (args.executable) then
of:write("#!/usr/bin/env ", args.executable[1] or ver, "\n")
end
of:write(rcode)
of:close()
f:close()