Merge branch 'automatic-help' into 'master'

Automatic help/usage

See merge request arkham/arkham!2
This commit is contained in:
Joe Bellus 2021-06-19 19:07:04 +00:00
commit 5a725c2aef
9 changed files with 148 additions and 28 deletions

22
README.md Normal file
View File

@ -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

View File

@ -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()));

View File

@ -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();
}

View File

@ -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<ActiveO
return run_command(app, cmd, &ignored, opts);
}
// Automatic command help display
if ignored.iter().any(|a| a == "-h" || a == "--help") {
super::command::print_command_help(cmd, &vec![]);
return Ok(());
}
// If any ignored parameters start with "-" we will throw an unknwon flag error.
if let Some(arg) = ignored.iter().find(|a| a.starts_with("-")) {
return Err(OptError::InvalidOpt(arg.clone()));
@ -215,7 +225,7 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
// Execute the command handler
if let Some(handler) = cmd.handler {
handler(app, &Context::new(opts.clone()), &ignored);
handler(app, &Context::new(cmd.clone(), opts.clone()), &ignored);
}
Ok(())

View File

@ -6,11 +6,14 @@ use crate::{
pub type Handler = fn(&App, &Context, &[String]);
#[derive(Clone)]
pub struct Command {
pub name: String,
pub commands: Vec<Command>,
pub handler: Option<Handler>,
pub opts: Vec<opt::Opt>,
pub long_desc: Option<String>,
pub short_desc: Option<String>,
}
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(),
)
}
}

View File

@ -1,21 +1,26 @@
use crate::opt::{ActiveOpt, OptKind};
use crate::{
opt::{ActiveOpt, OptKind},
Command,
};
#[derive(Debug)]
pub struct Context {
opts: Vec<ActiveOpt>,
cmd: Command,
}
impl Context {
pub(crate) fn new(opts: Vec<ActiveOpt>) -> Self {
Self { opts }
pub(crate) fn new(cmd: Command, opts: Vec<ActiveOpt>) -> 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<String> {
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![])
}
}

View File

@ -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::*;

View File

@ -8,6 +8,7 @@ pub struct Opt {
pub name: String,
pub short: String,
pub long: String,
pub desc: Option<String>,
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)]

View File

@ -1,4 +1,3 @@
use console::style;
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
use std::{
sync::{Arc, Mutex},