mxsh 0.2.0

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

mod support;

use proptest::prelude::*;
use support::{
    append_read_command, append_read_results, read_var_names, run_shell, semantic_output,
    shell_quote,
};

const READ_IFS_VALUES: &[&str] = &[" \t\n", " ", ""];
const GETOPTS_STRINGS: &[&str] = &[
    "a", ":a", "ab", ":ab", "f:", ":f:", "af:", ":af:", "abf:", ":abf:", "a:b", ":a:b",
];
const GETOPTS_ARGS: &[&str] = &[
    "-a", "-b", "-f", "-ab", "-ba", "-af", "-bf", "-abf", "-fa", "-fvalue", "-avalue", "-z", "--",
    "value", "arg", "",
];

fn read_input() -> impl Strategy<Value = String> {
    let ch = prop_oneof![Just('a'), Just('b'), Just(' '), Just(','), Just('\t'),];
    prop::collection::vec(ch, 0..8)
        .prop_map(|chars| {
            let mut s: String = chars.into_iter().collect();
            s.push('\n');
            s
        })
        .boxed()
}

fn read_var_count() -> impl Strategy<Value = usize> {
    1usize..=3usize
}

fn read_script(ifs: &str, raw_mode: bool, var_count: usize) -> String {
    let var_names = read_var_names(var_count);
    let mut script = String::new();
    append_read_command(&mut script, ifs, raw_mode, var_names);
    append_read_results(&mut script, var_names);
    script
}

fn getopts_args() -> impl Strategy<Value = Vec<String>> {
    prop::collection::vec(prop::sample::select(GETOPTS_ARGS), 0..6).prop_map(|args| {
        args.into_iter()
            .map(std::string::ToString::to_string)
            .collect::<Vec<_>>()
    })
}

fn getopts_script(optstring: &str, args: &[String], optind: i32, calls: u8) -> String {
    let args = args
        .iter()
        .map(|arg| shell_quote(arg))
        .collect::<Vec<_>>()
        .join(" ");
    let mut script = String::new();
    script.push_str(&format!("set -- {args}; "));
    script.push_str(&format!("OPTIND={optind}; "));
    script.push_str("OPTARG=keep; ");
    for _ in 0..calls {
        script.push_str(&format!("getopts {} opt; ", shell_quote(optstring)));
        script
            .push_str("printf '%s|%s|%s|%s\\n' \"$opt\" \"${OPTARG-unset}\" \"$OPTIND\" \"$?\"; ");
    }
    script
}

proptest! {
    #![proptest_config(ProptestConfig {
        cases: 128,
        max_local_rejects: 0,
        failure_persistence: None,
        .. ProptestConfig::default()
    })]

    #[test]
    fn read_matches_dash(
        ifs in prop::sample::select(READ_IFS_VALUES),
        raw_mode in any::<bool>(),
        var_count in read_var_count(),
        stdin in read_input(),
    ) {
        let script = read_script(ifs, raw_mode, var_count);
        let mxsh = run_shell("mxsh", &["-c", &script], &stdin);
        let sh = run_shell("/bin/sh", &["-c", &script], &stdin);
        prop_assert_eq!(
            semantic_output(&mxsh),
            semantic_output(&sh),
            "script={:?} stdin={:?}",
            script,
            stdin,
        );
    }

    #[test]
    fn getopts_matches_dash(
        optstring in prop::sample::select(GETOPTS_STRINGS),
        args in getopts_args(),
        optind in prop_oneof![Just(-1), Just(0), 1i32..=6i32, Just(999)],
        calls in 1u8..=5u8,
    ) {
        let script = getopts_script(optstring, &args, optind, calls);
        let mxsh = run_shell("mxsh", &["-c", &script], "");
        let sh = run_shell("/bin/sh", &["-c", &script], "");
        prop_assert_eq!(
            semantic_output(&mxsh),
            semantic_output(&sh),
            "script={:?}",
            script,
        );
    }
}