Files
tai/taicpp/docs/cli.md
T

4.3 KiB

cli: the argument parser is a port of argparse

Goal: detz parses its command line exactly like ../tai.py (build_parser() run by the Python installed here, 3.14). This document is the spec; it states what the code does today and lists the gaps at the end.

Layout (one class per .h/.cpp pair, src/cli/)

class file role
FlagTable / FlagSpec flag_spec.* the single flag table: option strings, dest, arity, choices, metavar, help. Usage, help and the parser all read it, so they cannot drift
TextWrap text_wrap.* port of Python textwrap.wrap (hyphen breaking, long-word breaking)
HelpFormatter help_formatter.* port of argparse.HelpFormatter (3.14): usage wrapping, help columns
ArgParser arg_parser.* port of ArgumentParser._parse_known_args (pattern string of A/O/-, abbreviations, explicit values, --)
Parser, Command, DetzCli, Options cli.* stable API others compile against; maps ArgParser output onto Options

Options, Parser::parse/parseOpts/helpText, Command::run keep their names and fields.

Behavior (mirrors argparse, each point has a differential case)

  • Unique prefixes of long options resolve (--vers is --version); an ambiguous prefix fails with ambiguous option: --ins could match --install, --install-telegram (matches listed in table order). --tui- resolves to --tui-local because --tui does not start with it.
  • --flag=value for value flags; an explicit value on a switch fails with argument --yes: ignored explicit argument '1'. Single-dash clusters (-hx) follow argparse.
  • A flag that needs a value and sees an option-like word, --, or the end fails with argument --profile: expected one argument. -1 and -.5 (anything matching -\.?\d) are values, not options. A word with a space in it is a value.
  • --debug takes an optional value (file when bare), checked against its choices.
  • --cap and --mplex take every remaining word (argparse REMAINDER), options included.
  • -- ends options; the first -- is dropped from the prompt, later ones are kept.
  • type=int accepts what Python int() accepts for ASCII input (surrounding whitespace, sign, single underscores between digits); anything else is invalid int value: 'abc'.
  • -h/--help acts the moment it is reached, before later words are examined, so -h --bogus prints help (exit 0) while --cap-permission-mode zzz -h is an error (exit 2). Options then carries only help = true.
  • Any parse error: full usage text on stderr, then tai: error: <message>, exit 2. Options::error holds both (usage then the error line, no trailing newline).
  • Terminal width: COLUMNS (if a positive integer), else the width of stdout's tty, else 80; the formatter uses width - 2, like shutil.get_terminal_size.
  • Flags that exist only in the old C++ parser (--model, --driver, --task, --cwd, --argv, --binary, --max-risk, --auto-answer, --allow-tool, --deny-tool) are not in tai.py and are rejected as unrecognized, like tai.py does. The Options fields stay so other modules compile.

Tests

Run all with make -f tests/cli.mk test (everything lands in build/cli).

  • tests/cli_test.cpp: unit cases (table, abbreviations, errors, REMAINDER, --, int, usage, wrap).
  • tests/cli_parity.py + tests/cli_cases.txt: every argv line of the list at COLUMNS 80 and 40, --help and the usage printed by an error at every width 12..200, and 1500 seeded random argvs, run through build_parser() of ../tai.py (in process, the installed Python) and through build/cli/cli_driver; stdout, stderr and exit code must be equal (2080 jobs, 0 differences).
  • make -f tests/cli.mk diff: the generated cases of tools/parity_diff.py (cli.*, tests/diff/cases/cli_generated.json) against build/cli/cli_main, a cli-only entry point that mirrors the first lines of main.cpp.

Gaps

  • Colored help (Python 3.14 colors help when FORCE_COLOR is set or stdout is a tty with color): not implemented, plain text only.
  • Widths count bytes, not code points (all help text is ASCII; user words only appear in error messages, which are not wrapped).
  • The --cap-manifest text and every action after parsing live in main.cpp, not here: case cli.cap_manifest fails against cli_main for that reason (55 of 56 cli.* cases pass).