feat(xce-test-cli-ux): build flag-descriptor parsing/rejection core
Adds flagspec_t descriptor table, getopt_long option-table builder, and the shared stderr-routed rejection path that every flag-accepting command will migrate onto in later tasks. TDD'd via test_flagspec.c (1273/1273 controller suite passing). Task 1.1 of .kiro/specs/xce-test-cli-ux/tasks.md. Requirements: 2.1, 2.2, 2.3, 2.4, 3.1 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
parent
7bcafa68e7
commit
0748f10cc6
6 changed files with 827 additions and 1 deletions
119
src/controller/testsuite/flagspec.c
Normal file
119
src/controller/testsuite/flagspec.c
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#define _GNU_SOURCE
|
||||
#include "flagspec.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* True iff `token` (a raw command-line argument, e.g. "-h", "--host", or
|
||||
* -- defensively -- a dash-free bare name) identifies the flag described
|
||||
* by `fs`. A "--name" token is matched against fs->long_opt; a "-X" token
|
||||
* is matched against fs->short_opt (single character only, so "-xy" never
|
||||
* falsely matches a single-letter short_opt). */
|
||||
static int flag_token_matches(const char* token, const flagspec_t* fs)
|
||||
{
|
||||
if (!token || !fs)
|
||||
return 0;
|
||||
|
||||
if (token[0] == '-' && token[1] == '-')
|
||||
return fs->long_opt != NULL && strcmp(token + 2, fs->long_opt) == 0;
|
||||
|
||||
if (token[0] == '-' && token[1] != '\0')
|
||||
return fs->short_opt != 0 && token[1] == fs->short_opt &&
|
||||
token[2] == '\0';
|
||||
|
||||
/* No leading dash at all -- not how a real getopt_long() failure
|
||||
* token looks, but handled so this helper never mis-signals on
|
||||
* unexpected input from a caller that skips the usual
|
||||
* argv[optind - 1] convention. */
|
||||
if (fs->long_opt != NULL && strcmp(token, fs->long_opt) == 0)
|
||||
return 1;
|
||||
return fs->short_opt != 0 && token[0] == fs->short_opt &&
|
||||
token[1] == '\0';
|
||||
}
|
||||
|
||||
int flagspec_build_opts(const flagspec_t* spec, size_t n,
|
||||
struct option* out_opts, char* out_optstring,
|
||||
size_t out_optstring_cap)
|
||||
{
|
||||
if (!spec || !out_opts || !out_optstring || n == 0 ||
|
||||
out_optstring_cap == 0)
|
||||
return -EINVAL;
|
||||
|
||||
size_t str_idx = 0;
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const flagspec_t* fs = &spec[i];
|
||||
|
||||
if (!fs->long_opt || !*fs->long_opt)
|
||||
return -EINVAL;
|
||||
if (fs->has_arg != no_argument && fs->has_arg != required_argument)
|
||||
return -EINVAL;
|
||||
|
||||
out_opts[i].name = fs->long_opt;
|
||||
out_opts[i].has_arg = fs->has_arg;
|
||||
out_opts[i].flag = NULL;
|
||||
out_opts[i].val = fs->short_opt
|
||||
? (int)fs->short_opt
|
||||
: (int)(FLAGSPEC_LONG_ONLY_BASE + i);
|
||||
|
||||
if (fs->short_opt != 0) {
|
||||
size_t need = (fs->has_arg == required_argument) ? 2 : 1;
|
||||
/* Leave room for the final NUL written after the loop. */
|
||||
if (str_idx + need >= out_optstring_cap)
|
||||
return -ENOSPC;
|
||||
out_optstring[str_idx++] = fs->short_opt;
|
||||
if (fs->has_arg == required_argument)
|
||||
out_optstring[str_idx++] = ':';
|
||||
}
|
||||
}
|
||||
|
||||
out_optstring[str_idx] = '\0';
|
||||
|
||||
/* Trailing all-zero sentinel getopt_long() requires to terminate the
|
||||
* table; out_opts must have room for n+1 entries (precondition). */
|
||||
out_opts[n].name = NULL;
|
||||
out_opts[n].has_arg = 0;
|
||||
out_opts[n].flag = NULL;
|
||||
out_opts[n].val = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flagspec_reject(const char* cmd_name, const flagspec_t* spec, size_t n,
|
||||
const char* bad_flag, const char* usage_text)
|
||||
{
|
||||
const char* display = bad_flag ? bad_flag : "?";
|
||||
const char* who = cmd_name ? cmd_name : "?";
|
||||
const flagspec_t* match = NULL;
|
||||
|
||||
if (spec) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (flag_token_matches(bad_flag, &spec[i])) {
|
||||
match = &spec[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match && match->has_arg == required_argument) {
|
||||
fprintf(stderr, "xce_test: %s: flag '%s' requires a value\n", who,
|
||||
display);
|
||||
} else if (match) {
|
||||
fprintf(stderr, "xce_test: %s: flag '%s' does not take a value\n",
|
||||
who, display);
|
||||
} else {
|
||||
fprintf(stderr, "xce_test: %s: unrecognized flag '%s'\n", who,
|
||||
display);
|
||||
}
|
||||
|
||||
if (usage_text && *usage_text) {
|
||||
size_t len = strlen(usage_text);
|
||||
if (len > 0 && usage_text[len - 1] == '\n')
|
||||
fprintf(stderr, "usage: %s", usage_text);
|
||||
else
|
||||
fprintf(stderr, "usage: %s\n", usage_text);
|
||||
}
|
||||
|
||||
return -EINVAL;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue