Allow partial option matches in C CLI parser as well

Matches behavior of Go parser and is nice to have
This commit is contained in:
Kovid Goyal 2025-04-28 12:43:09 +05:30
parent d7fb143ee0
commit 14792aa60e
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
2 changed files with 44 additions and 11 deletions

View file

@ -122,10 +122,49 @@ alloc_for_cli(CLISpec *spec, size_t sz) {
snprintf(buf, sz + 4, fmt, __VA_ARGS__); spec->errmsg = buf; \
}
static bool
add_to_listval(CLISpec *spec, CLIValue *v, const char *val) {
if (v->listval.count + 1 >= v->listval.capacity) {
size_t cap = MAX(64, v->listval.capacity * 2);
char **new = alloc_for_cli(spec, cap * sizeof(v->listval.items[0]));
if (!new) return false;
v->listval.capacity = cap;
if (v->listval.count) memcpy(new, v->listval.items, sizeof(new[0]) * v->listval.count);
v->listval.items = (void*)new;
}
v->listval.items[v->listval.count++] = val;
return true;
}
static const char*
dest_for_alias(CLISpec *spec, const char *alias) {
alias_hash_itr itr = vt_get(&spec->alias_map, alias);
if (vt_is_end(itr)) {
const char *match_key = NULL, *match_val = NULL;
size_t total = 0, count = 0;
alias_hash matches; vt_init(&matches);
alias_map_for_loop(&spec->alias_map) {
if (strstr(itr.data->key, alias) == itr.data->key) {
total += strlen(itr.data->key) + 8;
if (!match_key) { match_key = itr.data->key; match_val = itr.data->val; }
if (vt_is_end(vt_insert(&matches, itr.data->val, itr.data->key))) OOM;
count++;
}
}
if (match_key) {
if (count == 1) return match_val;
total += 256 + total;
char *buf = alloc_for_cli(spec, total);
if (!buf) OOM;
int n = snprintf(buf, total, "The flag %s is ambiguous. Possible matches:", alias);
alias_map_for_loop(&matches) {
n += snprintf(buf + n, total - n, " %s,", itr.data->val);
}
buf[n-1] = '.';
spec->errmsg = buf;
return NULL;
}
set_err("Unknown flag: %s use --help.", alias);
return NULL;
}
@ -145,15 +184,7 @@ add_list_value(CLISpec *spec, const char *dest, const char *val) {
cli_hash_itr itr = vt_get_or_insert(&spec->value_map, dest, (CLIValue){.type=CLI_VALUE_LIST});
if (vt_is_end(itr)) OOM;
CLIValue v = itr.data->val;
if (v.listval.count + 1 >= v.listval.capacity) {
size_t cap = MAX(64, v.listval.capacity * 2);
char **new = alloc_for_cli(spec, cap * sizeof(v.listval.items[0]));
if (!new) OOM;
v.listval.capacity = cap;
if (v.listval.count) memcpy(new, v.listval.items, sizeof(new[0]) * v.listval.count);
v.listval.items = (void*)new;
}
v.listval.items[v.listval.count++] = val;
if (!add_to_listval(spec, &v, val)) OOM;
if (vt_is_end(vt_insert(&spec->value_map, dest, v))) OOM;
}
@ -184,8 +215,9 @@ process_cli_arg(CLISpec* spec, const char *alias, const char *payload) {
if (strcmp(payload, flag->defval.listval.items[c]) == 0) { val.strval = payload; break; }
}
if (!val.strval) {
size_t bufsz = 128 + strlen(alias) + strlen(payload);
size_t bufsz = 0;
for (size_t c = 0; c < flag->defval.listval.count; c++) bufsz += strlen(flag->defval.listval.items[c]) + 8;
bufsz += 256 + strlen(alias) + strlen(payload) + bufsz;
char *buf = alloc_for_cli(spec, bufsz);
int n = snprintf(buf, bufsz, "%s is an invalid value for %s. Valid values are:",
payload[0] ? payload : "<empty>", alias);

View file

@ -106,13 +106,14 @@ def t(args, leftover=(), fails=False, version_called=False, help_called=False, *
t('-01=y', bool_reset=False, bool_set=True)
t('-01=n', bool_reset=False, bool_set=False)
t('--simple-string xx moo -1', leftover=['moo', '-1'], simple_string='xx')
t('--simple-string -0 -- -1', leftover=['-1'], simple_string='-0')
t('--si -0 -- -1', leftover=['-1'], simple_string='-0')
t('--simple-string=-0 -- -1', leftover=['-1'], simple_string='-0')
t('--simple-string=--help -- -1', leftover=['-1'], simple_string='--help')
t('--simple-string --help -- -1', leftover=['-1'], simple_string='--help')
t('-1l=a --list=b -c b --list c', bool_set=True, choice='b', list=list('abc'))
t('-1s= -l "" --list= x', leftover=['x'], bool_set=True, simple_string='', list=['', ''])
t('--choice moo', fails='a, b, c')
t('--bool', fails='mbiguous')
t('-1c moo', fails='a, b, c')
t('-10c=moo', fails='a, b, c')
t('-1 -h', fails=True, help_called=True)