Files
argparse/sources/argparse.c
T
2026-09-12 14:53:31 -05:00

1574 lines
41 KiB
C

#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "malunal/argparse.h"
#define ARGPARSE_NPOS ((malunal_size_t)-1)
#define ARGPARSE_HELP_SLOT 0
#define ARGPARSE_HELP_COLUMN 24
#define argparse_error(errcode) \
((error_t){ .domain = &ERROR_DOMAIN_ARGPARSE_T, .code = (errcode) })
static
malunal_cstr_t
describe(malunal_int32_t code) {
switch (code) {
case ARGPARSE_ERROR_NULL_INPUT:
return "a required input was null";
case ARGPARSE_ERROR_INVALID_DEFINITION:
return "argument was defined inconsistently";
case ARGPARSE_ERROR_DUPLICATE_DEFINITION:
return "argument name is already in use";
case ARGPARSE_ERROR_HELP_REQUESTED:
return "help was requested";
case ARGPARSE_ERROR_UNKNOWN_OPTION:
return "unrecognized arguments";
case ARGPARSE_ERROR_AMBIGUOUS_OPTION:
return "ambiguous option";
case ARGPARSE_ERROR_UNKNOWN_SUBCOMMAND:
return "invalid choice";
case ARGPARSE_ERROR_MISSING_VALUE:
return "expected one argument";
case ARGPARSE_ERROR_UNEXPECTED_VALUE:
return "ignored explicit argument";
case ARGPARSE_ERROR_INVALID_VALUE:
return "invalid value";
case ARGPARSE_ERROR_MISSING_REQUIRED_OPTION:
case ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL:
return "the following arguments are required";
case ARGPARSE_ERROR_TOO_MANY_POSITIONALS:
return "unrecognized arguments";
case ARGPARSE_ERROR_UNKNOWN_NAME:
return "no argument is registered under that name";
case ARGPARSE_ERROR_TYPE_MISMATCH:
return "argument type does not match the accessor";
case ARGPARSE_ERROR_NOT_PRESENT:
return "argument has no value";
case ARGPARSE_ERROR_OUT_OF_RANGE:
return "value index is out of range";
}
return "Unknown argparse error";
}
const error_domain_t ERROR_DOMAIN_ARGPARSE_T = {
.describe = &describe,
.name = "malunal.argparse.error"
};
typedef struct {
malunal_cstr_t name;
malunal_cstr_t desc;
allocator_mptr_t allocator;
argparser_iptr_t parent;
vector_container_t options;
vector_container_t positionals;
vector_container_t subcommands;
} parser_impl_t;
typedef parser_impl_t* parser_impl_mptr_t;
typedef const parser_impl_t* parser_impl_iptr_t;
_Static_assert(
sizeof(argparser_t) == sizeof(parser_impl_t),
"Argparser must be the size of its implementation."
);
typedef union {
strview_t string;
malunal_int64_t number;
malunal_double_t floating;
malunal_bool_t boolean;
} argitem_t;
typedef struct {
malunal_size_t slot;
argitem_t item;
} argentry_t;
typedef struct {
argparser_iptr_t parser;
allocator_mptr_t allocator;
vector_container_t counts; /* malunal_size_t per slot. */
vector_container_t entries; /* argentry_t. */
argresult_mptr_t subresult;
argparser_iptr_t error_parser;
malunal_size_t error_slot;
strview_t error_arg;
} result_impl_t;
typedef result_impl_t* result_impl_mptr_t;
typedef const result_impl_t* result_impl_iptr_t;
_Static_assert(
sizeof(argresult_t) == sizeof(result_impl_t),
"Argresult must be the size of its implementation."
);
/* -------------------------------------------------------------------------
* Small helpers over vectors and argument definitions.
* ---------------------------------------------------------------------- */
static
malunal_size_t
vector_count(
vector_container_iptr_t vector
) {
malunal_size_t count = 0;
vector_container_count(vector, &count);
return count;
}
static
error_t
vector_release(
vector_container_mptr_t vector
) {
malunal_size_t stride = 0;
vector_container_stride(vector, &stride);
return stride != 0
? vector_container_free(vector)
: NO_ERROR;
}
static
malunal_size_t
option_keys(
argoption_iptr_t option,
strview_t keys[2]
) {
malunal_size_t count = 0;
if (option->isflag) {
if (option->short_name != '\0')
keys[count++] = strview(&option->short_name, 1);
if (option->long_name != null)
keys[count++] = strview_from_cstr(option->long_name);
} else if (option->long_name != null) {
keys[count++] = strview_from_cstr(option->long_name);
}
return count;
}
static
malunal_size_t
parser_slot_count(
parser_impl_iptr_t self
) {
return
vector_count(&self->options) +
vector_count(&self->positionals);
}
static
argoption_t
parser_slot(
parser_impl_iptr_t self,
malunal_size_t slot
) {
argoption_t option = { 0 };
malunal_size_t count = vector_count(&self->options);
if (slot < count)
vector_container_get(&self->options, slot, &option);
else
vector_container_get(&self->positionals, slot - count, &option);
return option;
}
static
malunal_bool_t
strview_iequals_cstr(
strview_iptr_t view,
malunal_cstr_t text
) {
malunal_size_t length = strlen(text);
if (strview_length(view) != length)
return false;
malunal_size_t index;
for (index = 0; index < length; index++)
if (tolower(view->beg[index]) != text[index])
return false;
return true;
}
static
error_t
argitem_convert(
strview_iptr_t token,
argoption_type_t type,
argitem_t* out
) {
switch (type) {
case ARGOPTION_TYPE_STRING:
out->string = *token;
return NO_ERROR;
case ARGOPTION_TYPE_NUMBER:
return strview_to_int64(token, &out->number).domain != null
? argparse_error(ARGPARSE_ERROR_INVALID_VALUE)
: NO_ERROR;
case ARGOPTION_TYPE_FLOAT:
return strview_to_double(token, &out->floating).domain != null
? argparse_error(ARGPARSE_ERROR_INVALID_VALUE)
: NO_ERROR;
case ARGOPTION_TYPE_BOOLEAN:
if (
strview_iequals_cstr(token, "true") ||
strview_iequals_cstr(token, "yes") ||
strview_iequals_cstr(token, "on") ||
strview_iequals_cstr(token, "1")
) {
out->boolean = true;
return NO_ERROR;
}
if (
strview_iequals_cstr(token, "false") ||
strview_iequals_cstr(token, "no") ||
strview_iequals_cstr(token, "off") ||
strview_iequals_cstr(token, "0")
) {
out->boolean = false;
return NO_ERROR;
}
return argparse_error(ARGPARSE_ERROR_INVALID_VALUE);
case ARGOPTION_TYPE_FLAG:
break;
}
return argparse_error(ARGPARSE_ERROR_INVALID_VALUE);
}
/* -------------------------------------------------------------------------
* Parser construction.
* ---------------------------------------------------------------------- */
static
malunal_bool_t
parser_name_taken(
parser_impl_iptr_t self,
argoption_iptr_t option
) {
strview_t keys[2];
malunal_size_t nkeys = option_keys(option, keys);
malunal_size_t slots = parser_slot_count(self);
malunal_size_t slot;
for (slot = 0; slot < slots; slot++) {
argoption_t existing = parser_slot(self, slot);
strview_t others[2];
malunal_size_t nothers = option_keys(&existing, others);
malunal_size_t key, other;
for (key = 0; key < nkeys; key++)
for (other = 0; other < nothers; other++)
if (strview_equals(&keys[key], &others[other]))
return true;
}
return false;
}
static
error_t
parser_validate_option(
parser_impl_iptr_t self,
argoption_iptr_t option
) {
argoption_type_t type = option->value_type;
if (type > ARGOPTION_TYPE_BOOLEAN)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (option->isflag) {
malunal_char_t short_name = option->short_name;
malunal_cstr_t long_name = option->long_name;
if (short_name == '\0' && long_name == null)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (short_name == '-' || short_name == '=' || isspace((unsigned char)short_name))
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (long_name != null && (long_name[0] == '\0' || long_name[0] == '-' || strchr(long_name, '=') != null))
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (type == ARGOPTION_TYPE_FLAG && (option->required || option->default_value != null))
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
} else {
malunal_cstr_t value_name = option->long_name;
if (value_name == null || value_name[0] == '\0' || value_name[0] == '-')
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (type == ARGOPTION_TYPE_FLAG)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (vector_count(&self->subcommands) > 0)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
malunal_size_t npositionals = vector_count(&self->positionals);
if (npositionals > 0) {
argoption_t last;
vector_container_get(&self->positionals, npositionals - 1, &last);
if (last.multiple)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (option->required && !last.required)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
}
}
if (parser_name_taken(self, option))
return argparse_error(ARGPARSE_ERROR_DUPLICATE_DEFINITION);
malunal_cstr_t fallback = option->default_value;
if (fallback != null) {
argitem_t item = { 0 };
strview_t token = strview_from_cstr(fallback);
if (argitem_convert(&token, type, &item).domain != null)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
}
return NO_ERROR;
}
error_t
argparser_init(
malunal_cstr_t name,
malunal_cstr_t desc,
allocator_mptr_t allocator,
argparser_mptr_t parser
) {
if (parser == null || name == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
if (allocator == null)
allocator = libc_allocator();
parser_impl_mptr_t self = (parser_impl_mptr_t)parser;
memset(self, 0, sizeof(parser_impl_t));
self->name = name;
self->desc = desc;
self->allocator = allocator;
error_t result = vector_container_init(sizeof(argoption_t), allocator, &self->options);
if (result.domain == null)
result = vector_container_init(sizeof(argoption_t), allocator, &self->positionals);
if (result.domain == null)
result = vector_container_init(sizeof(argparser_mptr_t), allocator, &self->subcommands);
if (result.domain == null)
result = argparser_add_option(parser, &ARGOPTION_FLAG(
.short_name = 'h',
.long_name = "help",
.help_string = "show this help message and exit"
));
if (result.domain != null)
argparser_free(parser);
return result;
}
error_t
argparser_free(
argparser_mptr_t parser
) {
if (parser == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
parser_impl_mptr_t self = (parser_impl_mptr_t)parser;
error_t result = NO_ERROR;
malunal_size_t count = vector_count(&self->subcommands);
malunal_size_t index;
for (index = 0; index < count; index++) {
argparser_mptr_t child = null;
vector_container_get(&self->subcommands, index, &child);
argparser_free(child);
error_t disposed = allocator_dispose(self->allocator, child, sizeof(argparser_t));
if (result.domain == null)
result = disposed;
}
error_t released[3] = {
vector_release(&self->subcommands),
vector_release(&self->positionals),
vector_release(&self->options)
};
for (index = 0; index < 3; index++)
if (result.domain == null)
result = released[index];
memset(self, 0, sizeof(parser_impl_t));
return result;
}
error_t
argparser_add_option(
argparser_mptr_t parser,
argoption_iptr_t option
) {
if (parser == null || option == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
parser_impl_mptr_t self = (parser_impl_mptr_t)parser;
error_t result = parser_validate_option(self, option);
if (result.domain != null)
return result;
return vector_container_append(
option->isflag ? &self->options : &self->positionals,
option
);
}
error_t
argparser_add_subcommand(
argparser_mptr_t parser,
malunal_cstr_t name,
malunal_cstr_t desc,
argparser_mptr_t* out
) {
if (parser == null || name == null || out == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
parser_impl_mptr_t self = (parser_impl_mptr_t)parser;
if (name[0] == '\0' || name[0] == '-')
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
if (vector_count(&self->positionals) > 0)
return argparse_error(ARGPARSE_ERROR_INVALID_DEFINITION);
malunal_size_t count = vector_count(&self->subcommands);
malunal_size_t index;
for (index = 0; index < count; index++) {
argparser_mptr_t sibling = null;
vector_container_get(&self->subcommands, index, &sibling);
if (strcmp(((parser_impl_iptr_t)sibling)->name, name) == 0)
return argparse_error(ARGPARSE_ERROR_DUPLICATE_DEFINITION);
}
malunal_mptr_t memory = null;
error_t result = allocator_acquire(self->allocator, sizeof(argparser_t), &memory);
if (result.domain != null)
return result;
argparser_mptr_t child = (argparser_mptr_t)memory;
result = argparser_init(name, desc, self->allocator, child);
if (result.domain != null) {
allocator_dispose(self->allocator, memory, sizeof(argparser_t));
return result;
}
((parser_impl_mptr_t)child)->parent = parser;
result = vector_container_append(&self->subcommands, &child);
if (result.domain != null) {
argparser_free(child);
allocator_dispose(self->allocator, memory, sizeof(argparser_t));
return result;
}
*out = child;
return NO_ERROR;
}
/* -------------------------------------------------------------------------
* Result storage.
* ---------------------------------------------------------------------- */
static
error_t
result_init(
parser_impl_iptr_t parser,
result_impl_mptr_t self
) {
self->parser = (argparser_iptr_t)parser;
self->allocator = parser->allocator;
error_t result = vector_container_init(sizeof(malunal_size_t), parser->allocator, &self->counts);
if (result.domain == null)
result = vector_container_init(sizeof(argentry_t), parser->allocator, &self->entries);
if (result.domain == null)
result = vector_container_resize(&self->counts, parser_slot_count(parser));
if (result.domain != null) {
argresult_free((argresult_mptr_t)self);
return result;
}
malunal_size_t zero = 0;
malunal_size_t slots = parser_slot_count(parser);
malunal_size_t slot;
for (slot = 0; slot < slots; slot++)
vector_container_set(&self->counts, slot, &zero);
return NO_ERROR;
}
static
error_t
result_fail(
result_impl_mptr_t self,
parser_impl_iptr_t parser,
argparse_error_t code,
malunal_size_t slot,
strview_t arg
) {
self->error_parser = (argparser_iptr_t)parser;
self->error_slot = slot;
self->error_arg = arg;
return argparse_error(code);
}
static
malunal_size_t
result_slot_count(
result_impl_iptr_t self,
malunal_size_t slot
) {
malunal_size_t count = 0;
vector_container_get(&self->counts, slot, &count);
return count;
}
static
malunal_size_t
result_find_entry(
result_impl_iptr_t self,
malunal_size_t slot,
malunal_size_t nth,
malunal_size_t* total
) {
malunal_size_t found = 0;
malunal_size_t match = ARGPARSE_NPOS;
malunal_size_t count = vector_count(&self->entries);
malunal_size_t index;
for (index = 0; index < count; index++) {
argentry_t entry;
vector_container_get(&self->entries, index, &entry);
if (entry.slot != slot)
continue;
if (found == nth)
match = index;
found++;
if (total == null && match != ARGPARSE_NPOS)
break;
}
if (total != null)
*total = found;
return match;
}
static
error_t
result_store(
result_impl_mptr_t self,
malunal_size_t slot,
malunal_bool_t multiple,
argitem_t item
) {
argentry_t entry = { .slot = slot, .item = item };
if (!multiple) {
malunal_size_t index = result_find_entry(self, slot, 0, null);
if (index != ARGPARSE_NPOS)
return vector_container_set(&self->entries, index, &entry);
}
return vector_container_append(&self->entries, &entry);
}
static
malunal_void_t
result_bump(
result_impl_mptr_t self,
malunal_size_t slot
) {
malunal_size_t count = result_slot_count(self, slot) + 1;
vector_container_set(&self->counts, slot, &count);
}
static
error_t
result_store_token(
result_impl_mptr_t self,
parser_impl_iptr_t parser,
malunal_size_t slot,
argoption_iptr_t option,
strview_t token
) {
argitem_t item = { 0 };
error_t result = argitem_convert(&token, option->value_type, &item);
if (result.domain != null)
return result_fail(self, parser, ARGPARSE_ERROR_INVALID_VALUE, slot, token);
result = result_store(self, slot, option->multiple, item);
if (result.domain != null)
return result;
result_bump(self, slot);
return NO_ERROR;
}
/* -------------------------------------------------------------------------
* Parsing.
* ---------------------------------------------------------------------- */
static
malunal_bool_t
token_is_negative_number(
strview_iptr_t token
) {
malunal_size_t length = strview_length(token);
if (length < 2 || token->beg[0] != '-')
return false;
malunal_bool_t digits = false;
malunal_bool_t dotted = false;
malunal_size_t index;
for (index = 1; index < length; index++) {
malunal_char_t current = token->beg[index];
if (isdigit((unsigned char)current)) {
digits = true;
} else if (current == '.' && !dotted) {
dotted = true;
} else {
return false;
}
}
return digits && token->beg[length - 1] != '.';
}
static
malunal_bool_t
parser_has_numeric_option(
parser_impl_iptr_t self
) {
malunal_size_t count = vector_count(&self->options);
malunal_size_t index;
for (index = 0; index < count; index++) {
argoption_t option;
vector_container_get(&self->options, index, &option);
if (isdigit(option.short_name))
return true;
}
return false;
}
static
malunal_bool_t
parser_is_option_token(
parser_impl_iptr_t self,
strview_iptr_t token
) {
if (strview_length(token) < 2 || token->beg[0] != '-')
return false;
return
!token_is_negative_number(token) ||
parser_has_numeric_option(self);
}
static
malunal_bool_t
parser_can_take_value(
parser_impl_iptr_t self,
malunal_int32_t argc,
malunal_cstr_t* argv,
malunal_int32_t index
) {
if (index >= argc)
return false;
strview_t token = strview_from_cstr(argv[index]);
return !parser_is_option_token(self, &token);
}
static
malunal_size_t
parser_find_short(
parser_impl_iptr_t self,
malunal_char_t name
) {
malunal_size_t count = vector_count(&self->options);
malunal_size_t index;
for (index = 0; index < count; index++) {
argoption_t option;
vector_container_get(&self->options, index, &option);
if (option.short_name == name)
return index;
}
return ARGPARSE_NPOS;
}
static
error_t
parser_find_long(
parser_impl_iptr_t self,
strview_iptr_t name,
malunal_size_t* out
) {
malunal_size_t count = vector_count(&self->options);
malunal_size_t matches = 0;
malunal_size_t index;
*out = ARGPARSE_NPOS;
if (strview_length(name) == 0)
return argparse_error(ARGPARSE_ERROR_UNKNOWN_OPTION);
for (index = 0; index < count; index++) {
argoption_t option;
vector_container_get(&self->options, index, &option);
if (option.long_name == null)
continue;
strview_t candidate = strview_from_cstr(option.long_name);
if (strview_equals(&candidate, name)) {
*out = index;
return NO_ERROR;
}
if (strview_starts_with(&candidate, name)) {
if (matches == 0)
*out = index;
matches++;
}
}
if (matches == 1)
return NO_ERROR;
*out = ARGPARSE_NPOS;
return argparse_error(matches > 1
? ARGPARSE_ERROR_AMBIGUOUS_OPTION
: ARGPARSE_ERROR_UNKNOWN_OPTION);
}
static
error_t
parser_parse_long(
parser_impl_iptr_t self,
result_impl_mptr_t out,
malunal_int32_t argc,
malunal_cstr_t* argv,
malunal_int32_t* index
) {
strview_t token = strview_from_cstr(argv[*index]);
strview_t name = token;
strview_t value = strview(null, 0);
malunal_bool_t has_inline = false;
strview_ltrim(&name, 2);
malunal_cstr_t equals = memchr(name.beg, '=', strview_length(&name));
if (equals != null) {
value = (strview_t){ equals + 1, name.end };
name.end = equals;
has_inline = true;
}
malunal_size_t slot = ARGPARSE_NPOS;
error_t lookup = parser_find_long(self, &name, &slot);
if (lookup.domain != null)
return result_fail(out, self, lookup.code, ARGPARSE_NPOS, (strview_t){ token.beg, name.end });
if (slot == ARGPARSE_HELP_SLOT)
return result_fail(out, self, ARGPARSE_ERROR_HELP_REQUESTED, ARGPARSE_NPOS, token);
argoption_t option;
vector_container_get(&self->options, slot, &option);
*index += 1;
if (option.value_type == ARGOPTION_TYPE_FLAG) {
if (has_inline)
return result_fail(out, self, ARGPARSE_ERROR_UNEXPECTED_VALUE, slot, value);
result_bump(out, slot);
return NO_ERROR;
}
if (!has_inline) {
if (!parser_can_take_value(self, argc, argv, *index))
return result_fail(out, self, ARGPARSE_ERROR_MISSING_VALUE, slot, strview(null, 0));
value = strview_from_cstr(argv[*index]);
*index += 1;
}
return result_store_token(out, self, slot, &option, value);
}
static
error_t
parser_parse_short(
parser_impl_iptr_t self,
result_impl_mptr_t out,
malunal_int32_t argc,
malunal_cstr_t* argv,
malunal_int32_t* index
) {
strview_t token = strview_from_cstr(argv[*index]);
malunal_cstr_t cursor = token.beg + 1;
*index += 1;
while (cursor < token.end) {
malunal_size_t slot = parser_find_short(self, *cursor);
if (slot == ARGPARSE_NPOS)
return result_fail(out, self, ARGPARSE_ERROR_UNKNOWN_OPTION, ARGPARSE_NPOS, token);
if (slot == ARGPARSE_HELP_SLOT)
return result_fail(out, self, ARGPARSE_ERROR_HELP_REQUESTED, ARGPARSE_NPOS, token);
argoption_t option;
vector_container_get(&self->options, slot, &option);
cursor++;
if (option.value_type == ARGOPTION_TYPE_FLAG) {
result_bump(out, slot);
continue;
}
/* A value-bearing option consumes the rest of the token, or the next. */
strview_t value;
if (cursor < token.end) {
if (*cursor == '=')
cursor++;
value = (strview_t){ cursor, token.end };
} else {
if (!parser_can_take_value(self, argc, argv, *index))
return result_fail(out, self, ARGPARSE_ERROR_MISSING_VALUE, slot, strview(null, 0));
value = strview_from_cstr(argv[*index]);
*index += 1;
}
return result_store_token(out, self, slot, &option, value);
}
return NO_ERROR;
}
static
error_t
parser_parse_subcommand(
parser_impl_iptr_t self,
result_impl_mptr_t out,
malunal_int32_t argc,
malunal_cstr_t* argv,
malunal_int32_t index
) {
strview_t token = strview_from_cstr(argv[index]);
argparser_mptr_t child = null;
malunal_size_t count = vector_count(&self->subcommands);
malunal_size_t current;
for (current = 0; current < count; current++) {
argparser_mptr_t candidate = null;
vector_container_get(&self->subcommands, current, &candidate);
strview_t name = strview_from_cstr(((parser_impl_iptr_t)candidate)->name);
if (strview_equals(&name, &token)) {
child = candidate;
break;
}
}
if (child == null)
return result_fail(out, self, ARGPARSE_ERROR_UNKNOWN_SUBCOMMAND, ARGPARSE_NPOS, token);
malunal_mptr_t memory = null;
error_t result = allocator_acquire(self->allocator, sizeof(argresult_t), &memory);
if (result.domain != null)
return result;
out->subresult = (argresult_mptr_t)memory;
result = argparser_parse(child, argc - index, argv + index, out->subresult);
if (result.domain != null) {
result_impl_iptr_t sub = (result_impl_iptr_t)out->subresult;
out->error_parser = sub->error_parser;
out->error_slot = sub->error_slot;
out->error_arg = sub->error_arg;
}
return result;
}
static
error_t
parser_finalize(
parser_impl_iptr_t self,
result_impl_mptr_t out
) {
malunal_size_t noptions = vector_count(&self->options);
malunal_size_t slots = parser_slot_count(self);
malunal_size_t slot;
for (slot = 0; slot < slots; slot++) {
if (result_slot_count(out, slot) > 0)
continue;
argoption_t option = parser_slot(self, slot);
if (option.required)
return result_fail(
out,
self,
slot < noptions
? ARGPARSE_ERROR_MISSING_REQUIRED_OPTION
: ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL,
slot,
strview(null, 0)
);
malunal_cstr_t fallback = option.default_value;
if (fallback == null)
continue;
/* Defaults were validated when added, so conversion cannot fail. */
argitem_t item = { 0 };
strview_t token = strview_from_cstr(fallback);
argitem_convert(&token, option.value_type, &item);
error_t result = result_store(out, slot, false, item);
if (result.domain != null)
return result;
}
return NO_ERROR;
}
error_t
argparser_parse(
argparser_iptr_t parser,
malunal_int32_t argc,
malunal_cstr_t* argv,
argresult_mptr_t result
) {
if (result == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
result_impl_mptr_t out = (result_impl_mptr_t)result;
memset(out, 0, sizeof(result_impl_t));
out->error_slot = ARGPARSE_NPOS;
if (parser == null || argc < 0 || (argv == null && argc > 0))
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
parser_impl_iptr_t self = (parser_impl_iptr_t)parser;
error_t error = result_init(self, out);
if (error.domain != null)
return error;
malunal_bool_t options_ended = false;
malunal_bool_t dispatches = vector_count(&self->subcommands) > 0;
malunal_size_t noptions = vector_count(&self->options);
malunal_size_t npositionals = vector_count(&self->positionals);
malunal_size_t positional = 0;
malunal_int32_t index = 1;
while (index < argc) {
strview_t token = strview_from_cstr(argv[index]);
if (!options_ended && parser_is_option_token(self, &token)) {
if (strview_length(&token) == 2 && token.beg[1] == '-') {
options_ended = true;
index++;
continue;
}
error = token.beg[1] == '-'
? parser_parse_long(self, out, argc, argv, &index)
: parser_parse_short(self, out, argc, argv, &index);
if (error.domain != null)
return error;
continue;
}
if (dispatches) {
error = parser_parse_subcommand(self, out, argc, argv, index);
if (error.domain != null)
return error;
break;
}
if (positional >= npositionals)
return result_fail(out, self, ARGPARSE_ERROR_TOO_MANY_POSITIONALS, ARGPARSE_NPOS, token);
argoption_t option;
vector_container_get(&self->positionals, positional, &option);
error = result_store_token(out, self, noptions + positional, &option, token);
if (error.domain != null)
return error;
if (!option.multiple)
positional++;
index++;
}
return parser_finalize(self, out);
}
/* -------------------------------------------------------------------------
* Help and usage output.
* ---------------------------------------------------------------------- */
static
malunal_int32_t
print_prog(
FILE* stream,
parser_impl_iptr_t self
) {
malunal_int32_t written = 0;
if (self->parent != null)
written += print_prog(stream, (parser_impl_iptr_t)self->parent) + fprintf(stream, " ");
return written + fprintf(stream, "%s", self->name);
}
static
malunal_int32_t
print_metavar(
FILE* stream,
argoption_iptr_t option
) {
if (!option->isflag)
return fprintf(stream, "%s",
option->meta_name != null
? option->meta_name
: option->long_name);
if (option->meta_name != null)
return fprintf(stream, "%s", option->meta_name);
if (option->long_name == null)
return fprintf(stream, "%c", toupper((unsigned char)option->short_name));
malunal_cstr_t cursor = option->long_name;
for (; *cursor != '\0'; cursor++)
fputc(*cursor == '-' ? '_' : toupper((unsigned char)*cursor), stream);
return (malunal_int32_t)(cursor - option->long_name);
}
static
malunal_void_t
print_names(
FILE* stream,
argoption_iptr_t option
) {
if (!option->isflag) {
print_metavar(stream, option);
return;
}
if (option->short_name != '\0')
fprintf(stream, "-%c", option->short_name);
if (option->short_name != '\0' && option->long_name != null)
fputc('/', stream);
if (option->long_name != null)
fprintf(stream, "--%s", option->long_name);
}
static
malunal_void_t
print_option_usage(
FILE* stream,
argoption_iptr_t option
) {
if (!option->isflag) {
malunal_bool_t required = option->required;
if (option->multiple) {
if (required) {
print_metavar(stream, option);
fputc(' ', stream);
}
fputc('[', stream);
print_metavar(stream, option);
fputs(" ...]", stream);
} else {
if (!required) fputc('[', stream);
print_metavar(stream, option);
if (!required) fputc(']', stream);
}
return;
}
if (!option->required)
fputc('[', stream);
if (option->short_name != '\0')
fprintf(stream, "-%c", option->short_name);
else
fprintf(stream, "--%s", option->long_name);
if (option->value_type != ARGOPTION_TYPE_FLAG) {
fputc(' ', stream);
print_metavar(stream, option);
}
if (!option->required)
fputc(']', stream);
}
static
malunal_void_t
print_subcommand_choices(
FILE* stream,
parser_impl_iptr_t self
) {
malunal_size_t count = vector_count(&self->subcommands);
malunal_size_t index;
fputc('{', stream);
for (index = 0; index < count; index++) {
argparser_mptr_t child = null;
vector_container_get(&self->subcommands, index, &child);
fprintf(stream, index > 0 ? ",%s" : "%s", ((parser_impl_iptr_t)child)->name);
}
fputc('}', stream);
}
static
malunal_void_t
print_help_text(
FILE* stream,
malunal_int32_t column,
malunal_cstr_t help
) {
if (help == null) {
fputc('\n', stream);
return;
}
if (column + 2 <= ARGPARSE_HELP_COLUMN)
fprintf(stream, "%*s%s\n", ARGPARSE_HELP_COLUMN - column, "", help);
else
fprintf(stream, "\n%*s%s\n", ARGPARSE_HELP_COLUMN, "", help);
}
error_t
argparser_print_usage(
argparser_iptr_t parser
) {
if (parser == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
parser_impl_iptr_t self = (parser_impl_iptr_t)parser;
malunal_size_t slots = parser_slot_count(self);
fputs("usage: ", stdout);
print_prog(stdout, self);
malunal_size_t slot;
for (slot = 0; slot < slots; slot++) {
argoption_t option = parser_slot(self, slot);
fputc(' ', stdout);
print_option_usage(stdout, &option);
}
if (vector_count(&self->subcommands) > 0) {
fputc(' ', stdout);
print_subcommand_choices(stdout, self);
fputs(" ...", stdout);
}
fputc('\n', stdout);
return NO_ERROR;
}
error_t
argparser_print_help(
argparser_iptr_t parser
) {
error_t result = argparser_print_usage(parser);
if (result.domain != null)
return result;
parser_impl_iptr_t self = (parser_impl_iptr_t)parser;
malunal_size_t noptions = vector_count(&self->options);
malunal_size_t npositionals = vector_count(&self->positionals);
malunal_size_t nsubcommands = vector_count(&self->subcommands);
malunal_size_t index;
if (self->desc != null)
fprintf(stdout, "\n%s\n", self->desc);
if (npositionals > 0 || nsubcommands > 0)
fputs("\npositional arguments:\n", stdout);
for (index = 0; index < npositionals; index++) {
argoption_t option;
vector_container_get(&self->positionals, index, &option);
malunal_int32_t column = fprintf(stdout, " ") + print_metavar(stdout, &option);
print_help_text(stdout, column, option.help_string);
}
if (nsubcommands > 0) {
fputs(" ", stdout);
print_subcommand_choices(stdout, self);
fputc('\n', stdout);
}
for (index = 0; index < nsubcommands; index++) {
argparser_mptr_t child = null;
vector_container_get(&self->subcommands, index, &child);
parser_impl_iptr_t sub = (parser_impl_iptr_t)child;
malunal_int32_t column = fprintf(stdout, " %s", sub->name);
print_help_text(stdout, column, sub->desc);
}
fputs("\noptions:\n", stdout);
for (index = 0; index < noptions; index++) {
argoption_t option;
vector_container_get(&self->options, index, &option);
malunal_int32_t column = fprintf(stdout, " ");
if (option.short_name != '\0')
column += fprintf(stdout, "-%c", option.short_name);
if (option.short_name != '\0' && option.long_name != null)
column += fprintf(stdout, ", ");
if (option.long_name != null)
column += fprintf(stdout, "--%s", option.long_name);
if (option.value_type != ARGOPTION_TYPE_FLAG)
column += fprintf(stdout, " ") + print_metavar(stdout, &option);
print_help_text(stdout, column, option.help_string);
}
return NO_ERROR;
}
/* -------------------------------------------------------------------------
* Reading results back out.
* ---------------------------------------------------------------------- */
error_t
argresult_free(
argresult_mptr_t result
) {
if (result == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
result_impl_mptr_t self = (result_impl_mptr_t)result;
error_t status = NO_ERROR;
if (self->subresult != null) {
argresult_free(self->subresult);
status = allocator_dispose(self->allocator, self->subresult, sizeof(argresult_t));
}
error_t entries = vector_release(&self->entries);
error_t counts = vector_release(&self->counts);
if (status.domain == null)
status = entries;
if (status.domain == null)
status = counts;
memset(self, 0, sizeof(result_impl_t));
self->error_slot = ARGPARSE_NPOS;
return status;
}
error_t
argresult_print_error(
argresult_iptr_t result,
error_t error
) {
if (result == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
if (error.domain == null)
return NO_ERROR;
result_impl_iptr_t self = (result_impl_iptr_t)result;
argparser_iptr_t parser = self->error_parser != null
? self->error_parser
: self->parser;
malunal_bool_t ours = error.domain == &ERROR_DOMAIN_ARGPARSE_T;
if (ours && error.code == ARGPARSE_ERROR_HELP_REQUESTED && parser != null)
return argparser_print_help(parser);
malunal_cstr_t message = error.domain->describe != null
? error.domain->describe(error.code)
: error.domain->name;
if (parser == null) {
fprintf(stderr, "error: %s\n", message);
return NO_ERROR;
}
parser_impl_iptr_t impl = (parser_impl_iptr_t)parser;
argparser_print_usage(parser);
print_prog(stderr, impl);
fputs(": error: ", stderr);
malunal_bool_t has_slot = ours && self->error_slot != ARGPARSE_NPOS;
malunal_bool_t missing = ours && (
error.code == ARGPARSE_ERROR_MISSING_REQUIRED_OPTION ||
error.code == ARGPARSE_ERROR_MISSING_REQUIRED_POSITIONAL
);
argoption_t option = { 0 };
if (has_slot)
option = parser_slot(impl, self->error_slot);
if (missing && has_slot) {
fprintf(stderr, "%s: ", message);
print_names(stderr, &option);
} else {
if (has_slot) {
fputs("argument ", stderr);
print_names(stderr, &option);
fputs(": ", stderr);
}
fputs(message, stderr);
}
malunal_size_t length = strview_length(&self->error_arg);
if (ours && (length > 0 || error.code == ARGPARSE_ERROR_INVALID_VALUE))
fprintf(stderr, ": '%.*s'", (malunal_int32_t)length, self->error_arg.beg != null ? self->error_arg.beg : "");
fputc('\n', stderr);
return NO_ERROR;
}
static
error_t
result_lookup(
result_impl_iptr_t self,
malunal_cstr_t name,
malunal_size_t* outslot,
argoption_t* outoption
) {
if (self == null || self->parser == null || name == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
strview_t key = strview_from_cstr(name);
malunal_size_t stripped = 0;
while (stripped < 2 && strview_length(&key) > 1 && key.beg[0] == '-') {
strview_ltrim(&key, 1);
stripped++;
}
parser_impl_iptr_t parser = (parser_impl_iptr_t)self->parser;
malunal_size_t slots = parser_slot_count(parser);
malunal_size_t slot;
for (slot = 0; slot < slots; slot++) {
argoption_t option = parser_slot(parser, slot);
strview_t keys[2];
malunal_size_t nkeys = option_keys(&option, keys);
malunal_size_t index;
for (index = 0; index < nkeys; index++) {
if (strview_equals(&keys[index], &key)) {
*outslot = slot;
*outoption = option;
return NO_ERROR;
}
}
}
return argparse_error(ARGPARSE_ERROR_UNKNOWN_NAME);
}
static
error_t
result_get_item(
argresult_iptr_t result,
malunal_cstr_t name,
argoption_type_t type,
malunal_size_t index,
argitem_t* out
) {
result_impl_iptr_t self = (result_impl_iptr_t)result;
malunal_size_t slot = ARGPARSE_NPOS;
argoption_t option = { 0 };
error_t error = result_lookup(self, name, &slot, &option);
if (error.domain != null)
return error;
argoption_type_t actual = option.value_type;
if (type == ARGOPTION_TYPE_BOOLEAN && actual == ARGOPTION_TYPE_FLAG) {
if (index != 0)
return argparse_error(ARGPARSE_ERROR_OUT_OF_RANGE);
out->boolean = result_slot_count(self, slot) > 0;
return NO_ERROR;
}
if (actual != type)
return argparse_error(ARGPARSE_ERROR_TYPE_MISMATCH);
malunal_size_t total = 0;
malunal_size_t found = result_find_entry(self, slot, index, &total);
if (total == 0)
return argparse_error(ARGPARSE_ERROR_NOT_PRESENT);
if (found == ARGPARSE_NPOS)
return argparse_error(ARGPARSE_ERROR_OUT_OF_RANGE);
argentry_t entry;
vector_container_get(&self->entries, found, &entry);
*out = entry.item;
return NO_ERROR;
}
malunal_bool_t
argresult_has(
argresult_iptr_t result,
malunal_cstr_t name
) {
result_impl_iptr_t self = (result_impl_iptr_t)result;
malunal_size_t slot = ARGPARSE_NPOS;
argoption_t option = { 0 };
return result_lookup(self, name, &slot, &option).domain == null
&& result_slot_count(self, slot) > 0;
}
malunal_size_t
argresult_count(
argresult_iptr_t result,
malunal_cstr_t name
) {
result_impl_iptr_t self = (result_impl_iptr_t)result;
malunal_size_t slot = ARGPARSE_NPOS;
argoption_t option = { 0 };
if (result_lookup(self, name, &slot, &option).domain != null)
return 0;
if (option.value_type == ARGOPTION_TYPE_FLAG)
return result_slot_count(self, slot);
malunal_size_t total = 0;
result_find_entry(self, slot, 0, &total);
return total;
}
error_t
argresult_get_string(
argresult_iptr_t result,
malunal_cstr_t name,
strview_mptr_t out
) {
return argresult_get_string_at(result, name, 0, out);
}
error_t
argresult_get_number(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_int64_t* out
) {
return argresult_get_number_at(result, name, 0, out);
}
error_t
argresult_get_float(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_double_t* out
) {
return argresult_get_float_at(result, name, 0, out);
}
error_t
argresult_get_boolean(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_bool_t* out
) {
return argresult_get_boolean_at(result, name, 0, out);
}
error_t
argresult_get_string_at(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_size_t index,
strview_mptr_t out
) {
if (out == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
argitem_t item = { 0 };
error_t error = result_get_item(result, name, ARGOPTION_TYPE_STRING, index, &item);
if (error.domain == null)
*out = item.string;
return error;
}
error_t
argresult_get_number_at(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_size_t index,
malunal_int64_t* out
) {
if (out == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
argitem_t item = { 0 };
error_t error = result_get_item(result, name, ARGOPTION_TYPE_NUMBER, index, &item);
if (error.domain == null)
*out = item.number;
return error;
}
error_t
argresult_get_float_at(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_size_t index,
malunal_double_t* out
) {
if (out == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
argitem_t item = { 0 };
error_t error = result_get_item(result, name, ARGOPTION_TYPE_FLOAT, index, &item);
if (error.domain == null)
*out = item.floating;
return error;
}
error_t
argresult_get_boolean_at(
argresult_iptr_t result,
malunal_cstr_t name,
malunal_size_t index,
malunal_bool_t* out
) {
if (out == null)
return argparse_error(ARGPARSE_ERROR_NULL_INPUT);
argitem_t item = { 0 };
error_t error = result_get_item(result, name, ARGOPTION_TYPE_BOOLEAN, index, &item);
if (error.domain == null)
*out = item.boolean;
return error;
}
malunal_cstr_t
argresult_subcommand(
argresult_iptr_t result
) {
if (result == null)
return null;
result_impl_iptr_t self = (result_impl_iptr_t)result;
if (self->subresult == null)
return null;
result_impl_iptr_t sub = (result_impl_iptr_t)self->subresult;
return ((parser_impl_iptr_t)sub->parser)->name;
}
argresult_iptr_t
argresult_subresult(
argresult_iptr_t result
) {
return result != null
? ((result_impl_iptr_t)result)->subresult
: null;
}