commit 3c057c19119fbbfe3bdcecf42af3aaac1d845fbd Author: SoraKatadzuma Date: Sat Sep 12 14:53:31 2026 -0500 Initial Commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2744b01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.chinook/ +.environ/ +.vscode/ diff --git a/chinookfile b/chinookfile new file mode 100644 index 0000000..580bbd5 --- /dev/null +++ b/chinookfile @@ -0,0 +1,42 @@ +name: argparse +gpid: malunal +semv: 1.0.0 + +requires: +- remote: git@git.erasit.com:malunal/allocators + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/containers + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/microtest + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/strview + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/types + branch: v1.0.0 + +targets: +- name: malunal.argparse + type: archive + opts: + - -g + deps: + - malunal.allocators + - malunal.containers + - malunal.strview + - malunal.types + srcs: + - ./sources/argparse.c + +tests: +- name: malunal.argparse.tests + type: program + opts: + - -g + deps: + - malunal.microtest + - malunal.argparse + srcs: + - ./tests/argparse.c + +exports: +- malunal.argparse diff --git a/include/malunal/argparse.h b/include/malunal/argparse.h new file mode 100644 index 0000000..9537711 --- /dev/null +++ b/include/malunal/argparse.h @@ -0,0 +1,462 @@ +/** + * @file argparse.h + * @brief Contains the structures and functions for a small command-line + * argument parser built atop @c strview_t, modeled after Python's + * @c argparse module. + * @author John Christman (sorakatadzuma@gmail.com) + * @copyright Malunal Studios, LLC. + * + * @code + * argparser_t parser; + * argresult_t result; + * argparser_init("prog", "Does a thing.", null, &parser); + * argparser_add_option(&parser, &ARGOPTION_FLAG( + * .short_name = 'v', + * .long_name = "verbose", + * .help_string = "increase verbosity" + * )); + * argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + * .value_name = "input", + * .value_type = ARGOPTION_TYPE_STRING, + * .required = true, + * .help_string = "the file to read" + * )); + * + * error_t error = argparser_parse(&parser, argc, argv, &result); + * if (error.domain != null) { + * argresult_print_error(&result, error, stderr); + * ... + * } + * @endcode + */ +#include "malunal/allocator.h" +#include "malunal/strview.h" +#include "malunal/containers/vector.h" +#include "malunal/types/error.h" + +#ifndef MALUNAL_ARGPARSE_HEADER +#define MALUNAL_ARGPARSE_HEADER + + +/** + * @brief Imports the @c argparse error domain for error checking. + * @details The @c argparse error domain is specific to defining and parsing + * command-line arguments. + */ +extern +const error_domain_t +ERROR_DOMAIN_ARGPARSE_T; + +/** + * @brief Defines the set of errors that may be triggered by argparse. + * @details The first group is produced while defining a parser, the second + * while parsing a command line, and the last while reading values + * back out of a result. + */ +typedef enum { + ARGPARSE_ERROR_NULL_INPUT, + ARGPARSE_ERROR_INVALID_DEFINITION, + ARGPARSE_ERROR_DUPLICATE_DEFINITION, + + ARGPARSE_ERROR_HELP_REQUESTED, + ARGPARSE_ERROR_UNKNOWN_OPTION, + ARGPARSE_ERROR_AMBIGUOUS_OPTION, + ARGPARSE_ERROR_UNKNOWN_SUBCOMMAND, + ARGPARSE_ERROR_MISSING_VALUE, + ARGPARSE_ERROR_UNEXPECTED_VALUE, + ARGPARSE_ERROR_INVALID_VALUE, + ARGPARSE_ERROR_MISSING_REQUIRED_OPTION, + ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL, + ARGPARSE_ERROR_TOO_MANY_POSITIONALS, + + ARGPARSE_ERROR_UNKNOWN_NAME, + ARGPARSE_ERROR_TYPE_MISMATCH, + ARGPARSE_ERROR_NOT_PRESENT, + ARGPARSE_ERROR_OUT_OF_RANGE, +} argparse_error_t; + + +/** + * @brief Defines the type of value an option or positional argument holds. + * @details Only options may be @c ARGOPTION_TYPE_FLAG, which takes no value + * and instead counts its occurrences (like Python's @c store_true + * and @c count actions combined). + */ +typedef enum { + ARGOPTION_TYPE_FLAG, /**< No value; occurrences are counted. */ + ARGOPTION_TYPE_STRING, /**< A @c strview_t into the original argv. */ + ARGOPTION_TYPE_NUMBER, /**< A @c malunal_int64_t, in base 10. */ + ARGOPTION_TYPE_FLOAT, /**< A @c malunal_double_t. */ + ARGOPTION_TYPE_BOOLEAN, /**< true/false, yes/no, on/off, or 1/0. */ +} argoption_type_t; + +/** + * @struct argoption + * @brief Defines a single argument accepted by a parser. + * @details When @c isflag is set, this is an option matched by name on the + * command line (@c -o or @c --output) and the @c flag member is used. + * Otherwise it is a positional argument matched by order, and the + * @c pos member is used. Prefer the @c ARGOPTION_FLAG and + * @c ARGOPTION_POSITIONAL macros to construct these. + * @remarks All strings are borrowed, not copied, and must outlive the parser. + */ +define_struct(argoption) { + malunal_char_t short_name; + malunal_cstr_t long_name; + malunal_cstr_t meta_name; + malunal_cstr_t help_string; + malunal_cstr_t default_value; + argoption_type_t value_type; + malunal_uint8_t isflag : 1; + malunal_uint8_t required : 1; + malunal_uint8_t multiple : 1; + malunal_uint8_t reserved : 5; +}; + +/** + * @def ARGOPTION_FLAG + * @brief Constructs an option (named) @c argoption_t compound literal. + * @param ... Designated initializers for the @c flag member. + */ +#define ARGOPTION_FLAG(...) \ + ((argoption_t){ .isflag = true, __VA_ARGS__ }) + +/** + * @def ARGOPTION_POSITIONAL + * @brief Constructs a positional @c argoption_t compound literal. + * @param ... Designated initializers for the @c pos member. + */ +#define ARGOPTION_POSITIONAL(...) \ + ((argoption_t){ .isflag = false, __VA_ARGS__ }) + + +/** + * @struct argparser + * @brief Defines a command-line parser. + * @details It consists of a name, the options for this particular parser, and a + * set of positionals. The positionals can be either of values or sub- + * commands, but not both. Every parser is given a @c -h / @c --help + * flag when initialized. + */ +define_struct(argparser) { + malunal_size_t __opaque[22]; +}; + +/** + * @struct argresult + * @brief Holds the outcome of parsing a command line against a parser. + * @details Values are looked up by an argument's long name, short name, or + * positional value name. Leading dashes on the name are ignored, so + * "output", "--output", "o", and "-o" all find the same option. + * @remarks A result refers to its parser and to the argv it was parsed from; + * both must outlive it. + */ +define_struct(argresult) { + malunal_size_t __opaque[19]; +}; + + +/** + * @brief Initializes a parser with the given @c name and @c desc. + * @param name The name of the program, shown in usage output. + * @param desc A description shown in help output, or null. + * @param allocator The allocator to obtain memory from, or null to use + * @c libc_allocator(). + * @param parser A pointer to the parser to initialize. + * @returns An error if the parser could not be initialized. + */ +error_t +argparser_init( + malunal_cstr_t name, + malunal_cstr_t desc, + allocator_mptr_t allocator, + argparser_mptr_t parser +); + +/** + * @brief Frees the memory owned by a parser, including its subcommands. + * @param parser A pointer to the parser to free. + * @returns An error if the parser could not be freed. + */ +error_t +argparser_free( + argparser_mptr_t parser +); + +/** + * @brief Registers an option or positional argument with the parser. + * @param parser A pointer to the parser to register the argument with. + * @param option A pointer to the argument definition, which is copied. + * @returns @c ARGPARSE_ERROR_INVALID_DEFINITION if the definition is + * inconsistent, @c ARGPARSE_ERROR_DUPLICATE_DEFINITION if any of its + * names are already in use, or an allocation error. + */ +error_t +argparser_add_option( + argparser_mptr_t parser, + argoption_iptr_t option +); + +/** + * @brief Registers a nested subcommand parser (e.g. "git commit"). + * @param parser A pointer to the parent parser. + * @param name The subcommand's name, as typed on the command line. + * @param desc A description shown in both parsers' help output, or null. + * @param out Receives the subcommand's parser, which is owned by + * @c parser and freed along with it. + * @returns An error if the parser already has positionals, the name is in + * use by a sibling, or allocation failed. + */ +error_t +argparser_add_subcommand( + argparser_mptr_t parser, + malunal_cstr_t name, + malunal_cstr_t desc, + argparser_mptr_t* out +); + +/** + * @brief Parses a command line against a parser's definitions. + * @details Options and positionals may be interleaved. A @c -- stops option + * processing. Long options may be abbreviated to any unique prefix. + * The first bare argument given to a parser with subcommands selects + * the subcommand, which parses everything after it. + * @param parser The parser to parse against. + * @param argc The number of entries in @c argv. + * @param argv The arguments, as passed to @c main(); index 0 is the + * program name and is skipped. + * @param result Populated with the parsed values. It is always initialized, + * so it must be freed with @c argresult_free() whether or not + * parsing succeeded. + * @returns An error describing the first problem encountered. When help was + * requested, @c ARGPARSE_ERROR_HELP_REQUESTED is returned. + */ +error_t +argparser_parse( + argparser_iptr_t parser, + malunal_int32_t argc, + malunal_cstr_t* argv, + argresult_mptr_t result +); + +/** + * @brief Writes a one-line usage summary of the parser to @c stream. + * @param parser The parser to describe. + * @returns An error if either input was null. + */ +error_t +argparser_print_usage( + argparser_iptr_t parser +); + +/** + * @brief Writes the full help text of the parser to @c stream. + * @param parser The parser to describe. + * @returns An error if either input was null. + */ +error_t +argparser_print_help( + argparser_iptr_t parser +); + + +/** + * @brief Frees the memory owned by a result, including subcommand results. + * @param result A pointer to the result to free. + * @returns An error if the result could not be freed. + */ +error_t +argresult_free( + argresult_mptr_t result +); + +/** + * @brief Reports a parse error the way Python's argparse does. + * @details Writes the usage of the parser that failed followed by a + * "prog: error: ..." line. If @c error is + * @c ARGPARSE_ERROR_HELP_REQUESTED, the full help of the parser that + * saw the help flag is written instead. + * @param result The result that @c error was produced for. + * @param error The error returned by @c argparser_parse(). + * @param stream The stream to write to. + * @returns An error if @c result or @c stream was null. + */ +error_t +argresult_print_error( + argresult_iptr_t result, + error_t error +); + +/** + * @brief Indicates whether an argument was given on the command line. + * @param result The result to check. + * @param name The argument's name. + * @retval true If the argument was given at least once. + * @retval false If it was absent (even if it has a default), or unknown. + */ +malunal_bool_t +argresult_has( + argresult_iptr_t result, + malunal_cstr_t name +); + +/** + * @brief Provides how many times a flag was given, or how many values an + * option or positional holds (including a default). + * @param result The result to check. + * @param name The argument's name. + * @returns The count, or zero if the argument is unknown. + */ +malunal_size_t +argresult_count( + argresult_iptr_t result, + malunal_cstr_t name +); + +/** + * @brief Retrieves the first value of a string argument. + * @param result The result to read from. + * @param name The argument's name. + * @param out Populated with a view into the original argv. + * @returns @c ARGPARSE_ERROR_UNKNOWN_NAME, @c ARGPARSE_ERROR_TYPE_MISMATCH, + * or @c ARGPARSE_ERROR_NOT_PRESENT if no value can be provided. + */ +error_t +argresult_get_string( + argresult_iptr_t result, + malunal_cstr_t name, + strview_mptr_t out +); + +/** + * @brief Retrieves the first value of a number argument. + * @param result The result to read from. + * @param name The argument's name. + * @param out Populated with the value. + * @returns An error if no value can be provided, see @c argresult_get_string. + */ +error_t +argresult_get_number( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_int64_t* out +); + +/** + * @brief Retrieves the first value of a float argument. + * @param result The result to read from. + * @param name The argument's name. + * @param out Populated with the value. + * @returns An error if no value can be provided, see @c argresult_get_string. + */ +error_t +argresult_get_float( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_double_t* out +); + +/** + * @brief Retrieves the first value of a boolean argument. + * @details This also accepts flags, reporting whether they were given. + * @param result The result to read from. + * @param name The argument's name. + * @param out Populated with the value. + * @returns An error if no value can be provided, see @c argresult_get_string. + */ +error_t +argresult_get_boolean( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_bool_t* out +); + +/** + * @brief Retrieves the value at @c index of a string argument. + * @param result The result to read from. + * @param name The argument's name. + * @param index The index of the value, below @c argresult_count(). + * @param out Populated with a view into the original argv. + * @returns An error if no value can be provided, including + * @c ARGPARSE_ERROR_OUT_OF_RANGE when @c index is too large. + */ +error_t +argresult_get_string_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + strview_mptr_t out +); + +/** + * @brief Retrieves the value at @c index of a number argument. + * @param result The result to read from. + * @param name The argument's name. + * @param index The index of the value, below @c argresult_count(). + * @param out Populated with the value. + * @returns An error if no value can be provided, see + * @c argresult_get_string_at. + */ +error_t +argresult_get_number_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + malunal_int64_t* out +); + +/** + * @brief Retrieves the value at @c index of a float argument. + * @param result The result to read from. + * @param name The argument's name. + * @param index The index of the value, below @c argresult_count(). + * @param out Populated with the value. + * @returns An error if no value can be provided, see + * @c argresult_get_string_at. + */ +error_t +argresult_get_float_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + malunal_double_t* out +); + +/** + * @brief Retrieves the value at @c index of a boolean argument. + * @param result The result to read from. + * @param name The argument's name. + * @param index The index of the value, below @c argresult_count(). + * @param out Populated with the value. + * @returns An error if no value can be provided, see + * @c argresult_get_string_at. + */ +error_t +argresult_get_boolean_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + malunal_bool_t* out +); + +/** + * @brief Provides the name of the subcommand that was invoked, if any. + * @param result The result to read from. + * @returns The subcommand's name, or null if none was invoked. + */ +malunal_cstr_t +argresult_subcommand( + argresult_iptr_t result +); + +/** + * @brief Provides the result of the subcommand that was invoked, if any. + * @param result The result to read from. + * @returns The subcommand's result, owned by @c result, or null. + */ +argresult_iptr_t +argresult_subresult( + argresult_iptr_t result +); + +#endif /* MALUNAL_ARGPARSE_HEADER */ diff --git a/sources/argparse.c b/sources/argparse.c new file mode 100644 index 0000000..4a44205 --- /dev/null +++ b/sources/argparse.c @@ -0,0 +1,1573 @@ +#include +#include +#include +#include "malunal/argparse.h" + +#define ARGPARSE_NPOS ((malunal_size_t)-1) +#define ARGPARSE_HELP_SLOT 0 +#define ARGPARSE_HELP_COLUMN 24 + +#define argparse_error(errcode) \ + ((error_t){ .domain = &ERROR_DOMAIN_ARGPARSE_T, .code = (errcode) }) + + +static +malunal_cstr_t +describe(malunal_int32_t code) { + switch (code) { + case ARGPARSE_ERROR_NULL_INPUT: + return "a required input was null"; + case ARGPARSE_ERROR_INVALID_DEFINITION: + return "argument was defined inconsistently"; + case ARGPARSE_ERROR_DUPLICATE_DEFINITION: + return "argument name is already in use"; + case ARGPARSE_ERROR_HELP_REQUESTED: + return "help was requested"; + case ARGPARSE_ERROR_UNKNOWN_OPTION: + return "unrecognized arguments"; + case ARGPARSE_ERROR_AMBIGUOUS_OPTION: + return "ambiguous option"; + case ARGPARSE_ERROR_UNKNOWN_SUBCOMMAND: + return "invalid choice"; + case ARGPARSE_ERROR_MISSING_VALUE: + return "expected one argument"; + case ARGPARSE_ERROR_UNEXPECTED_VALUE: + return "ignored explicit argument"; + case ARGPARSE_ERROR_INVALID_VALUE: + return "invalid value"; + case ARGPARSE_ERROR_MISSING_REQUIRED_OPTION: + case ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL: + return "the following arguments are required"; + case ARGPARSE_ERROR_TOO_MANY_POSITIONALS: + return "unrecognized arguments"; + case ARGPARSE_ERROR_UNKNOWN_NAME: + return "no argument is registered under that name"; + case ARGPARSE_ERROR_TYPE_MISMATCH: + return "argument type does not match the accessor"; + case ARGPARSE_ERROR_NOT_PRESENT: + return "argument has no value"; + case ARGPARSE_ERROR_OUT_OF_RANGE: + return "value index is out of range"; + } + + return "Unknown argparse error"; +} + +const error_domain_t ERROR_DOMAIN_ARGPARSE_T = { + .describe = &describe, + .name = "malunal.argparse.error" +}; + + +typedef struct { + malunal_cstr_t name; + malunal_cstr_t desc; + allocator_mptr_t allocator; + argparser_iptr_t parent; + vector_container_t options; + vector_container_t positionals; + vector_container_t subcommands; +} parser_impl_t; + +typedef parser_impl_t* parser_impl_mptr_t; +typedef const parser_impl_t* parser_impl_iptr_t; +_Static_assert( + sizeof(argparser_t) == sizeof(parser_impl_t), + "Argparser must be the size of its implementation." +); + +typedef union { + strview_t string; + malunal_int64_t number; + malunal_double_t floating; + malunal_bool_t boolean; +} argitem_t; + +typedef struct { + malunal_size_t slot; + argitem_t item; +} argentry_t; + +typedef struct { + argparser_iptr_t parser; + allocator_mptr_t allocator; + vector_container_t counts; /* malunal_size_t per slot. */ + vector_container_t entries; /* argentry_t. */ + argresult_mptr_t subresult; + argparser_iptr_t error_parser; + malunal_size_t error_slot; + strview_t error_arg; +} result_impl_t; + +typedef result_impl_t* result_impl_mptr_t; +typedef const result_impl_t* result_impl_iptr_t; +_Static_assert( + sizeof(argresult_t) == sizeof(result_impl_t), + "Argresult must be the size of its implementation." +); + + +/* ------------------------------------------------------------------------- + * Small helpers over vectors and argument definitions. + * ---------------------------------------------------------------------- */ + +static +malunal_size_t +vector_count( + vector_container_iptr_t vector +) { + malunal_size_t count = 0; + vector_container_count(vector, &count); + return count; +} + +static +error_t +vector_release( + vector_container_mptr_t vector +) { + malunal_size_t stride = 0; + vector_container_stride(vector, &stride); + return stride != 0 + ? vector_container_free(vector) + : NO_ERROR; +} + +static +malunal_size_t +option_keys( + argoption_iptr_t option, + strview_t keys[2] +) { + malunal_size_t count = 0; + if (option->isflag) { + if (option->short_name != '\0') + keys[count++] = strview(&option->short_name, 1); + if (option->long_name != null) + keys[count++] = strview_from_cstr(option->long_name); + } else if (option->long_name != null) { + keys[count++] = strview_from_cstr(option->long_name); + } + + return count; +} + +static +malunal_size_t +parser_slot_count( + parser_impl_iptr_t self +) { + return + vector_count(&self->options) + + vector_count(&self->positionals); +} + +static +argoption_t +parser_slot( + parser_impl_iptr_t self, + malunal_size_t slot +) { + argoption_t option = { 0 }; + malunal_size_t count = vector_count(&self->options); + if (slot < count) + vector_container_get(&self->options, slot, &option); + else + vector_container_get(&self->positionals, slot - count, &option); + return option; +} + +static +malunal_bool_t +strview_iequals_cstr( + strview_iptr_t view, + malunal_cstr_t text +) { + malunal_size_t length = strlen(text); + if (strview_length(view) != length) + return false; + + malunal_size_t index; + for (index = 0; index < length; index++) + if (tolower(view->beg[index]) != text[index]) + return false; + return true; +} + +static +error_t +argitem_convert( + strview_iptr_t token, + argoption_type_t type, + argitem_t* out +) { + switch (type) { + case ARGOPTION_TYPE_STRING: + out->string = *token; + return NO_ERROR; + + case ARGOPTION_TYPE_NUMBER: + return strview_to_int64(token, &out->number).domain != null + ? argparse_error(ARGPARSE_ERROR_INVALID_VALUE) + : NO_ERROR; + + case ARGOPTION_TYPE_FLOAT: + return strview_to_double(token, &out->floating).domain != null + ? argparse_error(ARGPARSE_ERROR_INVALID_VALUE) + : NO_ERROR; + + case ARGOPTION_TYPE_BOOLEAN: + if ( + strview_iequals_cstr(token, "true") || + strview_iequals_cstr(token, "yes") || + strview_iequals_cstr(token, "on") || + strview_iequals_cstr(token, "1") + ) { + out->boolean = true; + return NO_ERROR; + } + + if ( + strview_iequals_cstr(token, "false") || + strview_iequals_cstr(token, "no") || + strview_iequals_cstr(token, "off") || + strview_iequals_cstr(token, "0") + ) { + out->boolean = false; + return NO_ERROR; + } + + return argparse_error(ARGPARSE_ERROR_INVALID_VALUE); + + case ARGOPTION_TYPE_FLAG: + break; + } + + return argparse_error(ARGPARSE_ERROR_INVALID_VALUE); +} + + +/* ------------------------------------------------------------------------- + * Parser construction. + * ---------------------------------------------------------------------- */ + +static +malunal_bool_t +parser_name_taken( + parser_impl_iptr_t self, + argoption_iptr_t option +) { + strview_t keys[2]; + malunal_size_t nkeys = option_keys(option, keys); + malunal_size_t slots = parser_slot_count(self); + + malunal_size_t slot; + for (slot = 0; slot < slots; slot++) { + argoption_t existing = parser_slot(self, slot); + strview_t others[2]; + malunal_size_t nothers = option_keys(&existing, others); + + malunal_size_t key, other; + for (key = 0; key < nkeys; key++) + for (other = 0; other < nothers; other++) + if (strview_equals(&keys[key], &others[other])) + return true; + } + + return false; +} + +static +error_t +parser_validate_option( + parser_impl_iptr_t self, + argoption_iptr_t option +) { + argoption_type_t type = option->value_type; + if (type > ARGOPTION_TYPE_BOOLEAN) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + + if (option->isflag) { + malunal_char_t short_name = option->short_name; + malunal_cstr_t long_name = option->long_name; + if (short_name == '\0' && long_name == null) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (short_name == '-' || short_name == '=' || isspace((unsigned char)short_name)) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (long_name != null && (long_name[0] == '\0' || long_name[0] == '-' || strchr(long_name, '=') != null)) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (type == ARGOPTION_TYPE_FLAG && (option->required || option->default_value != null)) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + } else { + malunal_cstr_t value_name = option->long_name; + if (value_name == null || value_name[0] == '\0' || value_name[0] == '-') + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (type == ARGOPTION_TYPE_FLAG) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (vector_count(&self->subcommands) > 0) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + + malunal_size_t npositionals = vector_count(&self->positionals); + if (npositionals > 0) { + argoption_t last; + vector_container_get(&self->positionals, npositionals - 1, &last); + if (last.multiple) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (option->required && !last.required) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + } + } + + if (parser_name_taken(self, option)) + return argparse_error(ARGPARSE_ERROR_DUPLICATE_DEFINITION); + + malunal_cstr_t fallback = option->default_value; + if (fallback != null) { + argitem_t item = { 0 }; + strview_t token = strview_from_cstr(fallback); + if (argitem_convert(&token, type, &item).domain != null) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + } + + return NO_ERROR; +} + +error_t +argparser_init( + malunal_cstr_t name, + malunal_cstr_t desc, + allocator_mptr_t allocator, + argparser_mptr_t parser +) { + if (parser == null || name == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + if (allocator == null) + allocator = libc_allocator(); + + parser_impl_mptr_t self = (parser_impl_mptr_t)parser; + memset(self, 0, sizeof(parser_impl_t)); + self->name = name; + self->desc = desc; + self->allocator = allocator; + + error_t result = vector_container_init(sizeof(argoption_t), allocator, &self->options); + if (result.domain == null) + result = vector_container_init(sizeof(argoption_t), allocator, &self->positionals); + if (result.domain == null) + result = vector_container_init(sizeof(argparser_mptr_t), allocator, &self->subcommands); + if (result.domain == null) + result = argparser_add_option(parser, &ARGOPTION_FLAG( + .short_name = 'h', + .long_name = "help", + .help_string = "show this help message and exit" + )); + + if (result.domain != null) + argparser_free(parser); + return result; +} + +error_t +argparser_free( + argparser_mptr_t parser +) { + if (parser == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + parser_impl_mptr_t self = (parser_impl_mptr_t)parser; + error_t result = NO_ERROR; + malunal_size_t count = vector_count(&self->subcommands); + + malunal_size_t index; + for (index = 0; index < count; index++) { + argparser_mptr_t child = null; + vector_container_get(&self->subcommands, index, &child); + argparser_free(child); + + error_t disposed = allocator_dispose(self->allocator, child, sizeof(argparser_t)); + if (result.domain == null) + result = disposed; + } + + error_t released[3] = { + vector_release(&self->subcommands), + vector_release(&self->positionals), + vector_release(&self->options) + }; + + for (index = 0; index < 3; index++) + if (result.domain == null) + result = released[index]; + + memset(self, 0, sizeof(parser_impl_t)); + return result; +} + +error_t +argparser_add_option( + argparser_mptr_t parser, + argoption_iptr_t option +) { + if (parser == null || option == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + parser_impl_mptr_t self = (parser_impl_mptr_t)parser; + error_t result = parser_validate_option(self, option); + if (result.domain != null) + return result; + + return vector_container_append( + option->isflag ? &self->options : &self->positionals, + option + ); +} + +error_t +argparser_add_subcommand( + argparser_mptr_t parser, + malunal_cstr_t name, + malunal_cstr_t desc, + argparser_mptr_t* out +) { + if (parser == null || name == null || out == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + parser_impl_mptr_t self = (parser_impl_mptr_t)parser; + if (name[0] == '\0' || name[0] == '-') + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + if (vector_count(&self->positionals) > 0) + return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION); + + malunal_size_t count = vector_count(&self->subcommands); + malunal_size_t index; + for (index = 0; index < count; index++) { + argparser_mptr_t sibling = null; + vector_container_get(&self->subcommands, index, &sibling); + if (strcmp(((parser_impl_iptr_t)sibling)->name, name) == 0) + return argparse_error(ARGPARSE_ERROR_DUPLICATE_DEFINITION); + } + + malunal_mptr_t memory = null; + error_t result = allocator_acquire(self->allocator, sizeof(argparser_t), &memory); + if (result.domain != null) + return result; + + argparser_mptr_t child = (argparser_mptr_t)memory; + result = argparser_init(name, desc, self->allocator, child); + if (result.domain != null) { + allocator_dispose(self->allocator, memory, sizeof(argparser_t)); + return result; + } + + ((parser_impl_mptr_t)child)->parent = parser; + result = vector_container_append(&self->subcommands, &child); + if (result.domain != null) { + argparser_free(child); + allocator_dispose(self->allocator, memory, sizeof(argparser_t)); + return result; + } + + *out = child; + return NO_ERROR; +} + + +/* ------------------------------------------------------------------------- + * Result storage. + * ---------------------------------------------------------------------- */ + +static +error_t +result_init( + parser_impl_iptr_t parser, + result_impl_mptr_t self +) { + self->parser = (argparser_iptr_t)parser; + self->allocator = parser->allocator; + + error_t result = vector_container_init(sizeof(malunal_size_t), parser->allocator, &self->counts); + if (result.domain == null) + result = vector_container_init(sizeof(argentry_t), parser->allocator, &self->entries); + if (result.domain == null) + result = vector_container_resize(&self->counts, parser_slot_count(parser)); + + if (result.domain != null) { + argresult_free((argresult_mptr_t)self); + return result; + } + + malunal_size_t zero = 0; + malunal_size_t slots = parser_slot_count(parser); + malunal_size_t slot; + for (slot = 0; slot < slots; slot++) + vector_container_set(&self->counts, slot, &zero); + return NO_ERROR; +} + +static +error_t +result_fail( + result_impl_mptr_t self, + parser_impl_iptr_t parser, + argparse_error_t code, + malunal_size_t slot, + strview_t arg +) { + self->error_parser = (argparser_iptr_t)parser; + self->error_slot = slot; + self->error_arg = arg; + return argparse_error(code); +} + +static +malunal_size_t +result_slot_count( + result_impl_iptr_t self, + malunal_size_t slot +) { + malunal_size_t count = 0; + vector_container_get(&self->counts, slot, &count); + return count; +} + +static +malunal_size_t +result_find_entry( + result_impl_iptr_t self, + malunal_size_t slot, + malunal_size_t nth, + malunal_size_t* total +) { + malunal_size_t found = 0; + malunal_size_t match = ARGPARSE_NPOS; + malunal_size_t count = vector_count(&self->entries); + + malunal_size_t index; + for (index = 0; index < count; index++) { + argentry_t entry; + vector_container_get(&self->entries, index, &entry); + if (entry.slot != slot) + continue; + + if (found == nth) + match = index; + found++; + + if (total == null && match != ARGPARSE_NPOS) + break; + } + + if (total != null) + *total = found; + return match; +} + +static +error_t +result_store( + result_impl_mptr_t self, + malunal_size_t slot, + malunal_bool_t multiple, + argitem_t item +) { + argentry_t entry = { .slot = slot, .item = item }; + if (!multiple) { + malunal_size_t index = result_find_entry(self, slot, 0, null); + if (index != ARGPARSE_NPOS) + return vector_container_set(&self->entries, index, &entry); + } + + return vector_container_append(&self->entries, &entry); +} + +static +malunal_void_t +result_bump( + result_impl_mptr_t self, + malunal_size_t slot +) { + malunal_size_t count = result_slot_count(self, slot) + 1; + vector_container_set(&self->counts, slot, &count); +} + +static +error_t +result_store_token( + result_impl_mptr_t self, + parser_impl_iptr_t parser, + malunal_size_t slot, + argoption_iptr_t option, + strview_t token +) { + argitem_t item = { 0 }; + error_t result = argitem_convert(&token, option->value_type, &item); + if (result.domain != null) + return result_fail(self, parser, ARGPARSE_ERROR_INVALID_VALUE, slot, token); + + result = result_store(self, slot, option->multiple, item); + if (result.domain != null) + return result; + + result_bump(self, slot); + return NO_ERROR; +} + + +/* ------------------------------------------------------------------------- + * Parsing. + * ---------------------------------------------------------------------- */ + +static +malunal_bool_t +token_is_negative_number( + strview_iptr_t token +) { + malunal_size_t length = strview_length(token); + if (length < 2 || token->beg[0] != '-') + return false; + + malunal_bool_t digits = false; + malunal_bool_t dotted = false; + malunal_size_t index; + for (index = 1; index < length; index++) { + malunal_char_t current = token->beg[index]; + if (isdigit((unsigned char)current)) { + digits = true; + } else if (current == '.' && !dotted) { + dotted = true; + } else { + return false; + } + } + + return digits && token->beg[length - 1] != '.'; +} + +static +malunal_bool_t +parser_has_numeric_option( + parser_impl_iptr_t self +) { + malunal_size_t count = vector_count(&self->options); + malunal_size_t index; + for (index = 0; index < count; index++) { + argoption_t option; + vector_container_get(&self->options, index, &option); + if (isdigit(option.short_name)) + return true; + } + + return false; +} + +static +malunal_bool_t +parser_is_option_token( + parser_impl_iptr_t self, + strview_iptr_t token +) { + if (strview_length(token) < 2 || token->beg[0] != '-') + return false; + + return + !token_is_negative_number(token) || + parser_has_numeric_option(self); +} + +static +malunal_bool_t +parser_can_take_value( + parser_impl_iptr_t self, + malunal_int32_t argc, + malunal_cstr_t* argv, + malunal_int32_t index +) { + if (index >= argc) + return false; + + strview_t token = strview_from_cstr(argv[index]); + return !parser_is_option_token(self, &token); +} + +static +malunal_size_t +parser_find_short( + parser_impl_iptr_t self, + malunal_char_t name +) { + malunal_size_t count = vector_count(&self->options); + malunal_size_t index; + for (index = 0; index < count; index++) { + argoption_t option; + vector_container_get(&self->options, index, &option); + if (option.short_name == name) + return index; + } + + return ARGPARSE_NPOS; +} + +static +error_t +parser_find_long( + parser_impl_iptr_t self, + strview_iptr_t name, + malunal_size_t* out +) { + malunal_size_t count = vector_count(&self->options); + malunal_size_t matches = 0; + malunal_size_t index; + + *out = ARGPARSE_NPOS; + if (strview_length(name) == 0) + return argparse_error(ARGPARSE_ERROR_UNKNOWN_OPTION); + + for (index = 0; index < count; index++) { + argoption_t option; + vector_container_get(&self->options, index, &option); + if (option.long_name == null) + continue; + + strview_t candidate = strview_from_cstr(option.long_name); + if (strview_equals(&candidate, name)) { + *out = index; + return NO_ERROR; + } + + if (strview_starts_with(&candidate, name)) { + if (matches == 0) + *out = index; + matches++; + } + } + + if (matches == 1) + return NO_ERROR; + + *out = ARGPARSE_NPOS; + return argparse_error(matches > 1 + ? ARGPARSE_ERROR_AMBIGUOUS_OPTION + : ARGPARSE_ERROR_UNKNOWN_OPTION); +} + +static +error_t +parser_parse_long( + parser_impl_iptr_t self, + result_impl_mptr_t out, + malunal_int32_t argc, + malunal_cstr_t* argv, + malunal_int32_t* index +) { + strview_t token = strview_from_cstr(argv[*index]); + strview_t name = token; + strview_t value = strview(null, 0); + malunal_bool_t has_inline = false; + strview_ltrim(&name, 2); + + malunal_cstr_t equals = memchr(name.beg, '=', strview_length(&name)); + if (equals != null) { + value = (strview_t){ equals + 1, name.end }; + name.end = equals; + has_inline = true; + } + + malunal_size_t slot = ARGPARSE_NPOS; + error_t lookup = parser_find_long(self, &name, &slot); + if (lookup.domain != null) + return result_fail(out, self, lookup.code, ARGPARSE_NPOS, (strview_t){ token.beg, name.end }); + if (slot == ARGPARSE_HELP_SLOT) + return result_fail(out, self, ARGPARSE_ERROR_HELP_REQUESTED, ARGPARSE_NPOS, token); + + argoption_t option; + vector_container_get(&self->options, slot, &option); + *index += 1; + + if (option.value_type == ARGOPTION_TYPE_FLAG) { + if (has_inline) + return result_fail(out, self, ARGPARSE_ERROR_UNEXPECTED_VALUE, slot, value); + result_bump(out, slot); + return NO_ERROR; + } + + if (!has_inline) { + if (!parser_can_take_value(self, argc, argv, *index)) + return result_fail(out, self, ARGPARSE_ERROR_MISSING_VALUE, slot, strview(null, 0)); + value = strview_from_cstr(argv[*index]); + *index += 1; + } + + return result_store_token(out, self, slot, &option, value); +} + +static +error_t +parser_parse_short( + parser_impl_iptr_t self, + result_impl_mptr_t out, + malunal_int32_t argc, + malunal_cstr_t* argv, + malunal_int32_t* index +) { + strview_t token = strview_from_cstr(argv[*index]); + malunal_cstr_t cursor = token.beg + 1; + *index += 1; + + while (cursor < token.end) { + malunal_size_t slot = parser_find_short(self, *cursor); + if (slot == ARGPARSE_NPOS) + return result_fail(out, self, ARGPARSE_ERROR_UNKNOWN_OPTION, ARGPARSE_NPOS, token); + if (slot == ARGPARSE_HELP_SLOT) + return result_fail(out, self, ARGPARSE_ERROR_HELP_REQUESTED, ARGPARSE_NPOS, token); + + argoption_t option; + vector_container_get(&self->options, slot, &option); + cursor++; + + if (option.value_type == ARGOPTION_TYPE_FLAG) { + result_bump(out, slot); + continue; + } + + /* A value-bearing option consumes the rest of the token, or the next. */ + strview_t value; + if (cursor < token.end) { + if (*cursor == '=') + cursor++; + value = (strview_t){ cursor, token.end }; + } else { + if (!parser_can_take_value(self, argc, argv, *index)) + return result_fail(out, self, ARGPARSE_ERROR_MISSING_VALUE, slot, strview(null, 0)); + value = strview_from_cstr(argv[*index]); + *index += 1; + } + + return result_store_token(out, self, slot, &option, value); + } + + return NO_ERROR; +} + +static +error_t +parser_parse_subcommand( + parser_impl_iptr_t self, + result_impl_mptr_t out, + malunal_int32_t argc, + malunal_cstr_t* argv, + malunal_int32_t index +) { + strview_t token = strview_from_cstr(argv[index]); + argparser_mptr_t child = null; + malunal_size_t count = vector_count(&self->subcommands); + + malunal_size_t current; + for (current = 0; current < count; current++) { + argparser_mptr_t candidate = null; + vector_container_get(&self->subcommands, current, &candidate); + + strview_t name = strview_from_cstr(((parser_impl_iptr_t)candidate)->name); + if (strview_equals(&name, &token)) { + child = candidate; + break; + } + } + + if (child == null) + return result_fail(out, self, ARGPARSE_ERROR_UNKNOWN_SUBCOMMAND, ARGPARSE_NPOS, token); + + malunal_mptr_t memory = null; + error_t result = allocator_acquire(self->allocator, sizeof(argresult_t), &memory); + if (result.domain != null) + return result; + + out->subresult = (argresult_mptr_t)memory; + result = argparser_parse(child, argc - index, argv + index, out->subresult); + if (result.domain != null) { + result_impl_iptr_t sub = (result_impl_iptr_t)out->subresult; + out->error_parser = sub->error_parser; + out->error_slot = sub->error_slot; + out->error_arg = sub->error_arg; + } + + return result; +} + +static +error_t +parser_finalize( + parser_impl_iptr_t self, + result_impl_mptr_t out +) { + malunal_size_t noptions = vector_count(&self->options); + malunal_size_t slots = parser_slot_count(self); + + malunal_size_t slot; + for (slot = 0; slot < slots; slot++) { + if (result_slot_count(out, slot) > 0) + continue; + + argoption_t option = parser_slot(self, slot); + if (option.required) + return result_fail( + out, + self, + slot < noptions + ? ARGPARSE_ERROR_MISSING_REQUIRED_OPTION + : ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL, + slot, + strview(null, 0) + ); + + malunal_cstr_t fallback = option.default_value; + if (fallback == null) + continue; + + /* Defaults were validated when added, so conversion cannot fail. */ + argitem_t item = { 0 }; + strview_t token = strview_from_cstr(fallback); + argitem_convert(&token, option.value_type, &item); + + error_t result = result_store(out, slot, false, item); + if (result.domain != null) + return result; + } + + return NO_ERROR; +} + +error_t +argparser_parse( + argparser_iptr_t parser, + malunal_int32_t argc, + malunal_cstr_t* argv, + argresult_mptr_t result +) { + if (result == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + result_impl_mptr_t out = (result_impl_mptr_t)result; + memset(out, 0, sizeof(result_impl_t)); + out->error_slot = ARGPARSE_NPOS; + + if (parser == null || argc < 0 || (argv == null && argc > 0)) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + parser_impl_iptr_t self = (parser_impl_iptr_t)parser; + error_t error = result_init(self, out); + if (error.domain != null) + return error; + + malunal_bool_t options_ended = false; + malunal_bool_t dispatches = vector_count(&self->subcommands) > 0; + malunal_size_t noptions = vector_count(&self->options); + malunal_size_t npositionals = vector_count(&self->positionals); + malunal_size_t positional = 0; + malunal_int32_t index = 1; + + while (index < argc) { + strview_t token = strview_from_cstr(argv[index]); + + if (!options_ended && parser_is_option_token(self, &token)) { + if (strview_length(&token) == 2 && token.beg[1] == '-') { + options_ended = true; + index++; + continue; + } + + error = token.beg[1] == '-' + ? parser_parse_long(self, out, argc, argv, &index) + : parser_parse_short(self, out, argc, argv, &index); + if (error.domain != null) + return error; + continue; + } + + if (dispatches) { + error = parser_parse_subcommand(self, out, argc, argv, index); + if (error.domain != null) + return error; + break; + } + + if (positional >= npositionals) + return result_fail(out, self, ARGPARSE_ERROR_TOO_MANY_POSITIONALS, ARGPARSE_NPOS, token); + + argoption_t option; + vector_container_get(&self->positionals, positional, &option); + error = result_store_token(out, self, noptions + positional, &option, token); + if (error.domain != null) + return error; + + if (!option.multiple) + positional++; + index++; + } + + return parser_finalize(self, out); +} + + +/* ------------------------------------------------------------------------- + * Help and usage output. + * ---------------------------------------------------------------------- */ + +static +malunal_int32_t +print_prog( + FILE* stream, + parser_impl_iptr_t self +) { + malunal_int32_t written = 0; + if (self->parent != null) + written += print_prog(stream, (parser_impl_iptr_t)self->parent) + fprintf(stream, " "); + return written + fprintf(stream, "%s", self->name); +} + +static +malunal_int32_t +print_metavar( + FILE* stream, + argoption_iptr_t option +) { + if (!option->isflag) + return fprintf(stream, "%s", + option->meta_name != null + ? option->meta_name + : option->long_name); + + if (option->meta_name != null) + return fprintf(stream, "%s", option->meta_name); + + if (option->long_name == null) + return fprintf(stream, "%c", toupper((unsigned char)option->short_name)); + + malunal_cstr_t cursor = option->long_name; + for (; *cursor != '\0'; cursor++) + fputc(*cursor == '-' ? '_' : toupper((unsigned char)*cursor), stream); + return (malunal_int32_t)(cursor - option->long_name); +} + + +static +malunal_void_t +print_names( + FILE* stream, + argoption_iptr_t option +) { + if (!option->isflag) { + print_metavar(stream, option); + return; + } + + if (option->short_name != '\0') + fprintf(stream, "-%c", option->short_name); + if (option->short_name != '\0' && option->long_name != null) + fputc('/', stream); + if (option->long_name != null) + fprintf(stream, "--%s", option->long_name); +} + +static +malunal_void_t +print_option_usage( + FILE* stream, + argoption_iptr_t option +) { + if (!option->isflag) { + malunal_bool_t required = option->required; + if (option->multiple) { + if (required) { + print_metavar(stream, option); + fputc(' ', stream); + } + fputc('[', stream); + print_metavar(stream, option); + fputs(" ...]", stream); + } else { + if (!required) fputc('[', stream); + print_metavar(stream, option); + if (!required) fputc(']', stream); + } + return; + } + + if (!option->required) + fputc('[', stream); + + if (option->short_name != '\0') + fprintf(stream, "-%c", option->short_name); + else + fprintf(stream, "--%s", option->long_name); + + if (option->value_type != ARGOPTION_TYPE_FLAG) { + fputc(' ', stream); + print_metavar(stream, option); + } + + if (!option->required) + fputc(']', stream); +} + +static +malunal_void_t +print_subcommand_choices( + FILE* stream, + parser_impl_iptr_t self +) { + malunal_size_t count = vector_count(&self->subcommands); + malunal_size_t index; + + fputc('{', stream); + for (index = 0; index < count; index++) { + argparser_mptr_t child = null; + vector_container_get(&self->subcommands, index, &child); + fprintf(stream, index > 0 ? ",%s" : "%s", ((parser_impl_iptr_t)child)->name); + } + fputc('}', stream); +} + +static +malunal_void_t +print_help_text( + FILE* stream, + malunal_int32_t column, + malunal_cstr_t help +) { + if (help == null) { + fputc('\n', stream); + return; + } + + if (column + 2 <= ARGPARSE_HELP_COLUMN) + fprintf(stream, "%*s%s\n", ARGPARSE_HELP_COLUMN - column, "", help); + else + fprintf(stream, "\n%*s%s\n", ARGPARSE_HELP_COLUMN, "", help); +} + +error_t +argparser_print_usage( + argparser_iptr_t parser +) { + if (parser == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + parser_impl_iptr_t self = (parser_impl_iptr_t)parser; + malunal_size_t slots = parser_slot_count(self); + + fputs("usage: ", stdout); + print_prog(stdout, self); + + malunal_size_t slot; + for (slot = 0; slot < slots; slot++) { + argoption_t option = parser_slot(self, slot); + fputc(' ', stdout); + print_option_usage(stdout, &option); + } + + if (vector_count(&self->subcommands) > 0) { + fputc(' ', stdout); + print_subcommand_choices(stdout, self); + fputs(" ...", stdout); + } + + fputc('\n', stdout); + return NO_ERROR; +} + +error_t +argparser_print_help( + argparser_iptr_t parser +) { + error_t result = argparser_print_usage(parser); + if (result.domain != null) + return result; + + parser_impl_iptr_t self = (parser_impl_iptr_t)parser; + malunal_size_t noptions = vector_count(&self->options); + malunal_size_t npositionals = vector_count(&self->positionals); + malunal_size_t nsubcommands = vector_count(&self->subcommands); + malunal_size_t index; + + if (self->desc != null) + fprintf(stdout, "\n%s\n", self->desc); + + if (npositionals > 0 || nsubcommands > 0) + fputs("\npositional arguments:\n", stdout); + + for (index = 0; index < npositionals; index++) { + argoption_t option; + vector_container_get(&self->positionals, index, &option); + malunal_int32_t column = fprintf(stdout, " ") + print_metavar(stdout, &option); + print_help_text(stdout, column, option.help_string); + } + + if (nsubcommands > 0) { + fputs(" ", stdout); + print_subcommand_choices(stdout, self); + fputc('\n', stdout); + } + + for (index = 0; index < nsubcommands; index++) { + argparser_mptr_t child = null; + vector_container_get(&self->subcommands, index, &child); + + parser_impl_iptr_t sub = (parser_impl_iptr_t)child; + malunal_int32_t column = fprintf(stdout, " %s", sub->name); + print_help_text(stdout, column, sub->desc); + } + + fputs("\noptions:\n", stdout); + for (index = 0; index < noptions; index++) { + argoption_t option; + vector_container_get(&self->options, index, &option); + + malunal_int32_t column = fprintf(stdout, " "); + if (option.short_name != '\0') + column += fprintf(stdout, "-%c", option.short_name); + if (option.short_name != '\0' && option.long_name != null) + column += fprintf(stdout, ", "); + if (option.long_name != null) + column += fprintf(stdout, "--%s", option.long_name); + if (option.value_type != ARGOPTION_TYPE_FLAG) + column += fprintf(stdout, " ") + print_metavar(stdout, &option); + + print_help_text(stdout, column, option.help_string); + } + + return NO_ERROR; +} + + +/* ------------------------------------------------------------------------- + * Reading results back out. + * ---------------------------------------------------------------------- */ + +error_t +argresult_free( + argresult_mptr_t result +) { + if (result == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + result_impl_mptr_t self = (result_impl_mptr_t)result; + error_t status = NO_ERROR; + if (self->subresult != null) { + argresult_free(self->subresult); + status = allocator_dispose(self->allocator, self->subresult, sizeof(argresult_t)); + } + + error_t entries = vector_release(&self->entries); + error_t counts = vector_release(&self->counts); + if (status.domain == null) + status = entries; + if (status.domain == null) + status = counts; + + memset(self, 0, sizeof(result_impl_t)); + self->error_slot = ARGPARSE_NPOS; + return status; +} + +error_t +argresult_print_error( + argresult_iptr_t result, + error_t error +) { + if (result == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + if (error.domain == null) + return NO_ERROR; + + result_impl_iptr_t self = (result_impl_iptr_t)result; + argparser_iptr_t parser = self->error_parser != null + ? self->error_parser + : self->parser; + + malunal_bool_t ours = error.domain == &ERROR_DOMAIN_ARGPARSE_T; + if (ours && error.code == ARGPARSE_ERROR_HELP_REQUESTED && parser != null) + return argparser_print_help(parser); + + malunal_cstr_t message = error.domain->describe != null + ? error.domain->describe(error.code) + : error.domain->name; + + if (parser == null) { + fprintf(stderr, "error: %s\n", message); + return NO_ERROR; + } + + parser_impl_iptr_t impl = (parser_impl_iptr_t)parser; + argparser_print_usage(parser); + print_prog(stderr, impl); + fputs(": error: ", stderr); + + malunal_bool_t has_slot = ours && self->error_slot != ARGPARSE_NPOS; + malunal_bool_t missing = ours && ( + error.code == ARGPARSE_ERROR_MISSING_REQUIRED_OPTION || + error.code == ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL + ); + + argoption_t option = { 0 }; + if (has_slot) + option = parser_slot(impl, self->error_slot); + + if (missing && has_slot) { + fprintf(stderr, "%s: ", message); + print_names(stderr, &option); + } else { + if (has_slot) { + fputs("argument ", stderr); + print_names(stderr, &option); + fputs(": ", stderr); + } + fputs(message, stderr); + } + + malunal_size_t length = strview_length(&self->error_arg); + if (ours && (length > 0 || error.code == ARGPARSE_ERROR_INVALID_VALUE)) + fprintf(stderr, ": '%.*s'", (malunal_int32_t)length, self->error_arg.beg != null ? self->error_arg.beg : ""); + + fputc('\n', stderr); + return NO_ERROR; +} + +static +error_t +result_lookup( + result_impl_iptr_t self, + malunal_cstr_t name, + malunal_size_t* outslot, + argoption_t* outoption +) { + if (self == null || self->parser == null || name == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + strview_t key = strview_from_cstr(name); + malunal_size_t stripped = 0; + while (stripped < 2 && strview_length(&key) > 1 && key.beg[0] == '-') { + strview_ltrim(&key, 1); + stripped++; + } + + parser_impl_iptr_t parser = (parser_impl_iptr_t)self->parser; + malunal_size_t slots = parser_slot_count(parser); + + malunal_size_t slot; + for (slot = 0; slot < slots; slot++) { + argoption_t option = parser_slot(parser, slot); + strview_t keys[2]; + malunal_size_t nkeys = option_keys(&option, keys); + + malunal_size_t index; + for (index = 0; index < nkeys; index++) { + if (strview_equals(&keys[index], &key)) { + *outslot = slot; + *outoption = option; + return NO_ERROR; + } + } + } + + return argparse_error(ARGPARSE_ERROR_UNKNOWN_NAME); +} + +static +error_t +result_get_item( + argresult_iptr_t result, + malunal_cstr_t name, + argoption_type_t type, + malunal_size_t index, + argitem_t* out +) { + result_impl_iptr_t self = (result_impl_iptr_t)result; + malunal_size_t slot = ARGPARSE_NPOS; + argoption_t option = { 0 }; + error_t error = result_lookup(self, name, &slot, &option); + if (error.domain != null) + return error; + + argoption_type_t actual = option.value_type; + if (type == ARGOPTION_TYPE_BOOLEAN && actual == ARGOPTION_TYPE_FLAG) { + if (index != 0) + return argparse_error(ARGPARSE_ERROR_OUT_OF_RANGE); + out->boolean = result_slot_count(self, slot) > 0; + return NO_ERROR; + } + + if (actual != type) + return argparse_error(ARGPARSE_ERROR_TYPE_MISMATCH); + + malunal_size_t total = 0; + malunal_size_t found = result_find_entry(self, slot, index, &total); + if (total == 0) + return argparse_error(ARGPARSE_ERROR_NOT_PRESENT); + if (found == ARGPARSE_NPOS) + return argparse_error(ARGPARSE_ERROR_OUT_OF_RANGE); + + argentry_t entry; + vector_container_get(&self->entries, found, &entry); + *out = entry.item; + return NO_ERROR; +} + +malunal_bool_t +argresult_has( + argresult_iptr_t result, + malunal_cstr_t name +) { + result_impl_iptr_t self = (result_impl_iptr_t)result; + malunal_size_t slot = ARGPARSE_NPOS; + argoption_t option = { 0 }; + return result_lookup(self, name, &slot, &option).domain == null + && result_slot_count(self, slot) > 0; +} + +malunal_size_t +argresult_count( + argresult_iptr_t result, + malunal_cstr_t name +) { + result_impl_iptr_t self = (result_impl_iptr_t)result; + malunal_size_t slot = ARGPARSE_NPOS; + argoption_t option = { 0 }; + if (result_lookup(self, name, &slot, &option).domain != null) + return 0; + + if (option.value_type == ARGOPTION_TYPE_FLAG) + return result_slot_count(self, slot); + + malunal_size_t total = 0; + result_find_entry(self, slot, 0, &total); + return total; +} + +error_t +argresult_get_string( + argresult_iptr_t result, + malunal_cstr_t name, + strview_mptr_t out +) { + return argresult_get_string_at(result, name, 0, out); +} + +error_t +argresult_get_number( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_int64_t* out +) { + return argresult_get_number_at(result, name, 0, out); +} + +error_t +argresult_get_float( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_double_t* out +) { + return argresult_get_float_at(result, name, 0, out); +} + +error_t +argresult_get_boolean( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_bool_t* out +) { + return argresult_get_boolean_at(result, name, 0, out); +} + +error_t +argresult_get_string_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + strview_mptr_t out +) { + if (out == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + argitem_t item = { 0 }; + error_t error = result_get_item(result, name, ARGOPTION_TYPE_STRING, index, &item); + if (error.domain == null) + *out = item.string; + return error; +} + +error_t +argresult_get_number_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + malunal_int64_t* out +) { + if (out == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + argitem_t item = { 0 }; + error_t error = result_get_item(result, name, ARGOPTION_TYPE_NUMBER, index, &item); + if (error.domain == null) + *out = item.number; + return error; +} + +error_t +argresult_get_float_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + malunal_double_t* out +) { + if (out == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + argitem_t item = { 0 }; + error_t error = result_get_item(result, name, ARGOPTION_TYPE_FLOAT, index, &item); + if (error.domain == null) + *out = item.floating; + return error; +} + +error_t +argresult_get_boolean_at( + argresult_iptr_t result, + malunal_cstr_t name, + malunal_size_t index, + malunal_bool_t* out +) { + if (out == null) + return argparse_error(ARGPARSE_ERROR_NULL_INPUT); + + argitem_t item = { 0 }; + error_t error = result_get_item(result, name, ARGOPTION_TYPE_BOOLEAN, index, &item); + if (error.domain == null) + *out = item.boolean; + return error; +} + +malunal_cstr_t +argresult_subcommand( + argresult_iptr_t result +) { + if (result == null) + return null; + + result_impl_iptr_t self = (result_impl_iptr_t)result; + if (self->subresult == null) + return null; + + result_impl_iptr_t sub = (result_impl_iptr_t)self->subresult; + return ((parser_impl_iptr_t)sub->parser)->name; +} + +argresult_iptr_t +argresult_subresult( + argresult_iptr_t result +) { + return result != null + ? ((result_impl_iptr_t)result)->subresult + : null; +} diff --git a/tests/argparse.c b/tests/argparse.c new file mode 100644 index 0000000..b83bd42 --- /dev/null +++ b/tests/argparse.c @@ -0,0 +1,804 @@ +#include "malunal/microtest.h" +#include "malunal/argparse.h" + +#define PARSE(parser, result, ...) \ + argparser_parse( \ + (parser), \ + (malunal_int32_t)( \ + sizeof((malunal_cstr_t[]){ __VA_ARGS__ }) / sizeof(malunal_str_t) \ + ), \ + (malunal_cstr_t[]){ __VA_ARGS__ }, \ + (result) \ + ) + +static +malunal_bool_t +view_is( + strview_t view, + malunal_cstr_t text +) { + strview_t other = strview_from_cstr(text); + return strview_equals(&view, &other); +} + +/* ------------------------------------------------------------------------- + * Parser construction. + * ---------------------------------------------------------------------- */ + +MICROTEST(argparser_add_option, accepts_flag_with_short_and_long) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'v', + .long_name = "verbose" + )); + MICROTEST_EXPECT_NULL(err.domain); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_option_without_names) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .value_type = ARGOPTION_TYPE_STRING + )); + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_long_name_with_dashes) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .long_name = "--verbose" + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_duplicate_short_name) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'v', + .long_name = "verbose" + )); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'v', + .long_name = "version" + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_DUPLICATE_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_duplicate_long_name) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'v', + .long_name = "verbose" + )); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .long_name = "verbose" + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_DUPLICATE_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_conflict_with_builtin_help) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'h', + .long_name = "host" + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_DUPLICATE_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_positional_named_like_option) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .long_name = "input", + .value_type = ARGOPTION_TYPE_STRING + )); + + error_t err = argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "input", + .value_type = ARGOPTION_TYPE_STRING + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_DUPLICATE_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_required_flag) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'v', + .required = true + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_flag_typed_positional) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "input", + .value_type = ARGOPTION_TYPE_FLAG + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_positional_after_multiple) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "files", + .value_type = ARGOPTION_TYPE_STRING, + .multiple = true + )); + + error_t err = argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "extra", + .value_type = ARGOPTION_TYPE_STRING + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_required_positional_after_optional) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "first", + .value_type = ARGOPTION_TYPE_STRING + )); + + error_t err = argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "second", + .value_type = ARGOPTION_TYPE_STRING, + .required = true + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_unconvertible_default) { + argparser_t parser; + argparser_init("prog", null, null, &parser); + + error_t err = argparser_add_option(&parser, &ARGOPTION_FLAG( + .long_name = "jobs", + .value_type = ARGOPTION_TYPE_NUMBER, + .default_value = "many" + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_option, rejects_positional_when_subcommands_exist) { + argparser_t parser; + argparser_mptr_t sub; + argparser_init("git", null, null, &parser); + argparser_add_subcommand(&parser, "commit", null, &sub); + + error_t err = argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "x", + .value_type = ARGOPTION_TYPE_STRING + )); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_subcommand, rejects_when_positionals_exist) { + argparser_t parser; + argparser_mptr_t sub; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "x", + .value_type = ARGOPTION_TYPE_STRING + )); + + error_t err = argparser_add_subcommand(&parser, "sub", null, &sub); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_DEFINITION); + + argparser_free(&parser); +} + +MICROTEST(argparser_add_subcommand, rejects_duplicate_name) { + argparser_t parser; + argparser_mptr_t first; + argparser_mptr_t second; + argparser_init("git", null, null, &parser); + argparser_add_subcommand(&parser, "build", null, &first); + + error_t err = argparser_add_subcommand(&parser, "build", null, &second); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_DUPLICATE_DEFINITION); + + argparser_free(&parser); +} + + +/* ------------------------------------------------------------------------- + * Parsing: flags. + * ---------------------------------------------------------------------- */ + +MICROTEST(argparser_parse, short_and_long_flags_are_recognized) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'a', .long_name = "all")); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'b', .long_name = "bold")); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'c', .long_name = "color")); + + error_t err = PARSE(&parser, &result, "prog", "-a", "--bold"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "all")); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "-b")); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "--bold")); + MICROTEST_EXPECT_FALSE(argresult_has(&result, "color")); + + malunal_bool_t color = true; + MICROTEST_EXPECT_NULL(argresult_get_boolean(&result, "color", &color).domain); + MICROTEST_EXPECT_FALSE(color); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, flags_count_occurrences_and_cluster) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'v', .long_name = "verbose")); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'q')); + + error_t err = PARSE(&parser, &result, "prog", "-vvq", "--verbose"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(argresult_count(&result, "verbose"), (malunal_size_t)3); + MICROTEST_EXPECT_EQ(argresult_count(&result, "q"), (malunal_size_t)1); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, flag_with_inline_value_fails) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'v', .long_name = "verbose")); + + error_t err = PARSE(&parser, &result, "prog", "--verbose=yes"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_UNEXPECTED_VALUE); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, unknown_options_fail) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + + error_t err = PARSE(&parser, &result, "prog", "--bogus"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_UNKNOWN_OPTION); + argresult_free(&result); + + err = PARSE(&parser, &result, "prog", "-z"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_UNKNOWN_OPTION); + argresult_free(&result); + + argparser_free(&parser); +} + +MICROTEST(argparser_parse, help_flag_is_reported) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + + error_t err = PARSE(&parser, &result, "prog", "-h"); + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_HELP_REQUESTED); + argresult_free(&result); + + err = PARSE(&parser, &result, "prog", "--help"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_HELP_REQUESTED); + argresult_free(&result); + + argparser_free(&parser); +} + + +/* ------------------------------------------------------------------------- + * Parsing: value-bearing options. + * ---------------------------------------------------------------------- */ + +MICROTEST(argparser_parse, string_option_accepts_every_spelling) { + argparser_t parser; + argresult_t result; + strview_t value; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'o', + .long_name = "output", + .value_type = ARGOPTION_TYPE_STRING + )); + + malunal_cstr_t spellings[][3] = { + { "-o", "out.txt", null }, + { "-oout.txt", null, null }, + { "-o=out.txt", null, null }, + { "--output", "out.txt", null }, + { "--output=out.txt", null, null }, + }; + + malunal_size_t index; + for (index = 0; index < sizeof(spellings) / sizeof(spellings[0]); index++) { + malunal_cstr_t argv[3] = { "prog", spellings[index][0], spellings[index][1] }; + malunal_int32_t argc = spellings[index][1] != null ? 3 : 2; + + error_t err = argparser_parse(&parser, argc, argv, &result); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_NULL(argresult_get_string(&result, "output", &value).domain); + MICROTEST_EXPECT_TRUE(view_is(value, "out.txt")); + argresult_free(&result); + } + + argparser_free(&parser); +} + +MICROTEST(argparser_parse, flag_cluster_ends_with_value_option) { + argparser_t parser; + argparser_mptr_t commit; + argresult_t result; + strview_t message; + argparser_init("git", null, null, &parser); + argparser_add_subcommand(&parser, "commit", null, &commit); + argparser_add_option(commit, &ARGOPTION_FLAG(.short_name = 'a', .long_name = "all")); + argparser_add_option(commit, &ARGOPTION_FLAG( + .short_name = 'm', + .long_name = "message", + .value_type = ARGOPTION_TYPE_STRING, + .required = true + )); + + error_t err = PARSE(&parser, &result, "git", "commit", "-am", "fix bug"); + MICROTEST_EXPECT_NULL(err.domain); + + argresult_iptr_t sub = argresult_subresult(&result); + MICROTEST_EXPECT_TRUE(argresult_has(sub, "all")); + MICROTEST_EXPECT_NULL(argresult_get_string(sub, "message", &message).domain); + MICROTEST_EXPECT_TRUE(view_is(message, "fix bug")); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, single_option_keeps_last_value) { + argparser_t parser; + argresult_t result; + strview_t value; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'o', + .value_type = ARGOPTION_TYPE_STRING + )); + + error_t err = PARSE(&parser, &result, "prog", "-o", "first", "-o", "second"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(argresult_count(&result, "o"), (malunal_size_t)1); + argresult_get_string(&result, "o", &value); + MICROTEST_EXPECT_TRUE(view_is(value, "second")); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, multiple_option_appends_values) { + argparser_t parser; + argresult_t result; + strview_t value; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .short_name = 'I', + .long_name = "include", + .value_type = ARGOPTION_TYPE_STRING, + .multiple = true + )); + + error_t err = PARSE(&parser, &result, "prog", "-Ifoo", "--include", "bar", "--include=baz"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(argresult_count(&result, "include"), (malunal_size_t)3); + + argresult_get_string_at(&result, "include", 0, &value); + MICROTEST_EXPECT_TRUE(view_is(value, "foo")); + argresult_get_string_at(&result, "include", 1, &value); + MICROTEST_EXPECT_TRUE(view_is(value, "bar")); + argresult_get_string_at(&result, "include", 2, &value); + MICROTEST_EXPECT_TRUE(view_is(value, "baz")); + + err = argresult_get_string_at(&result, "include", 3, &value); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_OUT_OF_RANGE); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, typed_options_convert_values) { + argparser_t parser; + argresult_t result; + malunal_int64_t number = 0; + malunal_double_t floating = 0.0; + malunal_bool_t boolean = false; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "jobs", .value_type = ARGOPTION_TYPE_NUMBER)); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "scale", .value_type = ARGOPTION_TYPE_FLOAT)); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "color", .value_type = ARGOPTION_TYPE_BOOLEAN)); + + error_t err = PARSE(&parser, &result, "prog", "--jobs", "-4", "--scale=2.5", "--color", "YES"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_NULL(argresult_get_number(&result, "jobs", &number).domain); + MICROTEST_EXPECT_EQ(number, (malunal_int64_t)-4); + MICROTEST_EXPECT_NULL(argresult_get_float(&result, "scale", &floating).domain); + MICROTEST_EXPECT_TRUE(floating > 2.49 && floating < 2.51); + MICROTEST_EXPECT_NULL(argresult_get_boolean(&result, "color", &boolean).domain); + MICROTEST_EXPECT_TRUE(boolean); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, invalid_values_fail) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "jobs", .value_type = ARGOPTION_TYPE_NUMBER)); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "color", .value_type = ARGOPTION_TYPE_BOOLEAN)); + + error_t err = PARSE(&parser, &result, "prog", "--jobs=abc"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_VALUE); + argresult_free(&result); + + err = PARSE(&parser, &result, "prog", "--color=maybe"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_INVALID_VALUE); + argresult_free(&result); + + argparser_free(&parser); +} + +MICROTEST(argparser_parse, option_missing_value_fails) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'o', .value_type = ARGOPTION_TYPE_STRING)); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'v')); + + error_t err = PARSE(&parser, &result, "prog", "-o"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_MISSING_VALUE); + argresult_free(&result); + + err = PARSE(&parser, &result, "prog", "-o", "-v"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_MISSING_VALUE); + argresult_free(&result); + + argparser_free(&parser); +} + +MICROTEST(argparser_parse, long_options_match_unique_prefixes) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "verbose")); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "version")); + argparser_add_option(&parser, &ARGOPTION_FLAG(.long_name = "quiet")); + + error_t err = PARSE(&parser, &result, "prog", "--q", "--verb"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "quiet")); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "verbose")); + argresult_free(&result); + + err = PARSE(&parser, &result, "prog", "--ver"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_AMBIGUOUS_OPTION); + argresult_free(&result); + + argparser_free(&parser); +} + +MICROTEST(argparser_parse, required_option_missing_fails) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .long_name = "jobs", + .value_type = ARGOPTION_TYPE_NUMBER, + .required = true + )); + + error_t err = PARSE(&parser, &result, "prog"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_MISSING_REQUIRED_OPTION); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, defaults_apply_when_absent) { + argparser_t parser; + argresult_t result; + malunal_int64_t jobs = 0; + strview_t mode; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG( + .long_name = "jobs", + .value_type = ARGOPTION_TYPE_NUMBER, + .default_value = "8" + )); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL( + .long_name = "mode", + .value_type = ARGOPTION_TYPE_STRING, + .default_value = "debug" + )); + + error_t err = PARSE(&parser, &result, "prog"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_FALSE(argresult_has(&result, "jobs")); + MICROTEST_EXPECT_EQ(argresult_count(&result, "jobs"), (malunal_size_t)1); + MICROTEST_EXPECT_NULL(argresult_get_number(&result, "jobs", &jobs).domain); + MICROTEST_EXPECT_EQ(jobs, (malunal_int64_t)8); + MICROTEST_EXPECT_NULL(argresult_get_string(&result, "mode", &mode).domain); + MICROTEST_EXPECT_TRUE(view_is(mode, "debug")); + argresult_free(&result); + + err = PARSE(&parser, &result, "prog", "--jobs", "2", "release"); + MICROTEST_EXPECT_NULL(err.domain); + argresult_get_number(&result, "jobs", &jobs); + MICROTEST_EXPECT_EQ(jobs, (malunal_int64_t)2); + argresult_get_string(&result, "mode", &mode); + MICROTEST_EXPECT_TRUE(view_is(mode, "release")); + argresult_free(&result); + + argparser_free(&parser); +} + + +/* ------------------------------------------------------------------------- + * Parsing: positionals. + * ---------------------------------------------------------------------- */ + +MICROTEST(argparser_parse, positionals_fill_in_order_around_options) { + argparser_t parser; + argresult_t result; + strview_t src; + strview_t dst; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'v')); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "src", .value_type = ARGOPTION_TYPE_STRING, .required = true)); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "dst", .value_type = ARGOPTION_TYPE_STRING, .required = true)); + + error_t err = PARSE(&parser, &result, "prog", "a.txt", "-v", "b.txt"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "v")); + argresult_get_string(&result, "src", &src); + argresult_get_string(&result, "dst", &dst); + MICROTEST_EXPECT_TRUE(view_is(src, "a.txt")); + MICROTEST_EXPECT_TRUE(view_is(dst, "b.txt")); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, multiple_positional_collects_remaining) { + argparser_t parser; + argresult_t result; + strview_t value; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "cmd", .value_type = ARGOPTION_TYPE_STRING, .required = true)); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "files", .value_type = ARGOPTION_TYPE_STRING, .multiple = true)); + + error_t err = PARSE(&parser, &result, "prog", "cat", "a", "b", "c"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(argresult_count(&result, "files"), (malunal_size_t)3); + argresult_get_string_at(&result, "files", 2, &value); + MICROTEST_EXPECT_TRUE(view_is(value, "c")); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, required_positional_missing_fails) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "files", .value_type = ARGOPTION_TYPE_STRING, .required = true, .multiple = true)); + + error_t err = PARSE(&parser, &result, "prog"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, extra_positional_fails) { + argparser_t parser; + argresult_t result; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "only", .value_type = ARGOPTION_TYPE_STRING)); + + error_t err = PARSE(&parser, &result, "prog", "one", "two"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_TOO_MANY_POSITIONALS); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, negative_numbers_are_values) { + argparser_t parser; + argresult_t result; + malunal_double_t value = 0.0; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "n", .value_type = ARGOPTION_TYPE_FLOAT, .required = true)); + + error_t err = PARSE(&parser, &result, "prog", "-1.5"); + MICROTEST_EXPECT_NULL(err.domain); + argresult_get_float(&result, "n", &value); + MICROTEST_EXPECT_TRUE(value < -1.49 && value > -1.51); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, double_dash_ends_options) { + argparser_t parser; + argresult_t result; + strview_t value; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'v')); + argparser_add_option(&parser, &ARGOPTION_POSITIONAL(.long_name = "file", .value_type = ARGOPTION_TYPE_STRING, .required = true)); + + error_t err = PARSE(&parser, &result, "prog", "-v", "--", "-v"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(argresult_count(&result, "v"), (malunal_size_t)1); + argresult_get_string(&result, "file", &value); + MICROTEST_EXPECT_TRUE(view_is(value, "-v")); + + argresult_free(&result); + argparser_free(&parser); +} + + +/* ------------------------------------------------------------------------- + * Parsing: subcommands. + * ---------------------------------------------------------------------- */ + +MICROTEST(argparser_parse, subcommand_is_dispatched_after_parent_options) { + argparser_t parser; + argparser_mptr_t push; + argparser_mptr_t commit; + argresult_t result; + strview_t remote; + argparser_init("git", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'v')); + argparser_add_subcommand(&parser, "commit", null, &commit); + argparser_add_subcommand(&parser, "push", null, &push); + argparser_add_option(push, &ARGOPTION_FLAG(.short_name = 'f', .long_name = "force")); + argparser_add_option(push, &ARGOPTION_POSITIONAL(.long_name = "remote", .value_type = ARGOPTION_TYPE_STRING)); + argparser_add_option(push, &ARGOPTION_POSITIONAL(.long_name = "refs", .value_type = ARGOPTION_TYPE_STRING, .multiple = true)); + + error_t err = PARSE(&parser, &result, "git", "-v", "push", "--force", "origin", "main", "dev"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(argresult_has(&result, "v")); + MICROTEST_EXPECT_TRUE(strcmp(argresult_subcommand(&result), "push") == 0); + + argresult_iptr_t sub = argresult_subresult(&result); + MICROTEST_EXPECT_NOT_NULL(sub); + MICROTEST_EXPECT_TRUE(argresult_has(sub, "force")); + argresult_get_string(sub, "remote", &remote); + MICROTEST_EXPECT_TRUE(view_is(remote, "origin")); + MICROTEST_EXPECT_EQ(argresult_count(sub, "refs"), (malunal_size_t)2); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, no_subcommand_leaves_subresult_null) { + argparser_t parser; + argparser_mptr_t commit; + argresult_t result; + argparser_init("git", null, null, &parser); + argparser_add_subcommand(&parser, "commit", null, &commit); + + error_t err = PARSE(&parser, &result, "git"); + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_NULL(argresult_subcommand(&result)); + MICROTEST_EXPECT_NULL(argresult_subresult(&result)); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, unknown_subcommand_fails) { + argparser_t parser; + argparser_mptr_t commit; + argresult_t result; + argparser_init("git", null, null, &parser); + argparser_add_subcommand(&parser, "commit", null, &commit); + + error_t err = PARSE(&parser, &result, "git", "bogus"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_UNKNOWN_SUBCOMMAND); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST(argparser_parse, sibling_subcommands_do_not_share_options) { + argparser_t parser; + argparser_mptr_t commit; + argparser_mptr_t push; + argresult_t result; + argparser_init("git", null, null, &parser); + argparser_add_subcommand(&parser, "commit", null, &commit); + argparser_add_subcommand(&parser, "push", null, &push); + argparser_add_option(commit, &ARGOPTION_FLAG(.short_name = 'm', .value_type = ARGOPTION_TYPE_STRING)); + + error_t err = PARSE(&parser, &result, "git", "push", "-m", "nope"); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_UNKNOWN_OPTION); + + argresult_free(&result); + argparser_free(&parser); +} + + +/* ------------------------------------------------------------------------- + * Reading results. + * ---------------------------------------------------------------------- */ + +MICROTEST(argresult_get, reports_lookup_errors) { + argparser_t parser; + argresult_t result; + malunal_int64_t number = 0; + strview_t value; + argparser_init("prog", null, null, &parser); + argparser_add_option(&parser, &ARGOPTION_FLAG(.short_name = 'o', .value_type = ARGOPTION_TYPE_STRING)); + + error_t err = PARSE(&parser, &result, "prog"); + MICROTEST_EXPECT_NULL(err.domain); + + err = argresult_get_string(&result, "missing", &value); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_UNKNOWN_NAME); + err = argresult_get_number(&result, "o", &number); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_TYPE_MISMATCH); + err = argresult_get_string(&result, "o", &value); + MICROTEST_EXPECT_EQ(err.code, ARGPARSE_ERROR_NOT_PRESENT); + + argresult_free(&result); + argparser_free(&parser); +} + +MICROTEST_MAIN()