From 0419c5c9636e0c903be76ee8785815a33e4bb621 Mon Sep 17 00:00:00 2001 From: funbiscuit Date: Sat, 27 Jan 2024 16:52:10 +0300 Subject: [PATCH] feat: support named options and flags in macro --- README.md | 2 +- demo.sh | 50 ++++-- embedded-cli-macros/src/command/help.rs | 212 +++++++++++++++++------ embedded-cli-macros/src/command/model.rs | 118 ++++++++++++- embedded-cli-macros/src/command/parse.rs | 196 +++++++++++++++++---- embedded-cli-macros/src/lib.rs | 2 +- embedded-cli/src/service.rs | 2 + embedded-cli/tests/cli/main.rs | 1 + embedded-cli/tests/cli/options.rs | 108 ++++++++++++ embedded-cli/tests/cli/wrapper.rs | 6 + examples/arduino/src/main.rs | 65 +++++-- examples/desktop/src/main.rs | 43 ++++- 12 files changed, 678 insertions(+), 127 deletions(-) create mode 100644 embedded-cli/tests/cli/options.rs diff --git a/README.md b/README.md index ec7998b..d379ba3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ for now. If you have suggestions - open an Issue or a Pull Request. - [x] No dynamic dispatch - [x] Configurable memory usage - [x] Declaration of commands with enums +- [x] Options and flags support - [x] Left/right support (move inside current input) - [x] Parsing of arguments to common types - [x] Autocompletion of command names (with tab) @@ -34,7 +35,6 @@ for now. If you have suggestions - open an Issue or a Pull Request. - [x] Formatted write with [ufmt](https://github.com/japaric/ufmt) - [x] No panicking branches in generated code, when optimized - [x] Any byte-stream interface is supported (`embedded_io::Write` as output stream, input bytes are given one-by-one) -- [ ] Move inside current input with left/right keypress - [ ] Colors through ANSI escape sequences - [ ] Navigation through history with search of current input - [ ] Support wrapping of generated str slices in user macro (useful for arduino progmem) diff --git a/demo.sh b/demo.sh index 4257bb7..9846fe6 100755 --- a/demo.sh +++ b/demo.sh @@ -9,34 +9,39 @@ # ffmpeg -i embedded-cli.mp4 frame%04d.png # gifski -o demo.gif -Q 50 frame*.png +SPEED=1 + +declare -i DELAY=(300)/SPEED +declare -i LONGER_DELAY=(350)/SPEED + type () { - xdotool type --delay 300 -- "$1" + xdotool type --delay $DELAY -- "$1" } submit () { - xdotool key --delay 500 Return + xdotool key --delay $LONGER_DELAY Return } backspace () { local repeat=${1:-1} - xdotool key --delay 400 --repeat $repeat BackSpace + xdotool key --delay $DELAY --repeat $repeat BackSpace } tab () { - xdotool key --delay 500 Tab + xdotool key --delay $LONGER_DELAY Tab } left () { local repeat=${1:-1} - xdotool key --delay 400 --repeat $repeat Left + xdotool key --delay $DELAY --repeat $repeat Left } right () { local repeat=${1:-1} - xdotool key --delay 400 --repeat $repeat Right + xdotool key --delay $DELAY --repeat $repeat Right } up () { local repeat=${1:-1} - xdotool key --delay 800 --repeat $repeat Up + xdotool key --delay $LONGER_DELAY --repeat $repeat Up } down () { local repeat=${1:-1} - xdotool key --delay 800 --repeat $repeat Down + xdotool key --delay $LONGER_DELAY --repeat $repeat Down } echo "Demo started" @@ -57,12 +62,9 @@ submit type "h" tab -sleep 0.5 - type "l" tab -sleep 0.5 submit up @@ -72,13 +74,23 @@ left 2 type "e" submit -sleep 0.5 - up 2 type "Rust" submit +up +type " -V" +submit + +up +backspace +type "-verbose" +submit + +type "hello -- --Rust--" +submit + type "got-l" left 5 type "help " @@ -95,7 +107,7 @@ tab type "-" tab -type "12" +type "--led 12" submit up 2 @@ -109,10 +121,16 @@ submit up type " 123 789" +submit + +up backspace 3 type "456" left 4 backspace 2 +left +type "--id " +right type "01" submit @@ -125,6 +143,10 @@ backspace 4 type "\\\"abc\\\" " submit +up +type " -- --arg" +submit + # Wait until keys disappear sleep 5 echo "Demo is finished" diff --git a/embedded-cli-macros/src/command/help.rs b/embedded-cli-macros/src/command/help.rs index 44aee88..ef0d29e 100644 --- a/embedded-cli-macros/src/command/help.rs +++ b/embedded-cli-macros/src/command/help.rs @@ -5,7 +5,7 @@ use quote::quote; use super::{model::Command, TargetType}; #[cfg(feature = "help")] -use super::model::CommandArgs; +use super::model::{CommandArgType, CommandArgs}; #[cfg(feature = "help")] pub fn derive_help( @@ -14,10 +14,7 @@ pub fn derive_help( commands: &[Command], ) -> Result { let help_all = create_help_all(commands, help_title)?; - let commands_help = commands - .iter() - .map(create_command_help) - .collect::>>()?; + let commands_help = commands.iter().map(create_command_help).collect::>(); let ident = target.ident(); let named_lifetime = target.named_lifetime(); @@ -90,7 +87,7 @@ fn create_help_all(commands: &[Command], title: &str) -> Result { } #[cfg(feature = "help")] -fn create_command_help(command: &Command) -> Result { +fn create_command_help(command: &Command) -> TokenStream { let name = command.name(); let help = if let Some(help) = command.help().long() { @@ -102,69 +99,186 @@ fn create_command_help(command: &Command) -> Result { quote! {} }; - let (args_str, args_help) = match command.args() { - CommandArgs::None => (quote! { "" }, quote! {}), + let usage = create_usage(command.args()); + let args_help = create_args_help(command.args()); + let options_help = create_options_help(command.args()); + + quote! { + #name => { + #help + #usage + #args_help + #options_help + }, + } +} + +#[cfg(feature = "help")] +fn create_args_help(args: &CommandArgs) -> TokenStream { + let help_lines = match args { + CommandArgs::None => vec![], CommandArgs::Named(args) => { - let args_str = args + // 2 is added to account for brackets + let longest_arg = args .iter() - .map(|arg| { - if arg.is_optional() { - format!(" [{}]", arg.name().to_uppercase()) - } else { - format!(" <{}>", arg.name().to_uppercase()) + .filter(|a| a.arg_type().is_positional()) + .map(|a| a.name().len() + 2) + .max() + .unwrap_or(0); + + args.iter() + .filter_map(|arg| match arg.arg_type() { + CommandArgType::Positional => { + let name = if arg.is_optional() { + format!("[{}]", arg.name().to_uppercase()) + } else { + format!("<{}>", arg.name().to_uppercase()) + }; + + let arg_help = arg.help().short().unwrap_or(""); + + Some(quote! { + writer.write_list_element(#name, #arg_help, #longest_arg)?; + }) } + _ => None, }) - .collect::(); - let longest_arg = args.iter().map(|a| a.name().len() + 2).max().unwrap_or(0); + .collect::>() + } + }; - let args_help = args - .iter() - .map(|arg| { - let name = if arg.is_optional() { + if help_lines.is_empty() { + quote! {} + } else { + quote! { + writer.write_title("Arguments:")?; + writer.writeln_str("")?; + #(#help_lines)* + writer.writeln_str("")?; + } + } +} + +#[cfg(feature = "help")] +fn create_options_help(args: &CommandArgs) -> TokenStream { + struct OptionHelp { + name: String, + help: String, + } + + let mut help_lines = match args { + CommandArgs::None => vec![], + CommandArgs::Named(args) => args + .iter() + .filter_map(|arg| match arg.arg_type() { + CommandArgType::Flag { long, short } => { + let name = short + .map(|name| format!("-{}", name)) + .into_iter() + .chain(long.iter().map(|name| format!("--{}", name))) + .collect::>() + .join(", "); + + let help = arg.help().short().unwrap_or("").to_string(); + + Some(OptionHelp { name, help }) + } + CommandArgType::Option { long, short } => { + let name = short + .map(|name| format!("-{}", name)) + .into_iter() + .chain(long.iter().map(|name| format!("--{}", name))) + .collect::>() + .join(", "); + + let value = if arg.is_optional() { format!("[{}]", arg.name().to_uppercase()) } else { format!("<{}>", arg.name().to_uppercase()) }; - let arg_help = arg.help().short().unwrap_or(""); + let name = format!("{} {}", name, value); + let help = arg.help().short().unwrap_or("").to_string(); + + Some(OptionHelp { name, help }) + } + CommandArgType::Positional => None, + }) + .collect::>(), + }; + help_lines.push(OptionHelp { + name: "-h, --help".to_string(), + help: "Print help".to_string(), + }); + let longest_name = help_lines.iter().map(|a| a.name.len()).max().unwrap(); + + let help_lines = help_lines + .into_iter() + .map(|help| { + let name = help.name; + let help = help.help; + quote! { + writer.write_list_element(#name, #help, #longest_name)?; + } + }) + .collect::>(); + + quote! { + writer.write_title("Options:")?; + writer.writeln_str("")?; + #(#help_lines)* + } +} + +#[cfg(feature = "help")] +fn create_usage(args: &CommandArgs) -> TokenStream { + let has_options; + let usage_args; + + match args { + CommandArgs::None => { + has_options = false; + usage_args = vec![quote! { writer.writeln_str("")?; }]; + } + CommandArgs::Named(args) => { + has_options = args.iter().any(|arg| !arg.arg_type().is_positional()); + + usage_args = args + .iter() + .filter_map(|arg| match arg.arg_type() { + crate::command::model::CommandArgType::Positional => { + let name = if arg.is_optional() { + format!("[{}]", arg.name().to_uppercase()) + } else { + format!("<{}>", arg.name().to_uppercase()) + }; + Some(name) + } + _ => None, + }) + .map(|line| { quote! { - writer.write_list_element(#name, #arg_help, #longest_arg)?; + writer.write_str(" ")?; + writer.writeln_str(#line)?; } }) - .collect::>(); - - let args_str = quote! { #args_str }; - let args_help = if args_help.is_empty() { - quote! {} - } else { - quote! { - writer.write_title("Arguments:")?; - writer.writeln_str("")?; - #(#args_help)* - writer.writeln_str("")?; - } - }; - - (args_str, args_help) + .collect::>() } }; - Ok(quote! { - #name => { - #help + let options = if has_options { + quote! { writer.write_str(" [OPTIONS]")?; } + } else { + quote! {} + }; + quote! { writer.write_title("Usage:")?; writer.write_str(" ")?; writer.write_str(command)?; - writer.writeln_str(#args_str)?; + #options + #(#usage_args)* writer.writeln_str("")?; - - #args_help - - writer.write_title("Options:")?; - writer.writeln_str("")?; - writer.write_list_element("-h, --help", "Print help", 10)?; - }, - }) + } } diff --git a/embedded-cli-macros/src/command/model.rs b/embedded-cli-macros/src/command/model.rs index f861742..60f2bb4 100644 --- a/embedded-cli-macros/src/command/model.rs +++ b/embedded-cli-macros/src/command/model.rs @@ -1,5 +1,5 @@ use convert_case::{Case, Casing}; -use darling::{Error, FromVariant, Result}; +use darling::{Error, FromField, FromMeta, FromVariant, Result}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{Field, Fields, Variant}; @@ -17,12 +17,89 @@ struct CommandAttrs { name: Option, } +#[derive(Debug)] +enum LongName { + Generated, + Fixed(String), +} + +impl FromMeta for LongName { + fn from_string(value: &str) -> Result { + if value.is_empty() { + return Err(Error::custom("Name must not be empty")); + } + Ok(Self::Fixed(value.to_string())) + } + + fn from_word() -> Result { + Ok(Self::Generated) + } +} + +#[derive(Debug)] +enum ShortName { + Generated, + Fixed(char), +} + +impl FromMeta for ShortName { + fn from_char(value: char) -> Result { + Ok(Self::Fixed(value)) + } + + fn from_string(value: &str) -> Result { + let mut it = value.chars(); + let value = it + .next() + .ok_or(Error::custom("Short name must have single char"))?; + if it.next().is_some() { + return Err(Error::custom("Short name must have single char")); + } + Self::from_char(value) + } + + fn from_word() -> Result { + Ok(Self::Generated) + } +} + +#[derive(Debug, FromField, Default)] +#[darling(default, attributes(arg), forward_attrs(allow, doc, cfg))] +struct ArgAttrs { + short: Option, + long: Option, +} + pub enum CommandArgs { None, Named(Vec), } +#[derive(Debug, Eq, PartialEq)] +pub enum CommandArgType { + /// Arg is flag and is enabled via long (--name) or short (-n) syntax. + /// At least one of long or short is set to Some + Flag { + long: Option, + short: Option, + }, + /// Arg is option and is set via long (--name) or short (-n) syntax. + /// At least one of long or short is set to Some + Option { + long: Option, + short: Option, + }, + Positional, +} + +impl CommandArgType { + pub fn is_positional(&self) -> bool { + self == &CommandArgType::Positional + } +} + pub struct CommandArg { + arg_type: CommandArgType, field_name: String, field_type: TokenStream, #[cfg(feature = "help")] @@ -32,18 +109,53 @@ pub struct CommandArg { impl CommandArg { fn parse(field: &Field) -> Result { + let arg_attrs = ArgAttrs::from_field(field)?; + let field_name = field .ident .as_ref() .expect("Only named fields are supported") .to_string(); + + let short = arg_attrs.short.map(|s| match s { + ShortName::Generated => field_name.chars().next().unwrap(), + ShortName::Fixed(c) => c, + }); + if let Some(short) = short { + if !short.is_ascii_alphabetic() { + return Err(Error::custom("Flag char must be alphabetic ASCII")); + } + } + + let long = arg_attrs.long.map(|s| match s { + LongName::Generated => field_name.from_case(Case::Snake).to_case(Case::Kebab), + LongName::Fixed(name) => name, + }); + if let Some(long) = &long { + if long.chars().any(|c| !c.is_ascii_alphabetic() && c != '-') { + return Err(Error::custom( + "Option name must consist of alphabetic ASCII chars", + )); + } + } + let aa = TypedArg::new(&field.ty); let ty = aa.ty(); let field_type = aa.inner(); let field_type = quote! { #field_type }; + let arg_type = if long.is_some() || short.is_some() { + if field_type.to_string() == "bool" { + CommandArgType::Flag { long, short } + } else { + CommandArgType::Option { long, short } + } + } else { + CommandArgType::Positional + }; Ok(Self { + arg_type, field_name, field_type, #[cfg(feature = "help")] @@ -52,6 +164,10 @@ impl CommandArg { }) } + pub fn arg_type(&self) -> &CommandArgType { + &self.arg_type + } + #[cfg(feature = "help")] pub fn help(&self) -> &Help { &self.help diff --git a/embedded-cli-macros/src/command/parse.rs b/embedded-cli-macros/src/command/parse.rs index 348a544..ffbc69d 100644 --- a/embedded-cli-macros/src/command/parse.rs +++ b/embedded-cli-macros/src/command/parse.rs @@ -1,3 +1,4 @@ +use convert_case::{Case, Casing}; use darling::Result; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; @@ -6,14 +7,14 @@ use crate::command::model::CommandArgs; use super::{ args::ArgType, - model::{Command, CommandArg}, + model::{Command, CommandArg, CommandArgType}, TargetType, }; pub fn derive_from_raw(target: &TargetType, commands: &[Command]) -> Result { let ident = target.ident(); - let command_parsing = create_command_parsing(ident, commands)?; + let parsing = create_parsing(ident, commands)?; let named_lifetime = target.named_lifetime(); @@ -21,7 +22,7 @@ pub fn derive_from_raw(target: &TargetType, commands: &[Command]) -> Result _cli::service::FromRaw<'a> for #ident #named_lifetime { fn parse(command: _cli::command::RawCommand<'a>) -> Result> { - #command_parsing + #parsing Ok(command) } } @@ -30,8 +31,8 @@ pub fn derive_from_raw(target: &TargetType, commands: &[Command]) -> Result Result { - let match_arms: Vec<_> = commands.iter().map(|c| match_arm(ident, c)).collect(); +fn create_parsing(ident: &Ident, commands: &[Command]) -> Result { + let match_arms: Vec<_> = commands.iter().map(|c| command_parsing(ident, c)).collect(); Ok(quote! { let command = match command.name() { @@ -41,7 +42,7 @@ fn create_command_parsing(ident: &Ident, commands: &[Command]) -> Result TokenStream { +fn command_parsing(ident: &Ident, command: &Command) -> TokenStream { let name = command.name(); let variant_name = command.ident(); let variant_fqn = quote! { #ident::#variant_name }; @@ -65,50 +66,175 @@ fn match_arm(ident: &Ident, command: &Command) -> TokenStream { fn create_arg_parsing(args: &[CommandArg]) -> (TokenStream, Vec) { let mut variables = vec![]; let mut arguments = vec![]; - let mut match_arms = vec![]; + let mut positional_value_arms = vec![]; + let mut extra_states = vec![]; + let mut option_name_arms = vec![]; + let mut option_value_arms = vec![]; - for (i, arg) in args.iter().enumerate() { + let mut arg_pos = 0usize; + for arg in args.iter() { let fi = format_ident!("{}", arg.name()); - let arg_decl = quote! { #fi: }; let ty = arg.field_type(); - let var_decl = quote! { - let mut #fi = None; - }; - - let match_arm = quote! { - #i => { - let v = <#ty as _cli::arguments::FromArgument>::from_arg(arg) - .map_err(|_| _cli::service::ParseError::ParseArgumentError { value: arg })?; - #fi = Some(v) + let arg_default; + + match arg.arg_type() { + CommandArgType::Flag { long, short } => { + arg_default = Some(quote! { false }); + + option_name_arms.push(create_option_name_arm( + short, + long, + quote! { + { + #fi = Some(true); + state = States::Normal; + } + }, + )); } - }; + CommandArgType::Option { long, short } => { + arg_default = None; + let state = format_ident!( + "Expect{}", + arg.name().from_case(Case::Snake).to_case(Case::Pascal) + ); + extra_states.push(quote! { #state, }); + + let parse_value = create_parse_arg_value(ty); + option_value_arms.push(quote! { + _cli::arguments::Arg::Value(val) if state == States::#state => { + #fi = Some(#parse_value); + state = States::Normal; + } + }); + + option_name_arms.push(create_option_name_arm( + short, + long, + quote! { state = States::#state }, + )); + } + CommandArgType::Positional => { + arg_default = None; + let parse_value = create_parse_arg_value(ty); + + positional_value_arms.push(quote! { + #arg_pos => { + #fi = Some(#parse_value); + }, + }); + arg_pos += 1; + } + } //TODO: correct errors - let var_name = match arg.ty() { - ArgType::Option => quote! { - #fi, - }, - ArgType::Normal => quote! { - #arg_decl #fi.ok_or(_cli::service::ParseError::NotEnoughArguments)?, - }, + let constructor_arg = match arg.ty() { + ArgType::Option => quote! { #fi }, + ArgType::Normal => { + if let Some(default) = arg_default { + quote! { + #fi: #fi.unwrap_or(#default) + } + } else { + quote! { + #fi: #fi.ok_or(_cli::service::ParseError::NotEnoughArguments)? + } + } + } }; - variables.push(var_decl); - arguments.push(var_name); - match_arms.push(match_arm); + + variables.push(quote! { + let mut #fi = None; + }); + arguments.push(quote! { + #constructor_arg, + }); } + let value_arm = if positional_value_arms.is_empty() { + quote! { + _cli::arguments::Arg::Value(_) if state == States::Normal => + return Err(_cli::service::ParseError::TooManyArguments{ + expected: arg_pos + }) + } + } else { + quote! { + _cli::arguments::Arg::Value(val) if state == States::Normal => { + match arg_pos { + #(#positional_value_arms)* + _ => return Err(_cli::service::ParseError::TooManyArguments{ + expected: arg_pos + }) + } + arg_pos += 1; + } + } + }; + let parsing = quote! { #(#variables)* - for (i, arg) in command.args().iter().enumerate() { - match i { - #(#match_arms)* - _ => return Err(_cli::service::ParseError::TooManyArguments{ - expected: i - }) + + #[derive(Eq, PartialEq)] + enum States { + Normal, + #(#extra_states)* + } + let mut state = States::Normal; + let mut arg_pos = 0; + + for arg in command.args().args() { + let arg = arg.map_err(|_| _cli::service::ParseError::Other(""))?; + match arg { + #(#option_name_arms)* + #(#option_value_arms)* + #value_arm, + _cli::arguments::Arg::Value(_) => unreachable!(), + _cli::arguments::Arg::LongOption(option) => { + return Err(_cli::service::ParseError::UnknownOption { name: option }) + } + _cli::arguments::Arg::ShortOption(option) => { + return Err(_cli::service::ParseError::UnknownFlag { flag: option }) + } + _cli::arguments::Arg::DoubleDash => {} } } }; (parsing, arguments) } + +fn create_option_name_arm( + short: &Option, + long: &Option, + action: TokenStream, +) -> TokenStream { + match (short, long) { + (Some(short), Some(long)) => { + quote! { + _cli::arguments::Arg::LongOption(#long) + | _cli::arguments::Arg::ShortOption(#short) => #action, + } + } + (Some(short), None) => { + quote! { + _cli::arguments::Arg::ShortOption(#short) => #action, + } + } + (None, Some(long)) => { + quote! { + _cli::arguments::Arg::LongOption(#long) => #action, + } + } + (None, None) => unreachable!(), + } +} + +fn create_parse_arg_value(ty: &TokenStream) -> TokenStream { + quote! { + <#ty as _cli::arguments::FromArgument>::from_arg(val).map_err(|_| + _cli::service::ParseError::ParseArgumentError { value: val } + )?, + } +} diff --git a/embedded-cli-macros/src/lib.rs b/embedded-cli-macros/src/lib.rs index c8ec7ad..2eb7beb 100644 --- a/embedded-cli-macros/src/lib.rs +++ b/embedded-cli-macros/src/lib.rs @@ -7,7 +7,7 @@ mod group; mod processor; mod utils; -#[proc_macro_derive(Command, attributes(command))] +#[proc_macro_derive(Command, attributes(command, arg))] pub fn derive_command(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); diff --git a/embedded-cli/src/service.rs b/embedded-cli/src/service.rs index 9652213..fc88cc2 100644 --- a/embedded-cli/src/service.rs +++ b/embedded-cli/src/service.rs @@ -20,6 +20,8 @@ pub enum ParseError<'a> { Other(&'a str), ParseArgumentError { value: &'a str }, TooManyArguments { expected: usize }, + UnknownFlag { flag: char }, + UnknownOption { name: &'a str }, UnknownCommand, } diff --git a/embedded-cli/tests/cli/main.rs b/embedded-cli/tests/cli/main.rs index 5a80082..e97ebe3 100644 --- a/embedded-cli/tests/cli/main.rs +++ b/embedded-cli/tests/cli/main.rs @@ -9,6 +9,7 @@ mod base; mod history; #[cfg(not(feature = "history"))] mod history_disabled; +mod options; mod terminal; mod wrapper; mod writer; diff --git a/embedded-cli/tests/cli/options.rs b/embedded-cli/tests/cli/options.rs new file mode 100644 index 0000000..dc637ce --- /dev/null +++ b/embedded-cli/tests/cli/options.rs @@ -0,0 +1,108 @@ +use embedded_cli::Command; +use rstest::rstest; + +use crate::impl_convert; +use crate::wrapper::CliWrapper; + +use crate::terminal::assert_terminal; + +#[derive(Debug, Clone, Command, PartialEq)] +enum CliTestCommand<'a> { + Cmd { + #[arg(short, long)] + name: Option<&'a str>, + + #[arg(long = "conf")] + config: &'a str, + + #[arg(short)] + level: u8, + + #[arg(short = 'V', long)] + verbose: bool, + + file: &'a str, + }, +} + +#[derive(Debug, Clone, PartialEq)] +enum TestCommand { + Cmd { + name: Option, + config: String, + level: u8, + verbose: bool, + file: String, + }, +} + +impl_convert! {CliTestCommand<'_> => TestCommand, command, { + match command { + cmd => cmd.into(), + } +}} + +impl<'a> From> for TestCommand { + fn from(value: CliTestCommand<'a>) -> Self { + match value { + CliTestCommand::Cmd { + name, + config, + level, + verbose, + file, + } => Self::Cmd { + name: name.map(|n| n.to_string()), + config: config.to_string(), + level, + verbose, + file: file.to_string(), + }, + } + } +} + +#[rstest] +#[case("cmd --name test-name --conf config -l 5 -V some-file", TestCommand::Cmd { + name: Some("test-name".to_string()), + config: "config".to_string(), + level: 5, + verbose: true, + file: "some-file".to_string(), +})] +#[case("cmd --conf config -l 35 --verbose some-file", TestCommand::Cmd { + name: None, + config: "config".to_string(), + level: 35, + verbose: true, + file: "some-file".to_string(), +})] +#[case("cmd --conf conf2 file -n name2 -Vl 25", TestCommand::Cmd { + name: Some("name2".to_string()), + config: "conf2".to_string(), + level: 25, + verbose: true, + file: "file".to_string(), +})] +#[case("cmd file3 --conf conf3 -l 17", TestCommand::Cmd { + name: None, + config: "conf3".to_string(), + level: 17, + verbose: false, + file: "file3".to_string(), +})] +fn options_parsing(#[case] command: &str, #[case] expected: TestCommand) { + let mut cli = CliWrapper::new(); + + cli.process_str(command); + + cli.send_enter(); + + assert_terminal!( + cli.terminal(), + 2, + vec![format!("$ {}", command), "$".to_string()] + ); + + assert_eq!(cli.received_commands(), vec![Ok(expected)]); +} diff --git a/embedded-cli/tests/cli/wrapper.rs b/embedded-cli/tests/cli/wrapper.rs index 7052942..eab3321 100644 --- a/embedded-cli/tests/cli/wrapper.rs +++ b/embedded-cli/tests/cli/wrapper.rs @@ -91,6 +91,8 @@ pub enum ParseError { Other(String), ParseArgumentError { value: String }, TooManyArguments { expected: usize }, + UnknownOption { name: String }, + UnknownFlag { flag: char }, UnknownCommand, } @@ -105,6 +107,10 @@ impl<'a> From> for ParseError { CliParseError::TooManyArguments { expected } => { ParseError::TooManyArguments { expected } } + CliParseError::UnknownOption { name } => ParseError::UnknownOption { + name: name.to_string(), + }, + CliParseError::UnknownFlag { flag } => ParseError::UnknownFlag { flag }, CliParseError::UnknownCommand => ParseError::UnknownCommand, } } diff --git a/examples/arduino/src/main.rs b/examples/arduino/src/main.rs index 0cf5534..b2257a2 100644 --- a/examples/arduino/src/main.rs +++ b/examples/arduino/src/main.rs @@ -11,6 +11,7 @@ use arduino_hal::port::Pin; use arduino_hal::prelude::_void_ResultVoidExt; use arduino_hal::usart::UsartWriter; use avr_progmem::progmem_str as F; +use embedded_cli::arguments::Arg; use embedded_cli::cli::CliBuilder; use embedded_cli::cli::CliHandle; use embedded_cli::command::RawCommand; @@ -28,6 +29,10 @@ enum Base<'a> { Hello { /// To whom to say hello (World by default) name: Option<&'a str>, + + /// Print extra info + #[arg(short = 'V', long)] + verbose: bool, }, /// Stop CLI and exit @@ -42,6 +47,7 @@ enum GetCommand { /// Get current LED value GetLed { /// ID of requested LED + #[arg(long)] led: u8, }, @@ -50,7 +56,12 @@ enum GetCommand { #[command(name = "getAdc")] GetAdc { /// ID of requested ADC + #[arg(long)] adc: u8, + + /// Sample count (16 by default) + #[arg(long)] + samples: Option, }, } @@ -105,14 +116,18 @@ fn on_get( 12 )?; } - GetCommand::GetAdc { adc } => { + GetCommand::GetAdc { adc, samples } => { + let samples = samples.unwrap_or(16); uwrite!( cli.writer(), - "{}{}{}{}", + "{}{}{}{}{}{}{}", F!("Current ADC"), adc, F!(" readings: "), - 23 + 23, + F!(" Used "), + samples, + F!(" samples"), )?; } } @@ -127,7 +142,15 @@ fn on_command( state.num_commands += 1; match command { - Base::Hello { name } => { + Base::Hello { name, verbose } => { + if verbose { + cli.writer().writeln_str(F!("Checking name"))?; + if name.is_none() { + cli.writer().writeln_str(F!("Name not found"))?; + } else { + cli.writer().writeln_str(F!("Name given"))?; + } + } // last write in command callback may or may not // end with newline. so both uwrite!() and uwriteln!() // will give identical results @@ -148,22 +171,26 @@ fn on_unknown( ) -> Result<(), Infallible> { state.num_commands += 1; // Use writeln to write separate lines - uwriteln!( - cli.writer(), - "{}{}", - F!("Received command: "), - command.name() - )?; - for (i, arg) in command.args().iter().enumerate() { - uwriteln!( - cli.writer(), - "{}{}{}{}'", - F!("Argument "), - i, - F!(": '"), - arg - )?; + cli.writer().writeln_str(F!("Received:"))?; + uwriteln!(cli.writer(), "{}{}", F!("Command: "), command.name())?; + + for arg in command.args().args().flatten() { + match arg { + Arg::DoubleDash => cli.writer().writeln_str("--")?, + Arg::LongOption(name) => { + cli.writer().write_str(F!("Long option: "))?; + cli.writer().writeln_str(name)?; + } + Arg::ShortOption(name) => { + uwriteln!(cli.writer(), "{}{}", F!("Short option: "), name)?; + } + Arg::Value(value) => { + cli.writer().write_str(F!("Value: "))?; + cli.writer().writeln_str(value)?; + } + } } + uwriteln!( cli.writer(), "{}{}", diff --git a/examples/desktop/src/main.rs b/examples/desktop/src/main.rs index 37eb22a..ef83298 100644 --- a/examples/desktop/src/main.rs +++ b/examples/desktop/src/main.rs @@ -1,5 +1,6 @@ #![warn(rust_2018_idioms)] +use embedded_cli::arguments::Arg; use embedded_cli::cli::{CliBuilder, CliHandle}; use embedded_cli::codes; use embedded_cli::command::RawCommand; @@ -19,6 +20,10 @@ enum Base<'a> { Hello { /// To whom to say hello (World by default) name: Option<&'a str>, + + /// Print extra info + #[arg(short = 'V', long)] + verbose: bool, }, /// Stop CLI and exit @@ -33,6 +38,7 @@ enum GetCommand { /// Get current LED value GetLed { /// ID of requested LED + #[arg(long)] led: u8, }, @@ -41,7 +47,12 @@ enum GetCommand { #[command(name = "getAdc")] GetAdc { /// ID of requested ADC + #[arg(long)] adc: u8, + + /// Sample count (16 by default) + #[arg(long)] + samples: Option, }, } @@ -93,12 +104,14 @@ fn on_get( rand::random::() )?; } - GetCommand::GetAdc { adc } => { + GetCommand::GetAdc { adc, samples } => { + let samples = samples.unwrap_or(16); uwrite!( cli.writer(), - "Current ADC{} readings: {}", + "Current ADC{} readings: {}. Used {} samples", adc, - rand::random::() + rand::random::(), + samples )?; } } @@ -113,7 +126,15 @@ fn on_command( state.num_commands += 1; match command { - Base::Hello { name } => { + Base::Hello { name, verbose } => { + if verbose { + cli.writer().writeln_str("Checking name")?; + if name.is_none() { + cli.writer().writeln_str("Name not found")?; + } else { + cli.writer().writeln_str("Name given")?; + } + } uwrite!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?; } Base::Exit => { @@ -130,10 +151,18 @@ fn on_unknown( command: RawCommand<'_>, ) -> Result<(), Infallible> { state.num_commands += 1; - uwriteln!(cli.writer(), "Received command: {}", command.name())?; - for (i, arg) in command.args().iter().enumerate() { - uwriteln!(cli.writer(), "Argument {}: '{}'", i, arg)?; + cli.writer().writeln_str("Received:")?; + uwriteln!(cli.writer(), "Command: {}", command.name())?; + + for arg in command.args().args().flatten() { + match arg { + Arg::DoubleDash => cli.writer().writeln_str("--")?, + Arg::LongOption(name) => uwriteln!(cli.writer(), "Long option: {}", name)?, + Arg::ShortOption(name) => uwriteln!(cli.writer(), "Short option: {}", name)?, + Arg::Value(value) => uwriteln!(cli.writer(), "Value: {}", value)?, + } } + uwriteln!(cli.writer(), "Total received: {}", state.num_commands)?; Ok(()) }