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.
This commit is contained in:
parent
25badbf51b
commit
6409e249e3
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use arkham::{App, Command, Opt, OptKind};
|
use arkham::{App, Command, Opt};
|
||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
fn parse_args(c: &mut Criterion) {
|
fn parse_args(c: &mut Criterion) {
|
||||||
|
@ -11,20 +11,10 @@ fn parse_args(c: &mut Criterion) {
|
||||||
"thing".into(),
|
"thing".into(),
|
||||||
];
|
];
|
||||||
let app = App::new()
|
let app = App::new()
|
||||||
.opt(Opt {
|
.opt(Opt::scalar("user").short("u").long("user"))
|
||||||
name: "user".into(),
|
|
||||||
short: "u".into(),
|
|
||||||
long: "user".into(),
|
|
||||||
kind: OptKind::String,
|
|
||||||
})
|
|
||||||
.command(
|
.command(
|
||||||
Command::new("thing")
|
Command::new("thing")
|
||||||
.opt(Opt {
|
.opt(Opt::scalar("config").short("c").long("config"))
|
||||||
name: "config".into(),
|
|
||||||
short: "c".into(),
|
|
||||||
long: "config".into(),
|
|
||||||
kind: OptKind::String,
|
|
||||||
})
|
|
||||||
.handler(|_, ctx, _| {
|
.handler(|_, ctx, _| {
|
||||||
assert_eq!(ctx.get_string("user"), Some("joe".into()));
|
assert_eq!(ctx.get_string("user"), Some("joe".into()));
|
||||||
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
|
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
use arkham::{App, Context, Opt};
|
use arkham::{App, Command, Context, Opt};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let _ = App::new()
|
let _ = App::new()
|
||||||
.name("Fibonacci App")
|
.name("Fibonacci App")
|
||||||
.version("1.0")
|
.version("1.0")
|
||||||
.opt(Opt::scalar("count").short("n").long("num"))
|
.command(
|
||||||
.handler(fibonacci_handler)
|
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()
|
.run()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
16
src/app.rs
16
src/app.rs
|
@ -35,7 +35,11 @@ impl App {
|
||||||
impl App {
|
impl App {
|
||||||
/// Contructs a new App instance which can have opts defined and subcommands attached.
|
/// Contructs a new App instance which can have opts defined and subcommands attached.
|
||||||
pub fn new() -> Self {
|
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.
|
/// 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<()> {
|
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);
|
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 any ignored parameters start with "-" we will throw an unknwon flag error.
|
||||||
if let Some(arg) = ignored.iter().find(|a| a.starts_with("-")) {
|
if let Some(arg) = ignored.iter().find(|a| a.starts_with("-")) {
|
||||||
return Err(OptError::InvalidOpt(arg.clone()));
|
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
|
// Execute the command handler
|
||||||
if let Some(handler) = cmd.handler {
|
if let Some(handler) = cmd.handler {
|
||||||
handler(app, &Context::new(opts.clone()), &ignored);
|
handler(app, &Context::new(cmd.clone(), opts.clone()), &ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -6,11 +6,14 @@ use crate::{
|
||||||
|
|
||||||
pub type Handler = fn(&App, &Context, &[String]);
|
pub type Handler = fn(&App, &Context, &[String]);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub commands: Vec<Command>,
|
pub commands: Vec<Command>,
|
||||||
pub handler: Option<Handler>,
|
pub handler: Option<Handler>,
|
||||||
pub opts: Vec<opt::Opt>,
|
pub opts: Vec<opt::Opt>,
|
||||||
|
pub long_desc: Option<String>,
|
||||||
|
pub short_desc: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
@ -20,6 +23,8 @@ impl Command {
|
||||||
commands: vec![],
|
commands: vec![],
|
||||||
handler: None,
|
handler: None,
|
||||||
opts: vec![],
|
opts: vec![],
|
||||||
|
long_desc: None,
|
||||||
|
short_desc: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,8 +37,54 @@ impl Command {
|
||||||
self.opts.push(opt);
|
self.opts.push(opt);
|
||||||
self
|
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());
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
use crate::opt::{ActiveOpt, OptKind};
|
use crate::{
|
||||||
|
opt::{ActiveOpt, OptKind},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
opts: Vec<ActiveOpt>,
|
opts: Vec<ActiveOpt>,
|
||||||
|
cmd: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub(crate) fn new(opts: Vec<ActiveOpt>) -> Self {
|
pub(crate) fn new(cmd: Command, opts: Vec<ActiveOpt>) -> Self {
|
||||||
Self { opts }
|
Self { opts, cmd }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks for the existance of a flag
|
||||||
pub fn flag(&self, name: &str) -> bool {
|
pub fn flag(&self, name: &str) -> bool {
|
||||||
self.opts
|
self.opts
|
||||||
.iter()
|
.iter()
|
||||||
.any(|o| o.definition.name == name && matches!(o.definition.kind, OptKind::Flag))
|
.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> {
|
pub fn get_string(&self, name: &str) -> Option<String> {
|
||||||
self.opts
|
self.opts
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -28,4 +33,9 @@ impl Context {
|
||||||
})
|
})
|
||||||
.flatten()
|
.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![])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,8 @@ mod app;
|
||||||
mod command;
|
mod command;
|
||||||
mod context;
|
mod context;
|
||||||
mod opt;
|
mod opt;
|
||||||
// mod style;
|
|
||||||
// mod tasks;
|
|
||||||
pub mod vox;
|
pub mod vox;
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use command::*;
|
pub use command::*;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
pub use opt::*;
|
pub use opt::*;
|
||||||
// pub use style::*;
|
|
||||||
|
|
32
src/opt.rs
32
src/opt.rs
|
@ -8,6 +8,7 @@ pub struct Opt {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub short: String,
|
pub short: String,
|
||||||
pub long: String,
|
pub long: String,
|
||||||
|
pub desc: Option<String>,
|
||||||
pub(crate) kind: OptKind,
|
pub(crate) kind: OptKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ impl Opt {
|
||||||
short: "".into(),
|
short: "".into(),
|
||||||
long: "".into(),
|
long: "".into(),
|
||||||
kind: OptKind::Flag,
|
kind: OptKind::Flag,
|
||||||
|
desc: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ impl Opt {
|
||||||
short: "".into(),
|
short: "".into(),
|
||||||
long: "".into(),
|
long: "".into(),
|
||||||
kind: OptKind::String,
|
kind: OptKind::String,
|
||||||
|
desc: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +70,35 @@ impl Opt {
|
||||||
self.long = long.into();
|
self.long = long.into();
|
||||||
self
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use console::style;
|
|
||||||
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
|
Loading…
Reference in New Issue