/* Lion's Standard (LS) ANSI C commandline argument parser with included help * renderer. * * Version: 2.4 * Website: https://libls.org * GitHub: https://github.com/libls/args * Mirror: https://git.libls.org/args.git * SPDX-License-Identifier: MIT * * ==== TABLE OF CONTENTS ==== * * 1. DESCRIPTION * 2. HOW TO USE * 3. LICENSE * * ==== 1. DESCRIPTION ==== * * A simpe, terse, but complete args parser. * * Supports the following syntaxes: * * - Short options: `-h`, `-f filename`, `-abc` (equivalent to `-a -b -c`) * - Long options: `--help`, `--file filename` * - Stop signals: `--` (everything after this is positional arguments) * - Positional arguments: `input.txt output.txt` * * Includes a help renderer. * * ==== 2. HOW TO USE ==== * * ls_args, like all LS libraries, is a header-only library in a single file. * To use it in your codebase, simply copy and paste it into your source tree, * in a place where includes are read from. * * Then include and use it. * * Define LS_ARGS_IMPLEMENTATION in exactly one source file before the include. * * Example: * * #include * // ... * ls_args args; * int help = 0; * const char* outfile = "out.txt"; * const char* infile; * const char* testfile; * * ls_args_init(&args); * args.help_description = "Some description"; // optional * ls_args_bool(&args, &help, "h", "help", "Prints help", 0); * ls_args_string(&args, &outfile, "o", "out", * "Specify the outfile, default 'out.txt'", 0); * ls_args_pos_string(&args, &infile, "input file", LS_ARGS_REQUIRED); * ls_args_pos_string(&args, &testfile, "test file", 0); * if (!ls_args_parse(&args, argc, argv)) { * if (help) { * puts(ls_args_help(&args)); * } else { * printf("Error: %s\n", args.last_error); * } * ls_args_free(&args); * return 1; * } * * // TODO: Do something here with your arguments! * * ls_args_free(&args); * * ==== 3. LICENSE ==== * * This file is provided under the MIT license. For commercial support and * maintenance, feel free to use the e-mail below to contact the author(s). * * The MIT License (MIT) * * Copyright (c) 2026 Lion Kortlepel * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #pragma once #include #ifndef LS_REALLOC #include #define LS_REALLOC realloc #endif #ifndef LS_FREE #include #define LS_FREE free #endif typedef enum ls_args_mode { LS_ARGS_OPTIONAL = 0, LS_ARGS_REQUIRED = 1 } ls_args_mode; typedef enum ls_args_type { LS_ARGS_TYPE_BOOL = 0, LS_ARGS_TYPE_STRING = 1 } ls_args_type; typedef struct ls_args_arg { int is_pos; union { struct _lsa_spec { const char* short_opt; const char* long_opt; } name; unsigned pos; } match; const char* help; ls_args_type type; void* val_ptr; ls_args_mode mode; int found; } ls_args_arg; typedef struct ls_args { /* The last error, if any. Might be dynamically allocated; if so, it's * free'd with `ls_args_free` automatically. Always a valid, printable * string. */ char* last_error; /* don't use the following fields outside the library */ ls_args_arg* args; size_t args_len; size_t args_cap; /* program name -- this is set on ls_args_parse, but you can change it here * if you want to, for whatever reason. */ const char* program_name; /* description rendered under the "usage" line in ls_args_help */ const char* help_description; /* some bookkeeping -- these are used to free dynamically allocated memory * for help or errors cleanly on `ls_args_free`. */ void* _allocated_error; void* _allocated_help; size_t _next_pos; } ls_args; /* Zero-initializes the arguments, does not allocate */ void ls_args_init(ls_args*); /* The following functions register arguments. Upon a call to `ls_args_parse`, * the given `val` parameter is filled. The `val` pointer must never be NULL. * * ONE of `short_opt` and `long_opt` may be NULL, if only a short- or long * version should exist. The `help` string may be null, but is used to construct * help with `ls_args_help`. * * In your short and long opts, you *can* have `-` or `--`, but you don't need * them and, really, you should not use them. For example "h" or "help" are * valid short- and long-opts respectively. * * You can call ls_args_* functions with the same `val` pointer, if multiple * arguments should affect the same memory, however the order of evaluation (and * thus the order of which they may overwrite each other) is the same order as * the registration. * * BE AWARE that, if an argument is not present, the corresponding `val` is NOT * touched. This means that, if you initialize a bool with `true` and then parse * the args, and the corresponding flag is not present, the flag will not be set * to `false` (it will instead stay untouched). This allows you to set defaults * for the case in which the argument isn't present. */ /* A "flag", aka a boolean argument. If the argument is present, `*val` is set * to 1, otherwise it's left untouched. * Can fail if the allocator fails. `args.last_error` is set on failure. */ int ls_args_bool(ls_args*, int* val, const char* short_opt, const char* long_opt, const char* help, ls_args_mode mode); /* An argument which requires a string parameter, for example `--file * hello.txt`. Can fail if the allocator fails. `args.last_error` is set on * failure. */ int ls_args_string(ls_args*, const char** val, const char* short_opt, const char* long_opt, const char* help, ls_args_mode mode); /* A positional argument * * ./hello -r hello1 -v -x hello2 --other-flag * ^^^^^^ ^^^^^^ * n=0 n=1 * * assuming that -r is a boolean flag (takes no arguments). * * Example 2: * * ./my-app --flag1 --flag2 -- --help.txt * ^^ ^^^^^^^^^^ * | n=0 * | * "stop" indicator * everything after the `--` is a positional argument, for example a filename * which starts with a dash (like in this example). * * The first call to this function declares the argument for n=0, the next for * n=1, and so on. * * If the first positional isn't LS_ARGS_REQUIRED, but the second is, * effectively both are required. */ int ls_args_pos_string( ls_args*, const char** val, const char* name, ls_args_mode mode); /* Does all the heavy lifting. Assumes that `argv` has `argc` elements. NULL * termination of the `argv` array doesn't matter, but null-termination of each * individual string is required of course. * * Returns 1 on success, 0 on failure (boolean behavior). * On failure, the `args.last_error` is set to a human-readable string. */ int ls_args_parse(ls_args* args, int argc, char** argv); /* Constructs a help message from the arguments registered on the args struct * via `ls_args_{bool, string, ...} functions. * The string is dynamically allocated using LS_REALLOC and is freed * automatically once ls_args_free() is called. The string may be * replaced/changed by the next invocation to this function, as the buffer is * reused. */ char* ls_args_help(ls_args*); /* Frees all memory allocated in the args. */ void ls_args_free(ls_args*); /* Define this in exactly ONE source file, or in an object file compiled * separately with -DLS_ARGS_IMPLEMENTATION. */ #ifdef LS_ARGS_IMPLEMENTATION #define _lsa_ALLOC_FAIL_STR "Allocation failure" #include #include #include #include /* for sprintf */ #include static int _lsa_set_error_va( ls_args* a, size_t len, const char* fmt, va_list ap) { a->_allocated_error = LS_REALLOC(a->_allocated_error, len); if (a->_allocated_error == NULL) { a->last_error = _lsa_ALLOC_FAIL_STR; return 0; } memset(a->_allocated_error, 0, len); vsprintf(a->_allocated_error, fmt, ap); a->last_error = a->_allocated_error; return 1; } static int _lsa_set_error(ls_args* a, size_t len, const char* fmt, ...) { int ret; va_list ap; va_start(ap, fmt); ret = _lsa_set_error_va(a, len, fmt, ap); va_end(ap); return ret; } /* 0 on failure, 1 on success */ static int _lsa_add(ls_args* a, ls_args_arg** arg) { /* a is already checked when this is called */ assert(arg != NULL); if (a->args_len + 1 > a->args_cap) { ls_args_arg* new_args; size_t new_cap = a->args_cap + a->args_cap / 2 + 8; size_t max_items = SIZE_MAX / sizeof(*a->args); if (new_cap > max_items) { /* would overflow size_t */ return 0; } new_args = LS_REALLOC(a->args, new_cap * sizeof(*new_args)); if (new_args == NULL) { /* allocation failure */ return 0; } a->args_cap = new_cap; a->args = new_args; } *arg = &a->args[a->args_len++]; return 1; } void ls_args_init(ls_args* a) { memset(a, 0, sizeof(*a)); a->last_error = "Success"; } int _lsa_register(ls_args* a, void* val, ls_args_type type, const char* short_opt, const char* long_opt, const char* help, ls_args_mode mode) { ls_args_arg* arg; int ret; assert(a != NULL); assert(val != NULL); /* only one can be NULL, not both, but neither have to be NULL */ assert(short_opt != NULL || long_opt != NULL); /* remove preceding dashes for later matching */ if (long_opt) while (*long_opt == '-') long_opt++; /* remove preceding dashes for later matching */ if (short_opt) while (*short_opt == '-') short_opt++; /* if short_opt isn't null, it must be 1 char */ assert(short_opt == NULL || strlen(short_opt) == 1); ret = _lsa_add(a, &arg); if (ret == 0) { a->last_error = _lsa_ALLOC_FAIL_STR; return 0; } /* TODO: sanity check that there are no dashes in there, because that would * be a misuse of the API. */ /* the rest may be NULL */ arg->type = type; arg->match.name.short_opt = short_opt; arg->match.name.long_opt = long_opt; arg->is_pos = 0; arg->help = help; arg->mode = mode; arg->val_ptr = val; return 1; } int ls_args_bool(ls_args* a, int* val, const char* short_opt, const char* long_opt, const char* help, ls_args_mode mode) { return _lsa_register( a, val, LS_ARGS_TYPE_BOOL, short_opt, long_opt, help, mode); } int ls_args_string(ls_args* a, const char** val, const char* short_opt, const char* long_opt, const char* help, ls_args_mode mode) { return _lsa_register( a, val, LS_ARGS_TYPE_STRING, short_opt, long_opt, help, mode); } int ls_args_pos_string( ls_args* a, const char** val, const char* name, ls_args_mode mode) { /* TODO: The semantics are unclear when the first arg is not required but * the second is. Effectively, the first becomes required, too, because the * second cannot be the second without the first. */ ls_args_arg* arg; int ret; assert(a != NULL); assert(val != NULL); ret = _lsa_add(a, &arg); if (ret == 0) { a->last_error = _lsa_ALLOC_FAIL_STR; return 0; } arg->type = LS_ARGS_TYPE_STRING; arg->match.pos = a->_next_pos++; arg->help = name; arg->mode = mode; arg->val_ptr = val; arg->is_pos = 1; return 1; } typedef enum _lsa_parsed_type { LS_ARGS_PARSED_ERROR = 0, LS_ARGS_PARSED_LONG = 1, LS_ARGS_PARSED_SHORT = 2, LS_ARGS_PARSED_STOP = 3, LS_ARGS_PARSED_POSITIONAL = 4 } _lsa_parsed_type; typedef struct _lsa_parsed { _lsa_parsed_type type; union { /* the full argument that caused the error */ const char* erroneous; /* the long arg without the `--` */ const char* long_arg; /* might be multiple, like for -abc it would be `abc` */ const char* short_args; /* an argument provided without `--`, in full */ const char* positional; } as; } _lsa_parsed; static _lsa_parsed _lsa_parse(const char* s) { size_t s_len = strlen(s); _lsa_parsed res; assert(s != NULL); /* empty string or `-` */ if (s_len == 0 || (s_len == 1 && s[0] == '-')) { res.type = LS_ARGS_PARSED_ERROR; res.as.erroneous = s; goto end; } if (s[0] == '-') { if (s[1] == '-') { /* long opt */ size_t remaining = s_len - 2; if (remaining == 0) { /* special case where `--` is provided on its own to signal * "everything after this is positional" */ res.type = LS_ARGS_PARSED_STOP; goto end; } res.type = LS_ARGS_PARSED_LONG; res.as.long_arg = &s[2]; } else { /* short opt */ /* guaranteed to be the right size due to earlier checks */ res.type = LS_ARGS_PARSED_SHORT; res.as.short_args = &s[1]; } } else { res.type = LS_ARGS_PARSED_POSITIONAL; res.as.positional = s; } end: return res; } static void _lsa_apply(ls_args_arg* arg, ls_args_arg** prev_arg) { arg->found = 1; switch (arg->type) { case LS_ARGS_TYPE_BOOL: *(int*)arg->val_ptr = 1; *prev_arg = NULL; break; case LS_ARGS_TYPE_STRING: *prev_arg = arg; break; } } static int _lsa_parse_long( ls_args* a, _lsa_parsed* parsed, ls_args_arg** prev_arg) { int found = 0; size_t k; for (k = 0; k < a->args_len; ++k) { if (a->args[k].is_pos) { continue; } if (a->args[k].match.name.long_opt != NULL && strcmp(parsed->as.long_arg, a->args[k].match.name.long_opt) == 0) { _lsa_apply(&a->args[k], prev_arg); found = 1; break; } } if (!found) { const size_t len = 32 + strlen(parsed->as.erroneous); if (!_lsa_set_error( a, len, "Invalid argument '--%s'", parsed->as.erroneous)) { return 0; } return 0; } return 1; } static int _lsa_parse_short( ls_args* a, _lsa_parsed* parsed, ls_args_arg** prev_arg) { const char* args = parsed->as.short_args; while (*args) { char arg = *args++; int found = 0; size_t k; if (*prev_arg) { struct _lsa_spec named = (*prev_arg)->match.name; const size_t len = 128 + strlen(named.short_opt); if (!_lsa_set_error(a, len, "Expected argument following '-%s', instead got another " "argument '-%c'", named.short_opt, arg)) { return 0; } return 0; } for (k = 0; k < a->args_len; ++k) { const char* opt; if (a->args[k].is_pos) { continue; } opt = a->args[k].match.name.short_opt; if (opt != NULL && opt[0] == arg) { _lsa_apply(&a->args[k], prev_arg); found = 1; break; } } if (!found) { const size_t len = 32; if (!_lsa_set_error(a, len, "Invalid argument '-%c'", arg)) { return 0; } return 0; } } return 1; } static int _lsa_parse_positional( ls_args* a, _lsa_parsed* parsed, unsigned pos) { const size_t len = 32; size_t i; for (i = 0; i < a->args_len; ++i) { ls_args_arg* arg = &a->args[i]; if (arg->is_pos && arg->match.pos == pos) { *(const char**)arg->val_ptr = parsed->as.positional; arg->found = 1; return 1; } } if (!_lsa_set_error( a, len, "Unexpected argument '%s'", parsed->as.positional)) { return 0; } return 0; } int ls_args_parse(ls_args* a, int argc, char** argv) { int i; unsigned pos_i = 0; ls_args_arg* prev_arg = NULL; assert(a != NULL); assert(argv != NULL); a->last_error = "Success"; a->program_name = argv[0]; /* set all args to not found in case this is called multiple times */ for (i = 0; i < (int)a->args_len; ++i) { a->args[i].found = 0; } for (i = 1; i < argc; ++i) { _lsa_parsed parsed = _lsa_parse(argv[i]); if (prev_arg) { if (parsed.type != LS_ARGS_PARSED_POSITIONAL) { /* argument for the previous param expected, but none given */ const size_t len = 64 + strlen(prev_arg->match.name.long_opt); if (!_lsa_set_error(a, len, "Expected argument following '--%s'", prev_arg->match.name.long_opt)) { return 0; } return 0; } if (prev_arg->type == LS_ARGS_TYPE_STRING) { *(const char**)prev_arg->val_ptr = parsed.as.positional; } prev_arg = NULL; continue; } switch (parsed.type) { case LS_ARGS_PARSED_ERROR: { const size_t len = 32 + strlen(parsed.as.erroneous); if (!_lsa_set_error( a, len, "Invalid argument '%s'", parsed.as.erroneous)) { return 0; } return 0; } case LS_ARGS_PARSED_LONG: { if (!_lsa_parse_long(a, &parsed, &prev_arg)) { return 0; } break; } case LS_ARGS_PARSED_SHORT: { if (!_lsa_parse_short(a, &parsed, &prev_arg)) { return 0; } break; } case LS_ARGS_PARSED_STOP: { i += 1; for (; i < argc; ++i) { _lsa_parsed parsed; parsed.type = LS_ARGS_PARSED_POSITIONAL; parsed.as.positional = argv[i]; if (!_lsa_parse_positional(a, &parsed, pos_i)) { return 0; } pos_i += 1; } /* that's all, no more parsing allowed */ i = argc; break; } case LS_ARGS_PARSED_POSITIONAL: if (!_lsa_parse_positional(a, &parsed, pos_i)) { return 0; } ++pos_i; break; } } if (prev_arg) { size_t len; /* argument for the previous param expected, but none given */ /* this can not be a positional argument, because in order to become a * prev_arg, it must have expected a value earlier. this is only the * case with -/--... arguments */ assert(!prev_arg->is_pos); len = 64 + strlen(prev_arg->match.name.long_opt); if (!_lsa_set_error(a, len, "Expected argument following '--%s'", prev_arg->match.name.long_opt)) { return 0; } return 0; } for (i = 0; i < (int)a->args_len; ++i) { if (a->args[i].mode == LS_ARGS_REQUIRED && !a->args[i].found) { size_t len; if (a->args[i].is_pos) { len = 64; if (!_lsa_set_error(a, len, "Required argument '%s' not provided", a->args[i].help)) { return 0; } } else { len = 64 + strlen(a->args[i].match.name.long_opt); if (!_lsa_set_error(a, len, "Required argument '--%s' not found", a->args[i].match.name.long_opt)) { return 0; } } return 0; } } return 1; } typedef struct _lsa_buffer { char* data; size_t length; size_t capacity; } _lsa_buffer; static int _lsa_buffer_reserve(_lsa_buffer* buffer, size_t required_capacity) { char* new_data; if (required_capacity <= buffer->capacity) return 1; new_data = (char*)LS_REALLOC(buffer->data, required_capacity); if (!new_data) return 0; buffer->data = new_data; buffer->capacity = required_capacity; return 1; } static int _lsa_buffer_append_bytes( _lsa_buffer* buffer, const void* source, size_t byte_count) { if (!_lsa_buffer_reserve(buffer, buffer->length + byte_count + 1)) { return 0; } memcpy(buffer->data + buffer->length, source, byte_count); buffer->length += byte_count; buffer->data[buffer->length] = '\0'; return 1; } static int _lsa_buffer_append_cstr(_lsa_buffer* buffer, const char* string) { return _lsa_buffer_append_bytes(buffer, string, strlen(string)); } char* ls_args_help(ls_args* a) { _lsa_buffer help; if (a->_allocated_help != NULL) { LS_FREE(a->_allocated_help); a->_allocated_help = NULL; } help.data = NULL; help.length = 0; help.capacity = 0; if (!_lsa_buffer_append_cstr(&help, "Usage: ")) { goto alloc_fail; } if (a->program_name == NULL) { a->program_name = ""; } if (!_lsa_buffer_append_cstr(&help, a->program_name)) { goto alloc_fail; } if (a->args_len > 0) { size_t i; for (i = 0; i < a->args_len; ++i) { if (!a->args[i].is_pos) { if (!_lsa_buffer_append_cstr(&help, " [OPTION]")) { goto alloc_fail; } break; } } for (i = 0; i < a->args_len; ++i) { const char* open, *close; if (!a->args[i].is_pos) { continue; } open = a->args[i].mode == LS_ARGS_REQUIRED ? " <" : " ["; close = a->args[i].mode == LS_ARGS_REQUIRED ? ">" : "]"; if (!_lsa_buffer_append_cstr(&help, open)) goto alloc_fail; if (!_lsa_buffer_append_cstr(&help, a->args[i].help)) goto alloc_fail; if (!_lsa_buffer_append_cstr(&help, close)) goto alloc_fail; } if (a->help_description) { if (!_lsa_buffer_append_cstr(&help, "\n\n")) goto alloc_fail; if (!_lsa_buffer_append_cstr(&help, a->help_description)) goto alloc_fail; } /* Only print "Options:" if there are non-positional options */ { int has_nonpositional = 0; for (i = 0; i < a->args_len; ++i) { if (!a->args[i].is_pos) { has_nonpositional = 1; break; } } if (has_nonpositional) { if (!_lsa_buffer_append_cstr(&help, "\n\nOptions:")) goto alloc_fail; for (i = 0; i < a->args_len; ++i) { if (!a->args[i].is_pos) { if (!_lsa_buffer_append_cstr(&help, "\n -")) goto alloc_fail; if (!_lsa_buffer_append_cstr( &help, a->args[i].match.name.short_opt)) goto alloc_fail; if (!_lsa_buffer_append_cstr(&help, " \t--")) goto alloc_fail; if (!_lsa_buffer_append_cstr( &help, a->args[i].match.name.long_opt)) goto alloc_fail; if (a->args[i].type != LS_ARGS_TYPE_BOOL) { if (!_lsa_buffer_append_cstr(&help, " \t")) goto alloc_fail; if (a->args[i].mode == LS_ARGS_REQUIRED) { if (!_lsa_buffer_append_cstr(&help, " \t")) goto alloc_fail; } else { if (!_lsa_buffer_append_cstr(&help, "[VALUE] \t")) goto alloc_fail; } } else { if (!_lsa_buffer_append_cstr(&help, " \t\t\t")) goto alloc_fail; } if (!_lsa_buffer_append_cstr(&help, a->args[i].help)) goto alloc_fail; } } } } } a->_allocated_help = help.data; a->last_error = "Success"; return a->_allocated_help; alloc_fail: a->_allocated_help = help.data; a->last_error = _lsa_ALLOC_FAIL_STR; return "Not enough memory available to generate help text."; } void ls_args_free(ls_args* a) { if (a) { LS_FREE(a->args); a->args = NULL; a->args_cap = 0; a->args_len = 0; LS_FREE(a->_allocated_error); a->_allocated_error = NULL; a->last_error = ""; LS_FREE(a->_allocated_help); a->_allocated_help = NULL; } } #endif