From 1b747133e6b6b02e91486c26651c81309bf94624 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Sat, 13 Nov 2021 21:32:30 -0500 Subject: [PATCH] General imrovements Added app! macro that can extract package name and version. Opts can be parsed for the application root prior to launching handling functions. Added further integration with the console create Improved display of description_lists, these now wrap the description text. Long and short opts are now optional and one or both can be used. --- Cargo.lock | 56 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 +++- src/app.rs | 70 ++++++++++++++++++++++++++++++++++++++++---------- src/command.rs | 11 +++++++- src/context.rs | 2 +- src/lib.rs | 4 +++ src/macros.rs | 8 ++++++ src/opt.rs | 42 ++++++++++++++++++++++-------- src/vox.rs | 63 +++++++++++++++++++++++++++++++++++++++++---- 9 files changed, 228 insertions(+), 33 deletions(-) create mode 100644 src/macros.rs diff --git a/Cargo.lock b/Cargo.lock index fb2677d..890f8a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "arkham" version = "0.1.1" @@ -10,6 +19,7 @@ dependencies = [ "criterion", "serde", "serde_json", + "textwrap 0.14.2", "toml", ] @@ -76,7 +86,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "bitflags", - "textwrap", + "textwrap 0.11.0", "unicode-width", ] @@ -312,6 +322,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "oorandom" version = "11.1.3" @@ -404,7 +420,10 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", + "thread_local", ] [[package]] @@ -508,6 +527,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "syn" version = "1.0.73" @@ -538,6 +563,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -563,6 +608,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + [[package]] name = "unicode-width" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index fc2e1b7..e3bb079 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,14 @@ console = "0.14.0" serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true} toml = { version="0.5.8", optional = true} +textwrap = "0.14.2" + [features] -config = ["serde"] +config = ["serialize"] config_toml = ["toml", "config"] config_json = ["serde_json", "config"] +serialize = ["serde"] [dev-dependencies] criterion = "0.3" diff --git a/src/app.rs b/src/app.rs index cebf0cf..5b58956 100644 --- a/src/app.rs +++ b/src/app.rs @@ -110,6 +110,29 @@ impl App { self } + /// Similar to `App::Command` but allows multiple commands to be added at once. + /// Adds a root level commands to the application. This command can then be executed with: + /// + /// myapp command_name + /// + /// Help flags will also be generated for the command which will display command + /// information for: + /// + /// myapp --help command_name or myapp help command_name + /// + /// Example: + /// ```rust + /// use arkham::{App, Command, Context}; + /// let names = vec!["one", "two", "three"]; + /// let commands = names.into_iter().map(|name| Command::new(name).handler(my_handler)).collect(); + /// App::new().commands(commands); + /// + /// fn my_handler(app: &App, ctx: &Context, args: &[String]) {} + /// ``` + pub fn commands(self, commands: Vec) -> Self { + commands.into_iter().fold(self, |app, cmd| app.command(cmd)) + } + /// Adds a root level opt/flag that is available to all commands. Opts are given a name which /// is used to reference them, as well as a short and long identifier. /// @@ -152,9 +175,7 @@ impl App { self } - /// Execute the app and any specified handlers based on the passed arguemnts. This function is - /// mostly used for testing or any situation where you need to pass arbitrary arguments instead - /// of using the ones passed to the application. + /// Execute the app and any specified handlers based on the passed arguemnts. This function is mostly used for testing or any situation where you need to pass arbitrary arguments instead of using the ones passed to the application. /// Example: /// ```rust /// use arkham::{App, Command, Context, Opt}; @@ -184,8 +205,7 @@ impl App { } } - /// Execute the app and any specified handlers based on the arguments passsed to the - /// application. + /// Execute the app and any specified handlers based on the arguments passsed to the application. /// /// Example: /// running with myapp --name alice @@ -203,11 +223,17 @@ impl App { pub fn run(&mut self) -> Result<()> { self.run_with(env::args().skip(1).collect()) } + + /// Generates a context for the root command allowing flags to be parsed. This can be useful to predetect certian root level arguments before processing sub commands and other app configurations. + pub fn root_context(&self) -> Result { + let mut ctx = Context::new(self.root.clone()); + let args = env::args().skip(1).collect::>(); + populate_args(&mut ctx, &self.root, &args)?; + Ok(ctx) + } } -/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run -/// and App::run_with functions. -fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> { +fn populate_args(ctx: &mut Context, cmd: &Command, args: &[String]) -> Result> { // Get an iterator for the incomming arguments let mut args = args.iter(); // We will keep track of any arguments that arent consumed by the current command. @@ -218,7 +244,11 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> while let Some(arg) = args.next() { // Check for long args if arg.starts_with("--") { - if let Some(opt) = cmd.opts.iter().find(|o| o.long == arg[2..]) { + if let Some(opt) = cmd + .opts + .iter() + .find(|o| o.long == Some(arg[2..].to_string())) + { match opt.kind { OptKind::Flag => { ctx.set_flag(opt); @@ -237,9 +267,13 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> continue; } // Check for short args - if arg.starts_with("-") { + if arg.starts_with('-') { for switch in arg.chars().skip(1) { - if let Some(opt) = cmd.opts.iter().find(|o| o.short == switch.to_string()) { + if let Some(opt) = cmd + .opts + .iter() + .find(|o| o.short == Some(switch.to_string())) + { match opt.kind { OptKind::Flag => { ctx.set_flag(opt); @@ -261,6 +295,14 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> ignored.push(arg.clone()); } + Ok(ignored) +} + +/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run +/// and App::run_with functions. +fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> { + let mut ignored = populate_args(ctx, cmd, args)?; + // Find an recurse into sub commands if the remaining argumetns match any subcommand name if let Some(cmd) = cmd .commands @@ -381,9 +423,9 @@ mod tests { .opt(Opt::flag("thingB").short("b").long("b")) .opt(Opt::flag("thingC").short("c").long("c")) .handler(|_, ctx, _| { - assert_eq!(ctx.flag("thingA"), true); - assert_eq!(ctx.flag("thingB"), true); - assert_eq!(ctx.flag("thingC"), true); + assert!(ctx.flag("thingA")); + assert!(ctx.flag("thingB")); + assert!(ctx.flag("thingC")); }) .run_with(vec!["-abc".into()]) .unwrap(); diff --git a/src/command.rs b/src/command.rs index 5b62b57..08b86ef 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::{ context::Context, opt::{self, Opt}, @@ -16,6 +18,12 @@ pub struct Command { pub short_desc: Option, } +impl Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").field("name", &self.name).finish() + } +} + impl Command { pub fn new>(name: T) -> Self { Command { @@ -79,7 +87,7 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) { } if !cmd.commands.is_empty() { - vox::header("Subcommands"); + vox::header("Commands"); vox::description_list( cmd.commands .iter() @@ -87,4 +95,5 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) { .collect(), ) } + std::process::exit(1); } diff --git a/src/context.rs b/src/context.rs index 31995cf..34a4aba 100644 --- a/src/context.rs +++ b/src/context.rs @@ -136,7 +136,7 @@ impl From for ContextValue { } pub struct Context { - pub(crate) cmd: Command, + pub cmd: Command, pub(crate) config_data: Map, pub(crate) env_prefix: Option, pub(crate) env_enabled: bool, diff --git a/src/lib.rs b/src/lib.rs index c85a1e1..0e3cccd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod macros; + mod app; mod command; mod context; @@ -8,3 +11,4 @@ pub use app::*; pub use command::*; pub use context::*; pub use opt::*; +pub use vox::*; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..15dfae2 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,8 @@ +#[macro_export] +macro_rules! app { + () => { + App::default() + .version(env!("CARGO_PKG_VERSION")) + .name(env!("CARGO_PKG_NAME")) + }; +} diff --git a/src/opt.rs b/src/opt.rs index 6a59416..444e685 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -6,8 +6,8 @@ pub enum OptError { #[derive(Clone, Debug)] pub struct Opt { pub name: String, - pub short: String, - pub long: String, + pub short: Option, + pub long: Option, pub desc: Option, pub(crate) kind: OptKind, } @@ -23,8 +23,8 @@ impl Opt { pub fn flag(name: &str) -> Self { Self { name: name.into(), - short: "".into(), - long: "".into(), + short: None, + long: None, kind: OptKind::Flag, desc: None, } @@ -40,8 +40,8 @@ impl Opt { pub fn scalar(name: &str) -> Self { Self { name: name.into(), - short: "".into(), - long: "".into(), + short: None, + long: None, kind: OptKind::String, desc: None, } @@ -55,7 +55,7 @@ impl Opt { /// App::new().opt(Opt::scalar("user").short("u").long("user")); ///``` pub fn short(mut self, short: &str) -> Self { - self.short = short.into(); + self.short = Some(short.into()); self } @@ -67,7 +67,7 @@ impl Opt { /// App::new().opt(Opt::scalar("user").short("u").long("user")); ///``` pub fn long(mut self, long: &str) -> Self { - self.long = long.into(); + self.long = Some(long.into()); self } @@ -92,10 +92,32 @@ impl Opt { pub(crate) fn usage(&self) -> String { match self.kind { OptKind::Flag => { - format!("-{}, --{}", self.short, self.long) + let short = self + .short + .as_ref() + .map(|s| format!("-{}", s)) + .unwrap_or_else(String::new); + + let long = self + .long + .as_ref() + .map(|s| format!("--{}", s)) + .unwrap_or_else(String::new); + vec![short, long].join(",").trim_matches(',').to_string() } OptKind::String => { - format!("-{} [value], --{} [value]", self.short, self.long) + let short = self + .short + .as_ref() + .map(|s| format!("-{} [value]", s)) + .unwrap_or_else(String::new); + + let long = self + .long + .as_ref() + .map(|s| format!("--{} [value]", s)) + .unwrap_or_else(String::new); + vec![short, long].join(",").trim_matches(',').to_string() } } } diff --git a/src/vox.rs b/src/vox.rs index b8bf027..0a09538 100644 --- a/src/vox.rs +++ b/src/vox.rs @@ -2,6 +2,41 @@ use console::style; use std::collections::HashMap; use std::fmt::Display; +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Copy)] +pub enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + Color256(u8), +} + +impl From for console::Color { + fn from(fr: Color) -> Self { + match fr { + Color::Black => Self::Black, + Color::Red => Self::Red, + Color::Green => Self::Green, + Color::Yellow => Self::Yellow, + Color::Blue => Self::Blue, + Color::Magenta => Self::Magenta, + Color::Cyan => Self::Cyan, + Color::White => Self::White, + Color::Color256(v) => Self::Color256(v), + } + } +} + +pub fn labeled(color: Color, label: &str, msg: &str) { + let label = format!("[{}]", label); + println!("{} {}", console::style(label).fg(color.into()).bold(), msg); +} + pub fn message(str: T) { println!("{}", style(str).white().bold()); } @@ -20,11 +55,29 @@ pub fn note(str: T) { } pub fn description_list(list: HashMap) { - let max_length = list.keys().map(|v| v.len()).max().unwrap_or(10) + 3; - for (name, desc) in list { - let spaced_name = format!("{:width$}", name, width = max_length); - println!("{}{}", style(spaced_name).bold(), style(desc).dim()) - } + let mut lines = list.into_iter().collect::>(); + let key_max_length = lines.iter().map(|(v, _)| v.len()).max().unwrap_or(10) + 3; + let desc_max_length = 80 - key_max_length; + let indent_string = " ".repeat(key_max_length); + lines.sort_by(|(a, _), (b, _)| a.cmp(b)); + let output = lines.iter().fold(String::new(), |output, (name, desc)| { + let spaced_key = format!("{:width$}", name, width = key_max_length); + let mut desc_lines: Vec = textwrap::fill(desc, desc_max_length) + .split('\n') + .map(|s| s.to_string()) + .collect(); + desc_lines.reverse(); + let first_line = desc_lines.pop().unwrap_or_else(String::new); + desc_lines.reverse(); + output + + &format!("{}{}", style(spaced_key).bold(), style(first_line).dim()) + + "\n" + + &desc_lines + .iter() + .map(|s| indent_string.clone() + s + "\n") + .collect::() + }); + println!("{}", output); } pub fn print(s: T) {