/* * Minimal bootstrap RTL (runtime library) - CLI parameter handling. * * Copyright © 2025 Samuel Lidén Borell * * SPDX-License-Identifier: EUPL-1.2+ OR LGPL-2.1-or-later */ #include #include #include #include #include "rtl.h" #include "internal.h" enum OptionKind { OK_BOOL, /* sets `false` value if default==`true` */ OK_UINT, OK_STRING, OK_INPUTREADER, OK_OUTPUTFILE }; struct OptionInfo { struct OptionInfo *next; enum OptionKind kind; /* TODO non-option arguments */ /* TODO multi-valued (array) options, e.g. -x 1 -x 2 */ /* TODO "level" options: -v, -vv, -vvvvv */ char shortopt; size_t longopt_len; const char *longopt; const struct String *helptext; union { bool *bool_; SlulInt *uint; /* TODO string type */ struct Reader **reader; struct File **file; } target_field; bool has_default; union { bool bool_; SlulInt uint; /* TODO string type */ const struct String *filename; } defval; /* Runtime state */ bool present; const char *arg_str; }; static const char *progname; static struct OptionInfo *optinfos = NULL; static struct OptionInfo *addparam( enum OptionKind kind, const struct String *opt_str, const struct String *helptext) { const unsigned char *opt; SlulInt len; struct OptionInfo *prm = malloc(sizeof(struct OptionInfo)); OOM_CHECK(prm); prm->next = optinfos; optinfos = prm; prm->kind = kind; prm->present = false; opt = SLUL__decode_string(opt_str, &len); assert(opt[0] == '-'); if (opt[1] != '-') { /* Short option */ prm->shortopt = (char)opt[1]; if (!opt[2]) { prm->longopt_len = 0; prm->longopt = NULL; goto opts_done; } assert(opt[2] == ','); opt += 3; len -= 3; } else { prm->shortopt = 0; } /* Long option */ assert(opt[0] == '-' && opt[1] == '-'); opt += 2; len -= 2; prm->longopt = (char *)opt; prm->longopt_len = len; opts_done: prm->helptext = helptext; return prm; } void bool__giveme__param( bool *field, const struct String *option, const struct String *helptext) { struct OptionInfo *prm = addparam( OK_BOOL, option, helptext); prm->target_field.bool_ = field; prm->has_default = false; } void bool__giveme__param_with_default( bool *field, const struct String *option, bool defval, const struct String *helptext) { struct OptionInfo *prm = addparam( OK_BOOL, option, helptext); prm->target_field.bool_ = field; prm->has_default = true; prm->defval.bool_ = defval; } void uint__giveme__param( SlulInt *field, const struct String *option, const struct String *helptext) { struct OptionInfo *prm = addparam( OK_UINT, option, helptext); prm->target_field.uint = field; prm->has_default = false; } void uint__giveme__param_with_default( SlulInt *field, const struct String *option, SlulInt defval, const struct String *helptext) { struct OptionInfo *prm = addparam( OK_UINT, option, helptext); prm->target_field.uint = field; prm->has_default = true; prm->defval.uint = defval; } void Reader__giveme__inputparam( /* XXX have some prefix for SLUL types? */ struct Reader **field, const struct String *option, const struct String *helptext) { struct OptionInfo *prm = addparam( OK_INPUTREADER, option, helptext); prm->target_field.reader = field; prm->has_default = false; } void Reader__giveme__inputparam_default_stdin( struct Reader **field, const struct String *option, const struct String *helptext) { struct OptionInfo *prm = addparam( OK_INPUTREADER, option, helptext); prm->target_field.reader = field; prm->has_default = true; prm->defval.filename = (const struct String *)"\001-"; } static SLUL_NORETURN void show_help(void) { /* TODO */ exit(EXIT_SUCCESS); } static SLUL_NORETURN void option_error( const char *msg, char c, const char *name, size_t namelen) { const char *dashprefix; char buff; if (c >= 32 && c < 127) { buff = c; name = &buff; namelen = 1; dashprefix = "-"; } else if (!c) { dashprefix = "--"; } else { /* Don't split UTF-8 characters */ name = ""; namelen = 0; dashprefix = ""; } /* But it would be nicer to check if there's any --help argument present first, but this is OK in the boostrap RTL. */ fprintf(stderr, "%s: error: %s: %s%.*s\n", progname, msg, dashprefix, (int)namelen, name); exit(EXIT_FAILURE); } static struct OptionInfo *find_option( char c, const char *name, size_t namelen, bool has_equals_arg) { struct OptionInfo *info; for (info = optinfos; info; info = info->next) { if (c) { if (info->shortopt != c) continue; } else { if (info->longopt_len != namelen || memcmp(info->longopt, name, namelen)) continue; } if (info->present) { /* TODO multi-valued (array) options, e.g. -x 1 -x 2 */ /* TODO "level" options: -v, -vv, -vvvvv */ option_error("duplicate option", c, name, namelen); } info->present = true; return info; } /* No match */ if (c == '?' || c == 'h') { show_help(); } else if (namelen == 4 && !memcmp(name, "help", 4)) { if (has_equals_arg) { option_error("option takes no argument", c, name, namelen); } show_help(); } option_error("invalid option", c, name, namelen); } static void collect_cli_args(int argc, char **argv) { bool stop_parsing = false; if (argc) { progname = argv[0]; argc--; argv++; } else { progname = "?"; } while (argc) { const char *arg = *(argv++); argc--; if (!stop_parsing && *arg == '-' && arg[1]) { /* Option argument */ if (arg[1] == '-' && !arg[2]) { stop_parsing = true; } else if (arg[1] == '-') { /* Long option */ struct OptionInfo *info; const char *eq; size_t namelen; arg += 2; eq = strchr(arg, '='); namelen = eq ? (size_t)(eq - arg) : strlen(arg); info = find_option(0, arg, namelen, eq!=NULL); if (info->kind == OK_BOOL) { if (eq) option_error("option takes no argument", 0, arg, namelen); } else if (eq) { /* For example: --param=value */ info->arg_str = eq+1; } else if (!argc) { option_error("option requires an argument", 0, arg, namelen); } else { /* For example: --param value */ info->arg_str = *argv; argv++; argc--; } } else { /* Short option(s) */ struct OptionInfo *info; char lastopt; arg++; do { lastopt = *(arg++); info = find_option(lastopt, NULL, 0, false); } while (info->kind == OK_BOOL && *arg); if (info) { /* Reached an option that requires an argument */ if (*arg) { /* For example: -n123 */ info->arg_str = arg; } else if (argc) { /* For example: -n 123 */ info->arg_str = *argv; argv++; argc--; } else { option_error("option requires an argument", lastopt, NULL, 0); } } } } else { /* Non-option argument */ /* TODO non-option arguments. These could simply be params with an empty string (or `none`?) specified as the "options". */ } } } static void assign_from_cli_arg(struct OptionInfo *info) { const char *errmsg; switch (info->kind) { case OK_BOOL: *info->target_field.bool_ = !info->defval.bool_; break; case OK_UINT: { SlulInt num = 0; const char *s = info->arg_str; for (;;) { const char c = *(s++); if (c >= '0' && c <= '9') { SlulInt check; if (num > 0xFFFFFFFF/10) { errmsg = "number too large"; goto error; } num *= 10; check = num; num += ((SlulInt)c - '0'); if (num < check) { errmsg = "number too large"; goto error; } } else if (c == '\0') { if (s == info->arg_str+1) { errmsg = "empty number"; goto error; } break; } else { errmsg = "invalid digit in number"; goto error; } } *info->target_field.uint = num; break; } case OK_STRING: /* TODO */ break; case OK_INPUTREADER: /* TODO */ break; case OK_OUTPUTFILE: /* TODO */ break; } return; error: option_error(errmsg, info->shortopt, info->longopt, info->longopt_len); } static void assign_default(struct OptionInfo *info) { switch (info->kind) { case OK_BOOL: *info->target_field.bool_ = info->defval.bool_; break; case OK_UINT: *info->target_field.uint = info->defval.uint; break; case OK_STRING: /* TODO */ break; case OK_INPUTREADER: /* TODO */ break; case OK_OUTPUTFILE: /* TODO */ break; } } static void assign_cli_args(void) { struct OptionInfo *info; for (info = optinfos; info; info = info->next) { if (info->present) { assign_from_cli_arg(info); } else if (info->has_default) { assign_default(info); } else { option_error("missing required option", info->shortopt, info->longopt, info->longopt_len); } } } void SLUL__parse_cli_args(int argc, char **argv) { collect_cli_args(argc, argv); assign_cli_args(); }