/** dropt.c * * A deliberately rudimentary command-line option parser. * * Copyright (C) 2006-2018 James D. Lin * * The latest version of this file can be downloaded from: * * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. */ #include #include #include #include #include #include "dropt.h" #include "dropt_string.h" #if __STDC_VERSION__ >= 199901L #include #include #else /* Compatibility junk for things that don't yet support ISO C99. */ #ifndef SIZE_MAX #define SIZE_MAX ((size_t) -1) #endif typedef enum { false, true } bool; #endif #ifndef MIN #define MIN(x, y) (((x) < (y)) ? (x) : (y)) #endif #ifndef ARRAY_LENGTH #define ARRAY_LENGTH(array) (sizeof (array) / sizeof (array)[0]) #endif #define IMPLIES(p, q) (!(p) || q) #define OPTION_TAKES_ARG(option) ((option)->arg_description != NULL) enum { default_help_indent = 2, default_description_start_column = 6, }; /** A string that might not be `NUL`-terminated. */ typedef struct { const dropt_char* s; /* The length of s, excluding any `NUL` terminator. */ size_t len; } char_array; /** A proxy for a `dropt_option` used for `qsort` and `bsearch`. Instead of * sorting the `dropt_option` table directly, we sort arrays of `option_proxy` * structures. This allows us to have separate arrays sorted by different * keys and allows passing along additional data. */ typedef struct { const dropt_option* option; /* The `qsort` and `bsearch` comparison callbacks don't pass along any * client-supplied contextual data, so we have to embed it alongside the * regular data. */ const dropt_context* context; } option_proxy; struct dropt_context { const dropt_option* options; size_t numOptions; /* These may be NULL. */ option_proxy* sortedByLong; option_proxy* sortedByShort; bool allowConcatenatedArgs; dropt_error_handler_func errorHandler; void* errorHandlerData; struct { dropt_error err; dropt_char* optionName; dropt_char* optionArgument; dropt_char* message; } errorDetails; /* This isn't named strncmp because platforms might provide a macro * version of strncmp, and we want to avoid a potential naming * conflict. */ dropt_strncmp_func ncmpstr; }; typedef struct { const dropt_option* option; const dropt_char* optionArgument; dropt_char** argNext; int argsLeft; } parse_state; /** make_char_array * * PARAMETERS: * IN s : A string. Might not be NUL-terminated. * May be `NULL`. * len : The length of `s`, excluding any `NUL` terminator. * * RETURNS: * The constructed char_array structure. */ static char_array make_char_array(const dropt_char* s, size_t len) { char_array a; assert(IMPLIES(s == NULL, len == 0)); a.s = s; a.len = len; return a; } /** cmp_key_option_proxy_long * * Comparison callback for `bsearch`. Compares a `char_array` structure * against an `option_proxy` structure based on long option names. * * PARAMETERS: * IN key : A pointer to the `char_array` structure to search for. * IN item : A pointer to the `option_proxy` structure being searched * against. * * RETURNS: * 0 if `key` and `item` are equivalent, * < 0 if `key` should precede `item`, * > 0 if `key` should follow `item`. */ static int cmp_key_option_proxy_long(const void* key, const void* item) { const char_array* longName = key; const option_proxy* op = item; size_t optionLen; int ret; assert(longName != NULL); assert(op != NULL); assert(op->option != NULL); assert(op->context != NULL); assert(op->context->ncmpstr != NULL); if (longName->s == op->option->long_name) { return 0; } else if (longName->s == NULL) { return -1; } else if (op->option->long_name == NULL) { return +1; } /* Although the `longName` key might not be `NUL`-terminated, the * `option_proxy` item we're searching against must be. */ optionLen = dropt_strlen(op->option->long_name); ret = op->context->ncmpstr(longName->s, op->option->long_name, MIN(longName->len, optionLen)); if (ret != 0) { return ret; } if (longName->len < optionLen) { return -1; } else if (longName->len > optionLen) { return +1; } return 0; } /** cmp_option_proxies_long * * Comparison callback for `qsort`. Compares two `option_proxy` * structures based on long option names. * * PARAMETERS: * IN p1, p2 : Pointers to the `option_proxy` structures to compare. * * RETURNS: * 0 if `p1` and `p2` are equivalent, * < 0 if `p1` should precede `p2`, * > 0 if `p1` should follow `p2`. */ static int cmp_option_proxies_long(const void* p1, const void* p2) { const option_proxy* o1 = p1; const option_proxy* o2 = p2; char_array ca1; assert(o1 != NULL); assert(o2 != NULL); assert(o1->option != NULL); assert(o1->context == o2->context); ca1 = make_char_array(o1->option->long_name, (o1->option->long_name == NULL) ? 0 : dropt_strlen(o1->option->long_name)); return cmp_key_option_proxy_long(&ca1, o2); } /** cmp_key_option_proxy_short * * Comparison callback for `bsearch`. Compares a `dropt_char` against an * `option_proxy` structure based on short option names. * * PARAMETERS: * IN key : A pointer to the `dropt_char` to search for. * IN item : A pointer to the `option_proxy` structure being searched * against. * * RETURNS: * 0 if `key` and `item` are equivalent, * < 0 if `key` should precede `item`, * > 0 if `key` should follow `item`. */ static int cmp_key_option_proxy_short(const void* key, const void* item) { const dropt_char* shortName = key; const option_proxy* op = item; assert(shortName != NULL); assert(op != NULL); assert(op->option != NULL); assert(op->context != NULL); assert(op->context->ncmpstr != NULL); return op->context->ncmpstr(shortName, &op->option->short_name, 1); } /** cmp_option_proxies_short * * Comparison callback for `qsort`. Compares two `option_proxy` * structures based on short option names. * * PARAMETERS: * IN p1, p2 : Pointers to the `option_proxy` structures to compare. * * RETURNS: * 0 if `p1` and `p2` are equivalent, * < 0 if `p1` should precede `p2`, * > 0 if `p1` should follow `p2`. */ static int cmp_option_proxies_short(const void* p1, const void* p2) { const option_proxy* o1 = p1; const option_proxy* o2 = p2; assert(o1 != NULL); assert(o2 != NULL); assert(o1->option != NULL); assert(o1->context == o2->context); return cmp_key_option_proxy_short(&o1->option->short_name, o2); } /** init_lookup_tables * * Initializes the sorted lookup tables in a dropt context if not already * initialized. * * PARAMETERS: * IN/OUT context : The dropt context. * Must not be `NULL`. */ static void init_lookup_tables(dropt_context* context) { const dropt_option* options; size_t n; assert(context != NULL); options = context->options; n = context->numOptions; if (context->sortedByLong == NULL) { context->sortedByLong = dropt_safe_malloc(n, sizeof *(context->sortedByLong)); if (context->sortedByLong != NULL) { size_t i; for (i = 0; i < n; i++) { context->sortedByLong[i].option = &options[i]; context->sortedByLong[i].context = context; } qsort(context->sortedByLong, n, sizeof *(context->sortedByLong), cmp_option_proxies_long); } } if (context->sortedByShort == NULL) { context->sortedByShort = dropt_safe_malloc(n, sizeof *(context->sortedByShort)); if (context->sortedByShort != NULL) { size_t i; for (i = 0; i < n; i++) { context->sortedByShort[i].option = &options[i]; context->sortedByShort[i].context = context; } qsort(context->sortedByShort, n, sizeof *(context->sortedByShort), cmp_option_proxies_short); } } } /** free_lookup_tables * * Frees the sorted lookup tables in a dropt context. * * PARAMETERS: * IN/OUT context : The dropt context. * May be `NULL`. */ static void free_lookup_tables(dropt_context* context) { if (context != NULL) { free(context->sortedByLong); context->sortedByLong = NULL; free(context->sortedByShort); context->sortedByShort = NULL; } } /** is_valid_option * * PARAMETERS: * IN option : Specification for an individual option. * * RETURNS: * true if the specified option is valid, false if it's a sentinel value. */ static bool is_valid_option(const dropt_option* option) { return option != NULL && !( option->long_name == NULL && option->short_name == DROPT_TEXT_LITERAL('\0') && option->description == NULL && option->arg_description == NULL && option->handler == NULL && option->dest == NULL && option->attr == 0 && option->extra_data == 0); } /** find_option_long * * Finds the option specification for a long option name (i.e., an option * of the form "--option"). * * PARAMETERS: * IN context : The dropt context. * IN longName : The long option name to search for (excluding leading * dashes). * `longName.s` must not be `NULL`. * * RETURNS: * A pointer to the corresponding option specification or `NULL` if not * found. */ static const dropt_option* find_option_long(const dropt_context* context, char_array longName) { assert(context != NULL); assert(longName.s != NULL); if (context->sortedByLong != NULL) { option_proxy* found = bsearch(&longName, context->sortedByLong, context->numOptions, sizeof *(context->sortedByLong), cmp_key_option_proxy_long); return (found == NULL) ? NULL : found->option; } /* Fall back to a linear search. */ { option_proxy item = { 0 }; item.context = context; for (item.option = context->options; is_valid_option(item.option); item.option++) { if (cmp_key_option_proxy_long(&longName, &item) == 0) { return item.option; } } } return NULL; } /** find_option_short * * Finds the option specification for a short option name (i.e., an * option of the form "-o"). * * PARAMETERS: * IN context : The dropt context. * IN shortName : The short option name to search for. * * RETURNS: * A pointer to the corresponding option specification or `NULL` if not * found. */ static const dropt_option* find_option_short(const dropt_context* context, dropt_char shortName) { assert(context != NULL); assert(shortName != DROPT_TEXT_LITERAL('\0')); assert(context->ncmpstr != NULL); if (context->sortedByShort != NULL) { option_proxy* found = bsearch(&shortName, context->sortedByShort, context->numOptions, sizeof *(context->sortedByShort), cmp_key_option_proxy_short); return (found == NULL) ? NULL : found->option; } /* Fall back to a linear search. */ { const dropt_option* option; for (option = context->options; is_valid_option(option); option++) { if (context->ncmpstr(&shortName, &option->short_name, 1) == 0) { return option; } } } return NULL; } /** set_error_details * * Generates error details in the dropt context. * * PARAMETERS: * IN/OUT context : The dropt context. * Must not be `NULL`. * IN err : The error code. * IN optionName : The name of the option we failed on. * `optionName.s` must not be `NULL`. * IN optionArgument : The value of the option we failed on. * Pass `NULL` if unwanted. */ static void set_error_details(dropt_context* context, dropt_error err, char_array optionName, const dropt_char* optionArgument) { assert(context != NULL); assert(optionName.s != NULL); context->errorDetails.err = err; free(context->errorDetails.optionName); free(context->errorDetails.optionArgument); context->errorDetails.optionName = dropt_strndup(optionName.s, optionName.len); context->errorDetails.optionArgument = (optionArgument == NULL) ? NULL : dropt_strdup(optionArgument); /* The message will be generated lazily on retrieval. */ free(context->errorDetails.message); context->errorDetails.message = NULL; } /** set_short_option_error_details * * Generates error details in the dropt context. * * PARAMETERS: * IN/OUT context : The dropt context. * IN err : The error code. * IN shortName : the "short" name of the option we failed on. * IN optionArgument : The value of the option we failed on. * Pass `NULL` if unwanted. */ static void set_short_option_error_details(dropt_context* context, dropt_error err, dropt_char shortName, const dropt_char* optionArgument) { /* "-?" is just a placeholder. */ dropt_char shortNameBuf[] = DROPT_TEXT_LITERAL("-?"); assert(context != NULL); assert(shortName != DROPT_TEXT_LITERAL('\0')); shortNameBuf[1] = shortName; set_error_details(context, err, make_char_array(shortNameBuf, ARRAY_LENGTH(shortNameBuf) - 1), optionArgument); } /** dropt_get_error * * PARAMETERS: * IN context : The dropt context. * Must not be NULL. * * RETURNS: * The current error code waiting in the dropt context. */ dropt_error dropt_get_error(const dropt_context* context) { if (context == NULL) { DROPT_MISUSE("No dropt context specified."); return dropt_error_bad_configuration; } return context->errorDetails.err; } /** dropt_get_error_details * * Retrieves details about the current error. * * PARAMETERS: * IN context : The dropt context. * OUT optionName : On output, the name of the option we failed * on. Do not free this string. * Pass `NULL` if unwanted. * OUT optionArgument : On output, the value (possibly `NULL`) of the * option we failed on. Do not free this * string. * Pass `NULL` if unwanted. */ void dropt_get_error_details(const dropt_context* context, dropt_char** optionName, dropt_char** optionArgument) { if (optionName != NULL) { *optionName = context->errorDetails.optionName; } if (optionArgument != NULL) { *optionArgument = context->errorDetails.optionArgument; } } /** dropt_get_error_message * * PARAMETERS: * IN context : The dropt context. * Must not be `NULL`. * * RETURNS: * The current error message waiting in the dropt context or the empty * string if there are no errors. Note that calling any dropt * function other than `dropt_get_error`, `dropt_get_error_details`, and * `dropt_get_error_message` may invalidate a previously-returned * string. */ const dropt_char* dropt_get_error_message(dropt_context* context) { if (context == NULL) { DROPT_MISUSE("No dropt context specified."); return DROPT_TEXT_LITERAL(""); } if (context->errorDetails.err == dropt_error_none) { return DROPT_TEXT_LITERAL(""); } if (context->errorDetails.message == NULL) { if (context->errorHandler != NULL) { context->errorDetails.message = context->errorHandler(context->errorDetails.err, context->errorDetails.optionName, context->errorDetails.optionArgument, context->errorHandlerData); } else { #ifndef DROPT_NO_STRING_BUFFERS context->errorDetails.message = dropt_default_error_handler(context->errorDetails.err, context->errorDetails.optionName, context->errorDetails.optionArgument); #endif } } return (context->errorDetails.message == NULL) ? DROPT_TEXT_LITERAL("Unknown error") : context->errorDetails.message; } /** dropt_clear_error * * Clears the error waiting in the dropt context. * * PARAMETERS: * IN/OUT context : The dropt context to free. * May be `NULL`. */ void dropt_clear_error(dropt_context* context) { if (context != NULL) { context->errorDetails.err = dropt_error_none; free(context->errorDetails.optionName); context->errorDetails.optionName = NULL; free(context->errorDetails.optionArgument); context->errorDetails.optionArgument = NULL; free(context->errorDetails.message); context->errorDetails.message = NULL; } } #ifndef DROPT_NO_STRING_BUFFERS /** dropt_default_error_handler * * Default error handler. * * PARAMETERS: * IN error : The error code. * IN optionName : The name of the option we failed on. * IN optionArgument : The value of the option we failed on. * Pass `NULL` if unwanted. * * RETURNS: * An allocated string for the given error. The caller is responsible for * calling `free()` on it when no longer needed. * May return `NULL`. */ dropt_char* dropt_default_error_handler(dropt_error error, const dropt_char* optionName, const dropt_char* optionArgument) { dropt_char* s = NULL; const dropt_char* separator = DROPT_TEXT_LITERAL(": "); if (optionArgument == NULL) { separator = optionArgument = DROPT_TEXT_LITERAL(""); } switch (error) { case dropt_error_none: /* This shouldn't happen (unless client code invokes this directly * with `dropt_error_none`), but it's here for completeness. */ break; case dropt_error_bad_configuration: s = dropt_strdup(DROPT_TEXT_LITERAL("Invalid option configuration")); break; case dropt_error_invalid_option: s = dropt_asprintf(DROPT_TEXT_LITERAL("Invalid option: %s"), optionName); break; case dropt_error_insufficient_arguments: s = dropt_asprintf(DROPT_TEXT_LITERAL("Value required after option %s"), optionName); break; case dropt_error_mismatch: s = dropt_asprintf(DROPT_TEXT_LITERAL("Invalid value for option %s%s%s"), optionName, separator, optionArgument); break; case dropt_error_overflow: s = dropt_asprintf(DROPT_TEXT_LITERAL("Value too large for option %s%s%s"), optionName, separator, optionArgument); break; case dropt_error_underflow: s = dropt_asprintf(DROPT_TEXT_LITERAL("Value too small for option %s%s%s"), optionName, separator, optionArgument); break; case dropt_error_insufficient_memory: s = dropt_strdup(DROPT_TEXT_LITERAL("Insufficient memory")); break; case dropt_error_unknown: default: s = dropt_asprintf(DROPT_TEXT_LITERAL("Unknown error handling option %s"), optionName); break; } return s; } /** dropt_get_help * * PARAMETERS: * IN context : The dropt context. * Must not be `NULL`. * IN helpParams : The help parameters. * Pass `NULL` to use the default help parameters. * * RETURNS: * An allocated help string for the available options. The caller is * responsible for calling `free()` on it when no longer needed. * Returns `NULL` on error. */ dropt_char* dropt_get_help(const dropt_context* context, const dropt_help_params* helpParams) { dropt_char* helpText = NULL; dropt_stringstream* ss = dropt_ssopen(); if (context == NULL) { DROPT_MISUSE("No dropt context specified."); } else if (ss != NULL) { const dropt_option* option; dropt_help_params hp; if (helpParams == NULL) { dropt_init_help_params(&hp); } else { hp = *helpParams; } for (option = context->options; is_valid_option(option); option++) { bool hasLongName = option->long_name != NULL && option->long_name[0] != DROPT_TEXT_LITERAL('\0'); bool hasShortName = option->short_name != DROPT_TEXT_LITERAL('\0'); /* The number of characters printed on the current line so far. */ int n; if ( option->description == NULL || (option->attr & dropt_attr_hidden)) { /* Undocumented option. Ignore it and move on. */ continue; } else if (hasLongName && hasShortName) { n = dropt_ssprintf(ss, DROPT_TEXT_LITERAL("%*s-%c, --%s"), hp.indent, DROPT_TEXT_LITERAL(""), option->short_name, option->long_name); } else if (hasLongName) { n = dropt_ssprintf(ss, DROPT_TEXT_LITERAL("%*s--%s"), hp.indent, DROPT_TEXT_LITERAL(""), option->long_name); } else if (hasShortName) { n = dropt_ssprintf(ss, DROPT_TEXT_LITERAL("%*s-%c"), hp.indent, DROPT_TEXT_LITERAL(""), option->short_name); } else { /* Comment text. Don't bother with indentation. */ assert(option->description != NULL); dropt_ssprintf(ss, DROPT_TEXT_LITERAL("%s\n"), option->description); goto next; } if (n < 0) { n = 0; } if (option->arg_description != NULL) { int m = dropt_ssprintf(ss, (option->attr & dropt_attr_optional_val) ? DROPT_TEXT_LITERAL("[=%s]") : DROPT_TEXT_LITERAL("=%s"), option->arg_description); if (m > 0) { n += m; } } /* Check for equality to make sure that there's at least one * space between the option name and its description. */ if ((unsigned int) n >= hp.description_start_column) { dropt_ssprintf(ss, DROPT_TEXT_LITERAL("\n")); n = 0; } { const dropt_char* line = option->description; while (line != NULL) { int lineLen; const dropt_char* nextLine; const dropt_char* newline = dropt_strchr(line, DROPT_TEXT_LITERAL('\n')); if (newline == NULL) { lineLen = dropt_strlen(line); nextLine = NULL; } else { lineLen = newline - line; nextLine = newline + 1; } dropt_ssprintf(ss, DROPT_TEXT_LITERAL("%*s%.*s\n"), hp.description_start_column - n, DROPT_TEXT_LITERAL(""), lineLen, line); n = 0; line = nextLine; } } next: if (hp.blank_lines_between_options) { dropt_ssprintf(ss, DROPT_TEXT_LITERAL("\n")); } } helpText = dropt_ssfinalize(ss); } return helpText; } /** dropt_print_help * * Prints help for the available options. * * PARAMETERS: * IN/OUT f : The file stream to print to. * IN context : The dropt context. * Must not be `NULL`. * IN helpParams : The help parameters. * Pass `NULL` to use the default help parameters. */ void dropt_print_help(FILE* f, const dropt_context* context, const dropt_help_params* helpParams) { dropt_char* helpText = dropt_get_help(context, helpParams); if (helpText != NULL) { dropt_fputs(helpText, f); free(helpText); } } #endif /* DROPT_NO_STRING_BUFFERS */ /** set_option_value * * Sets the value for a specified option by invoking the option's * handler callback. * * PARAMETERS: * IN/OUT context : The dropt context. * IN option : The option. * IN optionArgument : The option's value. May be `NULL`. * * RETURNS: * An error code. */ static dropt_error set_option_value(dropt_context* context, const dropt_option* option, const dropt_char* optionArgument) { assert(option != NULL); if (option->handler == NULL) { DROPT_MISUSE("No option handler specified."); return dropt_error_bad_configuration; } return option->handler(context, option, optionArgument, option->dest); } /** parse_option_arg * * Helper function to `parse_long_option` and `parse_short_option` to * deal with consuming possibly optional arguments. * * PARAMETERS: * IN/OUT context : The dropt context. * IN/OUT ps : The current parse state. * * RETURNS: * An error code. */ static dropt_error parse_option_arg(dropt_context* context, parse_state* ps) { dropt_error err; bool consumeNextArg = false; if (OPTION_TAKES_ARG(ps->option) && ps->optionArgument == NULL) { /* The option expects an argument, but none was specified with '='. * Try using the next item from the command-line. */ if (ps->argsLeft > 0 && *(ps->argNext) != NULL) { consumeNextArg = true; ps->optionArgument = *(ps->argNext); } else if (!(ps->option->attr & dropt_attr_optional_val)) { err = dropt_error_insufficient_arguments; goto exit; } } /* Even for options that don't ask for arguments, always parse and * consume an argument that was specified with '='. */ err = set_option_value(context, ps->option, ps->optionArgument); if ( err != dropt_error_none && (ps->option->attr & dropt_attr_optional_val) && consumeNextArg && ps->optionArgument != NULL) { /* The option's handler didn't like the argument we fed it. If the * argument was optional, try again without it. */ consumeNextArg = false; ps->optionArgument = NULL; err = set_option_value(context, ps->option, NULL); } exit: if (err == dropt_error_none && consumeNextArg) { ps->argNext++; ps->argsLeft--; } return err; } /** parse_long_option * * Helper function to `dropt_parse` to deal with consuming a long option. * * PARAMETERS: * IN/OUT context : The dropt context. * IN/OUT ps : The current parse state. * IN arg : The command-line argument with the long option. * (Examples: "--longName", "--longName=ARGUMENT") * * RETURNS: * true if parsing should continue, false if it should halt. */ static bool parse_long_option(dropt_context* context, parse_state* ps, const dropt_char* arg) { bool continueParsing = false; dropt_error err = dropt_error_none; const dropt_char* longName = arg + 2; const dropt_char* longNameEnd; if (longName[0] == DROPT_TEXT_LITERAL('\0')) { /* -- */ /* This is used to mark the end of the option processing * to prevent some arguments with leading '-' characters * from being treated as options. * * Don't pass this back to the caller. */ goto exit; } /* --longName */ longNameEnd = dropt_strchr(longName, DROPT_TEXT_LITERAL('=')); if (longNameEnd == longName) { /* Deal with the pathological case of a user supplying * "--=". */ err = dropt_error_invalid_option; set_error_details(context, err, make_char_array(arg, dropt_strlen(arg)), NULL); goto exit; } else if (longNameEnd == NULL) { longNameEnd = longName + dropt_strlen(longName); assert(ps->optionArgument == NULL); } else { /* --longName=ARGUMENT */ ps->optionArgument = longNameEnd + 1; } /* Pass the length of the option name so that we don't need * to mutate the original string by inserting a * `NUL`-terminator. */ ps->option = find_option_long(context, make_char_array(longName, longNameEnd - longName)); if (ps->option == NULL) { err = dropt_error_invalid_option; set_error_details(context, err, make_char_array(arg, longNameEnd - arg), NULL); } else { err = parse_option_arg(context, ps); if (err != dropt_error_none) { set_error_details(context, err, make_char_array(arg, longNameEnd - arg), ps->optionArgument); } } if ( err != dropt_error_none || (ps->option->attr & dropt_attr_halt)) { goto exit; } continueParsing = true; exit: return continueParsing; } /** parse_short_option * * Helper function to `dropt_parse` to deal with consuming one or more * short options. * * PARAMETERS: * IN/OUT context : The dropt context. * IN/OUT ps : The current parse state. * IN arg : The command-line argument with one or more short * options. * (Examples: "-a", "-abc", "-oARGUMENT", "-o=ARGUMENT") * * RETURNS: * true if parsing should continue, false if it should halt. */ static bool parse_short_option(dropt_context* context, parse_state* ps, const dropt_char* arg) { bool continueParsing = false; dropt_error err = dropt_error_none; size_t len; size_t j; const dropt_char* shortOptionGroup = arg + 1; const dropt_char* shortOptionGroupEnd = dropt_strchr(shortOptionGroup, DROPT_TEXT_LITERAL('=')); if (shortOptionGroupEnd == shortOptionGroup) { /* Deal with the pathological case of a user supplying * "-=". */ err = dropt_error_invalid_option; set_error_details(context, err, make_char_array(arg, dropt_strlen(arg)), NULL); goto exit; } else if (shortOptionGroupEnd != NULL) { /* -x=ARGUMENT */ len = shortOptionGroupEnd - shortOptionGroup; ps->optionArgument = shortOptionGroupEnd + 1; } else { len = dropt_strlen(shortOptionGroup); assert(ps->optionArgument == NULL); } for (j = 0; j < len; j++) { ps->option = find_option_short(context, shortOptionGroup[j]); if (ps->option == NULL) { err = dropt_error_invalid_option; set_short_option_error_details(context, err, shortOptionGroup[j], NULL); goto exit; } else if (j + 1 == len) { /* The last short option in a condensed list gets * to use an argument. */ err = parse_option_arg(context, ps); if (err != dropt_error_none) { set_short_option_error_details(context, err, shortOptionGroup[j], ps->optionArgument); goto exit; } } else if ( context->allowConcatenatedArgs && OPTION_TAKES_ARG(ps->option) && j == 0) { err = set_option_value(context, ps->option, &shortOptionGroup[j + 1]); if ( err != dropt_error_none && (ps->option->attr & dropt_attr_optional_val)) { err = set_option_value(context, ps->option, NULL); } if (err != dropt_error_none) { set_short_option_error_details(context, err, shortOptionGroup[j], &shortOptionGroup[j + 1]); goto exit; } /* Skip to the next argument. */ break; } else if ( OPTION_TAKES_ARG(ps->option) && !(ps->option->attr & dropt_attr_optional_val)) { /* Short options with required arguments can't be used * in condensed lists except in the last position. * * e.g. -abcd ARGUMENT * ^ */ err = dropt_error_insufficient_arguments; set_short_option_error_details(context, err, shortOptionGroup[j], NULL); goto exit; } else { err = set_option_value(context, ps->option, NULL); if (err != dropt_error_none) { set_short_option_error_details(context, err, shortOptionGroup[j], NULL); goto exit; } } if (ps->option->attr & dropt_attr_halt) { goto exit; } } assert(err == dropt_error_none); continueParsing = true; exit: return continueParsing; } /** dropt_parse * * Parses command-line options. * * PARAMETERS: * IN/OUT context : The dropt context. * Must not be `NULL`. * IN argc : The maximum number of arguments to parse from argv. * Pass -1 to parse all arguments up to a `NULL` sentinel * value. * IN argv : The list of command-line arguments, not including the * initial program name. * * RETURNS: * A pointer to the first unprocessed element in `argv`. */ dropt_char** dropt_parse(dropt_context* context, int argc, dropt_char** argv) { dropt_char* arg; parse_state ps; ps.option = NULL; ps.optionArgument = NULL; ps.argNext = argv; if (argv == NULL) { /* Nothing to do. */ goto exit; } if (context == NULL) { DROPT_MISUSE("No dropt context specified."); set_error_details(context, dropt_error_bad_configuration, make_char_array(DROPT_TEXT_LITERAL(""), 0), NULL); goto exit; } #ifdef DROPT_NO_STRING_BUFFERS if (context->errorHandler == NULL) { DROPT_MISUSE("No error handler specified."); set_error_details(context, dropt_error_bad_configuration, make_char_array(DROPT_TEXT_LITERAL(""), 0), NULL); goto exit; } #endif if (argc == -1) { argc = 0; while (argv[argc] != NULL) { argc++; } } if (argc == 0) { /* Nothing to do. */ goto exit; } init_lookup_tables(context); ps.argsLeft = argc; while ( ps.argsLeft-- > 0 && (arg = *ps.argNext) != NULL && arg[0] == DROPT_TEXT_LITERAL('-')) { if (arg[1] == DROPT_TEXT_LITERAL('\0')) { /* - */ /* This intentionally leaves "-" unprocessed for the caller to * deal with. This allows construction of programs that treat * "-" to mean `stdin`. */ goto exit; } ps.argNext++; if (arg[1] == DROPT_TEXT_LITERAL('-')) { if (!parse_long_option(context, &ps, arg)) { goto exit; } } else { /* Short name. (-x) */ if (!parse_short_option(context, &ps, arg)) { goto exit; } } ps.option = NULL; ps.optionArgument = NULL; } exit: return ps.argNext; } /** dropt_new_context * * Creates a new dropt context. * * PARAMETERS: * IN options : The list of option specifications. * Must not be `NULL`. * The list is *not* copied and must outlive the dropt * context. * * RETURNS: * An allocated dropt context. The caller is responsible for freeing * it with `dropt_free_context` when no longer needed. * Returns `NULL` on error. */ dropt_context* dropt_new_context(const dropt_option* options) { dropt_context* context = NULL; size_t n; if (options == NULL) { DROPT_MISUSE("No option list specified."); goto exit; } /* Sanity-check the options. */ for (n = 0; is_valid_option(&options[n]); n++) { if ( options[n].short_name == DROPT_TEXT_LITERAL('=') || ( options[n].long_name != NULL && dropt_strchr(options[n].long_name, DROPT_TEXT_LITERAL('=')) != NULL)) { DROPT_MISUSE("Invalid option list. '=' may not be used in an option name."); goto exit; } } context = malloc(sizeof *context); if (context == NULL) { goto exit; } else { dropt_context emptyContext = { 0 }; *context = emptyContext; context->options = options; context->numOptions = n; dropt_set_strncmp(context, NULL); } exit: return context; } /** dropt_free_context * * Frees a dropt context. * * PARAMETERS: * IN/OUT context : The dropt context to free. * May be `NULL`. */ void dropt_free_context(dropt_context* context) { dropt_clear_error(context); free_lookup_tables(context); free(context); } /** dropt_get_options * * PARAMETERS: * IN context : The dropt context. * Must not be `NULL`. * * RETURNS: * The context's list of option specifications. */ const dropt_option* dropt_get_options(const dropt_context* context) { if (context == NULL) { DROPT_MISUSE("No dropt context specified."); return NULL; } return context->options; } /** dropt_init_help_params * * Initializes a `dropt_help_params` structure with the default values. * * PARAMETERS: * OUT helpParams : On output, set to the default help parameters. * Must not be `NULL`. */ void dropt_init_help_params(dropt_help_params* helpParams) { if (helpParams == NULL) { DROPT_MISUSE("No dropt help parameters specified."); return; } helpParams->indent = default_help_indent; helpParams->description_start_column = default_description_start_column; helpParams->blank_lines_between_options = true; } /** dropt_set_error_handler * * Sets the callback function used to generate error strings from error * codes. * * PARAMETERS: * IN/OUT context : The dropt context. * Must not be `NULL`. * IN handler : The error handler callback. * Pass `NULL` to use the default error handler. * IN handlerData : Caller-defined callback data. */ void dropt_set_error_handler(dropt_context* context, dropt_error_handler_func handler, void* handlerData) { if (context == NULL) { DROPT_MISUSE("No dropt context specified."); return; } context->errorHandler = handler; context->errorHandlerData = handlerData; } /** dropt_set_strncmp * * Sets the callback function used to compare strings. * * PARAMETERS: * IN/OUT context : The dropt context. * Must not be `NULL`. * IN cmp : The string comparison function. * Pass `NULL` to use the default string comparison * function. */ void dropt_set_strncmp(dropt_context* context, dropt_strncmp_func cmp) { if (context == NULL) { DROPT_MISUSE("No dropt context specified."); return; } if (cmp == NULL) { cmp = dropt_strncmp; } context->ncmpstr = cmp; /* Changing the sort method invalidates our existing lookup tables. */ free_lookup_tables(context); } /** dropt_allow_concatenated_arguments * * Specifies whether "short" options are allowed to have concatenated * arguments (i.e. without space or "=" separators, such as "-oARGUMENT"). * * (Concatenated arguments are disallowed by default.) * * PARAMETERS: * IN/OUT context : The dropt context. * IN allow : Pass 1 if concatenated arguments should be allowed, * 0 otherwise. */ void dropt_allow_concatenated_arguments(dropt_context* context, dropt_bool allow) { if (context == NULL) { DROPT_MISUSE("No dropt context specified."); return; } context->allowConcatenatedArgs = (allow != 0); } /** dropt_misuse * * Prints a diagnostic for logical errors caused by external clients * calling into dropt improperly. * * In debug builds, terminates the program and prints the filename and * line number of the failure. * * For logical errors entirely internal to dropt, use `assert()` * instead. * * PARAMETERS: * IN message : The error message. * Must not be `NULL`. * IN filename : The name of the file where the logical error occurred. * Must not be `NULL`. * IN line : The line number where the logical error occurred. */ void dropt_misuse(const char* message, const char* filename, int line) { #ifdef NDEBUG fprintf(stderr, "dropt: %s\n", message); #else fprintf(stderr, "dropt: %s (%s: %d)\n", message, filename, line); abort(); #endif }