From 63b64853c76ea837496174353bb43bb7c9c23e97 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 11:34:53 +0100 Subject: [PATCH 1/9] WIP: Adding support for named arguments. --- Cargo.toml | 7 +- examples/simple.rs | 62 ++++++++++---- src/lib.rs | 202 ++++++++++++++++++++++++++++++--------------- 3 files changed, 187 insertions(+), 84 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e61c5f..7a414b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "menu" -version = "0.1.1" +version = "0.2.0" authors = ["Jonathan 'theJPster' Pallant "] description = "A simple #[no_std] command line interface." license = "MIT OR Apache-2.0" +edition = "2018" [dependencies] + + +[dev-dependencies] +pancurses = "0.16" diff --git a/examples/simple.rs b/examples/simple.rs index 35d0b49..77ca0dc 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,16 +1,29 @@ extern crate menu; -use std::io::{self, Read, Write}; use menu::*; +use pancurses::{endwin, initscr, noecho, Input}; const FOO_ITEM: Item = Item { - item_type: ItemType::Callback(select_foo), + item_type: ItemType::Callback { + function: select_foo, + parameters: &[ + Parameter::Mandatory("a"), + Parameter::Optional("b"), + Parameter::Named { + parameter_name: "verbose", + argument_name: "VALUE", + }, + ], + }, command: "foo", help: Some("makes a foo appear"), }; const BAR_ITEM: Item = Item { - item_type: ItemType::Callback(select_bar), + item_type: ItemType::Callback { + function: select_bar, + parameters: &[], + }, command: "bar", help: Some("fandoggles a bar"), }; @@ -29,13 +42,19 @@ const ROOT_MENU: Menu = Menu { }; const BAZ_ITEM: Item = Item { - item_type: ItemType::Callback(select_baz), + item_type: ItemType::Callback { + function: select_baz, + parameters: &[], + }, command: "baz", help: Some("thingamobob a baz"), }; const QUUX_ITEM: Item = Item { - item_type: ItemType::Callback(select_quux), + item_type: ItemType::Callback { + function: select_quux, + parameters: &[], + }, command: "quux", help: Some("maximum quux"), }; @@ -47,33 +66,40 @@ const SUB_MENU: Menu = Menu { exit: Some(exit_sub), }; -struct Output; +struct Output(pancurses::Window); impl std::fmt::Write for Output { fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> { - let mut stdout = io::stdout(); - write!(stdout, "{}", s).unwrap(); - stdout.flush().unwrap(); + self.0.printw(s); Ok(()) } } fn main() { + let window = initscr(); + noecho(); let mut buffer = [0u8; 64]; - let mut o = Output; + let mut o = Output(window); let mut r = Runner::new(&ROOT_MENU, &mut buffer, &mut o); loop { - let mut ch = [0x00u8; 1]; - // Wait for char - if let Ok(_) = io::stdin().read(&mut ch) { - // Fix newlines - if ch[0] == 0x0A { - ch[0] = 0x0D; + match r.context.0.getch() { + Some(Input::Character('\n')) => { + r.input_byte(b'\r'); } - // Feed char to runner - r.input_byte(ch[0]); + Some(Input::Character(c)) => { + let mut buf = [0; 4]; + for b in c.encode_utf8(&mut buf).bytes() { + r.input_byte(b); + } + } + Some(Input::KeyDC) => break, + Some(input) => { + r.context.0.addstr(&format!("{:?}", input)); + } + None => (), } } + endwin(); } fn enter_root(_menu: &Menu) { diff --git a/src/lib.rs b/src/lib.rs index 0e8a29b..b5e3388 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,26 @@ type MenuCallbackFn = fn(menu: &Menu); type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &str, context: &mut T); +/// Describes a parameter to the command +pub enum Parameter<'a> { + Mandatory(&'a str), + Optional(&'a str), + Named { + parameter_name: &'a str, + argument_name: &'a str, + }, +} + +/// Do we enter a sub-menu when this command is entered, or call a specific +/// function? pub enum ItemType<'a, T> where T: 'a, { - Callback(ItemCallbackFn), + Callback { + function: ItemCallbackFn, + parameters: &'a [Parameter<'a>], + }, Menu(&'a Menu<'a, T>), } @@ -71,7 +86,7 @@ where pub fn prompt(&mut self, newline: bool) { if newline { - write!(self.context, "\n").unwrap(); + writeln!(self.context).unwrap(); } if self.depth != 0 { let mut depth = 1; @@ -92,64 +107,10 @@ where return; } let outcome = if input == 0x0D { - write!(self.context, "\n").unwrap(); - if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { - if s == "help" { - let menu = self.menus[self.depth].unwrap(); - for item in menu.items { - if let Some(help) = item.help { - writeln!(self.context, "{} - {}", item.command, help).unwrap(); - } else { - writeln!(self.context, "{}", item.command).unwrap(); - } - } - if self.depth != 0 { - writeln!(self.context, "exit - leave this menu.").unwrap(); - } - writeln!(self.context, "help - print this help text.").unwrap(); - Outcome::CommandProcessed - } else if s == "exit" && self.depth != 0 { - if self.depth == self.menus.len() { - writeln!(self.context, "Can't enter menu - structure too deep.").unwrap(); - } else { - self.menus[self.depth] = None; - self.depth -= 1; - } - Outcome::CommandProcessed - } else { - let mut parts = s.split(' '); - if let Some(cmd) = parts.next() { - let mut found = false; - let menu = self.menus[self.depth].unwrap(); - for item in menu.items { - if cmd == item.command { - match item.item_type { - ItemType::Callback(f) => f(menu, item, s, &mut self.context), - ItemType::Menu(m) => { - self.depth += 1; - self.menus[self.depth] = Some(m); - } - } - found = true; - break; - } - } - if !found { - writeln!(self.context, "Command {:?} not found. Try 'help'.", cmd) - .unwrap(); - } - Outcome::CommandProcessed - } else { - writeln!(self.context, "Input empty").unwrap(); - Outcome::CommandProcessed - } - } - } else { - writeln!(self.context, "Input not valid UTF8").unwrap(); - Outcome::CommandProcessed - } - } else if input == 0x08 { - // Handling backspace + writeln!(self.context).unwrap(); + self.process_command() + } else if (input == 0x08) || (input == 0x7F) { + // Handling backspace or delete if self.used > 0 { write!(self.context, "\u{0008} \u{0008}").unwrap(); self.used -= 1; @@ -159,15 +120,17 @@ where self.buffer[self.used] = input; self.used += 1; - let valid = if let Ok(_) = core::str::from_utf8(&self.buffer[0..self.used]) { - true - } else { - false - }; + // We have to do this song and dance because `self.prompt()` needs + // a mutable reference to self, and we can't have that while + // holding a reference to the buffer at the same time. + // This line grabs the buffer, checks it's OK, then releases it again + let valid = core::str::from_utf8(&self.buffer[0..self.used]).is_ok(); + // Now we've released the buffer, we can draw the prompt if valid { write!(self.context, "\r").unwrap(); self.prompt(false); } + // Grab the buffer again to render it to the screen if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { write!(self.context, "{}", s).unwrap(); } @@ -184,6 +147,115 @@ where Outcome::NeedMore => {} } } + + fn process_command(&mut self) -> Outcome { + if let Ok(command_line) = core::str::from_utf8(&self.buffer[0..self.used]) { + if command_line == "help" { + let menu = self.menus[self.depth].unwrap(); + for item in menu.items { + self.print_help(&item); + } + if self.depth != 0 { + writeln!(self.context, "* exit - leave this menu.").unwrap(); + } + writeln!(self.context, "* help - print this help text").unwrap(); + Outcome::CommandProcessed + } else if command_line == "exit" && self.depth != 0 { + if self.depth == self.menus.len() { + writeln!(self.context, "Can't enter menu - structure too deep.").unwrap(); + } else { + self.menus[self.depth] = None; + self.depth -= 1; + } + Outcome::CommandProcessed + } else { + let mut parts = command_line.split(' '); + if let Some(cmd) = parts.next() { + let mut found = false; + let menu = self.menus[self.depth].unwrap(); + for item in menu.items { + if cmd == item.command { + match item.item_type { + ItemType::Callback { + function, + parameters, + } => self.call_function( + function, + parameters, + menu, + item, + command_line, + ), + ItemType::Menu(m) => { + self.depth += 1; + self.menus[self.depth] = Some(m); + } + } + found = true; + break; + } + } + if !found { + writeln!(self.context, "Command {:?} not found. Try 'help'.", cmd).unwrap(); + } + Outcome::CommandProcessed + } else { + writeln!(self.context, "Input empty").unwrap(); + Outcome::CommandProcessed + } + } + } else { + writeln!(self.context, "Input not valid UTF8").unwrap(); + Outcome::CommandProcessed + } + } + + fn print_help(&mut self, item: &Item) { + match item.item_type { + ItemType::Callback { parameters, .. } => { + if !parameters.is_empty() { + write!(self.context, "* {}", item.command).unwrap(); + for param in parameters.iter() { + match param { + Parameter::Mandatory(name) => { + write!(self.context, " <{}>", name).unwrap(); + } + Parameter::Optional(name) => { + write!(self.context, " [ <{}> ]", name).unwrap(); + } + Parameter::Named { + parameter_name, + argument_name, + } => { + write!(self.context, " [ --{}={} ]", parameter_name, argument_name) + .unwrap(); + } + } + } + } else { + write!(self.context, "* {}", item.command).unwrap(); + } + } + ItemType::Menu(_menu) => { + write!(self.context, "* {}", item.command).unwrap(); + } + } + if let Some(help) = item.help { + write!(self.context, " - {}", help).unwrap(); + } + writeln!(self.context).unwrap(); + } + + fn call_function( + &self, + _function: ItemCallbackFn, + _parameters: &[Parameter], + _parent_menu: &Menu, + _item: &Item, + _command: &str, + ) { + + } } #[cfg(test)] From 22836f01bb750d1457fc4f41a602805bd42e4199 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 16:57:06 +0100 Subject: [PATCH 2/9] Callbacks working again. --- examples/simple.rs | 38 +++++++++++--------- src/lib.rs | 90 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 96 insertions(+), 32 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 77ca0dc..4f4d9aa 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -2,6 +2,7 @@ extern crate menu; use menu::*; use pancurses::{endwin, initscr, noecho, Input}; +use std::fmt::Write; const FOO_ITEM: Item = Item { item_type: ItemType::Callback { @@ -102,34 +103,39 @@ fn main() { endwin(); } -fn enter_root(_menu: &Menu) { - println!("In enter_root"); +fn enter_root(_menu: &Menu, context: &mut Output) { + writeln!(context, "In enter_root").unwrap(); } -fn exit_root(_menu: &Menu) { - println!("In exit_root"); +fn exit_root(_menu: &Menu, context: &mut Output) { + writeln!(context, "In exit_root").unwrap(); } -fn select_foo<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_foo: {}", input); +fn select_foo<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { + writeln!(context, "In select_foo. Args = {:?}", args).unwrap(); } -fn select_bar<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_bar: {}", input); +fn select_bar<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { + writeln!(context, "In select_bar. Args = {:?}", args).unwrap(); } -fn enter_sub(_menu: &Menu) { - println!("In enter_sub"); +fn enter_sub(_menu: &Menu, context: &mut Output) { + writeln!(context, "In enter_sub").unwrap(); } -fn exit_sub(_menu: &Menu) { - println!("In exit_sub"); +fn exit_sub(_menu: &Menu, context: &mut Output) { + writeln!(context, "In exit_sub").unwrap(); } -fn select_baz<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_baz: {}", input); +fn select_baz<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { + writeln!(context, "In select_baz: Args = {:?}", args).unwrap(); } -fn select_quux<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_quux: {}", input); +fn select_quux<'a>( + _menu: &Menu, + _item: &Item, + args: &[&str], + context: &mut Output, +) { + writeln!(context, "In select_quux: Args = {:?}", args).unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index b5e3388..f0c786e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ #![no_std] -type MenuCallbackFn = fn(menu: &Menu); -type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &str, context: &mut T); +type MenuCallbackFn = fn(menu: &Menu, context: &mut T); +type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &[&str], context: &mut T); +#[derive(Debug)] /// Describes a parameter to the command pub enum Parameter<'a> { Mandatory(&'a str), @@ -24,6 +25,7 @@ where parameters: &'a [Parameter<'a>], }, Menu(&'a Menu<'a, T>), + _Dummy, } /// Menu Item @@ -71,7 +73,7 @@ where { pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], context: &'a mut T) -> Runner<'a, T> { if let Some(cb_fn) = menu.entry { - cb_fn(menu); + cb_fn(menu, context); } let mut r = Runner { menus: [Some(menu), None, None, None], @@ -156,9 +158,19 @@ where self.print_help(&item); } if self.depth != 0 { - writeln!(self.context, "* exit - leave this menu.").unwrap(); + let item = Item { + command: "exit", + help: Some("leave this menu"), + item_type: ItemType::_Dummy, + }; + self.print_help(&item); } - writeln!(self.context, "* help - print this help text").unwrap(); + let item = Item { + command: "help", + help: Some("show this help"), + item_type: ItemType::_Dummy, + }; + self.print_help(&item); Outcome::CommandProcessed } else if command_line == "exit" && self.depth != 0 { if self.depth == self.menus.len() { @@ -179,7 +191,8 @@ where ItemType::Callback { function, parameters, - } => self.call_function( + } => Self::call_function( + self.context, function, parameters, menu, @@ -190,6 +203,9 @@ where self.depth += 1; self.menus[self.depth] = Some(m); } + ItemType::_Dummy => { + unreachable!(); + } } found = true; break; @@ -214,7 +230,7 @@ where match item.item_type { ItemType::Callback { parameters, .. } => { if !parameters.is_empty() { - write!(self.context, "* {}", item.command).unwrap(); + write!(self.context, "{}", item.command).unwrap(); for param in parameters.iter() { match param { Parameter::Mandatory(name) => { @@ -233,11 +249,14 @@ where } } } else { - write!(self.context, "* {}", item.command).unwrap(); + write!(self.context, "{}", item.command).unwrap(); } } ItemType::Menu(_menu) => { - write!(self.context, "* {}", item.command).unwrap(); + write!(self.context, "{}", item.command).unwrap(); + } + ItemType::_Dummy => { + write!(self.context, "{}", item.command).unwrap(); } } if let Some(help) = item.help { @@ -247,14 +266,53 @@ where } fn call_function( - &self, - _function: ItemCallbackFn, - _parameters: &[Parameter], - _parent_menu: &Menu, - _item: &Item, - _command: &str, + context: &mut T, + callback_function: ItemCallbackFn, + parameters: &[Parameter], + parent_menu: &Menu, + item: &Item, + command: &str, ) { - + let mandatory_parameter_count = parameters + .iter() + .filter(|p| match p { + Parameter::Mandatory(_) => true, + _ => false, + }) + .count(); + if command.len() >= item.command.len() { + // Maybe arguments + let mut argument_buffer: [&str; 16] = [""; 16]; + let mut argument_count = 0; + let mut positional_arguments = 0; + for (slot, arg) in argument_buffer + .iter_mut() + .zip(command[item.command.len()..].split_whitespace()) + { + *slot = arg; + argument_count += 1; + if !arg.starts_with("--") { + positional_arguments += 1; + } + } + if positional_arguments >= mandatory_parameter_count { + callback_function( + parent_menu, + item, + &argument_buffer[0..argument_count], + context, + ); + } else { + writeln!(context, "Error: Insufficient arguments given").unwrap(); + } + } else { + // Definitely no arguments + if mandatory_parameter_count == 0 { + callback_function(parent_menu, item, &[], context); + } else { + writeln!(context, "Error: Insufficient arguments given").unwrap(); + } + } } } From 873dcb53f90ca38774068a589394d1b79485f84e Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 17:59:22 +0100 Subject: [PATCH 3/9] WIP: Now supported named, value-less arguments like --verbose. --- examples/simple.rs | 42 +++++++- src/lib.rs | 256 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 285 insertions(+), 13 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 4f4d9aa..3fde16b 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -10,10 +10,11 @@ const FOO_ITEM: Item = Item { parameters: &[ Parameter::Mandatory("a"), Parameter::Optional("b"), - Parameter::Named { - parameter_name: "verbose", - argument_name: "VALUE", - }, + Parameter::Named("verbose"), + // Parameter::NamedValue { + // parameter_name: "level", + // argument_name: "INT", + // }, ], }, command: "foo", @@ -78,6 +79,7 @@ impl std::fmt::Write for Output { fn main() { let window = initscr(); + window.scrollok(true); noecho(); let mut buffer = [0u8; 64]; let mut o = Output(window); @@ -111,8 +113,38 @@ fn exit_root(_menu: &Menu, context: &mut Output) { writeln!(context, "In exit_root").unwrap(); } -fn select_foo<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { +fn select_foo<'a>(menu: &Menu, item: &Item, args: &[&str], context: &mut Output) { writeln!(context, "In select_foo. Args = {:?}", args).unwrap(); + writeln!( + context, + "a = {:?}", + ::menu::argument_finder(item, args, "a") + ) + .unwrap(); + writeln!( + context, + "b = {:?}", + ::menu::argument_finder(item, args, "b") + ) + .unwrap(); + writeln!( + context, + "verbose = {:?}", + ::menu::argument_finder(item, args, "verbose") + ) + .unwrap(); + writeln!( + context, + "level = {:?}", + ::menu::argument_finder(item, args, "level") + ) + .unwrap(); + writeln!( + context, + "no_such_arg = {:?}", + ::menu::argument_finder(item, args, "no_such_arg") + ) + .unwrap(); } fn select_bar<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { diff --git a/src/lib.rs b/src/lib.rs index f0c786e..8bfee24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,14 @@ type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &[&str], conte #[derive(Debug)] /// Describes a parameter to the command pub enum Parameter<'a> { + /// A mandatory positional parameter Mandatory(&'a str), + /// An optional positional parameter. Must come after the mandatory positional arguments. Optional(&'a str), - Named { + /// A named parameter with no argument (e.g. `--verbose` or `--dry-run`) + Named(&'a str), + /// A named parameter with argument (e.g. `--mode=foo` or `--level=3`) + NamedValue { parameter_name: &'a str, argument_name: &'a str, }, @@ -62,6 +67,104 @@ where pub context: &'a mut T, } +/// Looks for the named parameter in the parameter list of the item, then +/// finds the correct argument. +/// +/// * Returns `Ok(None)` if `parameter_name` gives an optional or named +/// parameter and that argument was not given. +/// * Returns `Ok(arg)` if the argument corresponding to `parameter_name` was +/// found. `arg` is the empty string if the parameter was `Parameter::Named` +/// (and hence doesn't take a value). +/// * Returns `Err(())` if `parameter_name` was not in `item.parameter_list` +/// or `item` wasn't an Item::Callback +pub fn argument_finder<'a, T>( + item: &'a Item<'a, T>, + argument_list: &'a [&'a str], + name_to_find: &'a str, +) -> Result, ()> { + if let ItemType::Callback { parameters, .. } = item.item_type { + // Step 1 - Find `name_to_find` in the parameter list. + let mut found_param = None; + let mut mandatory_count = 0; + let mut optional_count = 0; + for param in parameters.iter() { + match param { + Parameter::Mandatory(name) => { + mandatory_count += 1; + if *name == name_to_find { + found_param = Some((param, mandatory_count)); + } + } + Parameter::Optional(name) => { + optional_count += 1; + if *name == name_to_find { + found_param = Some((param, optional_count)); + } + } + Parameter::Named(name) => { + if *name == name_to_find { + found_param = Some((param, 0)); + } + } + _ => { + unimplemented!(); + } + } + } + // Step 2 - What sort of parameter is it? + match found_param { + // Step 2a - Mandatory Positional + Some((Parameter::Mandatory(_name), mandatory_idx)) => { + // We want positional parameter number `mandatory_idx` of `mandatory_count`. + let mut positional_args_seen = 0; + for arg in argument_list { + if !arg.starts_with("--") { + // Positional + positional_args_seen += 1; + if positional_args_seen == mandatory_idx { + return Ok(Some(arg)); + } + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2b - Optional Positional + Some((Parameter::Optional(_name), optional_idx)) => { + // We want positional parameter number `mandatory_idx` of `mandatory_count`. + let mut positional_args_seen = 0; + for arg in argument_list { + if !arg.starts_with("--") { + // Positional + positional_args_seen += 1; + if positional_args_seen == (mandatory_count + optional_idx) { + return Ok(Some(arg)); + } + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2c - Named + Some((Parameter::Named(name), _)) => { + for arg in argument_list { + if arg.starts_with("--") && (&arg[2..] == *name) { + return Ok(Some("")); + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2d - NamedValue + // Step 2e - not found + _ => Err(()), + } + } else { + // Not an item with arguments + Err(()) + } +} + enum Outcome { CommandProcessed, NeedMore, @@ -239,7 +342,10 @@ where Parameter::Optional(name) => { write!(self.context, " [ <{}> ]", name).unwrap(); } - Parameter::Named { + Parameter::Named(name) => { + write!(self.context, " [ --{} ]", name).unwrap(); + } + Parameter::NamedValue { parameter_name, argument_name, } => { @@ -280,6 +386,14 @@ where _ => false, }) .count(); + let positional_parameter_count = parameters + .iter() + .filter(|p| match p { + Parameter::Mandatory(_) => true, + Parameter::Optional(_) => true, + _ => false, + }) + .count(); if command.len() >= item.command.len() { // Maybe arguments let mut argument_buffer: [&str; 16] = [""; 16]; @@ -291,19 +405,49 @@ where { *slot = arg; argument_count += 1; - if !arg.starts_with("--") { + if arg.starts_with("--") { + // Validate named argument + let mut found = false; + for param in parameters.iter() { + match param { + Parameter::Named(name) => { + if &arg[2..] == *name { + found = true; + break; + } + } + Parameter::NamedValue { parameter_name, .. } => { + if let Some(name) = arg[2..].split('=').next() { + if name == *parameter_name { + found = true; + break; + } + } + } + _ => { + // Ignore + } + } + } + if !found { + writeln!(context, "Error: Did not understand {:?}", arg).unwrap(); + return; + } + } else { positional_arguments += 1; } } - if positional_arguments >= mandatory_parameter_count { + if positional_arguments < mandatory_parameter_count { + writeln!(context, "Error: Insufficient arguments given").unwrap(); + } else if positional_arguments > positional_parameter_count { + writeln!(context, "Error: Too many arguments given").unwrap(); + } else { callback_function( parent_menu, item, &argument_buffer[0..argument_count], context, ); - } else { - writeln!(context, "Error: Insufficient arguments given").unwrap(); } } else { // Definitely no arguments @@ -318,8 +462,104 @@ where #[cfg(test)] mod tests { + use super::*; + + fn dummy(_menu: &Menu, _item: &Item, _args: &[&str], _context: &mut u32) {} + + #[test] + fn find_arg_mandatory() { + let item = Item { + command: "dummy", + help: None, + item_type: ItemType::Callback { + function: dummy, + parameters: &[ + Parameter::Mandatory("foo"), + Parameter::Mandatory("bar"), + Parameter::Mandatory("baz"), + ], + }, + }; + assert_eq!( + argument_finder(&item, &["a", "b", "c"], "foo"), + Ok(Some("a")) + ); + assert_eq!( + argument_finder(&item, &["a", "b", "c"], "bar"), + Ok(Some("b")) + ); + assert_eq!( + argument_finder(&item, &["a", "b", "c"], "baz"), + Ok(Some("c")) + ); + // Not an argument + assert_eq!(argument_finder(&item, &["a", "b", "c"], "quux"), Err(())); + } + #[test] - fn it_works() { - assert_eq!(2 + 2, 4); + fn find_arg_optional() { + let item = Item { + command: "dummy", + help: None, + item_type: ItemType::Callback { + function: dummy, + parameters: &[ + Parameter::Mandatory("foo"), + Parameter::Mandatory("bar"), + Parameter::Optional("baz"), + ], + }, + }; + assert_eq!( + argument_finder(&item, &["a", "b", "c"], "foo"), + Ok(Some("a")) + ); + assert_eq!( + argument_finder(&item, &["a", "b", "c"], "bar"), + Ok(Some("b")) + ); + assert_eq!( + argument_finder(&item, &["a", "b", "c"], "baz"), + Ok(Some("c")) + ); + // Not an argument + assert_eq!(argument_finder(&item, &["a", "b", "c"], "quux"), Err(())); + // Missing optional + assert_eq!(argument_finder(&item, &["a", "b"], "baz"), Ok(None)); + } + + #[test] + fn find_arg_named() { + let item = Item { + command: "dummy", + help: None, + item_type: ItemType::Callback { + function: dummy, + parameters: &[ + Parameter::Mandatory("foo"), + Parameter::Named("bar"), + Parameter::Named("baz"), + ], + }, + }; + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "foo"), + Ok(Some("a")) + ); + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "bar"), + Ok(Some("")) + ); + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "baz"), + Ok(Some("")) + ); + // Not an argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "quux"), + Err(()) + ); + // Missing named + assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None)); } } From 5230aa88fce6535b57d622665e8b2e3f98c96296 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 18:12:46 +0100 Subject: [PATCH 4/9] Named value arguments now work. --- examples/simple.rs | 10 +++--- src/lib.rs | 84 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 3fde16b..a4b0f53 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -11,10 +11,10 @@ const FOO_ITEM: Item = Item { Parameter::Mandatory("a"), Parameter::Optional("b"), Parameter::Named("verbose"), - // Parameter::NamedValue { - // parameter_name: "level", - // argument_name: "INT", - // }, + Parameter::NamedValue { + parameter_name: "level", + argument_name: "INT", + }, ], }, command: "foo", @@ -113,7 +113,7 @@ fn exit_root(_menu: &Menu, context: &mut Output) { writeln!(context, "In exit_root").unwrap(); } -fn select_foo<'a>(menu: &Menu, item: &Item, args: &[&str], context: &mut Output) { +fn select_foo<'a>(_menu: &Menu, item: &Item, args: &[&str], context: &mut Output) { writeln!(context, "In select_foo. Args = {:?}", args).unwrap(); writeln!( context, diff --git a/src/lib.rs b/src/lib.rs index 8bfee24..a2487d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,8 +106,10 @@ pub fn argument_finder<'a, T>( found_param = Some((param, 0)); } } - _ => { - unimplemented!(); + Parameter::NamedValue { parameter_name, .. } => { + if *parameter_name == name_to_find { + found_param = Some((param, 0)); + } } } } @@ -145,7 +147,7 @@ pub fn argument_finder<'a, T>( // Valid thing to ask for but we don't have it Ok(None) } - // Step 2c - Named + // Step 2c - Named (e.g. `--verbose`) Some((Parameter::Named(name), _)) => { for arg in argument_list { if arg.starts_with("--") && (&arg[2..] == *name) { @@ -155,7 +157,23 @@ pub fn argument_finder<'a, T>( // Valid thing to ask for but we don't have it Ok(None) } - // Step 2d - NamedValue + // Step 2d - NamedValue (e.g. `--level=123`) + Some((Parameter::NamedValue { parameter_name, .. }, _)) => { + let name_start = 2; + let equals_start = name_start + parameter_name.len(); + let value_start = equals_start + 1; + for arg in argument_list { + if arg.starts_with("--") + && (arg.len() >= value_start) + && (arg.get(equals_start..=equals_start) == Some("=")) + && (arg.get(name_start..equals_start) == Some(*parameter_name)) + { + return Ok(Some(&arg[value_start..])); + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } // Step 2e - not found _ => Err(()), } @@ -562,4 +580,62 @@ mod tests { // Missing named assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None)); } + + #[test] + fn find_arg_namedvalue() { + let item = Item { + command: "dummy", + help: None, + item_type: ItemType::Callback { + function: dummy, + parameters: &[ + Parameter::Mandatory("foo"), + Parameter::Named("bar"), + Parameter::NamedValue { + parameter_name: "baz", + argument_name: "BAZ", + }, + ], + }, + }; + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "foo"), + Ok(Some("a")) + ); + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "bar"), + Ok(Some("")) + ); + // No argument so mark as not found + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "baz"), + Ok(None) + ); + // Empty argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz="], "baz"), + Ok(Some("")) + ); + // Short argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz=1"], "baz"), + Ok(Some("1")) + ); + // Long argument + assert_eq!( + argument_finder( + &item, + &["a", "--bar", "--baz=abcdefghijklmnopqrstuvwxyz"], + "baz" + ), + Ok(Some("abcdefghijklmnopqrstuvwxyz")) + ); + // Not an argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "quux"), + Err(()) + ); + // Missing named + assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None)); + } } From 1a91d5ece066810b300e25ec4d451949d89b1751 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 18:42:37 +0100 Subject: [PATCH 5/9] Added some docs. --- examples/simple.rs | 3 +- src/lib.rs | 70 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index a4b0f53..7dc8016 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -82,8 +82,7 @@ fn main() { window.scrollok(true); noecho(); let mut buffer = [0u8; 64]; - let mut o = Output(window); - let mut r = Runner::new(&ROOT_MENU, &mut buffer, &mut o); + let mut r = Runner::new(&ROOT_MENU, &mut buffer, Output(window)); loop { match r.context.0.getch() { Some(Input::Character('\n')) => { diff --git a/src/lib.rs b/src/lib.rs index a2487d7..7d5afc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,15 @@ +//! # Menu +//! +//! A basic command-line interface for `#![no_std]` Rust programs. Peforms +//! zero heap allocation. #![no_std] +#![deny(missing_docs)] -type MenuCallbackFn = fn(menu: &Menu, context: &mut T); -type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &[&str], context: &mut T); +/// The type of function we call when we enter/exit a menu. +pub type MenuCallbackFn = fn(menu: &Menu, context: &mut T); + +/// The type of function we call when we a valid command has been entered. +pub type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &[&str], context: &mut T); #[derive(Debug)] /// Describes a parameter to the command @@ -14,7 +22,9 @@ pub enum Parameter<'a> { Named(&'a str), /// A named parameter with argument (e.g. `--mode=foo` or `--level=3`) NamedValue { + /// The bit that comes after the `--` parameter_name: &'a str, + /// The bit that comes after the `--name=`, e.g. `INT` or `FILE`. It's mostly for help text. argument_name: &'a str, }, } @@ -25,35 +35,54 @@ pub enum ItemType<'a, T> where T: 'a, { + /// Call a function when this command is entered Callback { + /// The function to call function: ItemCallbackFn, + /// The list of parameters for this function. Pass an empty list if there aren't any. parameters: &'a [Parameter<'a>], }, + /// This item is a sub-menu you can enter Menu(&'a Menu<'a, T>), + /// Internal use only - do not use _Dummy, } -/// Menu Item +/// An `Item` is a what our menus are made from. Each item has a `name` which +/// you have to enter to select this item. Each item can also have zero or +/// more parameters, and some optional help text. pub struct Item<'a, T> where T: 'a, { + /// The word you need to enter to activate this item. It is recommended + /// that you avoid whitespace in this string. pub command: &'a str, + /// Optional help text. Printed if you enter `help`. pub help: Option<&'a str>, + /// The type of this item - menu, callback, etc. pub item_type: ItemType<'a, T>, } -/// A Menu is made of Items +/// A `Menu` is made of one or more `Item`s. pub struct Menu<'a, T> where T: 'a, { + /// Each menu has a label which is visible in the prompt, unless you are + /// the root menu. pub label: &'a str, + /// A slice of menu items in this menu. pub items: &'a [&'a Item<'a, T>], + /// A function to call when this menu is entered. If this is the root menu, this is called when the runner is created. pub entry: Option>, + /// A function to call when this menu is exited. Never called for the root menu. pub exit: Option>, } +/// This structure handles the menu. You feed it bytes as they are read from +/// the console and it executes menu actions when commands are typed in +/// (followed by Enter). pub struct Runner<'a, T> where T: core::fmt::Write, @@ -64,7 +93,8 @@ where /// Maximum four levels deep menus: [Option<&'a Menu<'a, T>>; 4], depth: usize, - pub context: &'a mut T, + /// The context object the `Runner` carries around. + pub context: T, } /// Looks for the named parameter in the parameter list of the item, then @@ -192,9 +222,13 @@ impl<'a, T> Runner<'a, T> where T: core::fmt::Write, { - pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], context: &'a mut T) -> Runner<'a, T> { + /// Create a new `Runner`. You need to supply a top-level menu, and a + /// buffer that the `Runner` can use. Feel free to pass anything as the + /// `context` type - the only requirement is that the `Runner` can + /// `write!` to the context, which it will do for all text output. + pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], mut context: T) -> Runner<'a, T> { if let Some(cb_fn) = menu.entry { - cb_fn(menu, context); + cb_fn(menu, &mut context); } let mut r = Runner { menus: [Some(menu), None, None, None], @@ -207,6 +241,8 @@ where r } + /// Print out a new command prompt, including sub-menu names if + /// applicable. pub fn prompt(&mut self, newline: bool) { if newline { writeln!(self.context).unwrap(); @@ -224,6 +260,9 @@ where write!(self.context, "> ").unwrap(); } + /// Add a byte to the menu runner's buffer. If this byte is a + /// carriage-return, the buffer is scanned and the appropriate action + /// performed. pub fn input_byte(&mut self, input: u8) { // Strip carriage returns if input == 0x0A { @@ -271,8 +310,10 @@ where } } + /// Scan the buffer and do the right thing based on its contents. fn process_command(&mut self) -> Outcome { if let Ok(command_line) = core::str::from_utf8(&self.buffer[0..self.used]) { + // We have a valid string if command_line == "help" { let menu = self.menus[self.depth].unwrap(); for item in menu.items { @@ -313,7 +354,7 @@ where function, parameters, } => Self::call_function( - self.context, + &mut self.context, function, parameters, menu, @@ -342,7 +383,8 @@ where } } } else { - writeln!(self.context, "Input not valid UTF8").unwrap(); + // Hmm .. we did not have a valid string + writeln!(self.context, "Input was not valid UTF-8").unwrap(); Outcome::CommandProcessed } } @@ -435,10 +477,12 @@ where } } Parameter::NamedValue { parameter_name, .. } => { - if let Some(name) = arg[2..].split('=').next() { - if name == *parameter_name { - found = true; - break; + if arg.contains('=') { + if let Some(name) = arg[2..].split('=').next() { + if name == *parameter_name { + found = true; + break; + } } } } From d452e8ec13dac3149853b3456f913d00b2f68b34 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 18:45:31 +0100 Subject: [PATCH 6/9] Reformat example into a single root menu object. --- examples/simple.rs | 111 ++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 7dc8016..6b2a5ca 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -4,70 +4,65 @@ use menu::*; use pancurses::{endwin, initscr, noecho, Input}; use std::fmt::Write; -const FOO_ITEM: Item = Item { - item_type: ItemType::Callback { - function: select_foo, - parameters: &[ - Parameter::Mandatory("a"), - Parameter::Optional("b"), - Parameter::Named("verbose"), - Parameter::NamedValue { - parameter_name: "level", - argument_name: "INT", - }, - ], - }, - command: "foo", - help: Some("makes a foo appear"), -}; - -const BAR_ITEM: Item = Item { - item_type: ItemType::Callback { - function: select_bar, - parameters: &[], - }, - command: "bar", - help: Some("fandoggles a bar"), -}; - -const ENTER_ITEM: Item = Item { - item_type: ItemType::Menu(&SUB_MENU), - command: "sub", - help: Some("enter sub-menu"), -}; - const ROOT_MENU: Menu = Menu { label: "root", - items: &[&FOO_ITEM, &BAR_ITEM, &ENTER_ITEM], + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_foo, + parameters: &[ + Parameter::Mandatory("a"), + Parameter::Optional("b"), + Parameter::Named("verbose"), + Parameter::NamedValue { + parameter_name: "level", + argument_name: "INT", + }, + ], + }, + command: "foo", + help: Some("makes a foo appear"), + }, + &Item { + item_type: ItemType::Callback { + function: select_bar, + parameters: &[], + }, + command: "bar", + help: Some("fandoggles a bar"), + }, + &Item { + item_type: ItemType::Menu(&Menu { + label: "sub", + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_baz, + parameters: &[], + }, + command: "baz", + help: Some("thingamobob a baz"), + }, + &Item { + item_type: ItemType::Callback { + function: select_quux, + parameters: &[], + }, + command: "quux", + help: Some("maximum quux"), + }, + ], + entry: Some(enter_sub), + exit: Some(exit_sub), + }), + command: "sub", + help: Some("enter sub-menu"), + }, + ], entry: Some(enter_root), exit: Some(exit_root), }; -const BAZ_ITEM: Item = Item { - item_type: ItemType::Callback { - function: select_baz, - parameters: &[], - }, - command: "baz", - help: Some("thingamobob a baz"), -}; - -const QUUX_ITEM: Item = Item { - item_type: ItemType::Callback { - function: select_quux, - parameters: &[], - }, - command: "quux", - help: Some("maximum quux"), -}; - -const SUB_MENU: Menu = Menu { - label: "sub", - items: &[&BAZ_ITEM, &QUUX_ITEM], - entry: Some(enter_sub), - exit: Some(exit_sub), -}; - struct Output(pancurses::Window); impl std::fmt::Write for Output { From 3f7e6ca4cbb063b04b522a0a24e84017d9c43713 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 20:48:45 +0100 Subject: [PATCH 7/9] Updated README. Leave version bump to the master branch. --- Cargo.toml | 2 +- README.md | 176 ++++++++++++++++++++--- examples/simple.rs | 25 +++- src/lib.rs | 348 ++++++++++++++++++++++++++++++++------------- 4 files changed, 427 insertions(+), 124 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a414b5..24792e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "menu" -version = "0.2.0" +version = "0.1.0" authors = ["Jonathan 'theJPster' Pallant "] description = "A simple #[no_std] command line interface." license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 8dcfb84..7a209ed 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,179 @@ # Menu +## Introduction + A simple command-line menu system in Rust. Works on embedded systems, but also on your command-line. -``` -$ cargo run --example simple - Compiling menu v0.1.0 (file:///home/jonathan/Documents/programming/menu) +**NOTE:** This crates works only in `&str` - there's no heap allocation, but +there's also no automatic conversion to integers, boolean, etc. + +```console +user@host: ~/menu $ cargo run --example simple + Compiling menu v0.2.0 (file:///home/user/menu) Finished dev [unoptimized + debuginfo] target(s) in 0.84 secs Running `target/debug/examples/simple` In enter_root() > help -foo - makes a foo appear -bar - fandoggles a bar -sub - enter sub-menu -help - print this help text. +AVAILABLE ITEMS: + foo [ ] [ --verbose ] [ --level=INT ] - Makes a foo appear. + bar - fandoggles a bar + sub - enter sub-menu + help [ ] - Show this help, or get help on a specific command. + + +> help foo +SUMMARY: + foo [ ] [ --verbose ] [ --level=INT ] + +PARAMETERS: + + - This is the help text for 'a' + + - No help text found + --verbose + - No help text found + --level=INT + - Set the level of the dangle + + +DESCRIPTION: +Makes a foo appear. + +This is some extensive help text. + +It contains multiple paragraphs and should be preceeded by the parameter list. + +> foo --level=3 --verbose 1 2 +In select_foo. Args = ["--level=3", "--verbose", "1", "2"] +a = Ok(Some("1")) +b = Ok(Some("2")) +verbose = Ok(Some("")) +level = Ok(Some("3")) +no_such_arg = Err(()) + > foo -In select_foo(): foo +Error: Insufficient arguments given! + +> foo 1 2 3 3 +Error: Too many arguments given > sub -sub> help -baz - thingamobob a baz -quux - maximum quux -exit - leave this menu. -help - print this help text. +/sub> help +AVAILABLE ITEMS: + baz - thingamobob a baz + quux - maximum quux + exit - Leave this menu. + help [ ] - Show this help, or get help on a specific command. + > exit > help -foo - makes a foo appear -bar - fandoggles a bar -sub - enter sub-menu -help - print this help text. +AVAILABLE ITEMS: + foo [ ] [ --verbose ] [ --level=INT ] - Makes a foo appear. + bar - fandoggles a bar + sub - enter sub-menu + help [ ] - Show this help, or get help on a specific command. + > ^C -$ +user@host: ~/menu $ ``` + +## Using + +See `examples/simple.rs` for a working example that runs on Linux or Windows. Here's the menu definition from that example: + +```rust +const ROOT_MENU: Menu = Menu { + label: "root", + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_foo, + parameters: &[ + Parameter::Mandatory { + parameter_name: "a", + help: Some("This is the help text for 'a'"), + }, + Parameter::Optional { + parameter_name: "b", + help: None, + }, + Parameter::Named { + parameter_name: "verbose", + help: None, + }, + Parameter::NamedValue { + parameter_name: "level", + argument_name: "INT", + help: Some("Set the level of the dangle"), + }, + ], + }, + command: "foo", + help: Some( + "Makes a foo appear. + +This is some extensive help text. + +It contains multiple paragraphs and should be preceeded by the parameter list. +", + ), + }, + &Item { + item_type: ItemType::Callback { + function: select_bar, + parameters: &[], + }, + command: "bar", + help: Some("fandoggles a bar"), + }, + &Item { + item_type: ItemType::Menu(&Menu { + label: "sub", + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_baz, + parameters: &[], + }, + command: "baz", + help: Some("thingamobob a baz"), + }, + &Item { + item_type: ItemType::Callback { + function: select_quux, + parameters: &[], + }, + command: "quux", + help: Some("maximum quux"), + }, + ], + entry: Some(enter_sub), + exit: Some(exit_sub), + }), + command: "sub", + help: Some("enter sub-menu"), + }, + ], + entry: Some(enter_root), + exit: Some(exit_root), +}; + +``` + +## Changelog + +### Unreleased changes + +* Parameter / Argument support +* Re-worked help text system +* Example uses `pancurses` + +### v0.1.0 + +* First release diff --git a/examples/simple.rs b/examples/simple.rs index 6b2a5ca..e450108 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -11,17 +11,34 @@ const ROOT_MENU: Menu = Menu { item_type: ItemType::Callback { function: select_foo, parameters: &[ - Parameter::Mandatory("a"), - Parameter::Optional("b"), - Parameter::Named("verbose"), + Parameter::Mandatory { + parameter_name: "a", + help: Some("This is the help text for 'a'"), + }, + Parameter::Optional { + parameter_name: "b", + help: None, + }, + Parameter::Named { + parameter_name: "verbose", + help: None, + }, Parameter::NamedValue { parameter_name: "level", argument_name: "INT", + help: Some("Set the level of the dangle"), }, ], }, command: "foo", - help: Some("makes a foo appear"), + help: Some( + "Makes a foo appear. + +This is some extensive help text. + +It contains multiple paragraphs and should be preceeded by the parameter list. +", + ), }, &Item { item_type: ItemType::Callback { diff --git a/src/lib.rs b/src/lib.rs index 7d5afc8..cbfd11b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,17 +15,34 @@ pub type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &[&str], c /// Describes a parameter to the command pub enum Parameter<'a> { /// A mandatory positional parameter - Mandatory(&'a str), + Mandatory { + /// A name for this mandatory positional parameter + parameter_name: &'a str, + /// Help text + help: Option<&'a str>, + }, /// An optional positional parameter. Must come after the mandatory positional arguments. - Optional(&'a str), - /// A named parameter with no argument (e.g. `--verbose` or `--dry-run`) - Named(&'a str), - /// A named parameter with argument (e.g. `--mode=foo` or `--level=3`) + Optional { + /// A name for this optional positional parameter + parameter_name: &'a str, + /// Help text + help: Option<&'a str>, + }, + /// An optional named parameter with no argument (e.g. `--verbose` or `--dry-run`) + Named { + /// The bit that comes after the `--` + parameter_name: &'a str, + /// Help text + help: Option<&'a str>, + }, + /// A optional named parameter with argument (e.g. `--mode=foo` or `--level=3`) NamedValue { /// The bit that comes after the `--` parameter_name: &'a str, /// The bit that comes after the `--name=`, e.g. `INT` or `FILE`. It's mostly for help text. argument_name: &'a str, + /// Help text + help: Option<&'a str>, }, } @@ -119,20 +136,20 @@ pub fn argument_finder<'a, T>( let mut optional_count = 0; for param in parameters.iter() { match param { - Parameter::Mandatory(name) => { + Parameter::Mandatory { parameter_name, .. } => { mandatory_count += 1; - if *name == name_to_find { + if *parameter_name == name_to_find { found_param = Some((param, mandatory_count)); } } - Parameter::Optional(name) => { + Parameter::Optional { parameter_name, .. } => { optional_count += 1; - if *name == name_to_find { + if *parameter_name == name_to_find { found_param = Some((param, optional_count)); } } - Parameter::Named(name) => { - if *name == name_to_find { + Parameter::Named { parameter_name, .. } => { + if *parameter_name == name_to_find { found_param = Some((param, 0)); } } @@ -146,41 +163,37 @@ pub fn argument_finder<'a, T>( // Step 2 - What sort of parameter is it? match found_param { // Step 2a - Mandatory Positional - Some((Parameter::Mandatory(_name), mandatory_idx)) => { - // We want positional parameter number `mandatory_idx` of `mandatory_count`. + Some((Parameter::Mandatory { .. }, mandatory_idx)) => { + // We want positional parameter number `mandatory_idx`. let mut positional_args_seen = 0; - for arg in argument_list { - if !arg.starts_with("--") { - // Positional - positional_args_seen += 1; - if positional_args_seen == mandatory_idx { - return Ok(Some(arg)); - } + for arg in argument_list.iter().filter(|x| !x.starts_with("--")) { + // Positional + positional_args_seen += 1; + if positional_args_seen == mandatory_idx { + return Ok(Some(arg)); } } // Valid thing to ask for but we don't have it Ok(None) } // Step 2b - Optional Positional - Some((Parameter::Optional(_name), optional_idx)) => { - // We want positional parameter number `mandatory_idx` of `mandatory_count`. + Some((Parameter::Optional { .. }, optional_idx)) => { + // We want positional parameter number `mandatory_count + optional_idx`. let mut positional_args_seen = 0; - for arg in argument_list { - if !arg.starts_with("--") { - // Positional - positional_args_seen += 1; - if positional_args_seen == (mandatory_count + optional_idx) { - return Ok(Some(arg)); - } + for arg in argument_list.iter().filter(|x| !x.starts_with("--")) { + // Positional + positional_args_seen += 1; + if positional_args_seen == (mandatory_count + optional_idx) { + return Ok(Some(arg)); } } // Valid thing to ask for but we don't have it Ok(None) } // Step 2c - Named (e.g. `--verbose`) - Some((Parameter::Named(name), _)) => { + Some((Parameter::Named { parameter_name, .. }, _)) => { for arg in argument_list { - if arg.starts_with("--") && (&arg[2..] == *name) { + if arg.starts_with("--") && (&arg[2..] == *parameter_name) { return Ok(Some("")); } } @@ -269,8 +282,9 @@ where return; } let outcome = if input == 0x0D { - writeln!(self.context).unwrap(); - self.process_command() + // Handle the command + self.process_command(); + Outcome::CommandProcessed } else if (input == 0x08) || (input == 0x7F) { // Handling backspace or delete if self.used > 0 { @@ -311,42 +325,48 @@ where } /// Scan the buffer and do the right thing based on its contents. - fn process_command(&mut self) -> Outcome { + fn process_command(&mut self) { + // Go to the next line, below the prompt + writeln!(self.context).unwrap(); if let Ok(command_line) = core::str::from_utf8(&self.buffer[0..self.used]) { // We have a valid string - if command_line == "help" { + let mut parts = command_line.split_whitespace(); + if let Some(cmd) = parts.next() { let menu = self.menus[self.depth].unwrap(); - for item in menu.items { - self.print_help(&item); - } - if self.depth != 0 { - let item = Item { - command: "exit", - help: Some("leave this menu"), - item_type: ItemType::_Dummy, - }; - self.print_help(&item); - } - let item = Item { - command: "help", - help: Some("show this help"), - item_type: ItemType::_Dummy, - }; - self.print_help(&item); - Outcome::CommandProcessed - } else if command_line == "exit" && self.depth != 0 { - if self.depth == self.menus.len() { - writeln!(self.context, "Can't enter menu - structure too deep.").unwrap(); - } else { + if cmd == "help" { + match parts.next() { + Some(arg) => match menu.items.iter().find(|i| i.command == arg) { + Some(item) => { + self.print_long_help(&item); + } + None => { + writeln!(self.context, "I can't help with {:?}", arg).unwrap(); + } + }, + _ => { + writeln!(self.context, "AVAILABLE ITEMS:").unwrap(); + for item in menu.items { + self.print_short_help(&item); + } + if self.depth != 0 { + self.print_short_help(&Item { + command: "exit", + help: Some("Leave this menu."), + item_type: ItemType::_Dummy, + }); + } + self.print_short_help(&Item { + command: "help [ ]", + help: Some("Show this help, or get help on a specific command."), + item_type: ItemType::_Dummy, + }); + } + } + } else if cmd == "exit" && self.depth != 0 { self.menus[self.depth] = None; self.depth -= 1; - } - Outcome::CommandProcessed - } else { - let mut parts = command_line.split(' '); - if let Some(cmd) = parts.next() { + } else { let mut found = false; - let menu = self.menus[self.depth].unwrap(); for item in menu.items { if cmd == item.command { match item.item_type { @@ -376,59 +396,151 @@ where if !found { writeln!(self.context, "Command {:?} not found. Try 'help'.", cmd).unwrap(); } - Outcome::CommandProcessed - } else { - writeln!(self.context, "Input empty").unwrap(); - Outcome::CommandProcessed } + } else { + writeln!(self.context, "Input was empty?").unwrap(); } } else { // Hmm .. we did not have a valid string writeln!(self.context, "Input was not valid UTF-8").unwrap(); - Outcome::CommandProcessed } } - fn print_help(&mut self, item: &Item) { + fn print_short_help(&mut self, item: &Item) { match item.item_type { ItemType::Callback { parameters, .. } => { + write!(self.context, "\t{}", item.command).unwrap(); if !parameters.is_empty() { - write!(self.context, "{}", item.command).unwrap(); for param in parameters.iter() { match param { - Parameter::Mandatory(name) => { - write!(self.context, " <{}>", name).unwrap(); + Parameter::Mandatory { parameter_name, .. } => { + write!(self.context, " <{}>", parameter_name).unwrap(); } - Parameter::Optional(name) => { - write!(self.context, " [ <{}> ]", name).unwrap(); + Parameter::Optional { parameter_name, .. } => { + write!(self.context, " [ <{}> ]", parameter_name).unwrap(); } - Parameter::Named(name) => { - write!(self.context, " [ --{} ]", name).unwrap(); + Parameter::Named { parameter_name, .. } => { + write!(self.context, " [ --{} ]", parameter_name).unwrap(); } Parameter::NamedValue { parameter_name, argument_name, + .. } => { write!(self.context, " [ --{}={} ]", parameter_name, argument_name) .unwrap(); } } } - } else { - write!(self.context, "{}", item.command).unwrap(); } } ItemType::Menu(_menu) => { - write!(self.context, "{}", item.command).unwrap(); + write!(self.context, "\t{}", item.command).unwrap(); } ItemType::_Dummy => { - write!(self.context, "{}", item.command).unwrap(); + write!(self.context, "\t{}", item.command).unwrap(); } } if let Some(help) = item.help { - write!(self.context, " - {}", help).unwrap(); + let mut help_line_iter = help.split('\n'); + writeln!(self.context, " - {}", help_line_iter.next().unwrap()).unwrap(); + } + } + + fn print_long_help(&mut self, item: &Item) { + writeln!(self.context, "SUMMARY:").unwrap(); + match item.item_type { + ItemType::Callback { parameters, .. } => { + write!(self.context, "\t{}", item.command).unwrap(); + if !parameters.is_empty() { + for param in parameters.iter() { + match param { + Parameter::Mandatory { parameter_name, .. } => { + write!(self.context, " <{}>", parameter_name).unwrap(); + } + Parameter::Optional { parameter_name, .. } => { + write!(self.context, " [ <{}> ]", parameter_name).unwrap(); + } + Parameter::Named { parameter_name, .. } => { + write!(self.context, " [ --{} ]", parameter_name).unwrap(); + } + Parameter::NamedValue { + parameter_name, + argument_name, + .. + } => { + write!(self.context, " [ --{}={} ]", parameter_name, argument_name) + .unwrap(); + } + } + } + writeln!(self.context, "\n\nPARAMETERS:").unwrap(); + for param in parameters.iter() { + match param { + Parameter::Mandatory { + parameter_name, + help, + } => { + writeln!( + self.context, + "\t<{0}>\n\t\t- {1}", + parameter_name, + help.unwrap_or(""), + ) + .unwrap(); + } + Parameter::Optional { + parameter_name, + help, + } => { + writeln!( + self.context, + "\t<{0}>\n\t\t- {1}", + parameter_name, + help.unwrap_or("No help text found"), + ) + .unwrap(); + } + Parameter::Named { + parameter_name, + help, + } => { + writeln!( + self.context, + "\t--{0}\n\t\t- {1}", + parameter_name, + help.unwrap_or("No help text found"), + ) + .unwrap(); + } + Parameter::NamedValue { + parameter_name, + argument_name, + help, + } => { + writeln!( + self.context, + "\t--{0}={1}\n\t\t- {2}", + parameter_name, + argument_name, + help.unwrap_or("No help text found"), + ) + .unwrap(); + } + } + } + } + } + ItemType::Menu(_menu) => { + write!(self.context, "\t{}", item.command).unwrap(); + } + ItemType::_Dummy => { + write!(self.context, "\t{}", item.command).unwrap(); + } + } + if let Some(help) = item.help { + writeln!(self.context, "\n\nDESCRIPTION:\n{}", help).unwrap(); } - writeln!(self.context).unwrap(); } fn call_function( @@ -442,15 +554,15 @@ where let mandatory_parameter_count = parameters .iter() .filter(|p| match p { - Parameter::Mandatory(_) => true, + Parameter::Mandatory { .. } => true, _ => false, }) .count(); let positional_parameter_count = parameters .iter() .filter(|p| match p { - Parameter::Mandatory(_) => true, - Parameter::Optional(_) => true, + Parameter::Mandatory { .. } => true, + Parameter::Optional { .. } => true, _ => false, }) .count(); @@ -470,16 +582,16 @@ where let mut found = false; for param in parameters.iter() { match param { - Parameter::Named(name) => { - if &arg[2..] == *name { + Parameter::Named { parameter_name, .. } => { + if &arg[2..] == *parameter_name { found = true; break; } } Parameter::NamedValue { parameter_name, .. } => { if arg.contains('=') { - if let Some(name) = arg[2..].split('=').next() { - if name == *parameter_name { + if let Some(given_name) = arg[2..].split('=').next() { + if given_name == *parameter_name { found = true; break; } @@ -536,9 +648,18 @@ mod tests { item_type: ItemType::Callback { function: dummy, parameters: &[ - Parameter::Mandatory("foo"), - Parameter::Mandatory("bar"), - Parameter::Mandatory("baz"), + Parameter::Mandatory { + parameter_name: "foo", + help: Some("Some help for foo"), + }, + Parameter::Mandatory { + parameter_name: "bar", + help: Some("Some help for bar"), + }, + Parameter::Mandatory { + parameter_name: "baz", + help: Some("Some help for baz"), + }, ], }, }; @@ -566,9 +687,18 @@ mod tests { item_type: ItemType::Callback { function: dummy, parameters: &[ - Parameter::Mandatory("foo"), - Parameter::Mandatory("bar"), - Parameter::Optional("baz"), + Parameter::Mandatory { + parameter_name: "foo", + help: Some("Some help for foo"), + }, + Parameter::Mandatory { + parameter_name: "bar", + help: Some("Some help for bar"), + }, + Parameter::Optional { + parameter_name: "baz", + help: Some("Some help for baz"), + }, ], }, }; @@ -598,9 +728,18 @@ mod tests { item_type: ItemType::Callback { function: dummy, parameters: &[ - Parameter::Mandatory("foo"), - Parameter::Named("bar"), - Parameter::Named("baz"), + Parameter::Mandatory { + parameter_name: "foo", + help: Some("Some help for foo"), + }, + Parameter::Named { + parameter_name: "bar", + help: Some("Some help for bar"), + }, + Parameter::Named { + parameter_name: "baz", + help: Some("Some help for baz"), + }, ], }, }; @@ -633,11 +772,18 @@ mod tests { item_type: ItemType::Callback { function: dummy, parameters: &[ - Parameter::Mandatory("foo"), - Parameter::Named("bar"), + Parameter::Mandatory { + parameter_name: "foo", + help: Some("Some help for foo"), + }, + Parameter::Named { + parameter_name: "bar", + help: Some("Some help for bar"), + }, Parameter::NamedValue { parameter_name: "baz", - argument_name: "BAZ", + argument_name: "TEST", + help: Some("Some help for baz"), }, ], }, From 27ab5394d79ba85b7acac2f1a5ab06772fb66665 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 20:51:07 +0100 Subject: [PATCH 8/9] Ooops - get version number right. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 24792e3..093688b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "menu" -version = "0.1.0" +version = "0.1.1" authors = ["Jonathan 'theJPster' Pallant "] description = "A simple #[no_std] command line interface." license = "MIT OR Apache-2.0" From 6de62b5076e2c1fd83483e552122e284eed8ddfc Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sun, 11 Aug 2019 22:03:07 +0100 Subject: [PATCH 9/9] Replace tabs with two spaces, as Monotron is horizontally limited. --- src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index cbfd11b..bf9adec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -409,7 +409,7 @@ where fn print_short_help(&mut self, item: &Item) { match item.item_type { ItemType::Callback { parameters, .. } => { - write!(self.context, "\t{}", item.command).unwrap(); + write!(self.context, " {}", item.command).unwrap(); if !parameters.is_empty() { for param in parameters.iter() { match param { @@ -435,10 +435,10 @@ where } } ItemType::Menu(_menu) => { - write!(self.context, "\t{}", item.command).unwrap(); + write!(self.context, " {}", item.command).unwrap(); } ItemType::_Dummy => { - write!(self.context, "\t{}", item.command).unwrap(); + write!(self.context, " {}", item.command).unwrap(); } } if let Some(help) = item.help { @@ -451,7 +451,7 @@ where writeln!(self.context, "SUMMARY:").unwrap(); match item.item_type { ItemType::Callback { parameters, .. } => { - write!(self.context, "\t{}", item.command).unwrap(); + write!(self.context, " {}", item.command).unwrap(); if !parameters.is_empty() { for param in parameters.iter() { match param { @@ -483,7 +483,7 @@ where } => { writeln!( self.context, - "\t<{0}>\n\t\t- {1}", + " <{0}>\n - {1}", parameter_name, help.unwrap_or(""), ) @@ -495,7 +495,7 @@ where } => { writeln!( self.context, - "\t<{0}>\n\t\t- {1}", + " <{0}>\n - {1}", parameter_name, help.unwrap_or("No help text found"), ) @@ -507,7 +507,7 @@ where } => { writeln!( self.context, - "\t--{0}\n\t\t- {1}", + " --{0}\n - {1}", parameter_name, help.unwrap_or("No help text found"), ) @@ -520,7 +520,7 @@ where } => { writeln!( self.context, - "\t--{0}={1}\n\t\t- {2}", + " --{0}={1}\n - {2}", parameter_name, argument_name, help.unwrap_or("No help text found"), @@ -532,10 +532,10 @@ where } } ItemType::Menu(_menu) => { - write!(self.context, "\t{}", item.command).unwrap(); + write!(self.context, " {}", item.command).unwrap(); } ItemType::_Dummy => { - write!(self.context, "\t{}", item.command).unwrap(); + write!(self.context, " {}", item.command).unwrap(); } } if let Some(help) = item.help {