mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
#![cfg(all(feature = "embed", feature = "frontend", feature = "test-support"))]

use std::fs;
#[cfg(feature = "cli")]
use std::os::unix::fs::symlink;
#[cfg(feature = "cli")]
use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "cli")]
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};

use mxsh::ShellBuilder;
use mxsh::embed::StdioConfig;
use mxsh::policy::{StartupPolicy, VariableAttributes};
use mxsh::runtime::testing::{InMemoryRuntime, StringStdioIn, StringStdioOut};

fn temp_path(label: &str) -> PathBuf {
    static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
    std::env::temp_dir().join(format!(
        "mxsh-{label}-{}-{}",
        std::process::id(),
        NEXT_ID.fetch_add(1, Ordering::Relaxed)
    ))
}

#[cfg(feature = "cli")]
fn mxsh_command() -> Command {
    Command::new(env!("CARGO_BIN_EXE_mxsh"))
}

#[cfg(feature = "cli")]
fn real_cwd_fixture(label: &str) -> (PathBuf, PathBuf) {
    let root = temp_path(label);
    let real = root.join("real");
    fs::create_dir_all(&real).expect("real cwd should be creatable");
    (root, real)
}

#[cfg(feature = "cli")]
fn mxsh_with_pwd(current_dir: &Path, pwd: &Path) -> Command {
    let mut command = mxsh_command();
    command.current_dir(current_dir).env("PWD", pwd);
    command
}

#[test]
#[cfg(feature = "cli")]
fn startup_rejects_inherited_pwd_that_does_not_name_process_cwd() {
    let (root, real) = real_cwd_fixture("stale-startup-pwd");
    let stale = root.join("stale");
    fs::create_dir_all(&stale).expect("stale cwd should be creatable");

    let output = mxsh_with_pwd(&real, &stale)
        .arg("-c")
        .arg(
            "printf 'env=<%s>\\n' \"$PWD\"; \
             printf 'builtin=<%s>\\n' \"$(pwd)\"; \
             printf 'external=<%s>\\n' \"$(/bin/pwd)\"; \
             printf redirected >relative.out",
        )
        .output()
        .expect("mxsh should run");

    let expected_cwd = fs::canonicalize(&real).expect("real cwd should canonicalize");
    let expected = expected_cwd.display();
    assert_eq!(output.status.code(), Some(0));
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        format!("env=<{expected}>\nbuiltin=<{expected}>\nexternal=<{expected}>\n")
    );
    assert_eq!(
        fs::read_to_string(real.join("relative.out")).expect("redirected output in real cwd"),
        "redirected"
    );
    assert!(
        !stale.join("relative.out").exists(),
        "relative redirection should not use stale inherited PWD"
    );

    let _ = fs::remove_dir_all(root);
}

#[test]
#[cfg(feature = "cli")]
fn startup_uses_validated_cwd_for_script_file_lookup() {
    let (root, real) = real_cwd_fixture("script-startup-pwd");
    let stale = root.join("stale");
    fs::create_dir_all(&stale).expect("stale cwd should be creatable");
    fs::write(real.join("script.sh"), "echo real\n").expect("real script should be writable");
    fs::write(stale.join("script.sh"), "echo stale\n").expect("stale script should be writable");

    let output = mxsh_with_pwd(&real, &stale)
        .arg("script.sh")
        .output()
        .expect("mxsh should run script");

    assert_eq!(output.status.code(), Some(0));
    assert_eq!(String::from_utf8_lossy(&output.stdout), "real\n");
    assert_eq!(String::from_utf8_lossy(&output.stderr), "");

    let _ = fs::remove_dir_all(root);
}

#[test]
#[cfg(feature = "cli")]
fn startup_preserves_inherited_pwd_when_it_resolves_to_process_cwd() {
    let (root, real) = real_cwd_fixture("symlink-startup-pwd");
    let logical = root.join("logical");
    symlink(&real, &logical).expect("logical cwd symlink should be creatable");

    let output = mxsh_with_pwd(&real, &logical)
        .arg("-c")
        .arg("printf 'env=<%s>\\n' \"$PWD\"; printf 'builtin=<%s>\\n' \"$(pwd)\"")
        .output()
        .expect("mxsh should run");

    let expected = logical.display();
    assert_eq!(output.status.code(), Some(0));
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        format!("env=<{expected}>\nbuiltin=<{expected}>\n")
    );

    let _ = fs::remove_dir_all(root);
}

#[test]
fn explicit_none_startup_policy_disables_cli_env_hook_defaults() {
    let input = StringStdioIn::new("helper\nexit\n");
    let stdout = StringStdioOut::new();
    let stderr = StringStdioOut::new();
    let env_path = temp_path("cli-explicit-startup-policy");
    fs::write(&env_path, "helper() { echo from_env; }\n").expect("write ENV helper");

    let argv = vec!["mxsh".to_string()];
    let mut shell = ShellBuilder::new()
        .interactive(true)
        .startup_policy(StartupPolicy::None)
        .clear_inherited_env()
        .env(
            "ENV",
            env_path.display().to_string(),
            VariableAttributes::empty(),
        )
        .env("PS1", "", VariableAttributes::empty())
        .env("PS2", "", VariableAttributes::empty())
        .stdio(StdioConfig {
            stdin: input.fd(),
            stdout: stdout.fd(),
            stderr: stderr.fd(),
        })
        .build(InMemoryRuntime::new())
        .expect("shell should build");
    let outcome = shell.run_cli(&argv);

    input.join();
    let _ = fs::remove_file(&env_path);

    assert_eq!(outcome.status, 127);
    assert_eq!(outcome.exit_code, Some(127));
    assert_eq!(stdout.collect(), "");
    assert!(stderr.collect().contains("helper: command not found"));
}