Add support for multi-character flag aliases and enhance argument handling

- Introduced `MULTI_CHAR_ALIASES` for mapping long flag names to multi-character short aliases.
- Updated argument normalization to preprocess multi-character short-form flags.
- Refactored help output to include alias visibility and improve alignment.
- Enhanced CLI flag definition capabilities with better support for optional boolean arguments.
This commit is contained in:
2026-08-11 15:56:33 +02:00
parent b9b9986833
commit 45f2f13970
2 changed files with 45 additions and 19 deletions

View File

@@ -71,12 +71,12 @@ pub struct EditArgs {
pub name: String,
/// Enable or disable the self-monitor loopback.
#[arg(short, long, value_name = "BOOL")]
#[arg(short, long, num_args = 0..=1, default_missing_value = "true", value_name = "BOOL")]
pub loopback: Option<bool>,
/// Keep a mixed-in source audible in the loopback instead of isolating
/// it (2-node topology instead of 4).
#[arg(long = "loopback-no-mix", value_name = "BOOL")]
#[arg(long = "loopback-no-mix", num_args = 0..=1, default_missing_value = "true", value_name = "BOOL")]
pub loopback_no_mix: Option<bool>,
/// Loopback volume: fraction (0.8) or percent (80).
@@ -84,7 +84,7 @@ pub struct EditArgs {
pub volume: Option<f32>,
/// Mixed-in source volume: fraction (0.8) or percent (80).
#[arg(long = "source-volume", visible_alias = "sv", value_name = "PCT")]
#[arg(long = "source-volume", value_name = "PCT")]
pub source_volume: Option<f32>,
}

View File

@@ -44,12 +44,22 @@ fn main() {
}
}
/// clap's `#[arg(short)]` only supports single-character short flags, so
/// `-sv` can't be a real short flag. Rewrite it to `--source-volume` before
/// clap ever sees it.
/// `(long name, multi-char alias)` pairs for flags whose short form is more
/// than one character - clap's `#[arg(short)]` only supports single
/// characters, so these can't be real clap short flags. Rewritten to their
/// long form before clap ever sees them (`normalize_args`), and shown
/// alongside the long form in the top-level help (`flag_names`) so both
/// places stay in sync from one definition.
const MULTI_CHAR_ALIASES: &[(&str, &str)] = &[("source-volume", "sv")];
fn normalize_args(args: Vec<String>) -> Vec<String> {
args.into_iter()
.map(|a| if a == "-sv" { "--source-volume".to_string() } else { a })
.map(|a| {
a.strip_prefix('-')
.and_then(|rest| MULTI_CHAR_ALIASES.iter().find(|(_, alias)| *alias == rest))
.map(|(long, _)| format!("--{long}"))
.unwrap_or(a)
})
.collect()
}
@@ -100,13 +110,8 @@ fn print_full_help() {
let name = s.get_name();
let positionals = positional_args(s);
let about = s.get_about().map(|a| a.to_string()).unwrap_or_default();
let aliases = s.get_visible_aliases().collect::<Vec<_>>().join("/");
print!(" {name:name_w$} {} {about}", pad_visual(&positionals, pos_w));
if !aliases.is_empty() {
print!(" | {aliases}");
}
println!();
println!(" {name:name_w$} {} {about}", pad_visual(&positionals, pos_w));
let rows = &flag_rows_by_cmd[i];
for (flag, help) in rows {
@@ -120,6 +125,22 @@ fn print_full_help() {
}
println!();
let alias_rows: Vec<(String, String)> = subcommands
.iter()
.filter_map(|s| {
let aliases = s.get_visible_aliases().collect::<Vec<_>>().join(", ");
(!aliases.is_empty()).then(|| (s.get_name().to_string(), aliases))
})
.collect();
if !alias_rows.is_empty() {
println!("{}", ui::blue("Aliases:"));
let name_w = alias_rows.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (name, aliases) in &alias_rows {
println!(" {name:name_w$} {aliases}");
}
println!();
}
println!("{}", ui::blue("Options:"));
let opt_rows: Vec<(String, String)> = cmd
.get_arguments()
@@ -167,13 +188,18 @@ fn flag_rows(cmd: &clap::Command) -> Vec<(String, String)> {
}
/// `-x/--long` / `-x` / `--long` for a non-positional arg, or `None` for
/// one with no visible flag at all.
/// one with no visible flag at all. A long flag with an entry in
/// `MULTI_CHAR_ALIASES` shows that alias in the short-flag position (e.g.
/// `-sv/--source-volume`), since it works the same way in practice.
fn flag_names(arg: &clap::Arg) -> Option<String> {
match (arg.get_short(), arg.get_long()) {
(Some(s), Some(l)) => Some(format!("-{s}/--{l}")),
(Some(s), None) => Some(format!("-{s}")),
(None, Some(l)) => Some(format!("--{l}")),
(None, None) => None,
let long = arg.get_long();
let multi_char_alias = long.and_then(|l| MULTI_CHAR_ALIASES.iter().find(|(name, _)| *name == l)).map(|(_, a)| *a);
match (arg.get_short(), long, multi_char_alias) {
(Some(s), Some(l), _) => Some(format!("-{s}/--{l}")),
(Some(s), None, _) => Some(format!("-{s}")),
(None, Some(l), Some(a)) => Some(format!("-{a}/--{l}")),
(None, Some(l), None) => Some(format!("--{l}")),
(None, None, _) => None,
}
}