aboutsummaryrefslogtreecommitdiff
/* 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.h>
 * // ...
 *     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 <[email protected]>
 *
 * 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 <stddef.h>

#ifndef LS_REALLOC
#include <stdlib.h>
#define LS_REALLOC realloc
#endif
#ifndef LS_FREE
#include <stdlib.h>
#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 <assert.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h> /* for sprintf */
#include <string.h>

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 = "<program>";
    }
    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, "<VALUE> \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