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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,10 @@ start is a definition, not that description: `-x, --long` puts the long name fou
`--format <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
Expand Down
9 changes: 9 additions & 0 deletions src/runner/help_probe/flags/classify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,11 @@ Advanced:
"╭─ Options ────────────────────────────────────────────╮\n│ --force -f skip confirmation prompts │\n│ --limit -n <int> 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 <str> write output here │\n│ * --target -t <str> 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 <paths> │Semicolon separated paths to specific │\n│ │ │config files │\n",
Expand Down Expand Up @@ -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]
Expand Down
40 changes: 38 additions & 2 deletions src/runner/help_probe/flags/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>().join("\n")
}

fn shown(line: &str) -> String {
let mut out: Vec<char> = Vec::with_capacity(line.len());
let mut boxed = false;
let mut chars = line.chars();
while let Some(c) = chars.next() {
match c {
Expand All @@ -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<'_>) {
Expand Down Expand Up @@ -99,6 +119,22 @@ mod tests {
);
}

#[test]
fn a_required_marker_in_a_box_table_becomes_a_space() {
assert_eq!(
normalize("│ * --target -t <str> where to deploy [required] │"),
" --target -t <str> 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 <HEADER> 自定义请求头\n";
Expand Down
7 changes: 4 additions & 3 deletions src/runner/help_probe/snapshots/probe-rich-click__--help.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 5 additions & 4 deletions src/runner/help_probe/snapshots/probe-typer__--help.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 5 additions & 3 deletions tests/fixtures/help/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -692,7 +692,8 @@
["-f", "--force"],
["-n", "--limit"],
["--dry-run"],
["-cd", "--print-config-dir"]
["-cd", "--print-config-dir"],
["-t", "--target"]
]
},
{
Expand All @@ -711,7 +712,8 @@
["-n", "--limit"],
["--dry-run"],
["-cd", "--print-config-dir"],
["-o", "--output"]
["-o", "--output"],
["-t", "--target"]
]
},
{
Expand Down
14 changes: 8 additions & 6 deletions tests/fixtures/help/probe-rich-click__--help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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. │
╰──────────────────────────────────────────────────────────────────────────────╯
16 changes: 9 additions & 7 deletions tests/fixtures/help/probe-typer__--help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
typer probe

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --force -f skip confirmation prompts │
│ --limit -n <int> 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 <str> write output here │
│ --help Show this message and exit. │
│ --force -f skip confirmation prompts │
│ --limit -n <int> 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 <str> write output here │
│ * --target -t <str> where to deploy [required] │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯

4 changes: 3 additions & 1 deletion tests/fixtures/help/probes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/help/probes/python/py_richclick.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
3 changes: 2 additions & 1 deletion tests/fixtures/help/probes/python/py_typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading