Skip to content

Extract the completion and man page generators into a reusable C library #9

Description

@kjanat

envctl builds its shell completions, man page, per-command help, and PowerShell
module from one table in src/cli.c. That machinery is not envctl-specific, and
nothing comparable exists for C. This issue records what an extraction would
involve.

Why it might be worth doing

Rust has clap, Go has Cobra, Zig has args.zig, Lua has argparse. Each generates
completions from the same declaration the parser uses.
complgen covers four shells for C-ish
projects but works from a separate EBNF grammar, so the declaration lives beside
the parser and can drift from it. Man page generation from a declaration seems
to have no C implementation at all; the tools that exist go the other way and
derive completions from an existing man page.

The idea worth packaging is not the generators on their own. It is the loop:
one table drives parser validation, --help, roff, and four shells, and
make test regenerates the man page and fails when the checked-in copy differs.
A flag cannot exist without the documentation knowing about it.

What is already generic

gen/man.c (276 lines) is close to free. The roff mechanics carry no envctl
knowledge: escaping, .TP blocks, section emission, the Oxford joiner behind
"Valid for". Of its 23 mentions of "envctl", nearly all sit inside prose strings
that are already data. A sections[] array supplied by the host plus a program
name would finish it.

src/complete.c (593 lines) is generic in its mechanics too. The four script
skeletons, per-shell quoting and escaping, bitmask-driven flag emission, command
grouping, the zsh usage line, and the help/version injection all transfer
unchanged.

What blocks the completion half

Ten branches in src/complete.c test concrete command IDs, and they encode
domain knowledge rather than shell mechanics:

if (c->id == CMD_GET)          /* _envctl_keys active --env */
else if (c->id == CMD_LIST)    /* (( ${+opt_args[--env]} )) || _files */
else if (c->id == CMD_REDACT)  /* --env or --no-env suppress _files */

The zsh script also carries a generated _envctl_keys helper that shells out to
<self> list --all and parses the (disabled) suffix. That is envctl's own
output format, not completion infrastructure.

The real boundary is therefore the argument model, not "generator versus
envctl". A reusable library needs the host to declare, per positional slot, what
completes there. Something along the lines of:

typedef enum { SRC_NONE, SRC_FILES, SRC_WORDS, SRC_COMMAND } SourceKind;

typedef struct {
    SourceKind kind;
    const char *words;    /* SRC_WORDS: space-separated candidates */
    const char *command;  /* SRC_COMMAND: shell fragment emitting candidates */
} CompletionSource;

src/module.c (304 lines) is the least reusable of the three. Cmdlet naming and
KEY=VALUE to object parsing are domain work.

Suggested path

Step 1, worth doing on its own. Move positional completion sources into the
registry as data. Six of the ten command-ID branches disappear, src/complete.c
becomes fully table-driven, and envctl gains the cleanup whether or not anything
is ever extracted. The 239 golden cases cover the refactor.

Step 2, only with an appetite for maintaining it. Lift the generators out as
an stb-style single-file library, vendored into src/ rather than pulled in by
a package manager. envctl ships as one binary with no dependencies and an
extraction must not cost that.

Open questions

  • Does the man page generator want the same host-declared source model, or is a
    program name plus a section list enough?
  • Should the PowerShell module generator come along, or stay in envctl?
  • Name. Candidates so far: argot (contains arg, means the vocabulary of a
    trade, which is exactly the translation job), shellac, muster, scribe,
    herald, tabula, usher, codex, roster, chorus.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: cliCommand surface, flags, and outputbuildMakefile, CI, release artifactsenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions