#![cfg(test)] // Copyright (c) 2023 Google LLC All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. use argh::{ ArgsInfo, CommandInfoWithArgs, ErrorCodeInfo, FlagInfo, FlagInfoKind, FromArgs, Optionality, PositionalInfo, SubCommandInfo, }; fn assert_args_info(expected: &CommandInfoWithArgs) { let actual_value = T::get_args_info(); assert_eq!(expected, &actual_value) } const HELP_FLAG: FlagInfo<'_> = FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--help", short: None, description: "display usage information", hidden: false, }; /// Tests that exercise the JSON output for help text. #[test] fn args_info_test_subcommand() { #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] /// Top-level command. struct TopLevel { #[argh(subcommand)] nested: MySubCommandEnum, } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] #[argh(subcommand)] enum MySubCommandEnum { One(SubCommandOne), Two(SubCommandTwo), } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] /// First subcommand. #[argh(subcommand, name = "one")] struct SubCommandOne { #[argh(option)] /// how many x x: usize, } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] /// Second subcommand. #[argh(subcommand, name = "two")] struct SubCommandTwo { #[argh(switch)] /// whether to fooey fooey: bool, } let command_one = CommandInfoWithArgs { name: "one", description: "First subcommand.", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Option { arg_name: "x" }, optionality: Optionality::Required, long: "--x", short: None, description: "how many x", hidden: false, }, ], ..Default::default() }; assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", description: "Top-level command.", examples: &[], flags: &[HELP_FLAG], notes: &[], positionals: &[], error_codes: &[], commands: vec![ SubCommandInfo { name: "one", command: command_one.clone() }, SubCommandInfo { name: "two", command: CommandInfoWithArgs { name: "two", description: "Second subcommand.", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--fooey", short: None, description: "whether to fooey", hidden: false, }, ], ..Default::default() }, }, ], }); assert_args_info::(&command_one); } #[test] fn args_info_test_multiline_doc_comment() { #[derive(FromArgs, ArgsInfo)] /// Short description struct Cmd { #[argh(switch)] /// a switch with a description /// that is spread across /// a number of /// lines of comments. _s: bool, } assert_args_info::( &CommandInfoWithArgs { name: "Cmd", description: "Short description", flags: &[HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--s", short: None, description: "a switch with a description that is spread across a number of lines of comments.", hidden:false } ], ..Default::default() }); } #[test] fn args_info_test_basic_args() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] /// Basic command args demonstrating multiple types and cardinality. "With quotes" struct Basic { /// should the power be on. "Quoted value" should work too. #[argh(switch)] power: bool, /// option that is required because of no default and not Option<>. #[argh(option, long = "required")] required_flag: String, /// optional speed if not specified it is None. #[argh(option, short = 's')] speed: Option, /// repeatable option. #[argh(option, arg_name = "url")] link: Vec, } assert_args_info::(&CommandInfoWithArgs { name: "Basic", description: "Basic command args demonstrating multiple types and cardinality. \"With quotes\"", flags: &[ FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--help", short: None, description: "display usage information", hidden: false, }, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--power", short: None, description: "should the power be on. \"Quoted value\" should work too.", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "required" }, optionality: Optionality::Required, long: "--required", short: None, description: "option that is required because of no default and not Option<>.", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "speed" }, optionality: Optionality::Optional, long: "--speed", short: Some('s'), description: "optional speed if not specified it is None.", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "url" }, optionality: Optionality::Repeating, long: "--link", short: None, description: "repeatable option.", hidden: false, }, ], ..Default::default() }); } #[test] fn args_info_test_positional_args() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] /// Command with positional args demonstrating. "With quotes" struct Positional { /// the "root" position. #[argh(positional, arg_name = "root")] root_value: String, /// trunk value #[argh(positional)] trunk: String, /// leaves. There can be many leaves. #[argh(positional)] leaves: Vec, } assert_args_info::(&CommandInfoWithArgs { name: "Positional", description: "Command with positional args demonstrating. \"With quotes\"", flags: &[HELP_FLAG], positionals: &[ PositionalInfo { name: "root", description: "the \"root\" position.", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "trunk", description: "trunk value", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "leaves", description: "leaves. There can be many leaves.", optionality: Optionality::Repeating, hidden: false, }, ], ..Default::default() }); } #[test] fn args_info_test_optional_positional_args() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] /// Command with positional args demonstrating last value is optional struct Positional { /// the "root" position. #[argh(positional, arg_name = "root")] root_value: String, /// trunk value #[argh(positional)] trunk: String, /// leaves. There can be an optional leaves. #[argh(positional)] leaves: Option, } assert_args_info::(&CommandInfoWithArgs { name: "Positional", description: "Command with positional args demonstrating last value is optional", flags: &[FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--help", short: None, description: "display usage information", hidden: false, }], positionals: &[ PositionalInfo { name: "root", description: "the \"root\" position.", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "trunk", description: "trunk value", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "leaves", description: "leaves. There can be an optional leaves.", optionality: Optionality::Optional, hidden: false, }, ], ..Default::default() }); } #[test] fn args_info_test_default_positional_args() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] /// Command with positional args demonstrating last value is defaulted. struct Positional { /// the "root" position. #[argh(positional, arg_name = "root")] root_value: String, /// trunk value #[argh(positional)] trunk: String, /// leaves. There can be one leaf, defaults to hello. #[argh(positional, default = "String::from(\"hello\")")] leaves: String, } assert_args_info::(&CommandInfoWithArgs { name: "Positional", description: "Command with positional args demonstrating last value is defaulted.", flags: &[HELP_FLAG], positionals: &[ PositionalInfo { name: "root", description: "the \"root\" position.", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "trunk", description: "trunk value", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "leaves", description: "leaves. There can be one leaf, defaults to hello.", optionality: Optionality::Optional, hidden: false, }, ], ..Default::default() }); } #[test] fn args_info_test_notes_examples_errors() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] /// Command with Examples and usage Notes, including error codes. #[argh( note = r##" These usage notes appear for {command_name} and how to best use it. The formatting should be preserved. one two three then a blank and one last line with "quoted text"."##, example = r##" Use the command with 1 file: `{command_name} /path/to/file` Use it with a "wildcard": `{command_name} /path/to/*` a blank line and one last line with "quoted text"."##, error_code(0, "Success"), error_code(1, "General Error"), error_code(2, "Some error with \"quotes\"") )] struct NotesExamplesErrors { /// the "root" position. #[argh(positional, arg_name = "files")] fields: Vec, } assert_args_info::( &CommandInfoWithArgs { name: "NotesExamplesErrors", 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 ], positionals: &[ PositionalInfo{ name: "files", description: "the \"root\" position.", optionality: Optionality::Repeating, hidden:false } ], notes: &["\n These usage notes appear for {command_name} and how to best use it.\n The formatting should be preserved.\n one\n two\n three then a blank\n \n and one last line with \"quoted text\"."], error_codes: & [ErrorCodeInfo { code: 0, description: "Success" }, ErrorCodeInfo { code: 1, description: "General Error" }, ErrorCodeInfo { code: 2, description: "Some error with \"quotes\"" }], ..Default::default() }); } #[test] fn args_info_test_subcommands() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] ///Top level command with "subcommands". struct TopLevel { /// show verbose output #[argh(switch)] verbose: bool, /// this doc comment does not appear anywhere. #[argh(subcommand)] cmd: SubcommandEnum, } #[derive(FromArgs, ArgsInfo)] #[argh(subcommand)] /// Doc comments for subcommand enums does not appear in the help text. enum SubcommandEnum { Command1(Command1Args), Command2(Command2Args), Command3(Command3Args), } /// Command1 args are used for Command1. #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] #[argh(subcommand, name = "one")] struct Command1Args { /// the "root" position. #[argh(positional, arg_name = "root")] root_value: String, /// trunk value #[argh(positional)] trunk: String, /// leaves. There can be zero leaves, defaults to hello. #[argh(positional, default = "String::from(\"hello\")")] leaves: String, } /// Command2 args are used for Command2. #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] #[argh(subcommand, name = "two")] struct Command2Args { /// should the power be on. "Quoted value" should work too. #[argh(switch)] power: bool, /// option that is required because of no default and not Option<>. #[argh(option, long = "required")] required_flag: String, /// optional speed if not specified it is None. #[argh(option, short = 's')] speed: Option, /// repeatable option. #[argh(option, arg_name = "url")] link: Vec, } /// Command3 args are used for Command3 which has no options or arguments. #[derive(FromArgs, ArgsInfo)] #[argh(subcommand, name = "three")] struct Command3Args {} assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", description: "Top level command with \"subcommands\".", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--verbose", short: None, description: "show verbose output", hidden: false, }, ], positionals: &[], commands: vec![ SubCommandInfo { name: "one", command: CommandInfoWithArgs { name: "one", description: "Command1 args are used for Command1.", flags: &[HELP_FLAG], positionals: &[ PositionalInfo { name: "root", description: "the \"root\" position.", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "trunk", description: "trunk value", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "leaves", description: "leaves. There can be zero leaves, defaults to hello.", optionality: Optionality::Optional, hidden: false, }, ], ..Default::default() }, }, SubCommandInfo { name: "two", command: CommandInfoWithArgs { name: "two", description: "Command2 args are used for Command2.", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--power", short: None, description: "should the power be on. \"Quoted value\" should work too.", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "required" }, optionality: Optionality::Required, long: "--required", short: None, description: "option that is required because of no default and not Option<>.", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "speed" }, optionality: Optionality::Optional, long: "--speed", short: Some('s'), description: "optional speed if not specified it is None.", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "url" }, optionality: Optionality::Repeating, long: "--link", short: None, description: "repeatable option.", hidden: false, }, ], ..Default::default() }, }, SubCommandInfo { name: "three", command: CommandInfoWithArgs { name: "three", description: "Command3 args are used for Command3 which has no options or arguments.", flags: &[HELP_FLAG], positionals: &[], ..Default::default() }, }, ], ..Default::default() }); } #[test] fn args_info_test_subcommand_notes_examples() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] ///Top level command with "subcommands". #[argh( note = "Top level note", example = "Top level example", error_code(0, "Top level success") )] struct TopLevel { /// this doc comment does not appear anywhere. #[argh(subcommand)] cmd: SubcommandEnum, } #[derive(FromArgs, ArgsInfo)] #[argh(subcommand)] /// Doc comments for subcommand enums does not appear in the help text. enum SubcommandEnum { Command1(Command1Args), } /// Command1 args are used for subcommand one. #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] #[argh( subcommand, name = "one", note = "{command_name} is used as a subcommand of \"Top level\"", example = "\"Typical\" usage is `{command_name}`.", error_code(0, "one level success") )] struct Command1Args { /// the "root" position. #[argh(positional, arg_name = "root")] root_value: String, /// trunk value #[argh(positional)] trunk: String, /// leaves. There can be many leaves. #[argh(positional)] leaves: Vec, } let command_one = CommandInfoWithArgs { name: "one", description: "Command1 args are used for subcommand one.", error_codes: &[ErrorCodeInfo { code: 0, description: "one level success" }], examples: &["\"Typical\" usage is `{command_name}`."], flags: &[HELP_FLAG], notes: &["{command_name} is used as a subcommand of \"Top level\""], positionals: &[ PositionalInfo { name: "root", description: "the \"root\" position.", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "trunk", description: "trunk value", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "leaves", description: "leaves. There can be many leaves.", optionality: Optionality::Repeating, hidden: false, }, ], ..Default::default() }; assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", description: "Top level command with \"subcommands\".", error_codes: &[ErrorCodeInfo { code: 0, description: "Top level success" }], examples: &["Top level example"], flags: &[HELP_FLAG], notes: &["Top level note"], commands: vec![SubCommandInfo { name: "one", command: command_one.clone() }], ..Default::default() }); assert_args_info::(&command_one); } #[test] fn args_info_test_example() { #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] #[argh( description = "Destroy the contents of with a specific \"method of destruction\".", example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", note = "Use `{command_name} help ` for details on [] for a subcommand.", error_code(2, "The blade is too dull."), error_code(3, "Out of fuel.") )] struct HelpExample { /// force, ignore minor errors. This description is so long that it wraps to the next line. #[argh(switch, short = 'f')] force: bool, /// documentation #[argh(switch)] really_really_really_long_name_for_pat: bool, /// write repeatedly #[argh(option, short = 's')] scribble: String, /// say more. Defaults to $BLAST_VERBOSE. #[argh(switch, short = 'v')] verbose: bool, #[argh(subcommand)] command: HelpExampleSubCommands, } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] #[argh(subcommand)] enum HelpExampleSubCommands { BlowUp(BlowUp), Grind(GrindCommand), } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] #[argh(subcommand, name = "blow-up")] /// explosively separate struct BlowUp { /// blow up bombs safely #[argh(switch)] safely: bool, } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] #[argh(subcommand, name = "grind", description = "make smaller by many small cuts")] struct GrindCommand { /// wear a visor while grinding #[argh(switch)] safely: bool, } assert_args_info::( &CommandInfoWithArgs { name: "HelpExample", 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, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--force", short: Some('f'), description: "force, ignore minor errors. This description is so long that it wraps to the next line.", hidden:false }, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--really-really-really-long-name-for-pat", short: None, description: "documentation", hidden:false }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "scribble"}, optionality: Optionality::Required, long: "--scribble", short: Some('s'), description: "write repeatedly", hidden:false }, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--verbose", short: Some('v'), description: "say more. Defaults to $BLAST_VERBOSE.", hidden:false } ], notes: &["Use `{command_name} help ` for details on [] for a subcommand."], commands: vec![ SubCommandInfo { name: "blow-up", command: CommandInfoWithArgs { name: "blow-up", description: "explosively separate", flags:& [HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--safely", short: None, description: "blow up bombs safely", hidden:false } ], ..Default::default() } }, SubCommandInfo { name: "grind", command: CommandInfoWithArgs { name: "grind", description: "make smaller by many small cuts", flags: &[HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--safely", short: None, description: "wear a visor while grinding" ,hidden:false}], ..Default::default() } }], error_codes: &[ErrorCodeInfo { code: 2, description: "The blade is too dull." }, ErrorCodeInfo { code: 3, description: "Out of fuel." }], ..Default::default() } ); } #[test] fn positional_greedy() { #[allow(dead_code)] #[derive(FromArgs, ArgsInfo)] /// Woot struct LastRepeatingGreedy { #[argh(positional)] /// fooey pub a: u32, #[argh(switch)] /// woo pub b: bool, #[argh(option)] /// stuff pub c: Option, #[argh(positional, greedy)] /// fooey pub d: Vec, } assert_args_info::(&CommandInfoWithArgs { name: "LastRepeatingGreedy", description: "Woot", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--b", short: None, description: "woo", hidden: false, }, FlagInfo { kind: FlagInfoKind::Option { arg_name: "c" }, optionality: Optionality::Optional, long: "--c", short: None, description: "stuff", hidden: false, }, ], positionals: &[ PositionalInfo { name: "a", description: "fooey", optionality: Optionality::Required, hidden: false, }, PositionalInfo { name: "d", description: "fooey", optionality: Optionality::Greedy, hidden: false, }, ], ..Default::default() }); } #[test] fn hidden_help_attribute() { #[derive(FromArgs, ArgsInfo)] /// Short description struct Cmd { /// this one should be hidden #[argh(positional, hidden_help)] _one: String, #[argh(positional)] /// this one is real _two: String, /// this one should be hidden #[argh(option, hidden_help)] _three: String, } assert_args_info::(&CommandInfoWithArgs { name: "Cmd", description: "Short description", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Option { arg_name: "three" }, optionality: Optionality::Required, long: "--three", short: None, description: "this one should be hidden", hidden: true, }, ], positionals: &[ PositionalInfo { name: "one", description: "this one should be hidden", optionality: Optionality::Required, hidden: true, }, PositionalInfo { name: "two", description: "this one is real", optionality: Optionality::Required, hidden: false, }, ], ..Default::default() }); } #[test] fn test_dynamic_subcommand() { #[derive(PartialEq, Debug)] struct DynamicSubCommandImpl { got: String, } 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" }, ] } fn try_redact_arg_values( _command_name: &[&str], _args: &[&str], ) -> Option, argh::EarlyExit>> { Some(Err(argh::EarlyExit::from("Test should not redact".to_owned()))) } fn try_from_args( command_name: &[&str], args: &[&str], ) -> Option> { let command_name = match command_name.last() { Some(x) => *x, None => return Some(Err(argh::EarlyExit::from("No command".to_owned()))), }; let description = Self::commands().iter().find(|x| x.name == command_name)?.description; if args.len() > 1 { Some(Err(argh::EarlyExit::from("Too many arguments".to_owned()))) } else if let Some(arg) = args.first() { Some(Ok(DynamicSubCommandImpl { got: format!("{} got {:?}", description, arg) })) } else { Some(Err(argh::EarlyExit::from("Not enough arguments".to_owned()))) } } } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] /// Top-level command. struct TopLevel { #[argh(subcommand)] nested: MySubCommandEnum, } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] #[argh(subcommand)] enum MySubCommandEnum { One(SubCommandOne), Two(SubCommandTwo), #[argh(dynamic)] ThreeFourFive(DynamicSubCommandImpl), } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] /// First subcommand. #[argh(subcommand, name = "one")] struct SubCommandOne { #[argh(option)] /// how many x x: usize, } #[derive(FromArgs, ArgsInfo, PartialEq, Debug)] /// Second subcommand. #[argh(subcommand, name = "two")] struct SubCommandTwo { #[argh(switch)] /// whether to fooey fooey: bool, } assert_args_info::(&CommandInfoWithArgs { name: "TopLevel", description: "Top-level command.", flags: &[HELP_FLAG], commands: vec![ SubCommandInfo { name: "one", command: CommandInfoWithArgs { name: "one", description: "First subcommand.", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Option { arg_name: "x" }, optionality: Optionality::Required, long: "--x", short: None, description: "how many x", hidden: false, }, ], ..Default::default() }, }, SubCommandInfo { name: "two", command: CommandInfoWithArgs { name: "two", description: "Second subcommand.", flags: &[ HELP_FLAG, FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--fooey", short: None, description: "whether to fooey", hidden: false, }, ], ..Default::default() }, }, SubCommandInfo { name: "three", command: CommandInfoWithArgs { name: "three", description: "Third command", ..Default::default() }, }, SubCommandInfo { name: "four", command: CommandInfoWithArgs { name: "four", description: "Fourth command", ..Default::default() }, }, SubCommandInfo { name: "five", command: CommandInfoWithArgs { name: "five", description: "Fifth command", ..Default::default() }, }, ], ..Default::default() }) }