command-stream 1.12.0

Modern shell command execution library with streaming, async iteration, and event support
Documentation
use command_stream::CommandBuilder;

#[tokio::test]
async fn literal_arguments_and_exit_status() {
    let result = CommandBuilder::new(if cfg!(windows) { "cmd" } else { "sh" })
        .args(if cfg!(windows) {
            vec!["/C", "exit 7"]
        } else {
            vec!["-c", "exit 7"]
        })
        .run()
        .await
        .unwrap();
    assert_eq!(result.code, 7);
}

#[cfg(unix)]
#[tokio::test]
async fn hostile_arguments_stay_literal() {
    let result = CommandBuilder::new("printf")
        .args(["%s|", "", "a b", "$(echo injected)", "a'b"])
        .run()
        .await
        .unwrap();
    assert_eq!(result.stdout, "|a b|$(echo injected)|a'b|");
}

#[test]
fn fy_cli_creates_a_module_and_refuses_to_overwrite() {
    let directory = tempfile::tempdir().unwrap();
    let input = directory.path().join("script.sh");
    let output = directory.path().join("script.mjs");
    std::fs::write(&input, "echo hello\n").unwrap();
    let binary = env!("CARGO_BIN_EXE_command-stream");
    assert!(std::process::Command::new(binary)
        .arg("fy")
        .arg(&input)
        .arg(&output)
        .status()
        .unwrap()
        .success());
    let module = std::fs::read_to_string(&output).unwrap();
    assert!(module.contains("const { command } = await use"));
    assert!(module.contains("echo hello"));
    assert!(!std::process::Command::new(binary)
        .arg("fy")
        .arg(&input)
        .arg(&output)
        .output()
        .unwrap()
        .status
        .success());
    assert_eq!(std::fs::read_to_string(output).unwrap(), module);
}

#[test]
fn builder_composition_and_sync_execution() {
    let builder = CommandBuilder::new(if cfg!(windows) { "cmd" } else { "sh" })
        .args(if cfg!(windows) {
            vec!["/C", "exit 7"]
        } else {
            vec!["-c", "exit 7"]
        })
        .pipe(|builder| builder.stderr(command_stream::CommandStdio::Ignore));
    assert_eq!(builder.run_sync().unwrap().code, 7);
    assert_eq!(builder.run_sync().unwrap().code, 7);
}

#[cfg(unix)]
#[tokio::test]
async fn builder_env_layers_and_large_stdin() {
    let builder = CommandBuilder::new("sh")
        .args([
            "-c",
            "printf '%s|%s|' \"$BUILDER_A\" \"${BUILDER_B-unset}\"; cat",
        ])
        .env_clear()
        .envs([("BUILDER_A", "a"), ("BUILDER_B", "removed")])
        .env_remove("BUILDER_B")
        .stdin(command_stream::StdinOption::Content("input".repeat(32768)));
    let expected = format!("a|unset|{}", "input".repeat(32768));
    assert_eq!(builder.run().await.unwrap().stdout, expected);
    assert_eq!(builder.run_sync().unwrap().stdout, expected);
}

#[cfg(unix)]
#[tokio::test]
async fn cwd_and_stdio_policies_match_sync_and_async() {
    use command_stream::CommandStdio;
    let directory = tempfile::tempdir().unwrap();
    std::fs::write(directory.path().join("input.txt"), "from cwd").unwrap();
    let builder = CommandBuilder::new("sh")
        .args(["-c", "cat input.txt; printf hidden >&2"])
        .cwd(directory.path())
        .stderr(CommandStdio::Ignore);
    let result = builder.run().await.unwrap();
    assert_eq!(result.stdout, "from cwd");
    assert_eq!(result.stderr, "");
    let result = builder
        .clone()
        .stdout(CommandStdio::Ignore)
        .run_sync()
        .unwrap();
    assert_eq!(result.stdout, "");
    assert_eq!(result.stderr, "");
    assert_eq!(result.code, 0);
}