cli-command 0.1.0

A lightweight and ergonomic command-line argument parser for Rust
Documentation
use crate::cli_error::{CliError, CliErrorKind};
use crate::command::Command;
use std::collections::HashMap;
use std::env;
use std::iter::Peekable;

/// Parses command line arguments from `std::env::args()`.
///
/// This function automatically reads the command line arguments passed to the program
/// and parses them into a `Command` struct. It skips the program name (first argument).
///
/// # Returns
/// * `Ok(Command)` - If parsing succeeds
/// * `Err(CliError)` - If parsing fails
///
/// # Examples
/// ```rust
/// use cli_command::parse_command_string;
///
/// // When called with: myapp serve --port 8080 --host localhost
/// let cmd = parse_command_string("serve --port 8080 --host localhost").unwrap();
/// assert_eq!(cmd.name, "serve");
/// assert_eq!(cmd.get_argument("port"), Some("8080"));
/// assert_eq!(cmd.get_argument("host"), Some("localhost"));
/// ```
pub fn parse_command_line() -> Result<Command, CliError> {
    let command_line_arguments: Vec<String> = env::args().skip(1).collect::<Vec<String>>();
    let mut command_line_arguments = command_line_arguments.iter().map(|s| &s[..]).peekable();
    parse_command_line_args(&mut command_line_arguments)
}

/// Parses command line arguments from a string.
///
/// This function is useful for testing or when you have command line arguments
/// stored in a string rather than from the actual command line.
///
/// # Arguments
/// * `string` - The command line string to parse
///
/// # Returns
/// * `Ok(Command)` - If parsing succeeds
/// * `Err(CliError)` - If parsing fails
///
/// # Examples
/// ```rust
/// use cli_command::parse_command_string;
///
/// let cmd = parse_command_string("serve --port 8080 --host localhost").unwrap();
/// assert_eq!(cmd.name, "serve");
/// assert_eq!(cmd.get_argument("port"), Some("8080"));
/// assert_eq!(cmd.get_argument("host"), Some("localhost"));
/// ```
pub fn parse_command_string(string: &str) -> Result<Command, CliError> {
    parse_command_line_args(&mut string.split(" ").into_iter().peekable())
}

pub fn parse_command_line_args<'a, I>(args: &mut Peekable<I>) -> Result<Command, CliError>
where
    I: Iterator<Item = &'a str>,
{
    let mut arguments = HashMap::new();
    let command = match args.peek() {
        None => "",
        Some(&value) => {
            if !value.starts_with("-") {
                args.next();
                value
            } else {
                ""
            }
        }
    };

    loop {
        match parse_command_argument(args)? {
            None => break,
            Some((name, values)) => {
                arguments.insert(name, values);
            }
        }
    }
    Ok(Command {
        name: command.to_string(),
        arguments,
    })
}

fn parse_command_argument<'a, I>(
    args: &mut Peekable<I>,
) -> Result<Option<(String, Box<[String]>)>, CliError>
where
    I: Iterator<Item = &'a str>,
{
    match args.next() {
        None => Ok(None),
        Some(ref value) if value.starts_with("-") => {
            let values = parse_argument_values(args);
            let key_index: usize = if value.starts_with("--") { 2 } else { 1 };
            Ok(Some(((&value[key_index..]).to_string(), values)))
        }
        Some(_) => Err(CliErrorKind::ParseCommandLine.into()),
    }
}

fn parse_argument_values<'a, I>(args: &mut Peekable<I>) -> Box<[String]>
where
    I: Iterator<Item = &'a str>,
{
    let mut result = vec![];
    loop {
        match args.peek() {
            Some(&value) => {
                if !value.starts_with("-") {
                    result.push(args.next().unwrap().to_string());
                } else {
                    break;
                }
            }
            _ => break,
        }
    }

    result.into_boxed_slice()
}