use super::command::{help, Command, Handler}; use super::context::Context; use super::opt::{ActiveOpt, Opt, OptError, OptKind}; use std::env; type Result = std::result::Result; 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)) } /// 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) -> 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().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) -> 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 = 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); } // 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(opts.clone()), &ignored); } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::App; #[test] fn test_long_string() { let args: Vec = 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"); } }