Files
soldr-modules/tests_framework/lua/cliargs/parser.lua
T
2022-11-22 02:21:28 +03:00

317 lines
8.5 KiB
Lua

local K = require 'cliargs.constants'
-------------------------------------------------------------------------------
-- UTILS
-------------------------------------------------------------------------------
local shallow_copy = require 'cliargs.utils.shallow_copy'
local filter = require 'cliargs.utils.filter'
local disect_argument = require 'cliargs.utils.disect_argument'
local lookup = require 'cliargs.utils.lookup'
local function clone_table_shift(t)
local clone = shallow_copy(t)
table.remove(clone, 1)
return clone
end
local function clone_table_remove(t, index)
local clone = shallow_copy(t)
table.remove(clone, index)
return clone
end
-------------------------------------------------------------------------------
-- PARSE ROUTINES
-------------------------------------------------------------------------------
local p = {}
function p.invoke_command(args, options, done)
local commands = filter(options, 'type', K.TYPE_COMMAND)
for index, opt in ipairs(args) do
local command = filter(commands, '__key__', opt)[1]
if command then
local command_args = clone_table_remove(args, index)
if command.__action__ then
local parsed_command_args, err = command:parse(command_args)
if err then
return nil, err
end
return command.__action__(parsed_command_args)
elseif command.__file__ then
local filename = command.__file__
if type(filename) == 'function' then
filename = filename()
end
local run_command_file = function()
_G.arg = command_args
local res, err = assert(loadfile(filename))()
_G.arg = args
return res, err
end
return run_command_file()
end
end
end
return done()
end
function p.print_help(args, printer, done)
-- has --help or -h ? display the help listing and abort!
for _, v in pairs(args) do
if v == "--help" or v == "-h" then
return nil, printer.generate_help_and_usage()
end
end
return done()
end
function p.track_dump_request(args, done)
-- starts with --__DUMP__; set dump to true to dump the parsed arguments
if args[1] == "--__DUMP__" then
return done(true, clone_table_shift(args))
else
return done(false, args)
end
end
function p.process_arguments(args, options, done)
local values = {}
local cursor = 0
local argument_cursor = 1
local argument_delimiter_found = false
local function consume()
cursor = cursor + 1
return args[cursor]
end
local required = filter(options, 'type', K.TYPE_ARGUMENT)
while cursor < #args do
local curr_opt = consume()
local symbol, key, value, flag_negated = disect_argument(curr_opt)
-- end-of-options indicator:
if curr_opt == "--" then
argument_delimiter_found = true
-- an option:
elseif not argument_delimiter_found and symbol then
local entry = lookup(key, key, options)
if not key or not entry then
local option_type = value and "option" or "flag"
return nil, "unknown/bad " .. option_type .. ": " .. curr_opt
end
if flag_negated and not entry.negatable then
return nil, "flag '" .. curr_opt .. "' may not be negated using --no-"
end
-- a flag and a value specified? that's an error
if entry.flag and value then
return nil, "flag " .. curr_opt .. " does not take a value"
elseif entry.flag then
value = not flag_negated
-- an option:
else
-- the value might be in the next argument, e.g:
--
-- --compress lzma
if not value then
-- if the option contained a = and there's no value, it means they
-- want to nullify an option's default value. eg:
--
-- --compress=
if curr_opt:find('=') then
value = '__CLIARGS_NULL__'
else
-- NOTE: this has the potential to be buggy and swallow the next
-- entry as this entry's value even though that entry may be an
-- actual argument/option
--
-- this would be a user error and there is no determinate way to
-- figure it out because if there's no leading symbol (- or --)
-- in that entry it can be an actual argument. :shrug:
value = consume()
if not value then
return nil, "option " .. curr_opt .. " requires a value to be set"
end
end
end
end
table.insert(values, { entry = entry, value = value })
if entry.callback then
local altkey = entry.key
local status, err
if key == entry.key then
altkey = entry.expanded_key
else
key = entry.expanded_key
end
status, err = entry.callback(key, value, altkey, curr_opt)
if status == nil and err then
return nil, err
end
end
-- a regular argument:
elseif argument_cursor <= #required then
local entry = required[argument_cursor]
table.insert(values, { entry = entry, value = curr_opt })
if entry.callback then
local status, err = entry.callback(entry.key, curr_opt)
if status == nil and err then
return nil, err
end
end
argument_cursor = argument_cursor + 1
-- a splat argument:
else
local entry = filter(options, 'type', K.TYPE_SPLAT)[1]
if entry then
table.insert(values, { entry = entry, value = curr_opt })
if entry.callback then
local status, err = entry.callback(entry.key, curr_opt)
if status == nil and err then
return nil, err
end
end
end
argument_cursor = argument_cursor + 1
end
end
return done(values, argument_cursor - 1)
end
function p.validate(options, arg_count, done)
local required = filter(options, 'type', K.TYPE_ARGUMENT)
local splatarg = filter(options, 'type', K.TYPE_SPLAT)[1] or { maxcount = 0 }
local min_arg_count = #required
local max_arg_count = #required + splatarg.maxcount
-- missing any required arguments, or too many?
if arg_count < min_arg_count or arg_count > max_arg_count then
if splatarg.maxcount > 0 then
return nil, (
"bad number of arguments: " ..
min_arg_count .. "-" .. max_arg_count ..
" argument(s) must be specified, not " .. arg_count
)
else
return nil, (
"bad number of arguments: " ..
min_arg_count .. " argument(s) must be specified, not " .. arg_count
)
end
end
return done()
end
function p.collect_results(cli_values, options)
local results = {}
local function collect_with_default(entry)
local entry_values = {}
local _
for _, item in ipairs(cli_values) do
if item.entry == entry then
table.insert(entry_values, item.value)
end
end
if #entry_values == 0 then
return type(entry.default) == 'table' and entry.default or { entry.default }
else
return entry_values
end
end
local function write(entry, value)
if entry.key then results[entry.key] = value end
if entry.expanded_key then results[entry.expanded_key] = value end
end
for _, entry in pairs(options) do
local entry_cli_values = collect_with_default(entry)
local maxcount = entry.maxcount
if maxcount == nil then
maxcount = type(entry.default) == 'table' and 999 or 1
end
local entry_value = entry_cli_values
if maxcount == 1 and type(entry_cli_values) == 'table' then
-- take the last value
entry_value = entry_cli_values[#entry_cli_values]
if entry_value == '__CLIARGS_NULL__' then
entry_value = nil
end
end
write(entry, entry_value)
end
return results
end
return function(arguments, options, printer)
assert(arguments == nil or type(arguments) == "table",
"expected an argument table to be passed in, " ..
"got something of type " .. type(arguments)
)
local args = arguments or _G.arg or {}
-- the spiral of DOOM:
return p.invoke_command(args, options, function()
return p.track_dump_request(args, function(dump, args_without_dump)
return p.print_help(args_without_dump, printer, function()
return p.process_arguments(args_without_dump, options, function(values, arg_count)
return p.validate(options, arg_count, function()
if dump then
return nil, printer.dump_internal_state(values)
else
return p.collect_results(values, options)
end
end)
end)
end)
end)
end)
end