diff --git a/argh/examples/simple_example.rs b/argh/examples/simple_example.rs index f1e29b4..13f3a6e 100644 --- a/argh/examples/simple_example.rs +++ b/argh/examples/simple_example.rs @@ -20,7 +20,7 @@ enum MySubCommandEnum { #[derive(FromArgs, PartialEq, Debug)] /// First subcommand. -#[argh(subcommand, name = "one")] +#[argh(subcommand, name = "one", short = 'o')] struct SubCommandOne { #[argh(option)] /// how many x diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 3c73473..ea6e46d 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -276,6 +276,7 @@ //! // don't know about until runtime! //! commands.push(&*Box::leak(Box::new(CommandInfo { //! name: "dynamic_command", +//! short: &'d', //! description: "A dynamic command", //! }))); //! @@ -1152,7 +1153,9 @@ impl ParseStructSubCommand<'_> { remaining_args: &[&str], ) -> Result { for subcommand in self.subcommands.iter().chain(self.dynamic_subcommands.iter()) { - if subcommand.name == arg { + if subcommand.name == arg + || arg.chars().count() == 1 && arg.chars().next().unwrap() == *subcommand.short + { let mut command = cmd_name.to_owned(); command.push(subcommand.name); let prepended_help; diff --git a/argh/tests/args_info_tests.rs b/argh/tests/args_info_tests.rs index 8ceb0ea..a1b458d 100644 --- a/argh/tests/args_info_tests.rs +++ b/argh/tests/args_info_tests.rs @@ -59,6 +59,7 @@ fn args_info_test_subcommand() { let command_one = CommandInfoWithArgs { name: "one", + short: &'\0', description: "First subcommand.", flags: &[ HELP_FLAG, @@ -76,6 +77,7 @@ fn args_info_test_subcommand() { assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", + short: &'\0', description: "Top-level command.", examples: &[], flags: &[HELP_FLAG], @@ -88,6 +90,7 @@ fn args_info_test_subcommand() { name: "two", command: CommandInfoWithArgs { name: "two", + short: &'\0', description: "Second subcommand.", flags: &[ HELP_FLAG, @@ -124,6 +127,7 @@ fn args_info_test_multiline_doc_comment() { assert_args_info::( &CommandInfoWithArgs { name: "Cmd", + short: &'\0', description: "Short description", flags: &[HELP_FLAG, FlagInfo { @@ -163,6 +167,7 @@ fn args_info_test_basic_args() { } assert_args_info::(&CommandInfoWithArgs { name: "Basic", + short: &'\0', description: "Basic command args demonstrating multiple types and cardinality. \"With quotes\"", flags: &[ @@ -231,6 +236,7 @@ fn args_info_test_positional_args() { } assert_args_info::(&CommandInfoWithArgs { name: "Positional", + short: &'\0', description: "Command with positional args demonstrating. \"With quotes\"", flags: &[HELP_FLAG], positionals: &[ @@ -278,6 +284,7 @@ fn args_info_test_optional_positional_args() { } assert_args_info::(&CommandInfoWithArgs { name: "Positional", + short: &'\0', description: "Command with positional args demonstrating last value is optional", flags: &[FlagInfo { kind: FlagInfoKind::Switch, @@ -332,6 +339,7 @@ fn args_info_test_default_positional_args() { } assert_args_info::(&CommandInfoWithArgs { name: "Positional", + short: &'\0', description: "Command with positional args demonstrating last value is defaulted.", flags: &[HELP_FLAG], positionals: &[ @@ -393,6 +401,7 @@ fn args_info_test_notes_examples_errors() { assert_args_info::( &CommandInfoWithArgs { name: "NotesExamplesErrors", + short: &'\0', description: "Command with Examples and usage Notes, including error codes.", examples: &["\n Use the command with 1 file:\n `{command_name} /path/to/file`\n Use it with a \"wildcard\":\n `{command_name} /path/to/*`\n a blank line\n \n and one last line with \"quoted text\"."], flags: &[HELP_FLAG @@ -481,6 +490,7 @@ fn args_info_test_subcommands() { assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", + short: &'\0', description: "Top level command with \"subcommands\".", flags: &[ HELP_FLAG, @@ -499,6 +509,7 @@ fn args_info_test_subcommands() { name: "one", command: CommandInfoWithArgs { name: "one", + short: &'\0', description: "Command1 args are used for Command1.", flags: &[HELP_FLAG], positionals: &[ @@ -528,6 +539,7 @@ fn args_info_test_subcommands() { name: "two", command: CommandInfoWithArgs { name: "two", + short: &'\0', description: "Command2 args are used for Command2.", flags: &[ HELP_FLAG, @@ -573,6 +585,7 @@ fn args_info_test_subcommands() { name: "three", command: CommandInfoWithArgs { name: "three", + short: &'\0', description: "Command3 args are used for Command3 which has no options or arguments.", flags: &[HELP_FLAG], @@ -635,6 +648,7 @@ fn args_info_test_subcommand_notes_examples() { let command_one = CommandInfoWithArgs { name: "one", + short: &'\0', description: "Command1 args are used for subcommand one.", error_codes: &[ErrorCodeInfo { code: 0, description: "one level success" }], examples: &["\"Typical\" usage is `{command_name}`."], @@ -665,6 +679,7 @@ fn args_info_test_subcommand_notes_examples() { assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", + short: &'\0', description: "Top level command with \"subcommands\".", error_codes: &[ErrorCodeInfo { code: 0, description: "Top level success" }], examples: &["Top level example"], @@ -735,6 +750,7 @@ fn args_info_test_example() { assert_args_info::( &CommandInfoWithArgs { name: "HelpExample", + short: &'\0', description: "Destroy the contents of with a specific \"method of destruction\".", examples: &["Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp"], flags: &[HELP_FLAG, @@ -752,6 +768,7 @@ fn args_info_test_example() { commands: vec![ SubCommandInfo { name: "blow-up", command: CommandInfoWithArgs { name: "blow-up", + short: &'\0', description: "explosively separate", flags:& [HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--safely", short: None, description: "blow up bombs safely", @@ -796,6 +813,7 @@ fn positional_greedy() { } assert_args_info::(&CommandInfoWithArgs { name: "LastRepeatingGreedy", + short: &'\0', description: "Woot", flags: &[ HELP_FLAG, @@ -892,9 +910,9 @@ fn test_dynamic_subcommand() { impl argh::DynamicSubCommand for DynamicSubCommandImpl { fn commands() -> &'static [&'static argh::CommandInfo] { &[ - &argh::CommandInfo { name: "three", description: "Third command" }, - &argh::CommandInfo { name: "four", description: "Fourth command" }, - &argh::CommandInfo { name: "five", description: "Fifth command" }, + &argh::CommandInfo { name: "three", short: &'\0', description: "Third command" }, + &argh::CommandInfo { name: "four", short: &'\0', description: "Fourth command" }, + &argh::CommandInfo { name: "five", short: &'\0', description: "Fifth command" }, ] } @@ -960,6 +978,7 @@ fn test_dynamic_subcommand() { assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", + short: &'\0', description: "Top-level command.", flags: &[HELP_FLAG], commands: vec![ @@ -967,6 +986,7 @@ fn test_dynamic_subcommand() { name: "one", command: CommandInfoWithArgs { name: "one", + short: &'\0', description: "First subcommand.", flags: &[ HELP_FLAG, @@ -986,6 +1006,7 @@ fn test_dynamic_subcommand() { name: "two", command: CommandInfoWithArgs { name: "two", + short: &'\0', description: "Second subcommand.", flags: &[ HELP_FLAG, @@ -1005,6 +1026,7 @@ fn test_dynamic_subcommand() { name: "three", command: CommandInfoWithArgs { name: "three", + short: &'\0', description: "Third command", ..Default::default() }, @@ -1013,6 +1035,7 @@ fn test_dynamic_subcommand() { name: "four", command: CommandInfoWithArgs { name: "four", + short: &'\0', description: "Fourth command", ..Default::default() }, @@ -1021,6 +1044,7 @@ fn test_dynamic_subcommand() { name: "five", command: CommandInfoWithArgs { name: "five", + short: &'\0', description: "Fifth command", ..Default::default() }, diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index c1b6c98..88007ee 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -201,9 +201,9 @@ fn dynamic_subcommand_example() { impl argh::DynamicSubCommand for DynamicSubCommandImpl { fn commands() -> &'static [&'static argh::CommandInfo] { &[ - &argh::CommandInfo { name: "three", description: "Third command" }, - &argh::CommandInfo { name: "four", description: "Fourth command" }, - &argh::CommandInfo { name: "five", description: "Fifth command" }, + &argh::CommandInfo { name: "three", short: &'\0', description: "Third command" }, + &argh::CommandInfo { name: "four", short: &'\0', description: "Fourth command" }, + &argh::CommandInfo { name: "five", short: &'\0', description: "Fifth command" }, ] } @@ -1280,7 +1280,11 @@ Options: impl argh::DynamicSubCommand for HelpExamplePlugin { fn commands() -> &'static [&'static argh::CommandInfo] { - &[&argh::CommandInfo { name: "plugin", description: "Example dynamic command" }] + &[&argh::CommandInfo { + name: "plugin", + short: &'\0', + description: "Example dynamic command", + }] } fn try_redact_arg_values( @@ -1848,6 +1852,7 @@ fn subcommand_does_not_panic() { #[argh(subcommand)] enum SubCommandEnum { Cmd(SubCommand), + CmdTwo(SubCommandTwo), } #[derive(FromArgs, PartialEq, Debug)] diff --git a/argh/tests/short_alias.rs b/argh/tests/short_alias.rs new file mode 100644 index 0000000..ab6ec0a --- /dev/null +++ b/argh/tests/short_alias.rs @@ -0,0 +1,69 @@ +use argh::FromArgs; + +#[derive(FromArgs, PartialEq, Debug)] +/// Top-level command. +struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), +} + +#[derive(FromArgs, PartialEq, Debug)] +/// First subcommand. +#[argh(subcommand, name = "one", short = 'o')] +struct SubCommandOne { + #[argh(switch)] + /// fooey + fooey: bool, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Second subcommand. +#[argh(subcommand, name = "two")] +// No short alias for this one +struct SubCommandTwo { + #[argh(switch)] + /// bar + bar: bool, +} + +#[test] +fn test_short_alias_dispatch() { + let expected = TopLevel { nested: MySubCommandEnum::One(SubCommandOne { fooey: true }) }; + + // Test with full name "one" + let actual = TopLevel::from_args(&["cmd"], &["one", "--fooey"]).expect("failed parsing 'one'"); + assert_eq!(actual, expected); + + // Test with short alias "o" + let actual_short = + TopLevel::from_args(&["cmd"], &["o", "--fooey"]).expect("failed parsing 'o'"); + assert_eq!(actual_short, expected); +} + +#[test] +fn test_short_alias_redaction() { + // Verify that redaction also works with short aliases + let args = vec!["o", "--fooey"]; + let redacted = TopLevel::redact_arg_values(&["cmd"], &args).expect("redaction failed"); + // Since it's a switch, it might be kept or redacted depending on impl, but we check matching. + // Redaction usually returns the args with values redacted. For switch there are no values. + // We mainly want to ensure it doesn't error with "no subcommand matched". + assert!(!redacted.is_empty()); +} + +#[test] +fn test_no_short_alias_for_two() { + // "two" has no short alias, so "t" should fail (unless "t" is prefix matching? argh doesn't do prefix matching for subcommands by default I think, but let's see) + // Actually argh requires exact match for subcommands unless strict is disabled? + // Let's assume strict. + + let res = TopLevel::from_args(&["cmd"], &["t", "--bar"]); + assert!(res.is_err()); +} diff --git a/argh/tests/ui/duplicate-name/duplicate-long-name.stderr b/argh/tests/ui/duplicate-name/duplicate-long-name.stderr index 697f36b..baec112 100644 --- a/argh/tests/ui/duplicate-name/duplicate-long-name.stderr +++ b/argh/tests/ui/duplicate-name/duplicate-long-name.stderr @@ -9,8 +9,8 @@ error: The long name of "--foo" was already used here. error: Later usage here. --> tests/ui/duplicate-name/duplicate-long-name.rs:8:5 | -8 | / /// foo2 -9 | | #[argh(option, long = "foo")] + 8 | / /// foo2 + 9 | | #[argh(option, long = "foo")] 10 | | foo2: u32, | |_____________^ diff --git a/argh/tests/ui/duplicate-name/duplicate-short-name.stderr b/argh/tests/ui/duplicate-name/duplicate-short-name.stderr index 9262a7d..8c5944c 100644 --- a/argh/tests/ui/duplicate-name/duplicate-short-name.stderr +++ b/argh/tests/ui/duplicate-name/duplicate-short-name.stderr @@ -9,8 +9,8 @@ error: The short name of "-f" was already used here. error: Later usage here. --> tests/ui/duplicate-name/duplicate-short-name.rs:8:5 | -8 | / /// foo2 -9 | | #[argh(option, short = 'f')] + 8 | / /// foo2 + 9 | | #[argh(option, short = 'f')] 10 | | foo2: u32, | |_____________^ diff --git a/argh_derive/src/args_info.rs b/argh_derive/src/args_info.rs index 969acf0..7e5a0dd 100644 --- a/argh_derive/src/args_info.rs +++ b/argh_derive/src/args_info.rs @@ -148,6 +148,7 @@ fn impl_arg_info_enum( name: s.name, command: CommandInfoWithArgs { name: s.name, + short: s.short, description: s.description, ..Default::default() } @@ -173,6 +174,12 @@ fn impl_arg_info_enum( LitStr::new("", Span::call_site()) }; + let short_name = if let Some(id) = &type_attrs.short { + quote! { &#id } + } else { + quote! { &'\0' } + }; + let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); quote! { @@ -188,6 +195,7 @@ fn impl_arg_info_enum( argh::CommandInfoWithArgs { name: #cmd_name, + short: #short_name, /// A short description of the command's functionality. description: " enum of subcommands", commands: the_subcommands, @@ -345,9 +353,16 @@ fn impl_args_info_data<'a>( quote! { argh::ErrorCodeInfo{code:#code, description: #text} } }); + let short_name = if let Some(id) = &type_attrs.short { + quote! { &#id } + } else { + quote! { &'\0' } + }; + quote_spanned! { impl_span => argh::CommandInfoWithArgs { name: #subcommand_name, + short: #short_name, description: #description, examples: &[#( #examples, )*], notes: &[#( #notes, )*], diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index 99798b7..aef5d3c 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -264,7 +264,7 @@ fn positional_description(out: &mut String, field: &StructField<'_>) { } fn positional_description_format(out: &mut String, name: &str, description: &str) { - let info = argh_shared::CommandInfo { name, description }; + let info = argh_shared::CommandInfo { name, description, short: &'\0' }; argh_shared::write_description(out, &info); } @@ -294,6 +294,6 @@ fn option_description_format( } name.push_str(long_with_leading_dashes); - let info = argh_shared::CommandInfo { name: &name, description }; + let info = argh_shared::CommandInfo { name: &name, description, short: &'\0' }; argh_shared::write_description(out, &info); } diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index e9fbde5..ff439d0 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -689,11 +689,14 @@ fn top_or_sub_cmd_impl( errors.err(name, "`#[argh(name = \"...\")]` attribute is required for subcommands"); &empty_str }); + let short_name = + type_attrs.short.as_ref().map(|c| quote! { &#c }).unwrap_or_else(|| quote! { &'\0' }); quote! { #[automatically_derived] impl #impl_generics argh::SubCommand for #name #ty_generics #where_clause { const COMMAND: &'static argh::CommandInfo = &argh::CommandInfo { name: #subcommand_name, + short: #short_name, description: #description, }; } @@ -1110,7 +1113,11 @@ fn impl_from_args_enum( }; #( - if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name { + if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name + || (*<#variant_ty as argh::SubCommand>::COMMAND.short != '\0' + && subcommand_name.len() == 1 + && subcommand_name.starts_with(*<#variant_ty as argh::SubCommand>::COMMAND.short)) + { return ::core::result::Result::Ok(#name_repeating::#variant_names( <#variant_ty as argh::FromArgs>::from_args(command_name, args)? )); @@ -1130,7 +1137,11 @@ fn impl_from_args_enum( }; #( - if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name { + if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name + || (*<#variant_ty as argh::SubCommand>::COMMAND.short != '\0' + && subcommand_name.len() == 1 + && subcommand_name.starts_with(*<#variant_ty as argh::SubCommand>::COMMAND.short)) + { return <#variant_ty as argh::FromArgs>::redact_arg_values(command_name, args); } )* diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs index cd192db..ca7c1c0 100644 --- a/argh_derive/src/parse_attrs.rs +++ b/argh_derive/src/parse_attrs.rs @@ -277,6 +277,7 @@ pub fn has_argh_attrs(attrs: &[syn::Attribute]) -> bool { pub struct TypeAttrs { pub is_subcommand: Option, pub name: Option, + pub short: Option, pub description: Option, pub examples: Vec, pub notes: Vec, @@ -321,6 +322,10 @@ impl TypeAttrs { if let Some(m) = errors.expect_meta_name_value(&meta) { this.parse_attr_name(errors, m); } + } else if name.is_ident("short") { + if let Some(m) = errors.expect_meta_name_value(&meta) { + this.parse_attr_short(errors, m); + } } else if name.is_ident("note") { if let Some(m) = errors.expect_meta_name_value(&meta) { this.parse_attr_note(errors, m); @@ -344,7 +349,7 @@ impl TypeAttrs { concat!( "Invalid type-level `argh` attribute\n", "Expected one of: `description`, `error_code`, `example`, `name`, ", - "`note`, `subcommand`, `usage`", + "`note`, `short`, `subcommand`, `usage`", ), ); } @@ -415,6 +420,17 @@ impl TypeAttrs { } } + fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) { + if let Some(first) = &self.short { + errors.duplicate_attrs("short", first, m); + } else if let Some(lit_char) = errors.expect_lit_char(&m.value) { + self.short = Some(lit_char.clone()); + if !lit_char.value().is_ascii() { + errors.err(lit_char, "Short names must be ASCII"); + } + } + } + fn parse_attr_note(&mut self, errors: &Errors, m: &syn::MetaNameValue) { parse_attr_multi_string(errors, m, &mut self.notes) } @@ -691,6 +707,7 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: let TypeAttrs { is_subcommand, name, + short, description, examples, notes, @@ -715,6 +732,9 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: if let Some(name) = name { err_unused_enum_attr(errors, name); } + if let Some(short) = short { + err_unused_enum_attr(errors, short); + } if let Some(description) = description { if description.explicit { err_unused_enum_attr(errors, &description.content); diff --git a/argh_shared/src/lib.rs b/argh_shared/src/lib.rs index 4e3f5f8..4eb974f 100644 --- a/argh_shared/src/lib.rs +++ b/argh_shared/src/lib.rs @@ -10,16 +10,26 @@ pub struct CommandInfo<'a> { /// The name of the command. pub name: &'a str, + /// A short name for the command (alias). + pub short: &'a char, /// A short description of the command's functionality. pub description: &'a str, } +impl<'a> Default for CommandInfo<'a> { + fn default() -> Self { + Self { name: Default::default(), short: &'\0', description: Default::default() } + } +} + /// Information about the command line arguments for a given command. -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct CommandInfoWithArgs<'a> { /// The name of the command. pub name: &'a str, + /// A short name for the command (alias). + pub short: &'a char, /// A short description of the command's functionality. pub description: &'a str, /// Examples of usage @@ -36,6 +46,22 @@ pub struct CommandInfoWithArgs<'a> { pub error_codes: &'a [ErrorCodeInfo<'a>], } +impl<'a> Default for CommandInfoWithArgs<'a> { + fn default() -> Self { + Self { + name: Default::default(), + short: &'\0', + description: Default::default(), + examples: Default::default(), + flags: Default::default(), + notes: Default::default(), + commands: Default::default(), + positionals: Default::default(), + error_codes: Default::default(), + } + } +} + /// Information about a documented error code. #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize))]