diff --git a/README.md b/README.md index 08ced24..e49e296 100644 --- a/README.md +++ b/README.md @@ -451,9 +451,10 @@ start is a definition, not that description: `-x, --long` puts the long name fou `--format ` under an undescribed `-x` declares `--format`. A definition can sit at any indent. rsync, ffmpeg and Miller print theirs at column 0, typer and broot print them as -rows of a box table with the short name in a cell of its own, Thor brackets its long names (`-f, [--force]`), and fzf -has shorts that start with a plus (`+x, --no-extended`). Text is read as a terminal shows it: without ANSI escapes, with -groff's overstruck bold collapsed, and with TABs expanded. +rows of a box table with the short name in a cell of its own (typer marks a required option with `*` in a first cell, +which is not part of the name), Thor brackets its long names (`-f, [--force]`), and fzf has shorts that start with a +plus (`+x, --no-extended`). Text is read as a terminal shows it: without ANSI escapes, with groff's overstruck bold +collapsed, and with TABs expanded. Two shapes need more than a leading dash, because usage lines wrap into them. Dash-led text at column 0 is a definition only when a gap or a marker sets a description off from its names, or its description starts at the column the diff --git a/src/runner/help_probe/flags/classify.rs b/src/runner/help_probe/flags/classify.rs index 2340b52..a0cf8ec 100644 --- a/src/runner/help_probe/flags/classify.rs +++ b/src/runner/help_probe/flags/classify.rs @@ -484,6 +484,11 @@ Advanced: "╭─ Options ────────────────────────────────────────────╮\n│ --force -f skip confirmation prompts │\n│ --limit -n maximum number of results to return, a │\n│ deliberately long description that wraps │\n│ --dry-run print what would change │\n╰──────────────────────────────────────────────────────╯\n", &[&["--force", "-f"], &["--limit", "-n"], &["--dry-run"]], ), + ( + "typer 0.27.2: a required option, marked `*` in the first cell of its row", + "╭─ Options ────────────────────────────────────────────╮\n│ --output -o write output here │\n│ * --target -t where to deploy [required] │\n│ --help Show this message and exit. │\n╰──────────────────────────────────────────────────────╯\n", + &[&["--output", "-o"], &["--target", "-t"], &["--help"]], + ), ( "broot 1.56: a box table whose cells touch their edges", "│ -d │--dates │Show the last modified date of files │\n│ │--conf │Semicolon separated paths to specific │\n│ │ │config files │\n", @@ -753,6 +758,10 @@ Advanced: let typer = declared(&fixture("probe-typer__--help.txt")); assert!(typer.contains(&vec!["--limit".to_string(), "-n".to_string()])); + assert!(typer.contains(&vec!["--target".to_string(), "-t".to_string()])); + + let rich_click = declared(&fixture("probe-rich-click__--help.txt")); + assert!(rich_click.contains(&vec!["--target".to_string(), "-t".to_string()])); } #[test] diff --git a/src/runner/help_probe/flags/normalize.rs b/src/runner/help_probe/flags/normalize.rs index a109fa5..30b6966 100644 --- a/src/runner/help_probe/flags/normalize.rs +++ b/src/runner/help_probe/flags/normalize.rs @@ -9,13 +9,16 @@ const BELL: char = '\u{7}'; /// `raw`, line for line, with ANSI escape sequences removed, each overstruck /// character reduced to the one printed last (groff bold is `X\bX`), TABs /// expanded to the next stop of eight, and each vertical edge of a box table -/// turned into a gap. Lines keep their order and count. +/// turned into a gap. A box-table row that leads with a `*` cell, where typer +/// and rich-click mark a required option, has the marker blanked, so the row +/// leads with its name. Lines keep their order and count. pub(super) fn normalize(raw: &str) -> String { raw.lines().map(shown).collect::>().join("\n") } fn shown(line: &str) -> String { let mut out: Vec = Vec::with_capacity(line.len()); + let mut boxed = false; let mut chars = line.chars(); while let Some(c) = chars.next() { match c { @@ -27,13 +30,30 @@ fn shown(line: &str) -> String { let stop = (out.len() / TAB_STOP + 1) * TAB_STOP; out.resize(stop, ' '); } - '│' | '┃' | '║' => out.extend([' ', ' ']), + '│' | '┃' | '║' => { + boxed = true; + out.extend([' ', ' ']); + } _ => out.push(c), } } + if boxed { + blank_required_marker(&mut out); + } out.into_iter().collect() } +/// Blank a `*` that leads the row and stands in a cell of its own. +fn blank_required_marker(row: &mut [char]) { + let Some(first) = row.iter().position(|c| !c.is_whitespace()) else { + return; + }; + let alone = row.get(first + 1..first + 3) == Some(&[' ', ' ']); + if row[first] == '*' && alone { + row[first] = ' '; + } +} + /// Consume the rest of an escape sequence: a CSI sequence through its final /// byte, an OSC sequence through its terminator, or one character. fn skip_escape(chars: &mut std::str::Chars<'_>) { @@ -99,6 +119,22 @@ mod tests { ); } + #[test] + fn a_required_marker_in_a_box_table_becomes_a_space() { + assert_eq!( + normalize("│ * --target -t where to deploy [required] │"), + " --target -t where to deploy [required] " + ); + assert_eq!( + normalize(" * --all: every item, as a bullet"), + " * --all: every item, as a bullet" + ); + assert_eq!( + normalize("│ *.md files are read │"), + " *.md files are read " + ); + } + #[test] fn lines_keep_their_count_and_multibyte_text() { let raw = "选项:\n\n -H, --header
自定义请求头\n"; diff --git a/src/runner/help_probe/snapshots/probe-rich-click__--help.snap b/src/runner/help_probe/snapshots/probe-rich-click__--help.snap index 66864e8..09132f2 100644 --- a/src/runner/help_probe/snapshots/probe-rich-click__--help.snap +++ b/src/runner/help_probe/snapshots/probe-rich-click__--help.snap @@ -4,6 +4,7 @@ input_file: tests/fixtures/help/probe-rich-click__--help.txt --- 7 | --force, -f | skip confirmation prompts 8 | --limit, -n | maximum number of results to return, a deliberately long des - 10 | --dry-run | print what would change - 11 | --print-config-dir, -cd | multi-letter single-dash alias - 12 | --help | Show this message and exit. + 11 | --dry-run | print what would change + 12 | --print-config-dir, -cd | multi-letter single-dash alias + 13 | --target, -t | where to deploy [required] + 14 | --help | Show this message and exit. diff --git a/src/runner/help_probe/snapshots/probe-typer__--help.snap b/src/runner/help_probe/snapshots/probe-typer__--help.snap index 3ff61af..a246d67 100644 --- a/src/runner/help_probe/snapshots/probe-typer__--help.snap +++ b/src/runner/help_probe/snapshots/probe-typer__--help.snap @@ -4,7 +4,8 @@ input_file: tests/fixtures/help/probe-typer__--help.txt --- 7 | --force, -f | skip confirmation prompts 8 | --limit, -n | maximum number of results to return, a deliberately long des - 10 | --dry-run | print what would change - 11 | --print-config-dir, -cd | multi-letter single-dash alias - 12 | --output, -o | write output here - 13 | --help | Show this message and exit. + 11 | --dry-run | print what would change + 12 | --print-config-dir, -cd | multi-letter single-dash alias + 13 | --output, -o | write output here + 14 | --target, -t | where to deploy [required] + 15 | --help | Show this message and exit. diff --git a/tests/fixtures/help/index.json b/tests/fixtures/help/index.json index a735e34..993dba2 100644 --- a/tests/fixtures/help/index.json +++ b/tests/fixtures/help/index.json @@ -2,7 +2,7 @@ "probe_env": {"NO_COLOR": "1", "TERM": "dumb", "COLUMNS": "80", "PAGER": "cat"}, "images": { "scorer": {"id": "sha256:05db16cf226cd14a93a2682aea41b72d3e6b4d240cadba006f2881d3ffa996b3", "dockerfile": "agentnative-site:docker/score/Dockerfile"}, - "probes": {"id": "sha256:f1626f912f98f5152d1c34dc6ece1765025ccd4ee331ab479b34580b323fd327", "dockerfile": "probes/Dockerfile"} + "probes": {"id": "sha256:40f5b1b77bc4f747fb27d9ccba260ad7b7abdd27477be88eaa3eb2732c3e5b24", "dockerfile": "probes/Dockerfile"} }, "fixtures": [ { @@ -692,7 +692,8 @@ ["-f", "--force"], ["-n", "--limit"], ["--dry-run"], - ["-cd", "--print-config-dir"] + ["-cd", "--print-config-dir"], + ["-t", "--target"] ] }, { @@ -711,7 +712,8 @@ ["-n", "--limit"], ["--dry-run"], ["-cd", "--print-config-dir"], - ["-o", "--output"] + ["-o", "--output"], + ["-t", "--target"] ] }, { diff --git a/tests/fixtures/help/probe-rich-click__--help.txt b/tests/fixtures/help/probe-rich-click__--help.txt index d43d3d6..8e4e835 100644 --- a/tests/fixtures/help/probe-rich-click__--help.txt +++ b/tests/fixtures/help/probe-rich-click__--help.txt @@ -4,10 +4,12 @@ rich-click probe ╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --force -f skip confirmation prompts │ -│ --limit -n INTEGER maximum number of results to return, a │ -│ deliberately long description that wraps │ -│ --dry-run print what would change │ -│ --print-config-dir -cd multi-letter single-dash alias │ -│ --help Show this message and exit. │ +│ --force -f skip confirmation prompts │ +│ --limit -n INTEGER maximum number of results to return, a │ +│ deliberately long description that │ +│ wraps │ +│ --dry-run print what would change │ +│ --print-config-dir -cd multi-letter single-dash alias │ +│ * --target -t TEXT where to deploy [required] │ +│ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ diff --git a/tests/fixtures/help/probe-typer__--help.txt b/tests/fixtures/help/probe-typer__--help.txt index 61ccdda..0ed6bd5 100644 --- a/tests/fixtures/help/probe-typer__--help.txt +++ b/tests/fixtures/help/probe-typer__--help.txt @@ -4,12 +4,14 @@ typer probe ╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --force -f skip confirmation prompts │ -│ --limit -n maximum number of results to return, a │ -│ deliberately long description that wraps │ -│ --dry-run print what would change │ -│ --print-config-dir -cd multi-letter single-dash alias │ -│ --output -o write output here │ -│ --help Show this message and exit. │ +│ --force -f skip confirmation prompts │ +│ --limit -n maximum number of results to return, │ +│ a deliberately long description that │ +│ wraps │ +│ --dry-run print what would change │ +│ --print-config-dir -cd multi-letter single-dash alias │ +│ --output -o write output here │ +│ * --target -t where to deploy [required] │ +│ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ diff --git a/tests/fixtures/help/probes/Dockerfile b/tests/fixtures/help/probes/Dockerfile index 022773f..69b4d91 100644 --- a/tests/fixtures/help/probes/Dockerfile +++ b/tests/fixtures/help/probes/Dockerfile @@ -39,7 +39,9 @@ COPY python/requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir --require-hashes --requirement /tmp/requirements.txt FROM ruby:4.0.7-slim-trixie@sha256:073f6464ca7c0b66fa86715ec57339a39da08fdc803583140edc66e486af9aeb -RUN gem install thor --version 1.5.0 --no-document +ADD --chmod=644 --checksum=sha256:e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 \ + https://rubygems.org/downloads/thor-1.5.0.gem /tmp/thor-1.5.0.gem +RUN gem install --local --no-document /tmp/thor-1.5.0.gem COPY --from=python312 /usr/local/ /usr/local/ COPY --from=python313 /usr/local/ /usr/local/ diff --git a/tests/fixtures/help/probes/python/py_richclick.py b/tests/fixtures/help/probes/python/py_richclick.py index 99b12f0..2b3558a 100644 --- a/tests/fixtures/help/probes/python/py_richclick.py +++ b/tests/fixtures/help/probes/python/py_richclick.py @@ -4,6 +4,7 @@ @click.option("-n", "--limit", type=int, help="maximum number of results to return, a deliberately long description that wraps") @click.option("--dry-run", is_flag=True, help="print what would change") @click.option("-cd", "--print-config-dir", is_flag=True, help="multi-letter single-dash alias") +@click.option("-t", "--target", required=True, help="where to deploy") def main(**kw): """rich-click probe""" main() diff --git a/tests/fixtures/help/probes/python/py_typer.py b/tests/fixtures/help/probes/python/py_typer.py index fef2afa..f888adb 100644 --- a/tests/fixtures/help/probes/python/py_typer.py +++ b/tests/fixtures/help/probes/python/py_typer.py @@ -6,6 +6,7 @@ def main(force: bool = typer.Option(False, "-f", "--force", help="skip confirmat limit: Optional[int] = typer.Option(None, "-n", "--limit", help="maximum number of results to return, a deliberately long description that wraps"), dry_run: bool = typer.Option(False, "--dry-run", help="print what would change"), cd: bool = typer.Option(False, "-cd", "--print-config-dir", help="multi-letter single-dash alias"), - output: Optional[str] = typer.Option(None, "-o", "--output", help="write output here")): + output: Optional[str] = typer.Option(None, "-o", "--output", help="write output here"), + target: str = typer.Option(..., "-t", "--target", help="where to deploy")): """typer probe""" app()