minicli 0.1.0

A tiny, dynamic CLI parser driven by simple string definitions or markdown files.
Documentation



// === src/lib.rs ===

use std::collections::HashMap;
use colored::*;
use regex::Regex;

pub struct Command {
    pub name: String,
    pub args: HashMap<String, Option<String>>,
    pub required: Vec<String>,
    pub optional: Vec<String>,
    pub definition: String,
}

impl Command {
    pub fn get(&self, key: &str) -> Option<&str> {
        self.args.get(key).and_then(|v| v.as_deref())
    }

    pub fn flag(&self, key: &str) -> bool {
        self.args.contains_key(key)
    }

    pub fn has(&self, key: &str) -> bool {
        self.args.contains_key(key)
    }

    pub fn is(&self, name: &str) -> bool {
        self.name == name
    }

    pub fn check_required(&self) {
        for key in &self.required {
            if !self.args.contains_key(key) {
                println!("{} {}", "Error:".red().bold(), format!("Missing required argument: --{}", key));
                println!("{}\n{}", "Usage:".green(), self.definition);
                std::process::exit(1);
            }
        }
    }
}

pub fn cli(definition: &str, handler: impl Fn(Command)) {
    let args: Vec<String> = std::env::args().skip(1).collect();

    if args.is_empty() || args.contains(&"--help".to_string()) {
        println!("{}\n{}", "Help:".bold().green(), definition);
        return;
    }

    let (required, optional) = parse_definition(definition);

    let command_name = args[0].clone();
    let mut parsed_args = HashMap::new();
    let mut iter = args.into_iter().skip(1);

    while let Some(arg) = iter.next() {
        if arg.starts_with("--") {
            if let Some((key, val)) = arg.split_once('=') {
                parsed_args.insert(key.trim_start_matches("--").to_string(), Some(val.to_string()));
            } else {
                let key = arg.trim_start_matches("--").to_string();
                if let Some(next) = iter.clone().next() {
                    if !next.starts_with("--") {
                        parsed_args.insert(key, Some(iter.next().unwrap()));
                    } else {
                        parsed_args.insert(key, None);
                    }
                } else {
                    parsed_args.insert(key, None);
                }
            }
        }
    }

    let cmd = Command {
        name: command_name,
        args: parsed_args,
        required,
        optional,
        definition: definition.to_string(),
    };

    cmd.check_required();
    handler(cmd);
}

fn parse_definition(def: &str) -> (Vec<String>, Vec<String>) {
    let req_re = Regex::new(r"--(\\w+)=<\\w+>").unwrap();
    let opt_re = Regex::new(r"\\[--(\\w+)\\]").unwrap();

    let mut required = Vec::new();
    let mut optional = Vec::new();

    for cap in req_re.captures_iter(def) {
        required.push(cap[1].to_string());
    }
    for cap in opt_re.captures_iter(def) {
        optional.push(cap[1].to_string());
    }

    (required, optional)
}