use crate::print_command_help; use super::command::{help, Command, Handler}; use super::context::Context; use super::opt::{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, config_filename: Option<&'static str>, env_prefix: Option<&'static str>, } impl Default for App { fn default() -> Self { Self { name: None, version: None, root: Command::new("root"), env_prefix: None, config_filename: None, } } } 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 } /// Sets the environment variable prefix for option resolution. If set to something like /// APP_NAME a option with the name THING, will look for an environment variable named /// APP_NAME_THING. /// /// Example: /// ```rust /// use arkham::App; /// App::new().env_prefix("APP_NAME"); /// /// ``` pub fn env_prefix(mut self, prefix: &'static str) -> Self { self.env_prefix = Some(prefix); 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::scalar("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(&mut self, args: Vec) -> Result<()> { let mut ctx = Context::new(self.root.clone()); if let Some(filename) = self.config_filename { ctx.load_config_file(filename); } run_command(self, &self.root, &args, &mut ctx) } /// 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], ctx: &mut Context) -> 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 => { ctx.set_flag(opt); } OptKind::String => { if let Some(value) = args.next() { ctx.set_opt(opt, 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..]) { match opt.kind { OptKind::Flag => { ctx.set_flag(opt); } OptKind::String => { if let Some(value) = args.next() { ctx.set_opt(opt, value.clone()); } else { return Err(OptError::InvalidOpt(opt.name.clone())); } } } } 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, ctx); } // 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 { ctx.cmd = cmd.clone(); if let Some(prefix) = app.env_prefix { ctx.env_prefix = Some(prefix.to_string()); } handler(app, &ctx, &ignored); } else { crate::vox::print(app.application_header()); print_command_help(cmd, &vec![]) } Ok(()) } #[cfg(not(feature = "config"))] impl App { pub fn config_filename(&mut self, _filename: &'static str) -> Self { panic!("Not compiled with configuration capabilities. Add a config parser as a feature"); } } #[cfg(feature = "config")] impl App { pub fn config_filename(mut self, filename: &'static str) -> Self { self.config_filename = Some(filename); self } } #[cfg(test)] mod tests { use super::*; use crate::App; #[test] fn test_long_string() { let args: Vec = vec!["--user".into(), "joe".into()]; App::new() .opt(Opt::scalar("user").short("u").long("user")) .handler(|_, ctx, _| { assert_eq!(ctx.get_string("user"), Some("joe".into())); }) .run_with(args) .unwrap(); } #[test] fn test_subcommand() { let args = vec![ "--user".into(), "joe".into(), "--config".into(), "c.json".into(), "thing".into(), ]; 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())); }), ) .run_with(args) .unwrap(); } 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"); } #[test] fn test_env_prefix() { std::env::set_var("ARKHAM_thing", "1"); App::new() .env_prefix("ARKHAM") .opt(Opt::flag("thing").short("-t").long("--t")) .handler(|_, ctx, _| { assert_eq!(ctx.flag("thing"), true); }) .run_with(vec![]) .unwrap(); } }