mxsh 0.2.0

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

use mxsh::ShellBuilder;
use mxsh::ast::Program;
use mxsh::embed::{DeferredWorkKind, StdioConfig, TraceEvent, VariableAttributes};
use mxsh::runtime::testing::{InMemoryRuntime, StringStdioOut};

#[test]
fn runtime_owning_shell_runs_and_collects_trace() {
    let stdout = StringStdioOut::new();
    let mut shell = ShellBuilder::new()
        .env("GREETING", "hello", VariableAttributes::EXPORT)
        .stdio(StdioConfig {
            stdout: stdout.fd(),
            ..StdioConfig::default()
        })
        .build(InMemoryRuntime::new())
        .expect("shell should build");

    let outcome = shell.run("echo \"$GREETING world\"");

    assert_eq!(outcome.status, 0);
    assert!(outcome.diagnostics.is_empty());
    assert_eq!(stdout.collect(), "hello world\n");
    assert_eq!(outcome.trace.len(), 2);
    assert!(matches!(
        &outcome.trace[0],
        TraceEvent::RunStarted {
            raw_command: Some(command),
            ..
        } if command == "echo \"$GREETING world\""
    ));
    assert!(matches!(
        &outcome.trace[1],
        TraceEvent::RunFinished { status: 0, .. }
    ));
}

#[test]
fn embedded_run_trace_uses_exit_trap_status() {
    let mut shell = ShellBuilder::new()
        .build(InMemoryRuntime::new())
        .expect("shell should build");

    let outcome = shell.run("trap 'exit 7' EXIT; exit 3");

    assert_eq!(outcome.status, 7);
    assert_eq!(outcome.exit_code, Some(7));
    assert!(matches!(
        outcome.trace.last(),
        Some(TraceEvent::RunFinished { status: 7, .. })
    ));
}

#[test]
fn prepared_run_trace_uses_exit_trap_status() {
    let mut shell = ShellBuilder::new()
        .build(InMemoryRuntime::new())
        .expect("shell should build");
    let program = Program::parse("trap 'exit 9' EXIT; exit 2").expect("program should parse");
    let prepared = shell.prepare(&program);

    let outcome = shell.run_prepared(&prepared);

    assert_eq!(outcome.status, 9);
    assert_eq!(outcome.exit_code, Some(9));
    assert!(matches!(
        outcome.trace.last(),
        Some(TraceEvent::RunFinished { status: 9, .. })
    ));
}

#[test]
fn run_outcome_collects_deferred_work_trace_without_manual_sinks() {
    let stdout = StringStdioOut::new();
    let mut runtime = InMemoryRuntime::new();
    runtime.register_command("cat", |_argv, _env, _cwd, stdio| {
        let input = stdio.stdin_fd.dup().expect("dup cat stdin").read_all();
        let _ = stdio.stdout_fd.write_str(&input);
        0
    });
    let mut shell = ShellBuilder::new()
        .stdio(StdioConfig {
            stdout: stdout.fd(),
            ..StdioConfig::default()
        })
        .build(runtime)
        .expect("shell should build");

    let outcome = shell.run("unset X; echo ${X:=hi} | cat");

    assert_eq!(outcome.status, 0);
    assert_eq!(stdout.collect(), "hi\n");
    assert!(outcome.diagnostics.is_empty());
    assert!(outcome.trace.iter().any(|event| {
        matches!(
            event,
            TraceEvent::DeferredWorkStarted {
                kind: DeferredWorkKind::ParameterExpansion,
                ..
            }
        )
    }));
    assert!(outcome.trace.iter().any(|event| {
        matches!(
            event,
            TraceEvent::DeferredWorkFinished {
                kind: DeferredWorkKind::ParameterExpansion,
                status: 0,
                ..
            }
        )
    }));
}