nanoargs 0.6.0

A minimal, zero-dependency argument parser for Rust CLI applications
Documentation
use std::fmt;

use crate::parser::ArgParser;
use crate::validators::Validator;

/// Definition of a boolean flag (e.g. `--verbose` / `-v`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FlagDef {
    /// Long name used as `--name`.
    pub long: String,
    /// Optional single-character short form used as `-c`.
    pub short: Option<char>,
    /// Human-readable description shown in help text.
    pub description: String,
    /// When `true`, this flag is omitted from help text but still parsed.
    pub hidden: bool,
}

/// Definition of a key-value option (e.g. `--output FILE` / `-o FILE`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OptionDef {
    /// Long name used as `--name`.
    pub long: String,
    /// Optional single-character short form used as `-c`.
    pub short: Option<char>,
    /// Placeholder shown in help text (e.g. `FILE`, `NUM`).
    pub placeholder: String,
    /// Human-readable description shown in help text.
    pub description: String,
    /// When `true`, parsing fails if this option is not provided.
    pub required: bool,
    /// Default value used when the option is absent from CLI and env.
    pub default: Option<String>,
    /// Environment variable name to check as a fallback.
    pub env_var: Option<String>,
    /// When `true`, repeated occurrences are collected into a `Vec`.
    pub multi: bool,
    /// When `true`, this option is omitted from help text but still parsed.
    pub hidden: bool,
    /// Optional value validator invoked during parsing.
    pub validator: Option<Validator>,
}

/// Definition of a positional argument (e.g. `<input>` or `[extra]`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PositionalDef {
    /// Name shown in usage line and help text.
    pub name: String,
    /// Human-readable description shown in help text.
    pub description: String,
    /// When `true`, parsing fails if this positional is not provided.
    pub required: bool,
    /// Default value used when the positional is absent from CLI args.
    pub default: Option<String>,
    /// When `true`, this positional collects all remaining arguments.
    pub multi: bool,
    /// Optional value validator invoked during parsing.
    pub validator: Option<Validator>,
}

/// Definition of a subcommand: a name, description, and its own [`ArgParser`].
#[derive(Clone, Debug, PartialEq)]
pub struct SubcommandDef {
    /// Subcommand name as typed on the command line.
    pub name: String,
    /// Human-readable description shown in help text.
    pub description: String,
    /// Independent parser that handles arguments after the subcommand name.
    pub parser: ArgParser,
}

/// Definition of an argument group: at least one member must be provided.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GroupDef {
    /// Human-readable name for the group (used in error messages and help text).
    pub name: String,
    /// Long names of the member arguments (flags or options).
    pub members: Vec<String>,
}

/// Definition of a conflict set: at most one member may be provided.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConflictDef {
    /// Human-readable name for the conflict set (used in error messages and help text).
    pub name: String,
    /// Long names of the mutually exclusive arguments (flags or options).
    pub members: Vec<String>,
}

/// Errors produced during argument parsing.
///
/// The `HelpRequested` and `VersionRequested` variants carry the formatted
/// text that should be printed to stdout. All other variants represent actual
/// errors and carry a descriptive message suitable for display.
///
/// Implements [`std::fmt::Display`] and [`std::error::Error`]. When the
/// `color` feature is enabled, the `Display` output includes ANSI styling.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParseError {
    /// A required argument was not provided. Contains the argument name.
    MissingRequired(String),
    /// An option was provided without a value. Contains the option name.
    MissingValue(String),
    /// An unrecognized argument was encountered. Contains the raw token.
    UnknownArgument(String),
    /// A structural error in the parser definition. Contains a description.
    InvalidFormat(String),
    /// The `-h` / `--help` flag was encountered. Contains the formatted help text.
    HelpRequested(String),
    /// The `-V` / `--version` flag was encountered. Contains the formatted version text.
    VersionRequested(String),
    /// A subcommand was expected but none was provided. Contains available names.
    NoSubcommand(String),
    /// An unrecognized subcommand was provided. Contains the unknown name.
    UnknownSubcommand(String),
    /// A non-multi option was provided more than once. Contains the option name.
    DuplicateOption(String),
    /// A command-line argument contained bytes that are not valid UTF-8.
    /// Contains the lossy representation.
    InvalidUtf8(String),
    /// A value failed validation. Contains the argument name and error message.
    ValidationFailed {
        /// The argument name (e.g. option long name or positional name).
        name: String,
        /// The human-readable validation error message.
        message: String,
    },
    /// An argument group constraint was violated: none of the members were provided.
    GroupViolation {
        /// The group name.
        group: String,
        /// Long names of the group's member arguments.
        members: Vec<String>,
    },
    /// A conflict set constraint was violated: two or more mutually exclusive members were provided.
    ConflictViolation {
        /// The conflict set name.
        conflict: String,
        /// Long names of the conflicting arguments that were provided.
        provided: Vec<String>,
    },
}

// ── Leaf colorization helpers for ParseError ───────────────────────────────

/// Returns the "error: " prefix with bold+red styling when color is enabled,
/// or an empty string when color is disabled (matching the current plain output).
#[cfg(feature = "color")]
fn error_prefix() -> String {
    use nanocolor::Colorize;
    format!("{} ", "error:".bold().red())
}

#[cfg(not(feature = "color"))]
fn error_prefix() -> String {
    String::new()
}

/// Returns the argument string with yellow styling when color is enabled,
/// or the plain string when color is disabled.
#[cfg(feature = "color")]
fn yellow_arg(s: &str) -> String {
    use nanocolor::Colorize;
    s.yellow().to_string()
}

#[cfg(not(feature = "color"))]
fn yellow_arg(s: &str) -> String {
    s.to_string()
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ParseError::HelpRequested(text) | ParseError::VersionRequested(text) => {
                write!(f, "{text}")
            }
            ParseError::MissingRequired(name) => {
                write!(f, "{}missing required argument: {}", error_prefix(), yellow_arg(name))
            }
            ParseError::MissingValue(name) => {
                write!(
                    f,
                    "{}missing value for option: {}",
                    error_prefix(),
                    yellow_arg(&format!("--{name}"))
                )
            }
            ParseError::UnknownArgument(token) => {
                write!(f, "{}unknown argument: {}", error_prefix(), yellow_arg(token))
            }
            ParseError::InvalidFormat(msg) => {
                write!(f, "{}invalid format: {msg}", error_prefix())
            }
            ParseError::NoSubcommand(names) => {
                write!(f, "{}no subcommand provided. Available: {names}", error_prefix())
            }
            ParseError::UnknownSubcommand(name) => {
                write!(f, "{}unknown subcommand: {}", error_prefix(), yellow_arg(name))
            }
            ParseError::DuplicateOption(name) => {
                write!(
                    f,
                    "{}option {} was provided more than once (use .multi() to allow repeats)",
                    error_prefix(),
                    yellow_arg(&format!("--{name}"))
                )
            }
            ParseError::InvalidUtf8(lossy) => {
                write!(
                    f,
                    "{}argument is not valid UTF-8: {}",
                    error_prefix(),
                    yellow_arg(lossy)
                )
            }
            ParseError::ValidationFailed { name, message } => {
                write!(
                    f,
                    "{}validation failed for {}: {message}",
                    error_prefix(),
                    yellow_arg(name)
                )
            }
            ParseError::GroupViolation { group, members } => {
                let member_list: Vec<String> = members.iter().map(|m| yellow_arg(&format!("--{m}"))).collect();
                write!(
                    f,
                    "{}at least one of the following is required (group '{group}'): {}",
                    error_prefix(),
                    member_list.join(", ")
                )
            }
            ParseError::ConflictViolation { conflict, provided } => {
                let arg_list: Vec<String> = provided.iter().map(|m| yellow_arg(&format!("--{m}"))).collect();
                let joined = if arg_list.len() == 2 {
                    format!("{} and {}", arg_list[0], arg_list[1])
                } else {
                    arg_list.join(", ")
                };
                write!(
                    f,
                    "{}conflicting arguments ('{conflict}'): {joined} cannot be used together",
                    error_prefix(),
                )
            }
        }
    }
}

impl std::error::Error for ParseError {}