diff --git a/argparse/README.md b/argparse/README.md new file mode 100644 index 0000000..fdcf9a9 --- /dev/null +++ b/argparse/README.md @@ -0,0 +1,82 @@ +# argparse + +A small, allocation-free command-line argument parser. +Supports long and short options, grouped flags, string values, positional arguments, `--`, and built-in `-h` / `--help` handling. + +## Build + +From the repository root: +- release mode: `make argparse` +- debug mode: `make argparse BUILD=debug` +- unit tests: `make test-argparse BUILD=debug` + +The packaged archive and headers are in `dist//argparse/`. +The single `libargparse.a` includes all its dependencies. + +## Example + +```c +#include "argparse/argparse.h" + +#include + +int main(int argc, char **argv) { + bool verbose = false; + StringView output = {0}; + StringView input = {0}; + + ArgOption options[] = { + { .long_name = "verbose", .short_name = 'v', .kind = ARG_FLAG, + .value.flag = &verbose, .help = "Enable verbose output" }, + { .long_name = "output", .short_name = 'o', .kind = ARG_STRING, + .value.string = &output, .value_name = "FILE", + .help = "Output file" }, + }; + ArgPositional positionals[] = { + { .name = "input", .value = &input, .required = true, + .help = "Input file" }, + }; + ArgParser parser = { + .program_name = argv[0], + .options = options, + .option_count = sizeof options / sizeof options[0], + .positionals = positionals, + .positional_count = sizeof positionals / sizeof positionals[0], + }; + + ArgParseResult result = arg_parse(&parser, argc, argv); + if (result.status == ARGPARSE_HELP_REQUESTED) { + arg_print_help(stdout, &parser); + return 0; + } + if (result.status != ARGPARSE_OK) { + arg_print_error(stderr, result); + return 1; + } + + if (verbose) fputs("Input: ", stdout); + sv_write(stdout, input); + if (output.len != 0) { + fputs(" -> ", stdout); + sv_write(stdout, output); + } + fputc('\n', stdout); + return 0; +} + +``` + +Save the above code as `example.c` in the repository root and run: + +```sh +cc -std=c23 -Idist/release/argparse/include example.c \ + dist/release/argparse/lib/libargparse.a -o example +./example -v -o result.txt input.txt +./example --output=result.txt input.txt +./example --help +``` + +> [!IMPORTANT] +> Initialize destination variables before parsing. +> String values are **borrowed views into `argv`**: no copying occurs, and the argument strings must remain valid while the views are used. +> Parsing does not print or exit; handle the returned status in your application. diff --git a/argparse/include/argparse/argparse.h b/argparse/include/argparse/argparse.h new file mode 100644 index 0000000..39e8c46 --- /dev/null +++ b/argparse/include/argparse/argparse.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Unlicense + +/** + * @file argparse.h + * @brief Allocation-free command-line argument parsing for C applications. + * + * Options are declared in a caller-owned array. The parser supports long + * options (--output=file), short options (-o file), grouped short options + * (-abc), attached short-option values (-ofile), positional arguments, and the + * end-of-options marker (--). + * + * The built-in -h and --help options return ARGPARSE_HELP_REQUESTED; they never + * print or terminate the process automatically. + * + * String values are returned as non-owning StringViews into argv. All lengths + * and offsets are measured in bytes. No input strings are copied, and parsing + * requires no heap allocation. + * + * The caller owns the option and positional descriptors and all destination + * variables. They must remain valid throughout the call to arg_parse(). After + * parsing, StringView destinations remain valid only while the original argv + * strings remain alive and unchanged. + * + * Parsing may update destinations before reporting an error. Applications + * should use parsed values only if the status is ARGPARSE_OK. + */ + +#pragma once + +#include +#include +#include + +#include "stringview/stringview.h" + +/** + * @brief Kinds of supported named options. + */ +typedef enum { + ARG_FLAG, /**< Presence sets the destination bool to true. */ + ARG_STRING, /**< Consumes a byte string into a StringView destination. */ +} ArgOptionKind; + +/** + * @brief Declaration and parsing state for one named option. + * + * At least one of long_name or short_name must be supplied. Names must be + * unique within an ArgParser; the names "help" and 'h' are reserved. + * + * The value union member must agree with kind and point to a writable + * destination. The caller initializes destination values before parsing; absent + * options leave those values unchanged. + * + * arg_parse() resets seen to false at the start of every parse and sets it when + * the option occurs. If repeated, the last string value wins; flags remain + * true. Setting required causes absence to be reported as an error. + */ +typedef struct { + char const *long_name; /**< Name without leading '--'; NULL if absent. */ + char short_name; /**< Name without leading '-'; '\0' if absent. */ + ArgOptionKind kind; /**< Whether the option is a flag or takes a value. */ + union { + bool *flag; /**< Destination when kind is ARG_FLAG. */ + StringView *string; /**< Destination when kind is ARG_STRING. */ + } value; /**< Option destination selected by kind. */ + char const *value_name; /**< Help placeholder (e.g., "FILE"); optional. */ + char const *help; /**< Help description; NULL is permitted. */ + bool required; /**< Whether the option must appear. */ + bool seen; /**< Written by arg_parse(); initialize to false. */ +} ArgOption; + +/** + * @brief Declaration of one positional byte-string argument. + * + * Positional arguments are assigned left to right. All required positionals + * must precede optional positionals. No variadic positionals are supported. An + * absent optional position leaves its destination unchanged. + */ +typedef struct { + char const *name; /**< Nonempty name used in usage and errors. */ + StringView *value; /**< Destination for the borrowed argument string. */ + char const *help; /**< Help description; NULL is permitted. */ + bool required; /**< Whether this positional must be supplied. */ +} ArgPositional; + +/** + * @brief Caller-owned parser definition. + * + * Set options or positionals to NULL when their respective count is zero. + * Option descriptors are mutable because arg_parse() maintains their seen + * fields. program_name is displayed in generated usage text. + */ +typedef struct { + char const *program_name; /**< Program name for usage; must not be NULL. */ + char const *description; /**< Optional explanation for generated help. */ + ArgOption *options; /**< Array of option_count declarations. */ + size_t option_count; /**< Number of option declarations. */ + ArgPositional *positionals; /**< Array of positional_count declarations. */ + size_t positional_count; /**< Number of positional declarations. */ +} ArgParser; + +/** + * @brief Outcome of a call to arg_parse(). + */ +typedef enum { + ARGPARSE_OK, /**< All arguments parsed successfully. */ + ARGPARSE_HELP_REQUESTED, /**< -h or --help was encountered. */ + ARGPARSE_UNKNOWN_OPTION, /**< Option name is not declared. */ + ARGPARSE_MISSING_VALUE, /**< Value-taking option has no value. */ + ARGPARSE_UNEXPECTED_VALUE, /**< Flag was given an attached value. */ + ARGPARSE_MISSING_REQUIRED_OPTION, /**< Required named option not supplied. */ + ARGPARSE_MISSING_REQUIRED_POSITIONAL, /**< Required positional not supplied. */ + ARGPARSE_TOO_MANY_POSITIONALS, /**< More positionals than declared. */ + ARGPARSE_INVALID_SPEC, /**< Invalid or ambiguous declaration. */ +} ArgParseStatus; + +/** + * @brief Parse result with a borrowed diagnostic subject. + * + * On errors, subject references the offending argv token or the name of a + * missing/invalid declaration. Its bytes are not copied. argv_index is the + * index of the relevant argv token, or -1 if no token applies. + * + * On ARGPARSE_OK and ARGPARSE_HELP_REQUESTED, subject is an empty view. + */ +typedef struct { + ArgParseStatus status; /**< Parse outcome. */ + StringView subject; /**< Byte string relevant to an error. */ + int argv_index; /**< argv index of error, or -1. */ +} ArgParseResult; + +/** + * @brief Parses command-line arguments using a caller-defined specification. + * + * Supports --name=value, --name value, -n value, -nvalue, and groups of short + * flags such as -abc. A value-taking short option consumes the rest of its + * group or, if none remains, the next argv token. A lone '-' is treated as + * positional; '--' ends option recognition, so later tokens are positional. + * Long and short names are matched exactly and case-sensitively. + * + * Both --help and -h (including a grouped -h) request help. In that case, + * normal required-argument checks are skipped. Help is not printed here. + * + * @param[in,out] parser Parser specification and writable option state. + * @param[in] argc Number of argv entries, including the program name. + * @param[in] argv Null-terminated argument strings as passed to main(). + * + * @return Parse status and context for reporting errors or help requests. + * + * @pre @p parser and @p argv must be valid; @p argc must be at least 1. + * @pre Each option destination and positional destination must be valid. + * @note + * The caller must initialize option/positional destinations to their desired + * defaults before calling this function. + * @note + * This function resets only the option seen fields, not destinations. + * @warning On error, some destination variables may already have changed. + */ +ArgParseResult arg_parse(ArgParser *parser, int argc, char *const argv[]); + +/** + * @brief Prints generated usage and option descriptions. + * + * Includes declared options, positional arguments, and the built-in -h/--help + * option. Does not allocate application-owned memory or terminate. + * + * @param[in,out] stream Writable output stream, typically stdout. + * @param[in] parser Valid parser specification. + * + * @pre @p stream and @p parser must be non-NULL. + * @note + * Check ferror(stream) to detect stream write errors. + */ +void arg_print_help(FILE *stream, ArgParser const *parser); + +/** + * @brief Prints a human-readable diagnostic for a parsing error. + * + * This function prints the error described by result but does not print usage + * text or terminate the program. It should be called only for error statuses, + * not ARGPARSE_OK or ARGPARSE_HELP_REQUESTED. + * + * @param[in,out] stream Writable output stream, typically stderr. + * @param[in] result Result returned by arg_parse(). + * + * @pre @p stream must be non-NULL. + * @note + * Check ferror(stream) to detect stream write errors. + */ +void arg_print_error(FILE *stream, ArgParseResult result); diff --git a/argparse/library.mk b/argparse/library.mk new file mode 100644 index 0000000..58df66e --- /dev/null +++ b/argparse/library.mk @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Unlicense + +LIB_argparse_DEPS := stringview diff --git a/argparse/src/argparse.c b/argparse/src/argparse.c new file mode 100644 index 0000000..74a3833 --- /dev/null +++ b/argparse/src/argparse.c @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: Unlicense + +#include + +#include +#include + +static ArgParseResult arg_result(ArgParseStatus status, StringView subject, + int argv_index) { + return (ArgParseResult){ + .status = status, + .subject = subject, + .argv_index = argv_index, + }; +} + +static ArgParseResult arg_success(ArgParseStatus status) { + return arg_result(status, (StringView){0}, -1); +} + +static ArgParseResult arg_token_error(ArgParseStatus status, char const *token, + int argv_index) { + return arg_result(status, sv_from_cstr(token), argv_index); +} + +static StringView arg_option_name(ArgOption const *option) { + if (option->long_name != NULL) { + return sv_from_cstr(option->long_name); + } + + if (option->short_name != '\0') { + return sv_from_parts(&option->short_name, 1); + } + + return (StringView){0}; +} + +static ArgParseResult arg_invalid_spec(StringView subject) { + return arg_result(ARGPARSE_INVALID_SPEC, subject, -1); +} + +static bool arg_valid_long_name(char const name[]) { + if (name == NULL || name[0] == '\0' || name[0] == '-') { + return false; + } + + for (unsigned char const *p = (unsigned char const *)name; *p != 0; p++) { + if (*p <= ' ' || *p == 127 || *p == '=') { + return false; + } + } + + return strcmp(name, "help") != 0; +} + +static bool arg_valid_short_name(char name) { + unsigned char c = (unsigned char)name; + + return c > ' ' && c != 127 && c != '-' && c != '=' && c != 'h'; +} + +static ArgParseResult arg_validate_spec(ArgParser const *parser) { + if (parser == NULL || parser->program_name == NULL || + (parser->option_count != 0 && parser->options == NULL) || + (parser->positional_count != 0 && parser->positionals == NULL)) { + return arg_invalid_spec((StringView){0}); + } + + for (size_t i = 0; i < parser->option_count; i++) { + ArgOption const *option = &parser->options[i]; + StringView name = arg_option_name(option); + + if ((option->long_name == NULL && option->short_name == '\0') || + (option->long_name != NULL && + !arg_valid_long_name(option->long_name)) || + (option->short_name != '\0' && + !arg_valid_short_name(option->short_name)) || + (option->kind != ARG_FLAG && option->kind != ARG_STRING) || + (option->kind == ARG_FLAG && option->value.flag == NULL) || + (option->kind == ARG_STRING && option->value.string == NULL)) { + return arg_invalid_spec(name); + } + + for (size_t j = 0; j < i; j++) { + ArgOption const *earlier = &parser->options[j]; + + if ((option->long_name != NULL && earlier->long_name != NULL && + strcmp(option->long_name, earlier->long_name) == 0) || + (option->short_name != '\0' && + option->short_name == earlier->short_name)) { + return arg_invalid_spec(name); + } + } + } + + bool saw_optional = false; + for (size_t i = 0; i < parser->positional_count; i++) { + ArgPositional const *positional = &parser->positionals[i]; + StringView name = positional->name == NULL ? (StringView){0} + : sv_from_cstr(positional->name); + + if (positional->name == NULL || positional->name[0] == '\0' || + positional->value == NULL || (saw_optional && positional->required)) { + return arg_invalid_spec(name); + } + + if (!positional->required) { + saw_optional = true; + } + } + + return arg_success(ARGPARSE_OK); +} + +static ArgOption *arg_find_long(ArgParser *parser, char const *name, + size_t len) { + for (size_t i = 0; i < parser->option_count; i++) { + ArgOption *option = &parser->options[i]; + + if (option->long_name != NULL && strlen(option->long_name) == len && + memcmp(option->long_name, name, len) == 0) { + return option; + } + } + + return NULL; +} + +static ArgOption *arg_find_short(ArgParser *parser, char name) { + for (size_t i = 0; i < parser->option_count; i++) { + if (parser->options[i].short_name == name) { + return &parser->options[i]; + } + } + + return NULL; +} + +static ArgParseResult arg_parse_long(ArgParser *parser, int argc, + char *const argv[], int *index) { + char const *token = argv[*index]; + char const *name = token + 2; + char const *equal = strchr(name, '='); + size_t name_len = equal == NULL ? strlen(name) : (size_t)(equal - name); + + if (name_len == 4 && memcmp(name, "help", 4) == 0) { + return equal == NULL + ? arg_success(ARGPARSE_HELP_REQUESTED) + : arg_token_error(ARGPARSE_UNEXPECTED_VALUE, token, *index); + } + + ArgOption *option = arg_find_long(parser, name, name_len); + if (option == NULL) { + return arg_token_error(ARGPARSE_UNKNOWN_OPTION, token, *index); + } + + if (option->kind == ARG_FLAG) { + if (equal != NULL) { + return arg_token_error(ARGPARSE_UNEXPECTED_VALUE, token, *index); + } + *option->value.flag = true; + } else if (equal != NULL) { + *option->value.string = sv_from_cstr(equal + 1); + } else { + if (*index == argc - 1) { + return arg_token_error(ARGPARSE_MISSING_VALUE, token, *index); + } + ++*index; + *option->value.string = sv_from_cstr(argv[*index]); + } + + option->seen = true; + return arg_success(ARGPARSE_OK); +} + +static ArgParseResult arg_parse_short(ArgParser *parser, int argc, + char *const argv[], int *index) { + char const *token = argv[*index]; + + for (char const *name = token + 1; *name != '\0'; name++) { + if (*name == 'h') { + return arg_success(ARGPARSE_HELP_REQUESTED); + } + + ArgOption *option = arg_find_short(parser, *name); + if (option == NULL) { + return arg_token_error(ARGPARSE_UNKNOWN_OPTION, token, *index); + } + + if (option->kind == ARG_FLAG) { + *option->value.flag = true; + option->seen = true; + continue; + } + + // A value-taking option consumes the rest of its group, if present. + // Supports -o=value as well as -ovalue. + char const *attached = name + 1; + if (*attached != '\0') { + if (*attached == '=') { + attached++; + } + *option->value.string = sv_from_cstr(attached); + } else { + if (*index == argc - 1) { + return arg_token_error(ARGPARSE_MISSING_VALUE, token, *index); + } + ++*index; + *option->value.string = sv_from_cstr(argv[*index]); + } + + option->seen = true; + return arg_success(ARGPARSE_OK); + } + + return arg_success(ARGPARSE_OK); +} + +ArgParseResult arg_parse(ArgParser *parser, int argc, char *const argv[]) { + ArgParseResult valid = arg_validate_spec(parser); + + if (valid.status != ARGPARSE_OK) { + return valid; + } + + if (argc < 1 || argv == NULL || argv[0] == NULL) { + return arg_invalid_spec((StringView){0}); + } + + for (size_t i = 0; i < parser->option_count; i++) { + parser->options[i].seen = false; + } + + size_t positional_index = 0; + bool positional_only = false; + + for (int i = 1; i < argc; i++) { + char const *token = argv[i]; + + if (token == NULL) { + return arg_invalid_spec((StringView){0}); + } + + if (!positional_only && strcmp(token, "--") == 0) { + positional_only = true; + continue; + } + + if (!positional_only && token[0] == '-' && token[1] == '-' && + token[2] != '\0') { + ArgParseResult result = arg_parse_long(parser, argc, argv, &i); + + if (result.status != ARGPARSE_OK) { + return result; + } + continue; + } + + if (!positional_only && token[0] == '-' && token[1] != '\0') { + ArgParseResult result = arg_parse_short(parser, argc, argv, &i); + + if (result.status != ARGPARSE_OK) { + return result; + } + continue; + } + + if (positional_index == parser->positional_count) { + return arg_token_error(ARGPARSE_TOO_MANY_POSITIONALS, token, i); + } + + *parser->positionals[positional_index++].value = sv_from_cstr(token); + } + + for (size_t i = 0; i < parser->option_count; i++) { + ArgOption const *option = &parser->options[i]; + + if (option->required && !option->seen) { + return arg_result(ARGPARSE_MISSING_REQUIRED_OPTION, + arg_option_name(option), -1); + } + } + + if (positional_index < parser->positional_count && + parser->positionals[positional_index].required) { + return arg_result(ARGPARSE_MISSING_REQUIRED_POSITIONAL, + sv_from_cstr(parser->positionals[positional_index].name), + -1); + } + + return arg_success(ARGPARSE_OK); +} + +static void arg_print_option_name(FILE *stream, ArgOption const *option) { + if (option->short_name != '\0') { + fprintf(stream, "-%c", option->short_name); + } + + if (option->long_name != NULL) { + if (option->short_name != '\0') { + fputs(", ", stream); + } + fprintf(stream, "--%s", option->long_name); + } + + if (option->kind == ARG_STRING) { + fprintf(stream, " %s", + option->value_name != NULL ? option->value_name : "VALUE"); + } +} + +void arg_print_help(FILE *stream, ArgParser const *parser) { + fprintf(stream, "Usage: %s [-h]", parser->program_name); + + for (size_t i = 0; i < parser->option_count; i++) { + ArgOption const *option = &parser->options[i]; + fputc(' ', stream); + + 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->kind == ARG_STRING) { + fprintf(stream, " %s", + option->value_name != NULL ? option->value_name : "VALUE"); + } + if (!option->required) { + fputc(']', stream); + } + } + + for (size_t i = 0; i < parser->positional_count; i++) { + ArgPositional const *positional = &parser->positionals[i]; + fprintf(stream, positional->required ? " %s" : " [%s]", positional->name); + } + + fputc('\n', stream); + + if (parser->description != NULL && parser->description[0] != '\0') { + fprintf(stream, "\n%s\n", parser->description); + } + + if (parser->positional_count != 0) { + fputs("\nPositional arguments:\n", stream); + + for (size_t i = 0; i < parser->positional_count; i++) { + ArgPositional const *positional = &parser->positionals[i]; + fprintf(stream, " %s", positional->name); + + if (positional->help != NULL) { + fprintf(stream, "\t%s", positional->help); + } + + fputc('\n', stream); + } + } + + fputs("\nOptions:\n -h, --help\tShow this help message\n", stream); + + for (size_t i = 0; i < parser->option_count; i++) { + ArgOption const *option = &parser->options[i]; + fputs(" ", stream); + + arg_print_option_name(stream, option); + if (option->help != NULL) { + fprintf(stream, "\t%s", option->help); + } + + if (option->required) { + fputs(" (required)", stream); + } + + fputc('\n', stream); + } +} + +static void arg_print_subject(FILE *stream, StringView subject) { + if (subject.len != 0) { + (void)fwrite(subject.data, 1, subject.len, stream); + } +} + +void arg_print_error(FILE *stream, ArgParseResult result) { + char const *description = NULL; + + switch (result.status) { + case ARGPARSE_UNKNOWN_OPTION: + description = "unknown option"; + break; + case ARGPARSE_MISSING_VALUE: + description = "option requires a value"; + break; + case ARGPARSE_UNEXPECTED_VALUE: + description = "option does not accept a value"; + break; + case ARGPARSE_MISSING_REQUIRED_OPTION: + description = "missing required option"; + break; + case ARGPARSE_MISSING_REQUIRED_POSITIONAL: + description = "missing required positional argument"; + break; + case ARGPARSE_TOO_MANY_POSITIONALS: + description = "unexpected positional argument"; + break; + case ARGPARSE_INVALID_SPEC: + description = "invalid parser specification"; + break; + case ARGPARSE_OK: + case ARGPARSE_HELP_REQUESTED: + return; + } + + fprintf(stream, "error: %s", description); + if (result.subject.len != 0) { + fputs(": '", stream); + arg_print_subject(stream, result.subject); + fputc('\'', stream); + } + + fputc('\n', stream); +} diff --git a/argparse/tests/test_argparse.c b/argparse/tests/test_argparse.c new file mode 100644 index 0000000..9caaa34 --- /dev/null +++ b/argparse/tests/test_argparse.c @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Unlicense + +#include "argparse/argparse.h" +#include "test.h" + +#include + +#define COUNT(xs) ((int)(sizeof(xs) / sizeof((xs)[0]))) +#define PARSE(parser, args) arg_parse(&(parser), COUNT(args), (args)) + +static bool is(StringView sv, const char *text) { + size_t len = strlen(text); + return sv.len == len && (len == 0 || memcmp(sv.data, text, len) == 0); +} + +static int test_long_options_and_defaults(void) { + bool verbose = false; + StringView output = sv_from_cstr("default"), input = {0}, extra = {0}; + ArgOption options[] = { + {.long_name = "verbose", .short_name = 'v', .kind = ARG_FLAG, + .value.flag = &verbose}, + {.long_name = "output", .short_name = 'o', .kind = ARG_STRING, + .value.string = &output, .required = true}, + }; + ArgPositional positionals[] = { + {.name = "input", .value = &input, .required = true}, + {.name = "extra", .value = &extra}, + }; + ArgParser p = {.program_name = "tool", .options = options, .option_count = 2, + .positionals = positionals, .positional_count = 2}; + char *args[] = {"tool", "--verbose", "--output=first", "--output", "-second", + "input.c", "optional"}; + ArgParseResult r = PARSE(p, args); + CHECK(r.status == ARGPARSE_OK && r.subject.len == 0 && r.argv_index == -1); + CHECK(verbose && options[0].seen && options[1].seen); + CHECK(is(output, "-second") && output.data == args[4]); + CHECK(is(input, "input.c") && input.data == args[5] && is(extra, "optional")); + + options[0].seen = true; + char *again[] = {"tool", "--output="}; + CHECK(PARSE(p, again).status == ARGPARSE_MISSING_REQUIRED_POSITIONAL); + CHECK(!options[0].seen && options[1].seen && output.len == 0); + CHECK(verbose && is(input, "input.c")); // Missing args retain destinations. + return 0; +} + +static int test_short_options(void) { + bool a = false, b = false; + StringView out = {0}; + ArgOption options[] = { + {.short_name = 'a', .kind = ARG_FLAG, .value.flag = &a}, + {.short_name = 'b', .kind = ARG_FLAG, .value.flag = &b}, + {.short_name = 'o', .kind = ARG_STRING, .value.string = &out}, + }; + ArgParser p = {.program_name = "tool", .options = options, .option_count = 3}; + char *attached[] = {"tool", "-abofile"}; + CHECK(PARSE(p, attached).status == ARGPARSE_OK); + CHECK(a && b && is(out, "file")); + char *equal[] = {"tool", "-o=other"}; + CHECK(PARSE(p, equal).status == ARGPARSE_OK && is(out, "other")); + CHECK(!options[0].seen && !options[1].seen && options[2].seen); + char *separate[] = {"tool", "-o", "-literal"}; + CHECK(PARSE(p, separate).status == ARGPARSE_OK && is(out, "-literal")); + char *empty[] = {"tool", "-o="}; + CHECK(PARSE(p, empty).status == ARGPARSE_OK && out.len == 0); + char *missing[] = {"tool", "-o"}; + CHECK(PARSE(p, missing).status == ARGPARSE_MISSING_VALUE); + char *unknown[] = {"tool", "-az"}; + ArgParseResult r = PARSE(p, unknown); + CHECK(r.status == ARGPARSE_UNKNOWN_OPTION && r.argv_index == 1); + CHECK(is(r.subject, "-az")); + return 0; +} + +static int test_positionals_and_help(void) { + bool flag = false; + StringView a = {0}, b = {0}, c = {0}; + ArgOption option = {.short_name = 'f', .kind = ARG_FLAG, + .value.flag = &flag, .required = true}; + ArgPositional pos[] = { + {.name = "first", .value = &a, .required = true}, + {.name = "second", .value = &b}, + {.name = "third", .value = &c}, + }; + ArgParser p = {.program_name = "tool", .options = &option, .option_count = 1, + .positionals = pos, .positional_count = 3}; + char *args[] = {"tool", "-f", "-", "--", "--help", "-x"}; + CHECK(PARSE(p, args).status == ARGPARSE_OK); + CHECK(flag && is(a, "-") && is(b, "--help") && is(c, "-x")); + char *help[] = {"tool", "--help"}; + CHECK(PARSE(p, help).status == ARGPARSE_HELP_REQUESTED); + char *short_help[] = {"tool", "-fh"}; + CHECK(PARSE(p, short_help).status == ARGPARSE_HELP_REQUESTED); + char *help_value[] = {"tool", "--help=yes"}; + CHECK(PARSE(p, help_value).status == ARGPARSE_UNEXPECTED_VALUE); + char *too_many[] = {"tool", "-f", "1", "2", "3", "4"}; + ArgParseResult r = PARSE(p, too_many); + CHECK(r.status == ARGPARSE_TOO_MANY_POSITIONALS && r.argv_index == 5); + CHECK(is(r.subject, "4")); + char *no_args[] = {"tool"}; + r = PARSE(p, no_args); + CHECK(r.status == ARGPARSE_MISSING_REQUIRED_OPTION && is(r.subject, "f")); + char *no_input[] = {"tool", "-f"}; + r = PARSE(p, no_input); + CHECK(r.status == ARGPARSE_MISSING_REQUIRED_POSITIONAL && is(r.subject, "first")); + return 0; +} + +static int test_errors_and_spec(void) { + bool enabled = false; + StringView output = {0}; + ArgOption options[] = { + {.long_name = "enabled", .short_name = 'e', .kind = ARG_FLAG, + .value.flag = &enabled}, + {.long_name = "output", .short_name = 'o', .kind = ARG_STRING, + .value.string = &output}, + }; + ArgParser p = {.program_name = "tool", .options = options, .option_count = 2}; + char *unknown[] = {"tool", "--unknown"}; + ArgParseResult r = PARSE(p, unknown); + CHECK(r.status == ARGPARSE_UNKNOWN_OPTION && r.argv_index == 1); + CHECK(is(r.subject, "--unknown")); + char *missing[] = {"tool", "--output"}; + CHECK(PARSE(p, missing).status == ARGPARSE_MISSING_VALUE); + char *unexpected[] = {"tool", "--enabled=yes"}; + CHECK(PARSE(p, unexpected).status == ARGPARSE_UNEXPECTED_VALUE); + + char *empty[] = {"tool"}; +#define INVALID() CHECK(PARSE(p, empty).status == ARGPARSE_INVALID_SPEC) + options[1].long_name = "enabled"; INVALID(); // Duplicate long name. + options[1].long_name = "output"; options[1].short_name = 'e'; INVALID(); + options[1].short_name = 'h'; INVALID(); // Reserved short name. + options[1].short_name = 'o'; options[1].long_name = "help"; INVALID(); + options[1].long_name = "-bad"; INVALID(); + options[1].long_name = "bad=name"; INVALID(); + options[1].long_name = "bad name"; INVALID(); + options[1].long_name = "output"; options[1].value.string = NULL; INVALID(); + options[1].value.string = &output; options[1].kind = (ArgOptionKind)42; INVALID(); + options[1].kind = ARG_STRING; + options[1].long_name = NULL; options[1].short_name = '\0'; INVALID(); + options[1].short_name = 'o'; + p.options = NULL; INVALID(); p.options = options; + p.program_name = NULL; INVALID(); p.program_name = "tool"; + p.positional_count = 1; INVALID(); // Missing positional declarations. + StringView first = {0}, second = {0}; + ArgPositional pos[] = {{.name = "optional", .value = &first}, + {.name = "required", .value = &second, .required = true}}; + p.positionals = pos; p.positional_count = 2; INVALID(); + pos[1].required = false; pos[0].name = NULL; INVALID(); + pos[0].name = "optional"; pos[0].value = NULL; INVALID(); + p.positionals = NULL; p.positional_count = 0; + CHECK(arg_parse(NULL, 1, empty).status == ARGPARSE_INVALID_SPEC); + CHECK(arg_parse(&p, 0, empty).status == ARGPARSE_INVALID_SPEC); + CHECK(arg_parse(&p, 1, NULL).status == ARGPARSE_INVALID_SPEC); + char *bad_argv0[] = {NULL}; + CHECK(PARSE(p, bad_argv0).status == ARGPARSE_INVALID_SPEC); + char *bad_token[] = {"tool", NULL}; + CHECK(PARSE(p, bad_token).status == ARGPARSE_INVALID_SPEC); +#undef INVALID + return 0; +} + +static int test_output(void) { + bool verbose = false; + StringView input = {0}, path = {0}; + ArgOption opts[] = { + {.long_name = "verbose", .short_name = 'v', .kind = ARG_FLAG, + .value.flag = &verbose, .help = "Verbose"}, + {.long_name = "output", .kind = ARG_STRING, .value.string = &path, + .required = true, .value_name = "FILE"}, + }; + ArgPositional pos[] = {{.name = "input", .value = &input, + .help = "Input filename", .required = true}}; + ArgParser p = {.program_name = "tool", .description = "Tool description", + .options = opts, .option_count = 2, + .positionals = pos, .positional_count = 1}; + FILE *stream = tmpfile(); + CHECK(stream != NULL); + arg_print_help(stream, &p); + CHECK(fflush(stream) == 0 && fseek(stream, 0, SEEK_SET) == 0); + char buf[2048] = {0}; + size_t n = fread(buf, 1, sizeof(buf) - 1, stream); + CHECK(n > 0 && strstr(buf, "Usage: tool") && strstr(buf, "--output FILE")); + CHECK(strstr(buf, "-h, --help") && strstr(buf, "Tool description")); + CHECK(strstr(buf, "\n -v, --verbose\tVerbose\n")); + CHECK(strstr(buf, "Input filename") && strstr(buf, "(required)")); + CHECK(fclose(stream) == 0); + + const struct { ArgParseStatus status; const char *message; } cases[] = { + {ARGPARSE_UNKNOWN_OPTION, "unknown option"}, + {ARGPARSE_MISSING_VALUE, "option requires a value"}, + {ARGPARSE_UNEXPECTED_VALUE, "option does not accept a value"}, + {ARGPARSE_MISSING_REQUIRED_OPTION, "missing required option"}, + {ARGPARSE_MISSING_REQUIRED_POSITIONAL, "missing required positional"}, + {ARGPARSE_TOO_MANY_POSITIONALS, "unexpected positional"}, + {ARGPARSE_INVALID_SPEC, "invalid parser specification"}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + stream = tmpfile(); + CHECK(stream != NULL); + arg_print_error(stream, (ArgParseResult){cases[i].status, + sv_from_cstr("TEST"), 1}); + CHECK(fflush(stream) == 0 && fseek(stream, 0, SEEK_SET) == 0); + memset(buf, 0, sizeof(buf)); + CHECK(fread(buf, 1, sizeof(buf) - 1, stream) > 0); + CHECK(strstr(buf, cases[i].message) && strstr(buf, "TEST")); + CHECK(fclose(stream) == 0); + } + stream = tmpfile(); + CHECK(stream != NULL); + arg_print_error(stream, (ArgParseResult){.status = ARGPARSE_OK}); + arg_print_error(stream, (ArgParseResult){.status = ARGPARSE_HELP_REQUESTED}); + CHECK(fflush(stream) == 0 && ftell(stream) == 0); + CHECK(fclose(stream) == 0); + return 0; +} + +int main(void) { + RUN(test_long_options_and_defaults); + RUN(test_short_options); + RUN(test_positionals_and_help); + RUN(test_errors_and_spec); + RUN(test_output); + puts("argparse: all tests passed"); + return 0; +}