mxsh 0.2.0

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

use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use mxsh::ShellBuilder;
use mxsh::advanced::SessionState;
use mxsh::ast::Program;
use mxsh::runtime::Runtime;

pub const READ_VAR_NAMES: &[&str] = &["X", "Y", "Z"];

pub fn fresh_state() -> SessionState {
    ShellBuilder::new()
        .new_session()
        .expect("session should build")
}

pub fn fresh_state_with_builder(builder: ShellBuilder) -> SessionState {
    builder.new_session().expect("session should build")
}

pub fn run_script<R: Runtime>(script: &str, state: &mut SessionState, runtime: &mut R) -> i32 {
    let program = Program::parse(script).expect("script should parse");
    state.run_program(runtime, &program).status
}

pub 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)
    ))
}

pub fn shell_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "'\\''"))
}

pub fn shell_program(program: &str) -> &str {
    if program == "mxsh" {
        env!("CARGO_BIN_EXE_mxsh")
    } else {
        program
    }
}

fn spawn_shell(program: &str, args: &[&str], stdin: &str) -> std::process::Child {
    let mut child = Command::new(shell_program(program))
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap_or_else(|err| panic!("failed to spawn {program}: {err}"));
    child
        .stdin
        .take()
        .expect("stdin must be piped")
        .write_all(stdin.as_bytes())
        .unwrap_or_else(|err| panic!("failed to write stdin for {program}: {err}"));
    child
}

pub fn run_shell(program: &str, args: &[&str], stdin: &str) -> Output {
    spawn_shell(program, args, stdin)
        .wait_with_output()
        .unwrap_or_else(|err| panic!("failed to wait for {program}: {err}"))
}

pub fn run_shell_with_timeout(
    program: &str,
    args: &[&str],
    stdin: &str,
    timeout: Duration,
) -> Output {
    let child = spawn_shell(program, args, stdin);
    let pid = child.id();
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let result = child.wait_with_output();
        let _ = tx.send(result);
    });
    match rx.recv_timeout(timeout) {
        Ok(Ok(output)) => output,
        Ok(Err(err)) => panic!("failed to wait for {program}: {err}"),
        Err(mpsc::RecvTimeoutError::Timeout) => {
            unsafe {
                libc::kill(pid as i32, libc::SIGKILL);
            }
            match rx.recv() {
                Ok(Ok(output)) => panic!(
                    "{program} timed out after {:?} with stderr={:?}",
                    timeout,
                    String::from_utf8_lossy(&output.stderr),
                ),
                Ok(Err(err)) => panic!("failed to collect timed out {program}: {err}"),
                Err(err) => panic!("failed to receive timed out {program} output: {err}"),
            }
        }
        Err(mpsc::RecvTimeoutError::Disconnected) => {
            panic!("wait thread disconnected for {program}")
        }
    }
}

pub fn semantic_output(output: &Output) -> (i32, String) {
    (
        output.status.code().unwrap_or(-1),
        String::from_utf8_lossy(&output.stdout).into_owned(),
    )
}

pub fn read_var_names(var_count: usize) -> &'static [&'static str] {
    &READ_VAR_NAMES[..var_count]
}

pub fn append_read_command(script: &mut String, ifs: &str, raw_mode: bool, var_names: &[&str]) {
    script.push_str("IFS=");
    script.push_str(&shell_quote(ifs));
    script.push_str("; read");
    if raw_mode {
        script.push_str(" -r");
    }
    for name in var_names {
        script.push(' ');
        script.push_str(name);
    }
}

pub fn append_read_results(script: &mut String, var_names: &[&str]) {
    script.push_str("; printf '%s\\n' \"$?\"");
    for name in var_names {
        script.push_str("; if [ \"${");
        script.push_str(name);
        script.push_str("+set}\" = set ]; then printf 'set:%s\\n' \"$");
        script.push_str(name);
        script.push_str("\"; else printf 'unset\\n'; fi");
    }
}