Skip to content

refactor(globals): split parse_args into informational and evaluation modes (#206) - #222

Merged
TimD1 merged 1 commit into
devfrom
206_td_cleaner-arg-parsing
Aug 7, 2026
Merged

refactor(globals): split parse_args into informational and evaluation modes (#206)#222
TimD1 merged 1 commit into
devfrom
206_td_cleaner-arg-parsing

Conversation

@TimD1-bot

Copy link
Copy Markdown
Collaborator

Note

Authorship: the content below was drafted by Claude Opus 5 (an AI coding agent) and
filed via gh under @TimD1-bot, a bot account operated by @TimD1. It reflects the
agent's analysis, not a statement authored by @TimD1.

Closes #206. Also resolves #70, which recorded the -v overload as a latent defect and left the
decision open.

Problem

parse_args accepted -h, -v and -ci in two places with two meanings apiece. #70 recorded
-v as a latent defect — version in the argc < 4 pre-check, verbosity in the main option loop —
and left open whether to fix the overload or document it.

Neither is quite right. The overload is ambiguous only because both meanings can occur in a single
invocation. Decide the mode by argc before any flag is read and -v is unambiguous everywhere
it can appear, so the overload can stay.

Change

Two disjoint invocation modes.

Informational — fewer than the three mandatory arguments. Accepts only -h/--help,
-v/--version, -ci/--citation; none takes a value, each prints and exits 0. Anything else
warns Invalid usage., prints usage, and exits 0.

Evaluation — all three mandatory arguments present. Every optional flag consumes exactly one
following token. The three informational flags are rejected.

Token Informational mode Evaluation mode
-h, --help usage, exit 0 ERROR — informational only
-v version, exit 0 verbosity, takes a value
--version version, exit 0 ERROR — informational only
--verbosity Invalid usage. verbosity, takes a value
-ci, --citation citation, exit 0 ERROR — informational only
-n, --no-output-files Invalid usage. ERROR — Unexpected option
every other flag Invalid usage. takes exactly one value

The rejection names the token as typed rather than reusing the generic Unexpected option '%s':

Option '-h' is informational only; use it without the mandatory arguments

-h is a real option, so "unexpected" reads as "no such flag" and misdirects a user who has made
a scoping mistake rather than a typo.

-n removed

-n/--no-output-files was the only main-loop flag taking no value, so removing it is what makes
the one-value rule exceptionless. It was g.write's only writer, against nine readers, so the
field would have been permanently true. All of it goes: the eight always-taken guards
(main.cpp ×2, print.cpp ×6), the write_outputs row in parameters.tsv, and b2s(), which
existed solely for that row. Re-indentation from unwrapping those guards dominates the diff.

Verbosity pre-pass bug fix

One behavior change beyond the contract. The pre-pass loop header was for (int i = 0; i+1 < argc; i++), so i never exceeded argc-2, i == argc could not hold, and the missing-value ERROR
was unreachable — a trailing -v was silently accepted and the parse succeeded with verbosity
unchanged. Starting at 4 is correct now that -v is legal only past the positionals, and it makes
the guard fire. ParseArgs.VerbosityMissingNotReached, which documented the dead path rather than
enforcing it, becomes ParseArgs.VerbosityMissingErrors.

User-visible surface

parameters.tsv loses its write_outputs row. The file is row-keyed rather than columnar, so
nothing else shifts; only a consumer reading that key is affected, and the value documents nothing
once the flag is gone.

print_usage gains a second usage line for the informational form and moves -h/-v/-ci out of
Miscellaneous into an "Informational (use without arguments)" section.

The archived docs/v2.3.3, v2.3.4, v2.4.0 and v2.5.3 trees each document -n as that
release shipped it and are untouched — editing them would misrepresent those releases. There is
no current or v3 tree, so print_usage is the only live documentation of the flag set; the removal
gets reflected when the v3 docs are written under #52.

Tests

Builds on the parse_args coverage added in #202.

  • ParseArgs.NoOutputNoOutputRejected (informational mode) and NoOutputMainLoopErrors
    (evaluation mode); the g.write assertion goes with the field
  • HelpMainLoop, VersionMainLoop, CitationMainLoop flip from asserting printed output to
    asserting the informational-only ERROR and exit 1, joined by HelpLongMainLoop and
    CitationLongMainLoop
  • VerbosityMissingNotReachedVerbosityMissingErrors, asserting the now-reachable error
  • new VerbosityLongRejected pins --verbosity as invalid in informational mode
  • new DashVMeansVersionOrVerbosityByMode pins the -v pair as one disjoint-mode contract rather
    than two unrelated behaviors
  • PrintUsage.ListsDocumentedFlags drops -n, --no-output-files; PrintUsage.RequiredSection
    gains the second usage line
  • two cases used -n only incidentally and were retargeted: OptionalBeforeMandatoryWarns (needed
    any flag token before the positionals) and VerbositySkippedInMainLoop (needed a trailing flag
    the main loop consumes; now -q 5)
  • parse_capturing_stdout is deleted — the three main-loop cases were its only callers, and an
    unused helper in an anonymous namespace warns

Verification

  • Unit suite: 731/731 pass (726 → 731: two informational-only cases, two mode-rejection cases
    and the -v pair case added, NoOutput removed).
  • Integration suite: 98 passed.
  • chr20 fixture: 9 of 13 outputs byte-identical to the dev base. The four that differ are
    accounted for: parameters.tsv (the dropped write_outputs true row, plus the command row
    recording each binary's own path), summary.vcf (##CL= only, same reason), runtime.tsv
    (wall-clock), and stderr (log timestamps and durations — identical once normalized). Both sides
    built with make clean in between.
  • Contract table: each of the 11 rows above exercised by hand against the built binary; all
    match, including the exit codes.
  • Warnings: only the pre-existing dist.cpp:1141: unused parameter 'thread2', present on the
    base as well.

Notes

Based on current dev, which already includes #202's parse_args tests.

The informational-only rejection fires in the main option loop, so it lands after the VCFs are
opened and the reference FASTA is loaded — same ordering the pre-existing Unexpected option path
has always had. Erroring earlier would mean a second scan of argv before the positionals are
validated, which is a larger restructuring than this issue scopes.

… modes (#206)

parse_args accepted -h, -v and -ci in two places with two meanings apiece, and
the ambiguity was real only because both meanings could occur in one
invocation. Deciding the mode by argc before any flag is read makes them
disjoint, so the overload stays and the ambiguity goes.

Informational mode, entered when fewer than the three mandatory arguments are
present, accepts only -h/--help, -v/--version and -ci/--citation. None takes a
value; each prints and exits 0. Anything else, including --verbosity, warns
'Invalid usage.' and prints usage.

Evaluation mode accepts every other flag, each consuming exactly one following
token, and rejects the three informational flags by name:

    Option '-h' is informational only; use it without the mandatory arguments

rather than the generic "Unexpected option '%s'", which reads as "no such flag"
and misdirects a user who has made a scoping mistake rather than a typo.

Removing -n/--no-output-files is what makes "every flag takes a value"
exceptionless: it was the only main-loop flag that took none. Its one writer
gone, g.write would have been permanently true, so the eight always-taken
guards, the write_outputs row in parameters.tsv, and b2s() -- which existed for
that one call site -- all go with it. The re-indentation from unwrapping those
guards dominates the diff.

One behavior change beyond the contract, and it is a bug fix. The verbosity
pre-pass looped to argc-2, so i could never equal argc and the missing-value
ERROR was unreachable: a trailing -v was silently accepted and the parse
succeeded with verbosity unchanged. Starting the loop at 4 is correct now that
-v is legal only past the positionals, and it makes the guard fire.

parameters.tsv loses its write_outputs row. The file is row-keyed rather than
columnar, so nothing else shifts; only a consumer reading that key is affected,
and the value would document nothing once -n is gone.

The archived docs/v2.* trees still describe -n as those releases shipped it and
are left untouched. print_usage is the only live documentation of the flag set:
it gains a second usage line for the informational form and moves -h/-v/-ci out
of Miscellaneous into their own section.

731 unit tests and 98 pytest cases pass.
@TimD1
TimD1 merged commit 10ad24b into dev Aug 7, 2026
1 check passed
@TimD1
TimD1 deleted the 206_td_cleaner-arg-parsing branch August 7, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants