Initial Commit

This commit is contained in:
2026-09-12 14:53:31 -05:00
commit 3c057c1911
6 changed files with 2893 additions and 0 deletions
+9
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
.chinook/
.environ/
.vscode/
+42
View File
@@ -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
+462
View File
@@ -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 */
+1573
View File
File diff suppressed because it is too large Load Diff
+804
View File
@@ -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()