Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ once it reaches a published 0.1.0 release.

## [Unreleased]

### Fixed

- [S-153] A usage line's `[options]` no longer refuses a bare operand tail unless the tail word names a real command-table placeholder, so `mandible lcf` shows `dest_file`/`src_dir` and `mandible ranlib` shows `archive`.

## [0.8.0] - 2026-09-14

### Changed
Expand Down
6 changes: 6 additions & 0 deletions corpus/bpftrace/0.20.2/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ usage:
- ' bpftrace [options] filename'
- ' bpftrace [options] - <stdin input>'
- ' bpftrace [options] -e ''program'''
positionals:
- name: filename
required: true
provenance:
sources:
- help-text
flags:
- spellings:
- -B
Expand Down
6 changes: 6 additions & 0 deletions corpus/ffplay/6.1.1-3ubuntu5/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: ffplay
description: Simple media player
usage:
- 'usage: ffplay [options] input_file'
positionals:
- name: input_file
required: true
provenance:
sources:
- help-text
flags:
- spellings:
- -L
Expand Down
11 changes: 11 additions & 0 deletions corpus/lcf/3.0043+nmu1/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ name: lcf
description: 'Debian GNU/Linux lcf Revision: 3.00. This is free software; see the GNU General Public Licence for copying conditions. There is NO warranty.'
usage:
- 'Usage: lcf [options] dest_file src_dir'
positionals:
- name: dest_file
required: true
provenance:
sources:
- help-text
- name: src_dir
required: true
provenance:
sources:
- help-text
flags:
- spellings:
- -h
Expand Down
13 changes: 7 additions & 6 deletions corpus/lcf/3.0043+nmu1/meta.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
# operand walk ever started; the tree carried no POSITIONALS section and
# the rendered USAGE line was truncated to the bare program name.
#
# Fixing the gap width alone does not recover a positional here: the tail
# `dest_file src_dir` has no numbering (unlike S-136), so the pre-existing
# `[options] command` ambiguity guard (round 6) still declines it, the
# same as `psfaddtable`'s multi-word bare tail. This fixture states the
# real, honest outcome: USAGE reconstructs correctly, POSITIONALS stays
# empty.
# The `[options] command` ambiguity guard (round 6) used to decline the
# tail outright once the gap fix recovered it. Round 12 narrowed that
# guard to a closed vocabulary (`command`/`commands`/`subcommand`/
# `subcommands`/`cmd`/`action`/`verb`) plus a repetition-marker check:
# `dest_file`/`src_dir` name neither, so both now reach the tree as
# required positionals in source order.

[bless]
provenance = "agent"
Expand All @@ -30,3 +30,4 @@ stderr = "help.stderr.txt"
verdict_scope = ["flags", "usage"]
expected_framework = "generic"
must_contain_flags = ["-h", "-s", "-d", "-n", "-v"]
must_contain_positionals = ["dest_file", "src_dir"]
10 changes: 10 additions & 0 deletions corpus/mariadb-check/2.7.4/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ description: |-
The option -c (--check) will be used by default, if none was specified. You can change the default behavior by making a symbolic link, or copying this file somewhere with another name, the alternatives are: mysqlrepair: The default option will be -r mysqlanalyze: The default option will be -a mysqloptimize: The default option will be -o
usage:
- 'Usage: /usr/bin/mariadb-check [OPTIONS] database [tables]'
positionals:
- name: database
required: true
provenance:
sources:
- help-text
- name: tables
provenance:
sources:
- help-text
flags:
- spellings:
- --print-defaults
Expand Down
10 changes: 10 additions & 0 deletions corpus/mariadb-repair/audit-seed4/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ description: |-
The option -c (--check) will be used by default, if none was specified. You can change the default behavior by making a symbolic link, or copying this file somewhere with another name, the alternatives are: mysqlrepair: The default option will be -r mysqlanalyze: The default option will be -a mysqloptimize: The default option will be -o
usage:
- 'Usage: /usr/bin/mariadb-repair [OPTIONS] database [tables]'
positionals:
- name: database
required: true
provenance:
sources:
- help-text
- name: tables
provenance:
sources:
- help-text
flags:
- spellings:
- --print-defaults
Expand Down
10 changes: 10 additions & 0 deletions corpus/mariadbcheck/audit-seed4/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ description: |-
The option -c (--check) will be used by default, if none was specified. You can change the default behavior by making a symbolic link, or copying this file somewhere with another name, the alternatives are: mysqlrepair: The default option will be -r mysqlanalyze: The default option will be -a mysqloptimize: The default option will be -o
usage:
- 'Usage: /usr/bin/mariadbcheck [OPTIONS] database [tables]'
positionals:
- name: database
required: true
provenance:
sources:
- help-text
- name: tables
provenance:
sources:
- help-text
flags:
- spellings:
- --print-defaults
Expand Down
10 changes: 10 additions & 0 deletions corpus/mkfs.bfs/2.39.3/expected.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
name: mkfs.bfs
usage:
- 'Usage: mkfs.bfs [options] device [block-count]'
positionals:
- name: device
required: true
provenance:
sources:
- help-text
- name: block-count
provenance:
sources:
- help-text
flags:
- spellings:
- -N
Expand Down
2 changes: 2 additions & 0 deletions corpus/nfsidmap/audit-seed/meta.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ min_status = "ok"
# §7 Tier B forbids.)
must_contain_flags = ["-v", "-c", "-d", "-l"]
must_not_contain_flags = ["-u", "-g", "-r", "-t"]
# The `||`-joined alternation and its trailing `key desc` operands: see
# docs/shapes.md S-175.
7 changes: 7 additions & 0 deletions corpus/nfsidmap/audit-seed2/meta.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ min_subcommands = 0
# collapses, the rest genuine multi-character single-dash flags) — a
# pre-existing, general decision this fix does not revisit.
must_contain_flags = ["-v", "-c", "-d", "-l"]

# docs/shapes.md S-175: the `||`-joined OR alternation past `-c` is a
# shape no alternation recognizer here reads, so the trailing `key desc`
# operand pair never becomes positionals. Nothing currently rendered is
# lost — the full line still prints verbatim in USAGE — and the raw-shape
# family is 1 tool (nfsidmap itself), below the five-tool floor, so this
# stays a passing fixture rather than `[xfail]`.
11 changes: 11 additions & 0 deletions corpus/split-file-18/18.1.3/expected.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ name: split-file-18
description: 'OVERVIEW: Split input into multiple parts separated by regex ''^(.|//)--- '' and extract the part specified by ''^(.|//)--- <part>'''
usage:
- 'USAGE: split-file-18 [options] filename directory'
positionals:
- name: filename
required: true
provenance:
sources:
- help-text
- name: directory
required: true
provenance:
sources:
- help-text
flags:
- spellings:
- --help
Expand Down
87 changes: 74 additions & 13 deletions docs/shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,11 @@ entry's `tools` field and nothing else. It does not get a new entry.
fleet-wide.
- fleet: detector fires on 50 tools, 126 findings, over a 2318-tool sweep,
2026-09-04. The fix moved 12 tools with zero losses on the same sweep.
Round 12's S-153 `[options] command` guard narrowing moves some of this
detector's own remaining findings too (24 of the 30-tool family it
resolved on a `--tools`-pinned sweep were part of the ambiguous
remainder this detector counts); the updated full-`PATH` number needs a
full sweep this branch did not run, so is not restated here.

### S-110: "or"-joined alias where both spellings carry a value

Expand Down Expand Up @@ -3054,25 +3059,57 @@ entry's `tools` field and nothing else. It does not get a new entry.
Usage: cache_repair [options] {device|file}
usage: fc-scan [-bcVh] [-f FORMAT] ... [--help] font-file...
Usage: lcf [options] dest_file src_dir
- tools: cache_repair, fc-scan, apt-mark, jdeprscan, lcf
- handling: Fixed for the shapes evidence can settle, refused for the rest.
A trailing operand run after a bracketed option run now reaches the tree as
- tools: cache_repair, fc-scan, apt-mark, jdeprscan, lcf, btrfs-convert,
btrfs-image, btrfs-map-logical, btrfstune, dpkg-reconfigure, ffplay,
llvm-bitcode-strip-18, llvm-install-name-tool-18, make-bcache, ntfscluster,
ntfscp, ntfsfix, ntfsinfo, ntfsls, ntfsmove, ntfsundelete, ntfswipe,
split-file-18, ucf, ucfr, xfs_growfs, xfs_repair, xfs_scrub
- handling: Fixed for the shapes evidence can settle, refused for the rest. A
trailing operand run after a bracketed option run now reaches the tree as
positionals: a brace alternation naming one operand becomes one positional
keeping its source spelling and its members as choices (`{device|file}`), a
single ellipsis-marked name becomes one repeatable positional
(`font-file...`), and a flag paired with an ALL-CAPS value on the same line
no longer ends the walk. The description-gap cut that ran before the walk
also used a two-space gap, which truncated a line whose own operands are
two-space padded. REFUSED, and this is the honest part: a bare multi-word
tail with no numbering and no delimiter (`lcf`'s `dest_file src_dir`)
stays declined by the round-6 `[options] command` ambiguity guard, because
nothing in the text says whether the words are two operands or one command
plus its argument. `corpus/lcf/3.0043+nmu1` states that outcome instead of
asserting a positional it does not get.
- fleet: a full-`PATH` sweep of 2323 tools, 2026-09-13: `tail_operand_tools`
147 to 145, zero flag losses, zero subcommand movement, all nine named
controls byte-identical. `multi-operand-usage-tail` (S-109) is unchanged at
44 tools/110 findings, which is the ambiguous remainder `lcf` belongs to.
two-space padded. Round 12 narrowed the round-6 `[options] command`
ambiguity guard to a closed vocabulary
(`command`/`commands`/`subcommand`/`subcommands`/`cmd`/`action`/`verb`,
`is_command_placeholder`) plus a repetition-marker check on the earliest
operand: `lcf`'s own two-space `dest_file src_dir` tail and `ntfscp`'s
`device src_file dest_file` now reach the tree, since none of those words
is in the vocabulary and none carries a repetition marker. REFUSED and
still correctly silent: `apt`/`apt-cache`/`apt-cdrom`/`apt-config`/
`apt-ftparchive`/`apt-get`'s own tail word is literally `command`, and
`gcc`'s `[options] file...` carries the repetition marker, so both stay
declined by the narrowed guard. The `ranlib` family (`ranlib`,
`gcc-ranlib`, `gcc-ranlib-13`, `aarch64-linux-gnu-ranlib`,
`aarch64-linux-gnu-gcc-ranlib{,-13}`) stays at zero positionals too, for an
unrelated reason: their own `archive` operand sits on the usage line's
*primary* physical line, but S-152's still-open trailing-description fold
joins the very next physical line (`Generate an index to speed access to
archives`) into the same usage entry, so `primary_synopsis_lines` reports
two physical lines for that entry and this rule's own one-physical-line
gate refuses the whole tail. `corpus/lcf/3.0043+nmu1` now asserts
`dest_file`/`src_dir`; `corpus/gcc-ranlib-13/2.42` keeps its
`must_contain_positionals = ["archive"]` unmet and stays `[xfail]` under
S-152, not this shape.
- fleet: a raw-shape grep over `audit/queue-captures/` (2301 tools),
2026-09-13: a usage line reading `<prog> [options]` followed by nothing but
lowercase snake_case bare words to end of line is 36 tools; excluding the 6
apt tools (closed-vocabulary tail) leaves 30. Tree-level, on a
`--tools`-pinned sweep of those 30 plus the 6 apt tools plus the nine named
controls (`git`, `gcc`, `aarch64-linux-gnu-g++-13`, `ar`, `pnpm`,
`systemctl`, `tar`, `find`, `docker`), 2026-09-13: 24 of the 30 gain a
positional (0 to 1-3 each, `ntfscp` gains 3), the 6-tool `ranlib` family
stays at 0 (blocked by S-152, see above), all 6 apt tools stay at 0, and
all nine controls are unchanged. Zero losses. `xtask corpus` over the full
178-fixture tree also gains positionals on `bpftrace`, `mariadb-check`,
`mariadb-repair`, `mariadbcheck` and `mkfs.bfs`, none of them in the named
30, with zero fixtures newly failing. `multi-operand-usage-tail` (S-109)'s
own count is expected to drop by the tools this narrowing newly resolves
that its own detector also counted; an updated full-`PATH` number needs
the orchestrator's own sweep lock and is not remeasured this branch.

### S-154: a bracketed multi-word operand becomes one positional per word

Expand Down Expand Up @@ -3916,3 +3953,27 @@ entry's `tools` field and nothing else. It does not get a new entry.
tools have no fixture yet; rustc's own row is verified by a direct unit
test (`grammar.rs`'s
`a_nested_bracket_value_after_a_space_keeps_the_whole_outer_group`).

### S-175: a usage form's OR alternation is joined with `||`

- id: S-175
- looks like: |
nfsidmap: Usage: nfsidmap [-vh] [-c || [-u|-g|-r key] || -d || -l || [-t timeout] key desc]
- tools: nfsidmap
- handling: Open, counted only. No recognizer here reads `||` as an
alternation separator (`parse_flag_alternation`,
`parse_brace_alternation_group` both read a single `|`), so the whole
bracket run past the first `||` is unparsed and the trailing `key desc`
operand pair never reaches the tree as positionals; the flags `-c`,
`-d`, `-l` and `-v` (glued to `h` as a required value, below
`parse_bundled_shorts`'s own `MIN_CLUSTER_MEMBERS` floor) are still
recovered correctly by the ordinary single-dash reading and nothing
currently rendered is lost, since the full line still prints verbatim
in USAGE. Below the five-tool floor, so no fix ships.
- fleet: raw grep over `audit/queue-captures/` (2299 readable tool
directories), 2026-09-13: 1 tool, `nfsidmap` itself. `gh`, `docker`,
`dockerd` and `git-lfs` also contain `||` somewhere in their help text,
none of it a usage-line alternation (shell examples). `[` was also
checked and is not a member. `double-pipe-usage-alternation`
(`xtask/src/detector/double_pipe_usage_alternation.rs`) generalizes the
shape as a measurement-only detector.
27 changes: 27 additions & 0 deletions mandible-extract/src/help_text/sections/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,33 @@ pub fn is_option_list_placeholder(name: &str) -> bool {
.any(|p| name.eq_ignore_ascii_case(p))
}

/// Words a usage line's tail uses to name a command table rather than a
/// real operand: `apt`'s `[options] command` genuinely dispatches on a
/// verb, so reading the word itself as a positional would fabricate one.
/// A closed vocabulary, the same shape [`OPTION_LIST_PLACEHOLDERS`] is,
/// checked against the *first* word of a recovered trailing operand run
/// only — the word sitting directly behind the ambiguous `[options]`
/// context. See docs/shapes.md S-153 and
/// `mandible-extract/src/help_text/sections/multiword.rs`'s
/// `recover_primary_tail_operands`.
pub(in crate::help_text) const COMMAND_PLACEHOLDER_WORDS: &[&str] = &[
"command",
"commands",
"subcommand",
"subcommands",
"cmd",
"action",
"verb",
];

/// True when `name` (already unwrapped from its notation) is one of
/// [`COMMAND_PLACEHOLDER_WORDS`].
pub fn is_command_placeholder(name: &str) -> bool {
COMMAND_PLACEHOLDER_WORDS
.iter()
.any(|p| name.eq_ignore_ascii_case(p))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
22 changes: 14 additions & 8 deletions mandible-extract/src/help_text/sections/multiword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,14 +522,20 @@ pub(super) fn recover_primary_tail_operands(
}
return Vec::new();
}
// `[options] command`'s shape: a lone placeholder group ahead of a
// bare, required first operand reads as easily as "provide a
// subcommand" as "provide an operand" — see the doc comment above.
// Only the earliest operand in the run sits directly behind the
// ambiguous context, so only its own required-ness is checked. A
// brace alternation or a numbered-variadic tail is exempt: neither
// notation can be mistaken for a bare subcommand name.
if earlier_all_placeholder && collected[0].1 && !collected[0].3 && !collected[0].4 {
// `[options] command`'s shape: a lone placeholder group ahead of the
// run's earliest operand. Narrowed to a closed vocabulary
// (`is_command_placeholder`) plus a repetition-marker check on that
// same operand: `apt`'s tail word is literally `command` and `gcc`'s
// `file...` carries the marker, so both stay refused, while `ranlib`'s
// `archive` and `lcf`'s `dest_file` are neither and now reach the
// tree. A brace alternation or numbered-variadic tail is exempt. See
// docs/shapes.md S-153.
if earlier_all_placeholder
&& collected[0].1
&& !collected[0].3
&& !collected[0].4
&& (is_command_placeholder(&collected[0].0) || collected[0].2)
{
return Vec::new();
}
collected
Expand Down
Loading
Loading