arkham/src/app.rs

322 lines
10 KiB
Rust

use super::command::{help, Command, Handler};
use super::context::Context;
use super::opt::{ActiveOpt, Opt, OptError, OptKind};
use std::env;
type Result<T> = std::result::Result<T, OptError>;
pub struct App {
name: Option<&'static str>,
version: Option<&'static str>,
pub root: Command,
}
impl Default for App {
fn default() -> Self {
Self {
name: None,
version: None,
root: Command::new("root"),
}
}
}
impl App {
pub fn application_header(&self) -> String {
format!(
"{} - {}",
self.name.as_ref().unwrap_or(&env!("CARGO_PKG_NAME")),
self.version.as_ref().unwrap_or(&env!("CARGO_PKG_VERSION"))
)
}
}
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)
.short_desc("Displays help information"),
)
}
/// Sets the name of the application. If not set the cargo package name will be used.
///
/// Exmaple:
///
/// ```rust
/// use arkham::App;
/// let app = App::new().name("new app");
/// ```
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
/// Sets the version for the application. If not set the cargo package version is used.
///
/// Example:
///
/// ```rust
/// use arkham::App;
/// App::new().version("1.0.0");
/// ```
pub fn version(mut self, version: &'static str) -> Self {
self.version = Some(version);
self
}
/// Adds a root level command 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};
/// App::new().command(Command::new("subcommand").handler(my_handler));
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {}
/// ```
pub fn command(mut self, cmd: Command) -> Self {
self.root.commands.push(cmd);
self
}
/// 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.
///
/// Example:
/// ```rust
/// use arkham::{App, Opt};
/// App::new().opt(Opt::flag("verbose").short("v").long("verbose"));
/// ```
pub fn opt(mut self, opt: Opt) -> Self {
self.root.opts.push(opt);
self
}
/// Sets a handler function for the bare root command. If this is not set an error will be
/// generated and a help message will be displayed indicating the available subcommands.
/// The handler function takes an instance of the app, the context which contains the opts and
/// flags, and any additionally passeed arguments.
///
/// Example:
/// ```rust
/// use arkham::{App, Command, Context};
/// App::new().handler(my_handler);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {}
/// ```
pub fn handler(mut self, f: Handler) -> Self {
self.root.handler = Some(f);
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.
/// Example:
/// ```rust
/// use arkham::{App, Command, Context, Opt};
/// App::new()
/// .opt(Opt::flag("name").short("n").long("name"))
/// .handler(my_handler)
/// .run_with(vec!["-n".to_string(), "alice".to_string()]);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
/// println!("Hello, {}", ctx.get_string("name").unwrap());
/// }
/// ```
pub fn run_with(&self, args: Vec<String>) -> Result<()> {
run_command(self, &self.root, &args, &mut vec![])
}
/// Execute the app and any specified handlers based on the arguments passsed to the
/// application.
///
/// Example:
/// running with myapp --name alice
/// ```rust
/// use arkham::{App, Command, Context, Opt};
/// App::new()
/// .opt(Opt::flag("name").short("n").long("name"))
/// .handler(my_handler)
/// .run();
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
/// println!("Hello, {}", ctx.get_string("name").unwrap_or_else(|| "unnamed".into()));
/// }
/// ```
pub fn run(&mut self) -> Result<()> {
self.run_with(env::args().skip(1).collect())
}
}
/// 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], opts: &mut Vec<ActiveOpt>) -> 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.
// These will be either used to collect arguments from subcommands or passed as additional args
// to the command.
let mut ignored: Vec<String> = vec![];
// Loop through all passed in args
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..]) {
match opt.kind {
OptKind::Flag => {
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
}
OptKind::String => {
if let Some(value) = args.next() {
opts.push(ActiveOpt::new(opt.clone(), vec![value.clone()]));
} else {
return Err(OptError::InvalidOpt(opt.name.clone()));
}
}
}
} else {
ignored.push(arg.clone());
}
continue;
}
// Check for short args
if arg.starts_with("-") {
if let Some(opt) = cmd.opts.iter().find(|o| &o.short == &arg[1..]) {
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
} else {
ignored.push(arg.clone());
}
continue;
}
ignored.push(arg.clone());
}
// Find an recurse into sub commands if the remaining argumetns match any subcommand name
if let Some(cmd) = cmd
.commands
.iter()
.find(|cmd| ignored.iter().any(|a| *a == cmd.name))
{
ignored.retain(|a| *a != cmd.name);
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()));
}
// Execute the command handler
if let Some(handler) = cmd.handler {
handler(app, &Context::new(cmd.clone(), opts.clone()), &ignored);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::App;
#[test]
fn test_long_string() {
let args: Vec<String> = vec!["--user".into(), "joe".into()];
let app = App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_string("user"), Some("joe".into()));
});
let res = app.run_with(args);
assert!(res.is_ok());
}
#[test]
fn test_subcommand() {
let args = vec![
"--user".into(),
"joe".into(),
"--config".into(),
"c.json".into(),
"thing".into(),
];
let app = App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.command(
Command::new("thing")
.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()));
}),
);
let res = app.run_with(args);
assert!(res.is_ok());
}
fn function_handler(_app: &App, _ctx: &Context, _args: &[String]) {
assert!(true);
}
#[test]
fn test_function_handler() {
App::new().handler(function_handler);
}
#[test]
fn test_extra_args() {
let args = vec!["somefile".to_string()];
App::new()
.handler(|_, _, args| {
assert_eq!(args.len(), 1);
assert_eq!(args.first(), Some(&"somefile".to_string()));
})
.run_with(args)
.expect("app error");
}
#[test]
fn test_short_flag() {
App::new()
.opt(Opt::flag("verbose").short("v").long("verbose"))
.handler(|_, ctx, _| {
assert_eq!(ctx.flag("verbose"), true);
})
.run_with(vec!["-v".into()])
.unwrap();
}
#[test]
fn test_invalid_long_flag() {
let r = App::new()
.opt(Opt::flag("verbose").short("v").long("verbose"))
.run_with(vec!["--user".into()]);
assert!(r.is_err(), "Should error for invalid long flag");
}
#[test]
fn test_invalid_short_flag() {
let r = App::new()
.opt(Opt::flag("verbose").short("v").long("verbose"))
.run_with(vec!["-u".into()]);
assert!(r.is_err(), "Should error for invalid short flag");
}
}