2021-06-19 07:20:29 +00:00
|
|
|
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 {
|
2021-06-19 19:07:04 +00:00
|
|
|
App::default().command(
|
|
|
|
Command::new("help")
|
|
|
|
.handler(help)
|
|
|
|
.short_desc("Displays help information"),
|
|
|
|
)
|
2021-06-19 07:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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<()> {
|
2021-06-19 19:07:04 +00:00
|
|
|
self.run_with(env::args().skip(1).collect())
|
2021-06-19 07:20:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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);
|
|
|
|
}
|
|
|
|
|
2021-06-19 19:07:04 +00:00
|
|
|
// Automatic command help display
|
|
|
|
if ignored.iter().any(|a| a == "-h" || a == "--help") {
|
|
|
|
super::command::print_command_help(cmd, &vec![]);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2021-06-19 07:20:29 +00:00
|
|
|
// 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 {
|
2021-06-19 19:07:04 +00:00
|
|
|
handler(app, &Context::new(cmd.clone(), opts.clone()), &ignored);
|
2021-06-19 07:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
}
|