lazydns 0.2.63

A light and fast DNS server/forwarder implementation in Rust
Documentation
//! Lightweight command-line parsing helpers for lazydns.
//!
//! This module provides a minimal CLI parser implemented with `pico-args`.
//! It exposes `parse_args()` for normal execution (reading the process
//! arguments) and `parse_args_from_vec()` which accepts an explicit
//! `Vec<String>` for easier unit testing.

use pico_args::Arguments;

/// Parsed command-line options.
///
/// This structure contains the small set of options used by the
/// `lazydns` binary. Fields are public for easy access from `main`.
pub struct Args {
    /// Path to configuration file (default: `config.yaml`).
    pub config: String,

    /// Optional working directory to `chdir` into before startup.
    pub dir: Option<String>,

    /// Verbosity count: 0 = normal, 1 = -v (debug), 2 = -vv (trace), 3 = -vvv (trace + external)
    /// When `verbose == 0` this means no verbose flag was provided.
    #[cfg(feature = "log")]
    pub verbose: u8,
}

/// Print a short help text to stdout.
///
/// This is intentionally minimal to avoid pulling a heavy CLI help
/// generation dependency into the binary.
pub fn print_help() {
    println!("lazydns {}\n", env!("CARGO_PKG_VERSION"));
    println!("Usage: lazydns [OPTIONS]\n");
    println!("OPTIONS:");
    println!("  -c, --config <file>       Configuration file path (default: config.yaml)");
    println!("  -d, --dir <dir>           Working directory");
    #[cfg(feature = "log")]
    println!(
        "  -v, --verbose <count>     Verbosity: -v (debug), -vv (trace), -vvv (trace + external crate logs)"
    );
    println!("  -V, --version             Print version and exit");
    println!("  -h, --help                Print this help message");
}

/// Parse CLI arguments using `pico-args` from the current process args.
///
/// Returns `None` when help was printed (caller should exit gracefully).
pub fn parse_args() -> Option<Args> {
    let raw_args: Vec<String> = std::env::args().collect();
    parse_args_from_vec(raw_args)
}

/// Helper variant that accepts an explicit `Vec<String>` for easier testing.
///
/// The provided vector should mimic `std::env::args()` output where the
/// first element is the program name.
pub fn parse_args_from_vec(raw_args: Vec<String>) -> Option<Args> {
    if raw_args.len() <= 1 {
        print_help();
        return None;
    }

    let os_args: Vec<std::ffi::OsString> = raw_args
        .iter()
        .cloned()
        .map(std::ffi::OsString::from)
        .collect();
    let mut pargs = Arguments::from_vec(os_args);
    if pargs.contains(["-h", "--help"]) {
        print_help();
        return None;
    }

    let config = match pargs.opt_value_from_str(["-c", "--config"]) {
        Ok(Some(s)) => s,
        _ => "config.yaml".to_string(),
    };

    let dir = match pargs.opt_value_from_str(["-d", "--dir"]) {
        Ok(Some(s)) => Some(s),
        _ => None,
    };

    // Count verbose occurrences (supports -v, -vv, -vvv and --verbose repeated)
    #[cfg(feature = "log")]
    let mut verbose_count: u8 = 0;
    #[cfg(feature = "log")]
    for arg in &raw_args[1..] {
        if arg == "-v" || arg == "--verbose" {
            verbose_count = verbose_count.saturating_add(1);
        } else if arg.starts_with("-v") && arg.chars().skip(1).all(|c| c == 'v') {
            // -vv or -vvv style
            verbose_count = verbose_count.saturating_add(arg.chars().skip(1).count() as u8);
        }
    }

    let version = pargs.contains(["-V", "--version"]);

    // If version was requested, print and exit
    if version {
        println!("lazydns {}", env!("CARGO_PKG_VERSION"));
        return None;
    }

    Some(Args {
        config,
        dir,
        #[cfg(feature = "log")]
        verbose: verbose_count.min(3),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn returns_none_and_prints_help_with_no_args() {
        let args = vec!["lazydns".to_string()];
        let res = parse_args_from_vec(args);
        assert!(res.is_none());
    }

    #[test]
    fn returns_none_on_help_flag() {
        let args = vec!["lazydns".to_string(), "--help".to_string()];
        let res = parse_args_from_vec(args);
        assert!(res.is_none());
    }

    #[cfg(feature = "log")]
    #[test]
    fn parses_verbose_variants_and_config() {
        let args = vec![
            "lazydns".to_string(),
            "-c".to_string(),
            "myconf.yaml".to_string(),
            "-d".to_string(),
            "/tmp".to_string(),
            "-vv".to_string(),
        ];

        let res = parse_args_from_vec(args).expect("should parse args");
        assert_eq!(res.config, "myconf.yaml");
        assert_eq!(res.dir.as_deref(), Some("/tmp"));
        assert_eq!(res.verbose, 2);
        assert!(res.verbose > 0);
    }

    #[test]
    fn uses_defaults_when_options_missing() {
        let args = vec![
            "lazydns".to_string(),
            "-c".to_string(),
            "cfg.yml".to_string(),
        ];
        let res = parse_args_from_vec(args).expect("should parse");
        assert_eq!(res.config, "cfg.yml");
        #[cfg(feature = "log")]
        assert_eq!(res.verbose, 0);
    }

    #[test]
    fn version_flag_prints_and_exits() {
        let args = vec!["lazydns".to_string(), "-V".to_string()];
        let res = parse_args_from_vec(args);
        assert!(res.is_none());
    }
}