fcoreutils 0.22.0

High-performance GNU coreutils replacement with SIMD and parallelism
Documentation
// fprintenv — print all or part of environment
//
// Usage: printenv [OPTION]... [VARIABLE]...

use std::io::Write;
use std::process;

const TOOL_NAME: &str = "printenv";
const VERSION: &str = env!("CARGO_PKG_VERSION");

fn main() {
    coreutils_rs::common::reset_sigpipe();

    let mut null_terminated = false;
    let mut names: Vec<String> = Vec::new();
    let mut saw_dashdash = false;

    for arg in std::env::args().skip(1) {
        if saw_dashdash {
            names.push(arg);
            continue;
        }
        match arg.as_str() {
            "--help" => {
                println!("Usage: {} [OPTION]... [VARIABLE]...", TOOL_NAME);
                println!("Print the values of the specified environment VARIABLE(s).");
                println!("If no VARIABLE is specified, print name and value pairs for them all.");
                println!();
                println!("  -0, --null    end each output line with NUL, not newline");
                println!("      --help    display this help and exit");
                println!("      --version output version information and exit");
                return;
            }
            "--version" => {
                println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
                return;
            }
            "--null" | "-0" => null_terminated = true,
            "--" => saw_dashdash = true,
            s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
                for ch in s[1..].chars() {
                    match ch {
                        '0' => null_terminated = true,
                        _ => {
                            eprintln!("{}: invalid option -- '{}'", TOOL_NAME, ch);
                            eprintln!("Try '{} --help' for more information.", TOOL_NAME);
                            process::exit(2);
                        }
                    }
                }
            }
            _ => names.push(arg),
        }
    }

    let term: &[u8] = if null_terminated { b"\0" } else { b"\n" };
    let stdout = std::io::stdout();
    let mut out = stdout.lock();

    if names.is_empty() {
        // Print all environment variables using vars_os to avoid UTF-8 validation
        for (key, value) in std::env::vars_os() {
            let _ = out.write_all(key.as_encoded_bytes());
            let _ = out.write_all(b"=");
            let _ = out.write_all(value.as_encoded_bytes());
            let _ = out.write_all(term);
        }
    } else {
        let mut exit_code = 0;
        for name in &names {
            // GNU printenv silently rejects variable names containing '='
            if name.contains('=') {
                exit_code = 1;
                continue;
            }
            match std::env::var_os(name) {
                Some(val) => {
                    let _ = out.write_all(val.as_encoded_bytes());
                    let _ = out.write_all(term);
                }
                None => exit_code = 1,
            }
        }
        process::exit(exit_code);
    }
}

#[cfg(test)]
mod tests {
    use std::process::Command;

    fn cmd() -> Command {
        let mut path = std::env::current_exe().unwrap();
        path.pop();
        path.pop();
        path.push("fprintenv");
        Command::new(path)
    }

    #[test]
    fn test_printenv_all() {
        let output = cmd().output().unwrap();
        assert_eq!(output.status.code(), Some(0));
        let stdout = String::from_utf8_lossy(&output.stdout);
        // Should contain at least PATH
        assert!(stdout.contains("PATH="), "Should contain PATH");
    }

    #[test]
    fn test_printenv_specific() {
        let output = cmd().arg("PATH").output().unwrap();
        assert_eq!(output.status.code(), Some(0));
        let stdout = String::from_utf8_lossy(&output.stdout);
        assert!(!stdout.is_empty());
        // Should NOT contain "PATH=" prefix (just the value)
        assert!(!stdout.starts_with("PATH="), "Should print value only");
    }

    #[test]
    fn test_printenv_missing() {
        let output = cmd()
            .arg("DEFINITELY_NOT_A_REAL_ENV_VAR_12345")
            .output()
            .unwrap();
        assert_eq!(output.status.code(), Some(1));
        assert!(output.stdout.is_empty());
    }

    #[test]
    fn test_printenv_null_terminator() {
        let output = cmd().args(["-0", "PATH"]).output().unwrap();
        assert_eq!(output.status.code(), Some(0));
        let stdout = &output.stdout;
        assert!(stdout.ends_with(&[0u8]), "Should end with NUL byte");
        assert!(!stdout.ends_with(b"\n"), "Should not end with newline");
    }

    #[test]
    #[cfg(unix)]
    fn test_printenv_matches_gnu() {
        let gnu = Command::new("printenv").arg("PATH").output();
        if let Ok(gnu) = gnu {
            let ours = cmd().arg("PATH").output().unwrap();
            assert_eq!(ours.stdout, gnu.stdout, "STDOUT mismatch");
            assert_eq!(ours.status.code(), gnu.status.code(), "Exit code mismatch");
        }
    }

    #[test]
    fn test_printenv_multiple_vars() {
        let output = cmd()
            .args(["PATH", "MY_MULTI_TEST_VAR"])
            .env("MY_MULTI_TEST_VAR", "value2")
            .output()
            .unwrap();
        assert!(output.status.success());
        let stdout = String::from_utf8_lossy(&output.stdout);
        let lines: Vec<&str> = stdout.lines().collect();
        assert!(lines.len() >= 2);
    }

    #[test]
    fn test_printenv_mixed_exist_nonexist() {
        let output = cmd()
            .args(["PATH", "DEFINITELY_NOT_A_VAR_XYZ"])
            .output()
            .unwrap();
        // Should fail because one variable doesn't exist
        assert!(!output.status.success());
    }

    #[test]
    fn test_printenv_path_var() {
        let output = cmd().arg("PATH").output().unwrap();
        assert!(output.status.success());
        let stdout = String::from_utf8_lossy(&output.stdout);
        assert!(!stdout.trim().is_empty(), "PATH should not be empty");
    }

    #[test]
    fn test_printenv_null_all() {
        let output = cmd().arg("-0").output().unwrap();
        assert!(output.status.success());
        // Output should contain NUL bytes
        assert!(output.stdout.contains(&0u8));
    }

    #[test]
    fn test_printenv_custom_var() {
        let output = cmd()
            .arg("MY_TEST_VAR_123")
            .env("MY_TEST_VAR_123", "test_value")
            .output()
            .unwrap();
        assert!(output.status.success());
        assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test_value");
    }
}