zfish 0.1.10

Ultra-light, zero-dependency Rust CLI framework for building beautiful command-line applications
//! # Argument Parsing Module
//!
//! ```text
//! ╔═══════════════════════════════════════════════════════════════╗
//! ║  zfish — args.rs                                               ║
//! ║  Handcrafted argument parser with zero dependencies           ║
//! ║  Copyright © 2025 Jeet Karena <karenajeet@proton.me>        ║
//! ║  Licensed under MIT OR Apache-2.0                             ║
//! ╚═══════════════════════════════════════════════════════════════╝
//! ```
//!
//! This module provides a lightweight command-line argument parser that handles:
//! - Short flags (`-v`, `-abc` combined)
//! - Long flags (`--verbose`, `--help`)
//! - Options with values (`--file=foo.txt`, `--output file.txt`, `-o file.txt`)
//! - Positional arguments
//!
//! ## Examples
//!
//! ```
//! use zfish::Args;
//!
//! let args = Args::parse();
//!
//! // Check for flags
//! if args.has_flag("verbose") || args.has_flag("v") {
//!     println!("Verbose mode enabled");
//! }
//!
//! // Get option values
//! if let Some(file) = args.get_option("file") {
//!     println!("Processing file: {}", file);
//! }
//!
//! // Access positional arguments
//! for arg in &args.positional {
//!     println!("Positional: {}", arg);
//! }
//! ```
//!
//! ## Performance
//!
//! The parser is designed for speed:
//! - Single-pass parsing
//! - Minimal allocations
//! - No regex or complex state machines
//!
//! Benchmark: ~200ms to parse 1 million flags on Ryzen 3600.

use std::collections::HashMap;
use std::env;

/// Represents parsed command-line arguments.
#[derive(Debug, Clone)]
pub struct Args {
    /// Command name (typically the program name)
    pub command: String,
    /// Positional arguments
    pub positional: Vec<String>,
    /// Flag arguments (e.g., -v, --verbose)
    pub flags: HashMap<String, bool>,
    /// Option arguments with values (e.g., --file=foo.txt)
    pub options: HashMap<String, String>,
}

impl Args {
    /// Parse command-line arguments from the environment.
    pub fn parse() -> Self {
        let mut args: Vec<String> = env::args().collect();
        let command = if !args.is_empty() {
            args.remove(0)
        } else {
            String::new()
        };

        let mut positional = Vec::new();
        let mut flags = HashMap::new();
        let mut options = HashMap::new();

        let mut i = 0;
        while i < args.len() {
            let arg = &args[i];
            let consumed =
                Self::parse_argument(arg, &args, i, &mut flags, &mut options, &mut positional);
            i += consumed;
        }

        Args {
            command,
            positional,
            flags,
            options,
        }
    }

    /// Parse a single argument and return how many args were consumed
    fn parse_argument(
        arg: &str,
        args: &[String],
        index: usize,
        flags: &mut HashMap<String, bool>,
        options: &mut HashMap<String, String>,
        positional: &mut Vec<String>,
    ) -> usize {
        if let Some(without_prefix) = arg.strip_prefix("--") {
            Self::parse_long_argument(without_prefix, args, index, flags, options)
        } else if let Some(without_prefix) = arg.strip_prefix('-') {
            Self::parse_short_argument(without_prefix, arg, args, index, flags, options, positional)
        } else {
            positional.push(arg.to_string());
            1
        }
    }

    /// Parse long argument (--flag or --key=value or --key value)
    fn parse_long_argument(
        without_prefix: &str,
        args: &[String],
        index: usize,
        flags: &mut HashMap<String, bool>,
        options: &mut HashMap<String, String>,
    ) -> usize {
        if without_prefix.contains('=') {
            let parts: Vec<&str> = without_prefix.splitn(2, '=').collect();
            options.insert(parts[0].to_string(), parts[1].to_string());
            1
        } else if index + 1 < args.len() && !args[index + 1].starts_with('-') {
            options.insert(without_prefix.to_string(), args[index + 1].clone());
            2 // Consumed current + next
        } else {
            flags.insert(without_prefix.to_string(), true);
            1
        }
    }

    /// Parse short argument (-v or -abc)
    fn parse_short_argument(
        without_prefix: &str,
        arg: &str,
        args: &[String],
        index: usize,
        flags: &mut HashMap<String, bool>,
        options: &mut HashMap<String, String>,
        positional: &mut Vec<String>,
    ) -> usize {
        if without_prefix.is_empty() {
            positional.push(arg.to_string());
            return 1;
        }

        let mut consumed = 1;
        for (j, c) in without_prefix.chars().enumerate() {
            let flag_name = c.to_string();
            let is_last_char = j == without_prefix.len() - 1;
            let has_next_value = index + 1 < args.len() && !args[index + 1].starts_with('-');

            if is_last_char && has_next_value {
                options.insert(flag_name, args[index + 1].clone());
                consumed = 2;
                break;
            } else {
                flags.insert(flag_name, true);
            }
        }
        consumed
    }

    /// Check if a flag is present
    pub fn has_flag(&self, name: &str) -> bool {
        self.flags.get(name).copied().unwrap_or(false)
    }

    /// Get an option value or None if not present
    pub fn get_option(&self, name: &str) -> Option<&String> {
        self.options.get(name)
    }
}