From 6409e249e31bc157b1b8d66bf59cfc31671a45e3 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Sat, 19 Jun 2021 19:07:04 +0000 Subject: [PATCH] Automatic help output Added help generation for root and subcommands either using `app help [command]` or `app --help [command]` This help text displays all opts and their descriptions, available subcommands and any description for the current command. --- README.md | 22 ++++++++++++++++++ benches/arg_parsing.rs | 16 +++---------- examples/fib.rs | 15 +++++++++--- src/app.rs | 16 ++++++++++--- src/command.rs | 53 +++++++++++++++++++++++++++++++++++++++++- src/context.rs | 18 ++++++++++---- src/lib.rs | 3 --- src/opt.rs | 32 +++++++++++++++++++++++++ src/tasks.rs | 1 - 9 files changed, 148 insertions(+), 28 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..331e55f --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +Arkham is a framework for building CLI tools and applications. It provides basic +building blocks for building attractive and smooth CLIs + + +# CLI Features + +## Option Parsing + +* Opt/Flag handling for short, long command line options +* Nested subcommands with their own flags +* Opts are hierarchal and can be utilized from parent commands +* Automatic usage details for subcommands and bare execution + +## Styling + +* Canned helper methods for generating colored and formatted outputs for common +structures: Detail lists, headers, etc. + +# Basic Usage + +* [fib](https://git.5sigma.io/arkham/arkham/-/blob/master/examples/fib.rs) - An example using subcommands and command line options + diff --git a/benches/arg_parsing.rs b/benches/arg_parsing.rs index 3f3f92b..e12573a 100644 --- a/benches/arg_parsing.rs +++ b/benches/arg_parsing.rs @@ -1,4 +1,4 @@ -use arkham::{App, Command, Opt, OptKind}; +use arkham::{App, Command, Opt}; use criterion::{criterion_group, criterion_main, Criterion}; fn parse_args(c: &mut Criterion) { @@ -11,20 +11,10 @@ fn parse_args(c: &mut Criterion) { "thing".into(), ]; let app = App::new() - .opt(Opt { - name: "user".into(), - short: "u".into(), - long: "user".into(), - kind: OptKind::String, - }) + .opt(Opt::scalar("user").short("u").long("user")) .command( Command::new("thing") - .opt(Opt { - name: "config".into(), - short: "c".into(), - long: "config".into(), - kind: OptKind::String, - }) + .opt(Opt::scalar("config").short("c").long("config")) .handler(|_, ctx, _| { assert_eq!(ctx.get_string("user"), Some("joe".into())); assert_eq!(ctx.get_string("config"), Some("c.json".into())); diff --git a/examples/fib.rs b/examples/fib.rs index bc0b512..f0237ff 100644 --- a/examples/fib.rs +++ b/examples/fib.rs @@ -1,11 +1,20 @@ -use arkham::{App, Context, Opt}; +use arkham::{App, Command, Context, Opt}; fn main() { let _ = App::new() .name("Fibonacci App") .version("1.0") - .opt(Opt::scalar("count").short("n").long("num")) - .handler(fibonacci_handler) + .command( + Command::new("fib") + .opt( + Opt::scalar("count") + .short("n") + .long("num") + .desc("The index of the fibonacci number to return"), + ) + .short_desc("Calculates a fibonacci number") + .handler(fibonacci_handler), + ) .run() .unwrap(); } diff --git a/src/app.rs b/src/app.rs index 6fd94bb..333c9eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -35,7 +35,11 @@ impl App { impl App { /// Contructs a new App instance which can have opts defined and subcommands attached. pub fn new() -> Self { - App::default().command(Command::new("help").handler(help)) + App::default().command( + Command::new("help") + .handler(help) + .short_desc("Displays help information"), + ) } /// Sets the name of the application. If not set the cargo package name will be used. @@ -151,7 +155,7 @@ impl App { /// } /// ``` pub fn run(&mut self) -> Result<()> { - self.run_with(env::args().collect()) + self.run_with(env::args().skip(1).collect()) } } @@ -208,6 +212,12 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec, pub handler: Option, pub opts: Vec, + pub long_desc: Option, + pub short_desc: Option, } impl Command { @@ -20,6 +23,8 @@ impl Command { commands: vec![], handler: None, opts: vec![], + long_desc: None, + short_desc: None, } } @@ -32,8 +37,54 @@ impl Command { self.opts.push(opt); self } + + pub fn short_desc(mut self, short_desc: &str) -> Self { + self.short_desc = Some(short_desc.into()); + self + } + + pub fn long_desc(mut self, long_desc: &str) -> Self { + self.short_desc = Some(long_desc.into()); + self + } } -pub fn help(app: &App, _ctx: &Context, _args: &[String]) { +pub(crate) fn help(app: &App, _ctx: &Context, args: &[String]) { vox::print(app.application_header()); + print_command_help(&app.root, args); +} + +pub(crate) fn print_command_help(cmd: &Command, args: &[String]) { + if !args.is_empty() { + if let Some(cmd) = cmd.commands.iter().find(|c| Some(&c.name) == args.first()) { + return print_command_help(cmd, &args[1..]); + } + } + vox::print(""); + if let Some(desc) = cmd.long_desc.as_ref().or(cmd.short_desc.as_ref()) { + if cmd.name != "root" { + vox::header(&cmd.name.to_uppercase()); + } + vox::print(desc); + vox::print(""); + } + if !cmd.opts.is_empty() { + vox::header("OPTIONS"); + vox::description_list( + cmd.opts + .iter() + .map(|o| (o.usage(), o.desc.clone().unwrap_or_default())) + .collect(), + ) + } + + if !cmd.commands.is_empty() { + vox::header("Subcommands"); + vox::description_list( + cmd.commands + .iter() + .map(|c| (c.name.clone(), c.short_desc.clone().unwrap_or_default())) + .collect(), + ) + } } diff --git a/src/context.rs b/src/context.rs index 0ec5c4e..17f8463 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,21 +1,26 @@ -use crate::opt::{ActiveOpt, OptKind}; +use crate::{ + opt::{ActiveOpt, OptKind}, + Command, +}; -#[derive(Debug)] pub struct Context { opts: Vec, + cmd: Command, } impl Context { - pub(crate) fn new(opts: Vec) -> Self { - Self { opts } + pub(crate) fn new(cmd: Command, opts: Vec) -> Self { + Self { opts, cmd } } + /// Checks for the existance of a flag pub fn flag(&self, name: &str) -> bool { self.opts .iter() .any(|o| o.definition.name == name && matches!(o.definition.kind, OptKind::Flag)) } + /// Returns the value of an option if one exists pub fn get_string(&self, name: &str) -> Option { self.opts .iter() @@ -28,4 +33,9 @@ impl Context { }) .flatten() } + + /// Can be used to display the automatic help message for the current command. + pub fn display_help(&self) { + crate::command::print_command_help(&self.cmd, &vec![]) + } } diff --git a/src/lib.rs b/src/lib.rs index 5f387c9..e836237 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,8 @@ mod app; mod command; mod context; mod opt; -// mod style; -// mod tasks; pub mod vox; pub use app::*; pub use command::*; pub use context::*; pub use opt::*; -// pub use style::*; diff --git a/src/opt.rs b/src/opt.rs index dd6d457..6b6bbe9 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -8,6 +8,7 @@ pub struct Opt { pub name: String, pub short: String, pub long: String, + pub desc: Option, pub(crate) kind: OptKind, } @@ -25,6 +26,7 @@ impl Opt { short: "".into(), long: "".into(), kind: OptKind::Flag, + desc: None, } } @@ -41,6 +43,7 @@ impl Opt { short: "".into(), long: "".into(), kind: OptKind::String, + desc: None, } } @@ -67,6 +70,35 @@ impl Opt { self.long = long.into(); self } + + /// Sets the description for the option. This is displayed when listing via help commands + /// + /// Example: + /// ```rust + /// use arkham::{Opt, App}; + /// App::new() + /// .opt( + /// Opt::scalar("user") + /// .short("u") + /// .long("user") + /// .desc("The user to perform the action against") + /// ); + ///``` + pub fn desc(mut self, desc: &str) -> Self { + self.desc = Some(desc.into()); + self + } + + pub(crate) fn usage(&self) -> String { + match self.kind { + OptKind::Flag => { + format!("-{}, --{}", self.short, self.long) + } + OptKind::String => { + format!("-{} [value], --{} [value]", self.short, self.long) + } + } + } } #[derive(Clone, Debug)] diff --git a/src/tasks.rs b/src/tasks.rs index ccbb518..5666838 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,4 +1,3 @@ -use console::style; use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle}; use std::{ sync::{Arc, Mutex},