claptrap 0.3.0

Bring the power of Clap to shell scripts.
Documentation
const CLAPTRAP_BIN: &str = env!("CARGO_BIN_EXE_claptrap");

#[cfg(not(windows))]
mod posix {
    use crate::CLAPTRAP_BIN;
    use std::io::Write;
    use std::os::unix::fs::PermissionsExt;
    use std::time::Duration;
    use test_case::test_matrix;

    #[test_matrix(["bash", "zsh", "powershell", "fish"]; "test_script")]
    fn test_script(shell: &str) {
        let output = std::process::Command::new(CLAPTRAP_BIN)
            .arg("script")
            .arg("--spec")
            .arg(format!("tests/resources/script/{shell}/spec.toml"))
            .arg(shell)
            .output()
            .expect("failed to run claptrap");
        assert_eq!(Some(0), output.status.code());
        insta::assert_snapshot!(
            format!("test_script_{shell}"),
            String::from_utf8_lossy(&output.stdout)
        );
    }

    #[test_matrix(["bash", "zsh", "powershell", "fish"]; "test_pacman_script")]
    fn test_pacman_script(shell: &str) {
        let output = std::process::Command::new(CLAPTRAP_BIN)
            .arg("script")
            .arg("--spec")
            .arg("examples/pacman/spec.toml")
            .arg(shell)
            .output()
            .expect("failed to run claptrap");
        assert_eq!(Some(0), output.status.code());
        insta::assert_snapshot!(
            format!("test_pacman_script_{shell}"),
            String::from_utf8_lossy(&output.stdout)
        );
    }

    #[test_matrix(["bash", "zsh", "fish"]; "test_script_exec")]
    fn test_script_exec(shell: &str) {
        let output = std::process::Command::new(CLAPTRAP_BIN)
            .arg("script")
            .arg("--spec")
            .arg(format!("tests/resources/script/{shell}/spec.toml"))
            .arg(shell)
            .output()
            .expect("failed to run claptrap");
        assert_eq!(Some(0), output.status.code());
        let script = String::from_utf8_lossy(&output.stdout).to_string();
        let path_env = format!(
            "{}:{}",
            std::path::Path::new(CLAPTRAP_BIN)
                .parent()
                .unwrap()
                .display(),
            std::env::var("PATH").unwrap()
        );
        let run = |args: &[&str]| {
            let path = {
                let mut file = tempfile::NamedTempFile::with_suffix(".sh").expect("temp file");
                file.write_all(script.as_bytes()).expect("write script");
                let _ = file.as_file_mut().sync_all();
                file.into_temp_path()
            };
            std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("chmod");
            for _ in 0..12 {
                match std::process::Command::new(&path)
                    .args(args)
                    .env("PATH", &path_env)
                    .output()
                {
                    Ok(output) => return output,
                    Err(e) if e.raw_os_error() == Some(26) => {
                        std::thread::sleep(Duration::from_millis(25));
                    }
                    Err(e) => panic!("run script: {e}"),
                }
            }
            panic!("Failed to run script after multiple attempts");
        };
        let mut output_text = String::new();
        let out = run(&["foo", "--multi1", "one", "two"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        output_text.push_str("--\n");
        let out = run(&["first", "bar", "--multi2", "x", "y"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        output_text.push_str("--\n");
        let out = run(&["first", "nested", "--arg3", "q", "--multi3", "m1", "m2"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        output_text.push_str("--\n");
        let out = run(&["another", "baz", "--multi4", "a", "b"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        insta::assert_snapshot!(format!("test_script_exec_{shell}"), output_text);
    }
}

#[cfg(windows)]
mod windows {
    use crate::CLAPTRAP_BIN;
    use std::path::Path;
    use std::process::Command;

    fn powershell() -> &'static str {
        if Path::new(r"C:\Program Files\PowerShell").exists() {
            "pwsh"
        } else {
            "powershell"
        }
    }

    #[test]
    fn test_script_exec() {
        let output = Command::new(CLAPTRAP_BIN)
            .arg("script")
            .arg("--spec")
            .arg("tests/resources/script/powershell/spec.toml")
            .arg("powershell")
            .output()
            .expect("failed to run claptrap");
        assert_eq!(Some(0), output.status.code());
        let script = String::from_utf8_lossy(&output.stdout);
        let path = {
            let mut file = tempfile::NamedTempFile::with_suffix(".ps1").expect("temp file");
            std::io::Write::write_all(&mut file, script.as_bytes()).expect("write script");
            file.as_file_mut().sync_all().expect("sync script");
            file.into_temp_path()
        };
        let path_str = path.to_string_lossy().to_string();
        let path_env = format!(
            "{};{}",
            std::path::Path::new(CLAPTRAP_BIN)
                .parent()
                .unwrap()
                .display(),
            std::env::var("PATH").unwrap()
        );
        let run = |args: &[&str]| {
            Command::new(powershell())
                .args([
                    "-NoLogo",
                    "-NoProfile",
                    "-ExecutionPolicy",
                    "Bypass",
                    "-File",
                    &path_str,
                ])
                .args(args)
                .env("PATH", &path_env)
                .output()
                .expect("run script")
        };
        let mut output_text = String::new();
        let out = run(&["foo", "--multi1", "one", "two"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        output_text.push_str("--\n");
        let out = run(&["first", "bar", "--multi2", "x", "y"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        output_text.push_str("--\n");
        let out = run(&["first", "nested", "--arg3", "q", "--multi3", "m1", "m2"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        output_text.push_str("--\n");
        let out = run(&["another", "baz", "--multi4", "a", "b"]);
        assert_eq!(Some(0), out.status.code());
        output_text.push_str(&String::from_utf8_lossy(&out.stdout));
        insta::assert_snapshot!("test_script_exec_powershell", output_text);
    }
}