commandy 0.1.1

easy parsing of command line arguments
Documentation
use std::env;
use core::fmt::Write;

/*
 * TODO:
 * - support enum flags (--some-setting foo, with foo an enum)
 */

mod termcolors;
use crate::termcolors::*;

pub trait FlagParser {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String>;
}

pub struct Flag<'a> {
    pub option_names: &'static [&'static str],
    pub argument_descriptions: &'static [&'static str],
    pub field: &'a mut dyn FlagParser,
    pub description: &'static str,
}

pub struct Arguments<'a> {
    option_name: &'static str,
    min: usize,
    max: usize,
    field: &'a mut dyn FlagParser,
}

#[derive(Default)]
pub struct Command<'a> {
    pub command_name: &'static str,
    pub short_description: &'static str, // one line (without trailing .)
    pub flags_after_position_arguments: bool,
    pub flags: &'a mut [Flag<'a>],
    pub positional: Option<Arguments<'a>>,
}

pub trait ArgumentParser {
    #[allow(clippy::ptr_arg)]
    fn parse_arguments(&mut self, input_args: &mut Vec<String>, command_prefix: &str, action: ParseArgumentAction) -> Result<bool,String>;
    #[allow(clippy::ptr_arg)]
    fn insert_name(&self, names: &mut Vec<&'static str>);
}

#[derive(Debug,PartialEq,Copy,Clone)]
pub enum ParseArgumentAction {
    Parse,
    ShowSmallOverview,
}

#[derive(Debug,PartialEq,Copy,Clone)]
pub enum TypeOfCommand {
    WithSubCommands(),
    Other(),
}

impl<'a> Command<'a> {
    pub fn parse_args(&mut self, input_args: &mut Vec<String>, command_prefix: &str, type_of_command: &TypeOfCommand) -> Result<ParseArgumentAction,String> {
        // parse flags
        let has_subcommands = matches!(type_of_command, TypeOfCommand::WithSubCommands());
        let mut seen_other = false;
        let mut i = 0;
        'outer: while i < input_args.len() {
            let arg = &input_args[i];
            if arg == "--" {
                input_args.remove(i);
                break;
            }
            if !arg.starts_with('-') {
                if !self.flags_after_position_arguments {
                    break;
                }
                seen_other = true;
                i += 1;
                continue;
            }
            for flag in self.flags.iter_mut() {
                for name in flag.option_names {
                    if arg == *name {
                        // FIXME: check number of arguments to flags
                        if i+1 >= input_args.len() {
                            flag.field.parse_flag(&[])?;
                            input_args.drain(i..);
                        } else {
                            flag.field.parse_flag(&input_args[i+1..i+1+flag.argument_descriptions.len()])?;
                            input_args.drain(i..i+1+flag.argument_descriptions.len());
                        }
                        continue 'outer;
                    }
                }
            }
            if seen_other && has_subcommands {
                i += 1;
                continue;
            }
            if !seen_other && arg.eq_ignore_ascii_case("--help") {
                self.print_small_overview(command_prefix, type_of_command);
                self.print_flags("  ");
                return Ok(ParseArgumentAction::ShowSmallOverview);
            }
            eprintln!("unknown flag encountered: {}", arg);
            std::process::exit(-1);
        }
        if let Some(positional) = &mut self.positional {
            if positional.min > input_args.len() || input_args.len() > positional.max {
                eprintln!("{}: expected between {} and {} values, not {}", positional.option_name, positional.min, positional.max, input_args.len());
                std::process::exit(-1);
            }
            positional.field.parse_flag(input_args)?;
        }
        Ok(ParseArgumentAction::Parse)
    }

    pub fn positional(mut self, args: (&'static str, usize, usize), field: &'a mut dyn FlagParser) -> Self {
        self.positional = Some(Arguments{option_name: args.0, min: args.1, max: args.2, field});
        self
    }

    fn help_flags_overview(&self, suffix_for_help: &str) -> String {
        let mut retval = String::new();
        for flag in self.flags.iter() {
            if flag.argument_descriptions.is_empty() {
                write!(retval, " {TERM_BRIGHT_BLACK}[{TERM_BRIGHT_YELLOW}{}{TERM_BRIGHT_BLACK}]{TERM_RESET}", flag.option_names.join(&format!("{TERM_RESET},{TERM_BRIGHT_YELLOW}"))).unwrap();
            } else {
                write!(retval, " {TERM_BRIGHT_BLACK}[{TERM_BRIGHT_YELLOW}{} {TERM_BRIGHT_ORANGE}{}{TERM_BRIGHT_BLACK}]{TERM_RESET}", flag.option_names.join(&format!("{TERM_RESET},{TERM_BRIGHT_YELLOW}")), flag.argument_descriptions.join(" ")).unwrap();
            }
        }
        if let Some(positional) = &self.positional {
            write!(retval, " {TERM_UNDERLINE}{TERM_BRIGHT_ORANGE}{}{TERM_RESET}", positional.option_name).unwrap();
        } else if !suffix_for_help.is_empty()  {
            write!(retval, " {TERM_UNDERLINE}{TERM_BRIGHT_GREEN}{}{TERM_RESET}", suffix_for_help).unwrap();
        }
        retval
    }

    fn print_flags(&self, prefix: &str) {
        for flag in self.flags.iter() {
            println!("{}{TERM_BRIGHT_YELLOW}{} {TERM_BRIGHT_ORANGE}{}{TERM_RESET}", prefix, flag.option_names.join(&format!("{TERM_RESET},{TERM_BRIGHT_YELLOW}")), flag.argument_descriptions.join(" "));
            println!("      {}", flag.description);
        }
    }

    pub fn print_small_overview(&self, command_prefix: &str, type_of_command: &TypeOfCommand) {
        let suffix_for_help = match type_of_command {
            TypeOfCommand::WithSubCommands() => "subcommand",
            TypeOfCommand::Other() => "",
        };
        println!("{}{TERM_BOLD}{TERM_BRIGHT_GREEN}{}{TERM_RESET}{}", command_prefix, self.command_name, self.help_flags_overview(suffix_for_help));

        println!("{}{TERM_BRIGHT_WHITE}↳{TERM_RESET} {}", [' '].iter().cycle().take(command_prefix.len()).collect::<String>(), self.short_description.trim());
    }

}

pub fn parse_args<T: Default + ArgumentParser + 'static>() -> T {
    let mut args : Vec<String> = env::args().collect();

    let mut retval = T::default();
    if let Err(message) = retval.parse_arguments(&mut args, "", ParseArgumentAction::Parse) {
        println!("parse error on {:?}: {}", args, message);
        std::process::exit(-1);
    }

    retval
}

impl FlagParser for bool {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        if args.is_empty() {
            *self = !*self;
            Ok(())
        } else if args.len() == 1 {
            *self = args[0].parse().map_err(|e: std::str::ParseBoolError| e.to_string())?;
            Ok(())
        } else {
            Err(String::from("bool flag expects zero or one arguments"))
        }
    }
}
impl FlagParser for String {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        if args.len() != 1 {
            return Err(String::from("string flag expects one argument"));
        }
        *self = args[0].clone();
        Ok(())
    }
}
impl FlagParser for Vec<String> {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        for arg in args {
            self.push(arg.clone());
        }
        Ok(())
    }
}
impl FlagParser for usize {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        if args.len() != 1 {
            return Err(String::from("int flag expects one argument"));
        }
        *self = args[0].parse().map_err(|x: core::num::ParseIntError| x.to_string())?;
        Ok(())
    }
}
impl FlagParser for u16 {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        if args.len() != 1 {
            return Err(String::from("int flag expects one argument"));
        }
        *self = args[0].parse().map_err(|x: core::num::ParseIntError| x.to_string())?;
        Ok(())
    }
}
impl FlagParser for u32 {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        if args.len() != 1 {
            return Err(String::from("int flag expects one argument"));
        }
        *self = args[0].parse().map_err(|x: core::num::ParseIntError| x.to_string())?;
        Ok(())
    }
}
impl<T: FlagParser + Default> FlagParser for Option<T> {
    fn parse_flag(&mut self, args: &[String]) -> Result<(), String> {
        let mut value = T::default();
        value.parse_flag(args)?;
        *self = Some(value);
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}