medusa 0.3.0

General template for building command line interface (CLI) using (and written in) Rust
Documentation
use std::env::Args;
use std::process;

use crate::{Handler, Variant};

/// Struct that defines a CLI program in a programmatic way.
#[derive(Clone)]
pub struct CommandLine {
    handler: Option<Handler>,
    default_command: Option<fn()>,
    version: String,
    name: String,
}

impl CommandLine {
    /// Create a new empty `CommandLine` instance.
    ///
    /// # Example
    ///
    /// ```
    /// use medusa::CommandLine;
    ///
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// ```
    pub fn new() -> CommandLine {
        CommandLine {
            handler: None,
            default_command: None,
            version: String::from("0.0.1"),
            name: String::from("medusa"),
        }
    }

    /// Set handler for the `CommandLine` istance using `Handler` instance.
    ///
    /// Note that if using this method, then all existing handler in current
    /// `CommandLine` instance are **replaced** with the one passed to this method.
    /// To add `Handler` intance into `CommandLine` instance see `append_handler`.
    ///
    /// # Example
    ///
    /// ```
    /// use medusa::{CommandLine, Handler};
    ///
    /// fn show_help() {
    ///   println!("This is help message!");
    /// }
    ///
    /// let mut handler: Handler = Handler::new();
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// cli.set_handler(handler);
    /// ```
    pub fn set_handler(&mut self, handler: Handler) {
        self.handler = Some(handler);
    }

    /// Append handler for the `CommandLine` istance using `Handler` instance.
    ///
    /// Note that if using this method, then the `Handler` instance passed are
    /// added to the current `CommandLine` instance instead of replacing it.
    /// If no `Handler` found in current `CommandLine` instance then this method
    /// will set the current handler to the one passed in.
    ///
    /// # Example
    ///
    /// ```
    /// use medusa::{CommandLine, Handler};
    ///
    /// fn print_string(arg: String) {
    ///   println!("Got string : {}", arg);
    /// }
    ///
    /// let mut handler: Handler = Handler::new();
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// cli.append_handler(handler);
    /// ```
    pub fn append_handler(&mut self, handler: Handler) {
        match &mut self.handler {
            None => {
                self.set_handler(handler);
            }
            Some(current_handler) => {
                (*current_handler).append(handler);
            }
        }
    }

    /// Run the `CommandLine` instance with arguments passed to it in form of `std::env::Args`.
    /// `Handler` instance registered to this `CommandLine` will be checked for each of its
    /// action key to parse or process passed arguments when the CLI is executed.
    ///
    /// # Example
    ///
    /// ```
    /// // code ...
    ///
    /// use medusa::{CommandLine, Handler};
    ///
    /// use std::env;
    ///
    /// // code ...
    ///
    /// let mut handler: Handler = Handler::new();
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// cli.set_handler(handler);
    /// cli.run(env::args());
    /// ```
    pub fn run(&mut self, args: Args) {
        // run default command if no args passed
        if args.len() == 1 {
            self.run_default_command();
        }

        // turn into vector and clone passed args
        let args_vec: Vec<String> = args.collect();
        let args_vec_cloned: Vec<String> = args_vec.clone();

        // parse args into handler's "memory"
        if let Some(handler) = &mut self.handler {
            let _ = handler.parse_args(args_vec);
        } else {
            println!("error : no handler found on this command instance.");
            process::exit(1);
        }

        // skip self binary exec
        let args_iter = args_vec_cloned.iter().skip(1);

        // variable flags
        let mut func_to_process: Option<fn(&Handler, String)> = None;
        let mut have_arg: bool = false;

        for option in args_iter {
            // function to parse option arg and revert flag
            if have_arg {
                if let Some(f) = func_to_process {
                    if let Some(handler) = &self.handler {
                        f(handler, option.to_string());
                    }
                } else {
                    println!("error : error when handling the option arg {}", &option);
                    process::exit(1);
                }
                have_arg = false;
                func_to_process = None;
                continue;
            }

            if let Some(handler) = &self.handler {
                if let Some(action_map) = handler.get_actions() {
                    if let Some(variant) = action_map.get(option) {
                        match variant {
                            Variant::Plain(f) => f(handler),
                            Variant::WithArg(f) => {
                                have_arg = true;
                                func_to_process = Some(*f);
                            }
                        }
                    } else {
                        println!("error : option {} not handled.", &option);
                        process::exit(1);
                    }
                } else {
                    println!("error : no actions defined in this handler.");
                    process::exit(1);
                }
            } else {
                println!("error : no handler found on this command instance.");
                process::exit(1);
            }
        }
    }

    /// Set default function or action by calling it when the CLI are executed
    /// with no arguments passed at all.
    /// Usually this will show help or usage message.
    /// For example, a help function could be defined and what's next are
    /// to set the CLI's default function (action) "pointing" to that function.
    /// Since the CLI is executed with no arguments passed, the function parameter
    /// used for this method implementation is in form of `fn()`.
    ///
    /// By default, if this is not set, the CLI will print all of available
    /// function or action that is already set on the `CommandLine` instance
    /// when the binary is executed.
    ///
    /// # Example
    ///
    /// ```
    /// // code ...
    ///
    /// use medusa::{CommandLine, Handler};
    ///
    /// use std::env;
    ///
    /// fn show_help() {
    ///   println!("This is help message!");
    /// }
    ///
    /// // code ...
    ///
    /// let mut handler: Handler = Handler::new();
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// cli.set_handler(handler);
    /// cli.set_default_command(show_help);
    /// cli.run(env::args());
    /// ```
    pub fn set_default_command(&mut self, function: fn()) {
        self.default_command = Some(function);
    }

    /// Set version for the current CLI. By default when creating a new
    /// instance of `CommandLine` the version starts from `0.0.1`.
    /// Any instance of string can be passed here since currently there
    /// are no limitation whether or not to use semantic versioning.
    /// For example, a string like `v1.9.7-rc.2` can be passed to set
    /// the CLI's version.
    ///
    /// # Example
    ///
    /// ```
    /// use medusa::CommandLine;
    ///
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// cli.set_version("v1.9.7-rc.2");
    /// ```
    pub fn set_version(&mut self, version: &str) {
        self.version = version.to_string();
    }

    /// Set the name for the current CLI. By default when creating a new
    /// instance of `CommandLine` the name are `medusa` as this is the
    /// crates name.
    /// Any instance of string can be passed here to set the CLI's name.
    /// For example, a string like `mycli` can be passed to set the
    /// CLI's name.
    ///
    /// # Example
    ///
    /// ```
    /// use medusa::CommandLine;
    ///
    /// // code ...
    ///
    /// let mut cli: CommandLine = CommandLine::new();
    /// cli.set_name("mycli");
    /// ```
    pub fn set_name(&mut self, name: &str) {
        self.name = name.to_string();
    }

    fn run_default_command(&self) {
        if let Some(f) = self.default_command {
            f();
        } else {
            self.run_command_no_arg();
        }
    }

    fn run_command_no_arg(&self) {
        self.print_name_and_version();
        self.print_usage();
    }

    fn print_name_and_version(&self) {
        println!("\n{} version {}", self.name, self.version);
    }

    fn print_usage(&self) {
        if let Some(handler) = &self.handler {
            if let Some(hint_map) = handler.get_hints() {
                println!("\nOptions :\n");
                for (option, hint) in hint_map.iter() {
                    println!("    {} : {}", option, hint);
                }
                println!();
            }
        }
    }
}