Skip to content

Commit 41f1092

Browse files
xerialclaude
andcommitted
Send --help to stdout; reject duplicate subcommand names
Sixth codex pass: - help: split HelpPrinter into print() (stdout, for user-requested help) and print_error() (stderr, grouped with the error message). Piping `cmd --help > file` now works. - dispatch: LauncherWithSubs::command() panics on a duplicate name. A second registration was silently overshadowed because parsing stops at the first match, so the later handler was dead code. Panicking at startup makes the programmer error impossible to miss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ff48964 commit 41f1092

2 files changed

Lines changed: 25 additions & 3 deletions

File tree

runi-cli/src/launcher/dispatch.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ impl<G: Command + 'static> LauncherWithSubs<G> {
104104
where
105105
S: Command + SubCommandOf<G> + 'static,
106106
{
107+
// Silently accepting a duplicate would make later registrations
108+
// unreachable because parsing stops at the first match. That's a
109+
// programmer error — fail loudly at startup.
110+
assert!(
111+
!self.subs.iter().any(|e| e.schema.name == name),
112+
"duplicate subcommand name: {name}",
113+
);
107114
let mut schema = S::schema();
108115
schema.name = name.to_string();
109116
let name_owned = schema.name.clone();
@@ -192,14 +199,14 @@ fn report_error(err: Error, root: &CommandSchema) -> i32 {
192199
}
193200
inner => {
194201
eprintln!("error: {inner}");
195-
HelpPrinter::print(schema);
202+
HelpPrinter::print_error(schema);
196203
2
197204
}
198205
}
199206
}
200207
other => {
201208
eprintln!("error: {other}");
202-
HelpPrinter::print(root);
209+
HelpPrinter::print_error(root);
203210
2
204211
}
205212
}
@@ -486,6 +493,14 @@ mod tests {
486493
}
487494
}
488495

496+
#[test]
497+
#[should_panic(expected = "duplicate subcommand name: clone")]
498+
fn duplicate_subcommand_registration_panics() {
499+
let _ = Launcher::<GitApp>::of()
500+
.command::<CloneCmd>("clone")
501+
.command::<CloneCmd>("clone");
502+
}
503+
489504
#[test]
490505
fn compose_help_schema_prefixes_root_name_and_options() {
491506
let root = CommandSchema::new("git", "").flag("-v,--verbose", "Verbose");

runi-cli/src/launcher/help.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,15 @@ impl HelpPrinter {
5252
out
5353
}
5454

55-
/// Print help text to stderr.
55+
/// Print help text to stdout. Use this for user-requested help
56+
/// (`--help`) so output can be piped or redirected normally.
5657
pub fn print(schema: &CommandSchema) {
58+
print!("{}", Self::format(schema));
59+
}
60+
61+
/// Print help text to stderr. Use this alongside an error message so
62+
/// both are grouped on the same stream.
63+
pub fn print_error(schema: &CommandSchema) {
5764
eprint!("{}", Self::format(schema));
5865
}
5966
}

0 commit comments

Comments
 (0)