mxsh 0.2.0

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

mod support;

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use support::{run_shell, shell_quote, temp_path};

fn assert_success(output: &std::process::Output) {
    assert!(
        output.status.success(),
        "mxsh failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
        output.status.code(),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );
}

fn write_executable(dir: &Path, name: &str, output: &str) -> PathBuf {
    fs::create_dir_all(dir).expect("temp command dir should be creatable");
    let path = dir.join(name);
    fs::write(
        &path,
        format!("#!/bin/sh\nprintf '%s\\n' {}\n", shell_quote(output)),
    )
    .expect("temp command should be writable");
    let mut perms = fs::metadata(&path)
        .expect("temp command metadata should be readable")
        .permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&path, perms).expect("temp command should be executable");
    path
}

#[test]
fn temporary_path_assignment_is_used_for_command_lookup() {
    let dir = temp_path("path-assignment-lookup");
    write_executable(&dir, "foo", "ok");
    let dir_arg = dir.to_str().expect("temp path should be utf8");

    let output = run_shell("mxsh", &["-c", "PATH=\"$1\" foo", "mxsh", dir_arg], "");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout), "ok\n");

    fs::remove_dir_all(&dir).expect("temp dir should be removable");
}

#[test]
fn repeated_temporary_path_assignments_use_last_value_then_restore_original() {
    let original_dir = temp_path("path-assignment-original");
    let stale_dir = temp_path("path-assignment-stale");
    let replacement_dir = temp_path("path-assignment-replacement");
    write_executable(&original_dir, "foo", "original");
    write_executable(&stale_dir, "foo", "stale");
    write_executable(&replacement_dir, "foo", "replacement");

    let script = format!(
        "PATH={}; PATH={} PATH={} foo; foo",
        shell_quote(original_dir.to_str().expect("temp path should be utf8")),
        shell_quote(stale_dir.to_str().expect("temp path should be utf8")),
        shell_quote(replacement_dir.to_str().expect("temp path should be utf8")),
    );
    let output = run_shell("mxsh", &["-c", &script], "");

    assert_success(&output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        "replacement\noriginal\n"
    );

    fs::remove_dir_all(&original_dir).expect("temp dir should be removable");
    fs::remove_dir_all(&stale_dir).expect("temp dir should be removable");
    fs::remove_dir_all(&replacement_dir).expect("temp dir should be removable");
}

#[test]
fn path_assignment_expansion_runs_once_when_lookup_uses_it() {
    let dir = temp_path("path-assignment-command-substitution");
    write_executable(&dir, "foo", "ok");
    let marker = dir.join("marker");

    let script = format!(
        "PATH=$(printf '%s' {}; printf x >> {}) foo; /bin/cat {}",
        shell_quote(dir.to_str().expect("temp path should be utf8")),
        shell_quote(marker.to_str().expect("temp path should be utf8")),
        shell_quote(marker.to_str().expect("temp path should be utf8")),
    );
    let output = run_shell("mxsh", &["-c", &script], "");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout), "ok\nx");

    fs::remove_dir_all(&dir).expect("temp dir should be removable");
}

#[test]
fn explicit_relative_command_uses_shell_cwd_after_cd() {
    let dir = temp_path("explicit-relative-shell-cwd");
    write_executable(&dir, "foo", "ok");
    let script = format!(
        "cd {}; ./foo",
        shell_quote(dir.to_str().expect("temp path should be utf8"))
    );

    let output = run_shell("mxsh", &["-c", &script], "");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout), "ok\n");

    fs::remove_dir_all(&dir).expect("temp dir should be removable");
}

#[test]
fn relative_path_entry_uses_shell_cwd_after_cd() {
    let dir = temp_path("relative-path-entry-shell-cwd");
    write_executable(&dir.join("bin"), "foo", "ok");
    let script = format!(
        "cd {}; PATH=bin:/usr/bin:/bin foo",
        shell_quote(dir.to_str().expect("temp path should be utf8"))
    );

    let output = run_shell("mxsh", &["-c", &script], "");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout), "ok\n");

    fs::remove_dir_all(&dir).expect("temp dir should be removable");
}

#[test]
fn glob_expansion_uses_shell_cwd_after_cd() {
    let dir = temp_path("glob-shell-cwd-[literal]");
    fs::create_dir_all(&dir).expect("temp dir should be creatable");
    fs::write(dir.join("a"), "").expect("glob fixture should be writable");
    let script = format!(
        "cd {}; printf '<%s>\\n' *",
        shell_quote(dir.to_str().expect("temp path should be utf8"))
    );

    let output = run_shell("mxsh", &["-c", &script], "");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout), "<a>\n");

    fs::remove_dir_all(&dir).expect("temp dir should be removable");
}