diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 49ab386..d76a94d 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -796,6 +796,7 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,]; /// `parse_positionals`: Helper to parse positional arguments. /// `parse_subcommand`: Helper to parse a subcommand. /// `help_func`: Generate a help message. +/// `complete_func`: Generates all possible subcommands for tab completion. #[doc(hidden)] pub fn parse_struct_args( cmd_name: &[&str], @@ -804,8 +805,10 @@ pub fn parse_struct_args( mut parse_positionals: ParseStructPositionals<'_>, mut parse_subcommand: Option>, help_func: &dyn Fn() -> String, + complete_func: &dyn Fn() -> String, ) -> Result<(), EarlyExit> { let mut help = false; + let mut complete = false; let mut remaining_args = args; let mut positional_index = 0; let mut options_ended = false; @@ -817,6 +820,11 @@ pub fn parse_struct_args( continue; } + if (next_arg == "--complete" || next_arg == "complete") && !options_ended { + complete = true; + continue; + } + if next_arg.starts_with('-') && !options_ended { if next_arg == "--" { options_ended = true; @@ -827,6 +835,10 @@ pub fn parse_struct_args( return Err("Trailing arguments are not allowed after `help`.".to_string().into()); } + if complete { + return Err("Trailing arguments are not allowed after `complete`.".to_string().into()); + } + parse_options.parse(next_arg, &mut remaining_args)?; continue; } @@ -842,8 +854,11 @@ pub fn parse_struct_args( parse_positionals.parse(&mut positional_index, next_arg)?; } + // Prioritize a `help` request over a `complete` request. if help { Err(EarlyExit { output: help_func(), status: Ok(()) }) + } else if complete { + Err(EarlyExit { output: complete_func(), status: Ok(()) }) } else { Ok(()) } @@ -1025,6 +1040,16 @@ pub fn print_subcommands<'a>(commands: impl Iterator) -> out } +#[doc(hidden)] +pub fn print_subcommand_list<'a>(commands: impl Iterator) -> String { + let mut out = String::new(); + for cmd in commands { + out.push_str(cmd.name); + out.push('\n'); + } + out.trim().to_string() +} + fn unrecognized_arg(arg: &str) -> String { ["Unrecognized argument: ", arg, "\n"].concat() } diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index a208773..af94223 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -121,6 +121,39 @@ pub(crate) fn help( } } } +/// Returns a `TokenStream` generating a `String` list of subcommands. +pub(crate) fn complete( + subcommand: Option<&StructField<'_>>, +) -> TokenStream { + let mut format_lit = "".to_string(); + let subcommand_calculation; + let subcommand_format_arg; + if let Some(subcommand) = subcommand { + format_lit.push_str("{subcommands}"); + let subcommand_ty = subcommand.ty_without_wrapper; + subcommand_format_arg = quote! { subcommands = subcommands }; + subcommand_calculation = quote! { + let subcommands = argh::print_subcommand_list( + <#subcommand_ty as argh::SubCommands>::COMMANDS + .iter() + .copied() + .chain( + <#subcommand_ty as argh::SubCommands>::dynamic_commands() + .iter() + .copied()) + ); + }; + } else { + subcommand_calculation = TokenStream::new(); + subcommand_format_arg = TokenStream::new() + } + + quote! { { + #subcommand_calculation + format!(#format_lit, #subcommand_format_arg) + } } +} + /// A section composed of exactly just the literals provided to the program. fn lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr]) { if !lits.is_empty() { diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index c35a269..e6217d5 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -321,6 +321,9 @@ fn impl_from_args_struct_from_args<'a>( let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); + // Generate all subcommands of the current command to assist with auto-completion in the shell. + let complete = help::complete(subcommand); + let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result @@ -349,6 +352,7 @@ fn impl_from_args_struct_from_args<'a>( }, #parse_subcommands, &|| #help, + &|| #complete )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -436,6 +440,9 @@ fn impl_from_args_struct_redact_arg_values<'a>( let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); + // Generate all subcommands of the current command to assist with auto-completion in the shell. + let complete = help::complete(subcommand); + let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result, argh::EarlyExit> { #( #init_fields )* @@ -460,6 +467,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( }, #redact_subcommands, &|| #help, + &|| #complete, )?; let mut #missing_requirements_ident = argh::MissingRequirements::default();