#define _GNU_SOURCE #include "flagspec.h" #include #include #include #include /* 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; } /* Levenshtein edit distance between two NUL-terminated strings -- the * minimum number of single-character insertions/deletions/substitutions * that turns `a` into `b`. Small and self-contained: research.md confirms * no edit-distance utility exists anywhere in the codebase yet. Standard * two-row rolling DP, O(len(a)*len(b)) time, O(min(len(a),len(b))+1) * space -- more than fast enough for flag-name-sized strings. * * Returns (size_t)-1 on allocation failure, which callers must treat as * "not a close match" rather than a false zero-distance/exact-match * signal. */ static size_t levenshtein(const char* a, const char* b) { size_t la = strlen(a), lb = strlen(b); /* Keep the rolling row sized to the SHORTER string. */ if (lb > la) { const char* tmp = a; a = b; b = tmp; size_t t = la; la = lb; lb = t; } size_t* row = calloc(lb + 1, sizeof(size_t)); if (!row) return (size_t)-1; for (size_t j = 0; j <= lb; j++) row[j] = j; for (size_t i = 1; i <= la; i++) { size_t prev_diag = row[0]; row[0] = i; for (size_t j = 1; j <= lb; j++) { size_t saved = row[j]; size_t cost = (a[i - 1] == b[j - 1]) ? 0 : 1; size_t del = row[j] + 1; size_t ins = row[j - 1] + 1; size_t sub = prev_diag + cost; size_t best = del < ins ? del : ins; if (sub < best) best = sub; row[j] = best; prev_diag = saved; } } size_t result = row[lb]; free(row); return result; } /* True iff `candidate` (one flag's literal spelling, e.g. "-h" or * "--host") "closely matches" `bad_flag` for typo-suggestion purposes * (Requirement 10.1). * * Threshold, a deliberate design choice (design.md/research.md do not * mandate a specific algorithm or cutoff): floor(len(candidate)/4), * clamped to [1,3]. A fixed absolute threshold would be wrong at both * ends -- distance 1 from a 2-character short flag ("-h" vs "-z") is a * near-total rewrite, while distance 1 from an 8+ character long flag is * a single typo -- so the tolerance scales with the spelling's own * length instead. Exact matches (distance 0) are never "close" here: * flagspec_reject() only calls this from the already-unrecognized branch, * where an exact-spelling candidate would be a contradiction, and this * keeps the helper correct standalone too. */ static int is_close_match(const char* bad_flag, const char* candidate) { size_t dist = levenshtein(bad_flag, candidate); if (dist == 0 || dist == (size_t)-1) return 0; size_t len = strlen(candidate); size_t threshold = len / 4; if (threshold < 1) threshold = 1; if (threshold > 3) threshold = 3; return dist <= threshold; } /* Writes `fs`'s display label ("-h/--host" or, for a long-only flag, * "--json") into `buf`. */ static void format_flag_label(char* buf, size_t cap, const flagspec_t* fs) { if (fs->short_opt) snprintf(buf, cap, "-%c/--%s", fs->short_opt, fs->long_opt); else snprintf(buf, cap, "--%s", fs->long_opt); } /* Returns the single flagspec_t in `spec` whose short or long spelling * closely matches `bad_flag`, or NULL when zero or more than one entry * qualifies -- Requirement 10.1 only promises a guess when exactly one * flag is a plausible intended target; an ambiguous typo (two or more * equally-plausible candidates) gets no guess at all, the same as a typo * that matches nothing (Requirement 10.2). */ static const flagspec_t* find_typo_suggestion(const flagspec_t* spec, size_t n, const char* bad_flag) { if (!spec || !bad_flag || !*bad_flag) return NULL; const flagspec_t* found = NULL; for (size_t i = 0; i < n; i++) { const flagspec_t* fs = &spec[i]; int is_close = 0; if (fs->short_opt) { char short_form[3] = { '-', fs->short_opt, '\0' }; if (is_close_match(bad_flag, short_form)) is_close = 1; } if (!is_close && fs->long_opt && *fs->long_opt) { char long_form[128]; snprintf(long_form, sizeof(long_form), "--%s", fs->long_opt); if (is_close_match(bad_flag, long_form)) is_close = 1; } if (is_close) { if (found) return NULL; /* ambiguous -- more than one candidate */ found = fs; } } return found; } 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 { const flagspec_t* suggestion = find_typo_suggestion(spec, n, bad_flag); if (suggestion) { char label[144]; format_flag_label(label, sizeof(label), suggestion); fprintf(stderr, "xce_test: %s: unrecognized flag '%s' (did you mean " "'%s'?)\n", who, display, label); } 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; } char* flagspec_render_help(const flagspec_t* spec, size_t n) { if (!spec || n == 0) return NULL; char* buf = NULL; size_t size = 0; FILE* f = open_memstream(&buf, &size); if (!f) return NULL; for (size_t i = 0; i < n; i++) { const flagspec_t* fs = &spec[i]; if (fs->short_opt) fprintf(f, " -%c, --%s", fs->short_opt, fs->long_opt); else fprintf(f, " --%s", fs->long_opt); fprintf(f, " %s", fs->required ? "required" : "optional"); if (fs->default_desc && *fs->default_desc) fprintf(f, " (default: %s)", fs->default_desc); fprintf(f, "\n"); } if (fclose(f) != 0) { free(buf); return NULL; } return buf; }