wraith-lang/lib/dropt/dropt.c

1628 lines
45 KiB
C
Raw Permalink Normal View History

2021-04-28 22:21:51 +00:00
/** dropt.c
*
* A deliberately rudimentary command-line option parser.
*
* Copyright (C) 2006-2018 James D. Lin <jamesdlin@berkeley.edu>
*
* The latest version of this file can be downloaded from:
* <http://www.taenarum.com/software/dropt/>
*
* 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 <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <wctype.h>
#include <assert.h>
#include "dropt.h"
#include "dropt_string.h"
#if __STDC_VERSION__ >= 199901L
#include <stdint.h>
#include <stdbool.h>
#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
}