mirror of
https://github.com/vxcontrol/soldr-modules.git
synced 2026-07-21 17:05:24 -04:00
559 lines
16 KiB
Lua
559 lines
16 KiB
Lua
-- luacheck: ignore 212
|
|
|
|
local _
|
|
local disect = require('cliargs.utils.disect')
|
|
local lookup = require('cliargs.utils.lookup')
|
|
local filter = require('cliargs.utils.filter')
|
|
local shallow_copy = require('cliargs.utils.shallow_copy')
|
|
local create_printer = require('cliargs.printer')
|
|
local config_loader = require('cliargs.config_loader')
|
|
local parser = require('cliargs.parser')
|
|
local K = require 'cliargs.constants'
|
|
|
|
local function is_callable(fn)
|
|
return type(fn) == "function" or (getmetatable(fn) or {}).__call
|
|
end
|
|
|
|
local function cast_to_boolean(v)
|
|
if v == nil then
|
|
return v
|
|
else
|
|
return v and true or false
|
|
end
|
|
end
|
|
|
|
-- -------- --
|
|
-- CLI Main --
|
|
-- -------- --
|
|
local function create_core()
|
|
--- @module
|
|
---
|
|
--- The primary export you receive when you require the library. For example:
|
|
---
|
|
--- local cli = require 'cliargs'
|
|
local cli = {}
|
|
local colsz = { 0, 0 } -- column width, help text. Set to 0 for auto detect
|
|
local options = {}
|
|
|
|
cli.name = ""
|
|
cli.description = ""
|
|
|
|
cli.printer = create_printer(function()
|
|
return {
|
|
name = cli.name,
|
|
description = cli.description,
|
|
options = options,
|
|
colsz = colsz
|
|
}
|
|
end)
|
|
|
|
-- Used internally to add an option
|
|
local function define_option(k, ek, v, label, desc, default, callback)
|
|
local flag = (v == nil) -- no value, so it's a flag
|
|
local negatable = flag and (ek and ek:find('^%[no%-]') ~= nil)
|
|
|
|
if negatable then
|
|
ek = ek:sub(6)
|
|
end
|
|
|
|
-- guard against duplicates
|
|
if lookup(k, ek, options) then
|
|
error("Duplicate option: " .. (k or ek) .. ", please rename one of them.")
|
|
end
|
|
|
|
if negatable and lookup(nil, "no-"..ek, options) then
|
|
error("Duplicate option: " .. ("no-"..ek) .. ", please rename one of them.")
|
|
end
|
|
|
|
-- below description of full entry record, nils included for reference
|
|
local entry = {
|
|
type = K.TYPE_OPTION,
|
|
key = k,
|
|
expanded_key = ek,
|
|
desc = desc,
|
|
default = default,
|
|
label = label,
|
|
flag = flag,
|
|
negatable = negatable,
|
|
callback = callback
|
|
}
|
|
|
|
table.insert(options, entry)
|
|
end
|
|
|
|
local function define_command_option(key)
|
|
--- @module
|
|
---
|
|
--- This is a special instance of the [cli]() module that you receive when
|
|
--- you define a new command using [cli#command]().
|
|
local cmd = create_core()
|
|
|
|
cmd.__key__ = key
|
|
cmd.type = K.TYPE_COMMAND
|
|
|
|
--- Specify a file that the command should run. The rest of the arguments
|
|
--- are forward to that file to process, which is free to use or not use
|
|
--- lua_cliargs in turn.
|
|
---
|
|
--- @param {string} file_path
|
|
--- Absolute file-path to a lua script to execute.
|
|
function cmd:file(file_path)
|
|
cmd.__file__ = file_path
|
|
return cmd
|
|
end
|
|
|
|
--- Define a command handler. This callback will be invoked if the command
|
|
--- argument was supplied by the user at runtime. What you return from this
|
|
--- callback will be returned to the parent CLI library's parse routine and
|
|
--- it will return that in turn!
|
|
---
|
|
--- @param {function} callback
|
|
function cmd:action(callback)
|
|
cmd.__action__ = callback
|
|
return cmd
|
|
end
|
|
|
|
return cmd
|
|
end
|
|
|
|
-- ------------------------------------------------------------------------ --
|
|
-- PUBLIC API
|
|
-- ------------------------------------------------------------------------ --
|
|
|
|
--- CONFIG
|
|
|
|
--- Assigns the name of the program which will be used for logging.
|
|
function cli:set_name(in_name)
|
|
cli.name = in_name
|
|
|
|
return self
|
|
end
|
|
|
|
--- Write down a brief, 1-liner description of what the program does.
|
|
function cli:set_description(in_description)
|
|
cli.description = in_description
|
|
|
|
return self
|
|
end
|
|
|
|
--- Sets the amount of space allocated to the argument keys and descriptions
|
|
--- in the help listing.
|
|
---
|
|
--- The sizes are used for wrapping long argument keys and descriptions.
|
|
---
|
|
--- @param {number} [key_cols=0]
|
|
--- The number of columns assigned to the argument keys, set to 0 to
|
|
--- auto detect.
|
|
---
|
|
--- @param {number} [desc_cols=0]
|
|
--- The number of columns assigned to the argument descriptions, set to
|
|
--- 0 to auto set the total width to 72.
|
|
function cli:set_colsz(key_cols, desc_cols)
|
|
colsz = { key_cols or colsz[1], desc_cols or colsz[2] }
|
|
end
|
|
|
|
function cli:redefine_default(key, new_default)
|
|
local entry = lookup(key, key, options)
|
|
|
|
if not entry then
|
|
return nil
|
|
end
|
|
|
|
if entry.flag then
|
|
new_default = cast_to_boolean(new_default)
|
|
end
|
|
|
|
entry.default = shallow_copy(new_default)
|
|
|
|
return true
|
|
end
|
|
|
|
--- Load default values from a table.
|
|
---
|
|
--- @param {table} config
|
|
--- Your new set of defaults. The keys could either point to the short
|
|
--- or expanded option keys, and their values are the new defaults.
|
|
---
|
|
--- @param {boolean} [strict=false]
|
|
--- Turn this on to return nil and an error message if a key in the
|
|
--- config table could not be mapped to any CLI option.
|
|
---
|
|
--- @return {true}
|
|
--- When the new defaults were loaded successfully, or strict was not
|
|
--- set.
|
|
---
|
|
--- @return {union<nil, string>}
|
|
--- When strict was set and there was an error.
|
|
function cli:load_defaults(config, strict)
|
|
for k, v in pairs(config) do
|
|
local success = self:redefine_default(k, v)
|
|
|
|
if strict and not success then
|
|
return nil, "Unrecognized option with the key '" .. k .. "'"
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- Read config values from a configuration file.
|
|
---
|
|
--- @param {string} path
|
|
--- Absolute file path.
|
|
---
|
|
--- @param {string} [format=nil]
|
|
--- The config file format, which has to be one of:
|
|
--- "lua", "json", "ini", or "yaml".
|
|
--- When this is left blank, we try to auto-detect the format from the
|
|
--- file extension.
|
|
---
|
|
--- @param {boolean} [strict=false]
|
|
--- Forwarded to [#load_defaults](). See that method for the parameter
|
|
--- description.
|
|
---
|
|
--- @return {true|union<nil, string>}
|
|
--- Returns true on successful load. Otherwise, nil and an error
|
|
--- message are returned instead.
|
|
function cli:read_defaults(path, format)
|
|
if not format then
|
|
format = path:match('%.([^%.]+)$')
|
|
end
|
|
|
|
local loader = config_loader.FORMAT_LOADERS[format]
|
|
|
|
if not loader then
|
|
return nil, 'Unsupported file format "' .. format .. '"'
|
|
end
|
|
|
|
return config_loader[loader](path)
|
|
end
|
|
|
|
--- Define a required argument.
|
|
---
|
|
---
|
|
--- Required arguments do not take a symbol like `-` or `--`, may not have a
|
|
--- default value, and are parsed in the order they are defined.
|
|
---
|
|
---
|
|
--- For example:
|
|
---
|
|
--- ```lua
|
|
--- cli:argument('INPUT', 'path to the input file')
|
|
--- cli:argument('OUTPUT', 'path to the output file')
|
|
--- ```
|
|
---
|
|
--- At run-time, the arguments have to be specified using the following
|
|
--- notation:
|
|
---
|
|
--- ```bash
|
|
--- $ ./script.lua ./main.c ./a.out
|
|
--- ```
|
|
---
|
|
--- If the user does not pass a value to _every_ argument, the parser will
|
|
--- raise an error.
|
|
---
|
|
--- @param {string} key
|
|
---
|
|
--- The argument identifier that will be displayed to the user and
|
|
--- be used to reference the run-time value.
|
|
---
|
|
--- @param {string} desc
|
|
---
|
|
--- A description for this argument to display in usage help.
|
|
---
|
|
--- @param {function} [callback]
|
|
--- Callback to invoke when this argument is parsed.
|
|
function cli:argument(key, desc, callback)
|
|
assert(type(key) == "string" and type(desc) == "string",
|
|
"Key and description are mandatory arguments (Strings)"
|
|
)
|
|
|
|
assert(callback == nil or is_callable(callback),
|
|
"Callback argument must be a function"
|
|
)
|
|
|
|
if lookup(key, key, options) then
|
|
error("Duplicate argument: " .. key .. ", please rename one of them.")
|
|
end
|
|
|
|
table.insert(options, {
|
|
type = K.TYPE_ARGUMENT,
|
|
key = key,
|
|
desc = desc,
|
|
callback = callback
|
|
})
|
|
|
|
return self
|
|
end
|
|
|
|
--- Defines a "splat" (or catch-all) argument.
|
|
---
|
|
--- This is a special kind of argument that may be specified 0 or more times,
|
|
--- the values being appended to a list.
|
|
---
|
|
--- For example, let's assume our program takes a single output file and works
|
|
--- on multiple source files:
|
|
---
|
|
--- ```lua
|
|
--- cli:argument('OUTPUT', 'path to the output file')
|
|
--- cli:splat('INPUTS', 'the sources to compile', nil, 10) -- up to 10 source files
|
|
--- ```
|
|
---
|
|
--- At run-time, it could be invoked as such:
|
|
---
|
|
--- ```bash
|
|
--- $ ./script.lua ./a.out file1.c file2.c main.c
|
|
--- ```
|
|
---
|
|
--- If you want to make the output optional, you could do something like this:
|
|
---
|
|
--- ```lua
|
|
--- cli:option('-o, --output=FILE', 'path to the output file', './a.out')
|
|
--- cli:splat('INPUTS', 'the sources to compile', nil, 10)
|
|
--- ```
|
|
---
|
|
--- And now we may omit the output file path:
|
|
---
|
|
--- ```bash
|
|
--- $ ./script.lua file1.c file2.c main.c
|
|
--- ```
|
|
---
|
|
--- @param {string} key
|
|
--- The argument's "name" that will be displayed to the user.
|
|
---
|
|
--- @param {string} desc
|
|
--- A description of the argument.
|
|
---
|
|
--- @param {*} [default=nil]
|
|
--- A default value.
|
|
---
|
|
--- @param {number} [maxcount=1]
|
|
--- The maximum number of occurences allowed.
|
|
---
|
|
--- @param {function} [callback]
|
|
--- A function to call **everytime** a value for this argument is
|
|
--- parsed.
|
|
---
|
|
function cli:splat(key, desc, default, maxcount, callback)
|
|
assert(#filter(options, 'type', K.TYPE_SPLAT) == 0,
|
|
"Only one splat argument may be defined."
|
|
)
|
|
|
|
assert(type(key) == "string" and type(desc) == "string",
|
|
"Key and description are mandatory arguments (Strings)"
|
|
)
|
|
|
|
assert(type(default) == "string" or default == nil,
|
|
"Default value must either be omitted or be a string"
|
|
)
|
|
|
|
maxcount = tonumber(maxcount or 1)
|
|
|
|
assert(maxcount > 0 and maxcount < 1000,
|
|
"Maxcount must be a number from 1 to 999"
|
|
)
|
|
|
|
assert(is_callable(callback) or callback == nil,
|
|
"Callback argument: expected a function or nil"
|
|
)
|
|
|
|
local typed_default = default or {}
|
|
|
|
if type(typed_default) ~= 'table' then
|
|
typed_default = { typed_default }
|
|
end
|
|
|
|
table.insert(options, {
|
|
type = K.TYPE_SPLAT,
|
|
key = key,
|
|
desc = desc,
|
|
default = typed_default,
|
|
maxcount = maxcount,
|
|
callback = callback
|
|
})
|
|
|
|
return self
|
|
end
|
|
|
|
--- Defines an optional argument.
|
|
---
|
|
--- Optional arguments can use 3 different notations, and can accept a value.
|
|
---
|
|
--- @param {string} key
|
|
---
|
|
--- The argument identifier. This can either be `-key`, or
|
|
--- `-key, --expanded-key`.
|
|
--- Values can be specified either by appending a space after the
|
|
--- identifier (e.g. `-key value` or `--expanded-key value`) or by
|
|
--- separating them with a `=` (e.g. `-key=value` or
|
|
--- `--expanded-key=value`).
|
|
---
|
|
--- @param {string} desc
|
|
---
|
|
--- A description for the argument to be shown in --help.
|
|
---
|
|
--- @param {bool} [default=nil]
|
|
---
|
|
--- A default value to use in case the option was not specified at
|
|
--- run-time (the default value is nil if you leave this blank.)
|
|
---
|
|
--- @param {function} [callback]
|
|
---
|
|
--- A callback to invoke when this option is parsed.
|
|
---
|
|
--- @example
|
|
---
|
|
--- The following option will be stored in `args["i"]` and `args["input"]`
|
|
--- with a default value of `file.txt`:
|
|
---
|
|
--- cli:option("-i, --input=FILE", "path to the input file", "file.txt")
|
|
function cli:option(key, desc, default, callback)
|
|
assert(type(key) == "string" and type(desc) == "string",
|
|
"Key and description are mandatory arguments (Strings)"
|
|
)
|
|
|
|
assert(is_callable(callback) or callback == nil,
|
|
"Callback argument: expected a function or nil"
|
|
)
|
|
|
|
local k, ek, v = disect(key)
|
|
|
|
-- if there's no VALUE indicator anywhere, what they want really is a flag.
|
|
-- e.g:
|
|
--
|
|
-- cli:option('-q, --quiet', '...')
|
|
if v == nil then
|
|
return self:flag(key, desc, default, callback)
|
|
end
|
|
|
|
define_option(k, ek, v, key, desc, default, callback)
|
|
|
|
return self
|
|
end
|
|
|
|
--- Define an optional "flag" argument.
|
|
---
|
|
--- Flags are a special subset of options that can either be `true` or `false`.
|
|
---
|
|
--- For example:
|
|
--- ```lua
|
|
--- cli:flag('-q, --quiet', 'Suppress output.', true)
|
|
--- ```
|
|
---
|
|
--- At run-time:
|
|
---
|
|
--- ```bash
|
|
--- $ ./script.lua --quiet
|
|
--- $ ./script.lua -q
|
|
--- ```
|
|
---
|
|
--- Passing a value to a flag raises an error:
|
|
---
|
|
--- ```bash
|
|
--- $ ./script.lua --quiet=foo
|
|
--- $ echo $? # => 1
|
|
--- ```
|
|
---
|
|
--- Flags may be _negatable_ by prepending `[no-]` to their key:
|
|
---
|
|
--- ```lua
|
|
--- cli:flag('-c, --[no-]compress', 'whether to compress or not', true)
|
|
--- ```
|
|
---
|
|
--- Now the user gets to pass `--no-compress` if they want to skip
|
|
--- compression, or either specify `--compress` explicitly or leave it
|
|
--- unspecified to use compression.
|
|
---
|
|
--- @param {string} key
|
|
--- @param {string} desc
|
|
--- @param {*} default
|
|
--- @param {function} callback
|
|
function cli:flag(key, desc, default, callback)
|
|
if type(default) == "function" then
|
|
callback = default
|
|
default = nil
|
|
end
|
|
|
|
assert(type(key) == "string" and type(desc) == "string",
|
|
"Key and description are mandatory arguments (Strings)"
|
|
)
|
|
|
|
local k, ek, v = disect(key)
|
|
|
|
if v ~= nil then
|
|
error("A flag type option cannot have a value set: " .. key)
|
|
end
|
|
|
|
define_option(k, ek, nil, key, desc, cast_to_boolean(default), callback)
|
|
|
|
return self
|
|
end
|
|
|
|
--- Define a command argument.
|
|
---
|
|
--- @param {string} name
|
|
--- The name of the command and the argument that the user has to
|
|
--- supply to invoke it.
|
|
---
|
|
--- @param {string} [desc]
|
|
--- An optional string to show in the help listing which should
|
|
--- describe what the command does. It will be displayed if --help
|
|
--- was run on the main program.
|
|
---
|
|
---
|
|
--- @return {cmd}
|
|
--- Another instance of the CLI library which is scoped to that
|
|
--- command.
|
|
function cli:command(name, desc)
|
|
local cmd = define_command_option(name)
|
|
|
|
cmd:set_name(cli.name .. ' ' .. name)
|
|
cmd:set_description(desc)
|
|
|
|
table.insert(options, cmd)
|
|
|
|
return cmd
|
|
end
|
|
|
|
--- Parse the process arguments table.
|
|
---
|
|
--- @param {table<string>} [arguments=_G.arg]
|
|
--- The list of arguments to parse. Defaults to the global `arg` table
|
|
--- which contains the arguments the process was started with.
|
|
---
|
|
--- @return {table}
|
|
--- A table containing all the arguments, options, flags,
|
|
--- and splat arguments that were specified or had a default
|
|
--- (where applicable).
|
|
---
|
|
--- @return {array<nil, string>}
|
|
--- If a parsing error has occured, note that the --help option is
|
|
--- also considered an error.
|
|
function cli:parse(arguments)
|
|
return parser(arguments, options, cli.printer)
|
|
end
|
|
|
|
--- Prints the USAGE message.
|
|
---
|
|
--- @return {string}
|
|
--- The USAGE message.
|
|
function cli:print_usage()
|
|
cli.printer.print(cli:get_usage_message())
|
|
end
|
|
|
|
function cli:get_usage_message()
|
|
return cli.printer.generate_usage()
|
|
end
|
|
|
|
--- Prints the HELP information.
|
|
---
|
|
--- @return {string}
|
|
--- The HELP message.
|
|
function cli:print_help()
|
|
cli.printer.print(cli.printer.generate_help_and_usage())
|
|
end
|
|
|
|
return cli
|
|
end
|
|
|
|
return create_core |