minargs 0.1.0

🚀 Ultra-lightweight, blazing-fast, zero-dependency CLI parser — inspired by Clap.
Documentation
use crate::{arg::Arg, matches::Matches};
use std::{collections::HashMap, env, iter, process};

#[derive(Clone)]
pub struct App {
    name: String,
    version: Option<String>,
    about: Option<String>,
    args: Vec<Arg>,
    subcommands: HashMap<String, App>,
}

impl App {
    pub fn new(name: &str) -> Self {
        App {
            name: name.to_string(),
            version: None,
            about: None,
            args: Vec::new(),
            subcommands: HashMap::new(),
        }
    }

    pub fn version(mut self, ver: &str) -> Self {
        self.version = Some(ver.to_string());
        self
    }

    pub fn about(mut self, desc: &str) -> Self {
        self.about = Some(desc.to_string());
        self
    }

    pub fn arg<F>(mut self, name: &str, build: F) -> Self
    where
        F: FnOnce(Arg) -> Arg,
    {
        let arg = Arg::new(name);
        let arg = build(arg);
        self.args.push(arg);
        self
    }

    pub fn subcommand<F>(mut self, name: &str, build: F) -> Self
    where
        F: FnOnce(App) -> App,
    {
        let cmd = App::new(name);
        let cmd = build(cmd);
        self.subcommands.insert(name.to_string(), cmd);
        self
    }

    fn print_help(&self) {
        println!("Usage: {} [OPTIONS] [SUBCOMMAND]", self.name);
        if let Some(desc) = &self.about {
            println!("\n{}", desc);
        }

        if !self.args.is_empty() {
            println!("\nOptions:");
            for arg in &self.args {
                let mut flags = String::new();
                if let Some(s) = arg.short {
                    flags.push_str(&format!("-{}, ", s));
                }
                if let Some(l) = &arg.long {
                    flags.push_str(&format!("--{}", l));
                }
                println!("  {:<15} {}", flags, arg.help.clone().unwrap_or_default());
            }
        }

        if !self.subcommands.is_empty() {
            println!("\nSubcommands:");
            for (name, sub) in &self.subcommands {
                println!("  {:<15} {}", name, sub.about.clone().unwrap_or_default());
            }
        }

        process::exit(0);
    }

    pub fn parse(self) -> Matches {
        let mut args = env::args().skip(1);
        let mut values = HashMap::new();
        let mut flags = HashMap::new();
        let mut positional_args =
            self.args.iter().filter(|a| a.short.is_none() && a.long.is_none()).peekable();

        let mut specs = HashMap::new();
        for arg in &self.args {
            if let Some(short) = arg.short {
                specs.insert(format!("-{}", short), arg);
            }
            if let Some(long) = &arg.long {
                specs.insert(format!("--{}", long), arg);
            }
            specs.insert(arg.name.clone(), arg);
        }

        let mut subcommand = None;
        let mut sub_matches = None;

        while let Some(token) = args.next() {
            if token == "--help" || token == "-h" {
                self.print_help();
            }

            if self.subcommands.contains_key(&token) {
                let sub_app = self.subcommands.get(&token).unwrap();
                let new_args: Vec<String> = iter::once(token.clone()).chain(args).collect();
                unsafe { env::set_var("_MINARGS_SUB_REENTER", new_args.join("\n")) };
                let parsed = sub_app.clone().parse();
                subcommand = Some(token);
                sub_matches = Some(Box::new(parsed));
                break;
            } else if token.starts_with("--") || token.starts_with('-') {
                if let Some(arg) = specs.get(&token) {
                    if arg.takes_value {
                        if let Some(val) = args.next() {
                            values.insert(arg.name.clone(), val);
                        }
                    } else {
                        flags.insert(arg.name.clone(), true);
                    }
                }
            } else if let Some(pos_arg) = positional_args.next() {
                values.insert(pos_arg.name.clone(), token);
            }
        }

        for arg in &self.args {
            if !values.contains_key(&arg.name) {
                if let Some(default) = &arg.default {
                    values.insert(arg.name.clone(), default.clone());
                } else if arg.required {
                    eprintln!("Missing required argument: {}", arg.name);
                    process::exit(1);
                }
            }
        }

        Matches { values, flags, subcommand, sub_matches }
    }
}