use crate::chunk::Chunk;
use crate::value::Value;
pub trait ShellHost: Send {
fn glob(&mut self, pattern: &str, recursive: bool) -> Vec<String> {
let _ = recursive;
glob::glob(pattern)
.into_iter()
.flat_map(|paths| paths.filter_map(|p| p.ok()))
.map(|p| p.to_string_lossy().into_owned())
.collect()
}
fn tilde_expand(&mut self, s: &str) -> String {
s.to_string()
}
fn brace_expand(&mut self, s: &str) -> Vec<String> {
vec![s.to_string()]
}
fn word_split(&mut self, s: &str) -> Vec<String> {
s.split_whitespace().map(|w| w.to_string()).collect()
}
fn expand_param(&mut self, name: &str, modifier: u8, args: &[Value]) -> Value {
let _ = (name, modifier, args);
Value::str("")
}
fn array_index(&mut self, name: &str, index: &Value) -> Value {
let _ = (name, index);
Value::Undef
}
fn cmd_subst(&mut self, sub: &Chunk) -> String {
let _ = sub;
String::new()
}
fn process_sub_in(&mut self, sub: &Chunk) -> String {
let _ = sub;
String::new()
}
fn process_sub_out(&mut self, sub: &Chunk) -> String {
let _ = sub;
String::new()
}
fn redirect(&mut self, fd: u8, op: u8, target: &str) {
let _ = (fd, op, target);
}
fn heredoc(&mut self, content: &str) {
let _ = content;
}
fn herestring(&mut self, content: &str) {
let _ = content;
}
fn pipeline_begin(&mut self, n: u8) {
let _ = n;
}
fn pipeline_stage(&mut self) {}
fn pipeline_end(&mut self) -> i32 {
0
}
fn subshell_begin(&mut self) {}
fn subshell_end(&mut self) -> Option<i32> {
None
}
fn trap_set(&mut self, sig: &str, handler: &Chunk) {
let _ = (sig, handler);
}
fn trap_check(&mut self) {}
fn with_redirects_begin(&mut self, count: u8) {
let _ = count;
}
fn with_redirects_end(&mut self) {}
fn call_function(&mut self, name: &str, args: Vec<String>) -> Option<i32> {
let _ = (name, args);
None
}
fn exec(&mut self, args: Vec<String>) -> i32 {
use std::process::{Command, Stdio};
let cmd = match args.first() {
Some(c) => c,
None => return 0,
};
Command::new(cmd)
.args(&args[1..])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map(|s| s.code().unwrap_or(1))
.unwrap_or(127)
}
fn exec_bg(&mut self, args: Vec<String>) -> i32 {
use std::process::{Command, Stdio};
let cmd = match args.first() {
Some(c) => c,
None => return 0,
};
Command::new(cmd)
.args(&args[1..])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|c| c.id() as i32)
.unwrap_or(0)
}
fn str_match(&mut self, s: &str, pat: &str) -> bool {
s == pat
}
fn regex_match(&mut self, s: &str, regex: &str) -> bool {
let _ = (s, regex);
false
}
}
pub struct DefaultHost;
impl ShellHost for DefaultHost {}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk::Chunk;
#[test]
fn tilde_expand_is_identity_by_default() {
let mut h = DefaultHost;
assert_eq!(h.tilde_expand("~/foo"), "~/foo");
assert_eq!(h.tilde_expand(""), "");
}
#[test]
fn brace_expand_returns_single_element_vec_by_default() {
let mut h = DefaultHost;
assert_eq!(h.brace_expand("{a,b}"), vec!["{a,b}".to_string()]);
assert_eq!(h.brace_expand("plain"), vec!["plain".to_string()]);
}
#[test]
fn word_split_splits_on_whitespace() {
let mut h = DefaultHost;
assert_eq!(h.word_split("one two three"), vec!["one", "two", "three"]);
assert!(h.word_split("").is_empty());
assert!(h.word_split(" \t ").is_empty());
}
#[test]
fn expand_param_default_returns_empty_string() {
let mut h = DefaultHost;
let v = h.expand_param("VAR", 0, &[]);
assert_eq!(v, Value::str(""));
}
#[test]
fn array_index_default_returns_undef() {
let mut h = DefaultHost;
assert_eq!(h.array_index("arr", &Value::Int(0)), Value::Undef);
}
#[test]
fn cmd_subst_and_process_sub_default_to_empty_string() {
let mut h = DefaultHost;
let c = Chunk::new();
assert_eq!(h.cmd_subst(&c), "");
assert_eq!(h.process_sub_in(&c), "");
assert_eq!(h.process_sub_out(&c), "");
}
#[test]
fn pipeline_end_default_is_success() {
let mut h = DefaultHost;
h.pipeline_begin(2);
h.pipeline_stage();
assert_eq!(h.pipeline_end(), 0);
}
#[test]
fn call_function_default_returns_none() {
let mut h = DefaultHost;
assert_eq!(h.call_function("fn", vec!["a".into()]), None);
}
#[test]
fn str_match_default_is_exact_equality() {
let mut h = DefaultHost;
assert!(h.str_match("foo", "foo"));
assert!(!h.str_match("foo", "bar"));
assert!(!h.str_match("foo", "f*"), "default does not glob");
}
#[test]
fn regex_match_default_is_false() {
let mut h = DefaultHost;
assert!(!h.regex_match("anything", "."));
}
#[test]
fn noop_methods_do_not_panic() {
let mut h = DefaultHost;
h.redirect(1, 0, "file");
h.heredoc("body");
h.herestring("body");
h.subshell_begin();
h.subshell_end();
h.trap_check();
h.with_redirects_begin(1);
h.with_redirects_end();
h.trap_set("INT", &Chunk::new());
}
#[test]
fn exec_with_empty_args_returns_zero() {
let mut h = DefaultHost;
assert_eq!(h.exec(vec![]), 0);
assert_eq!(h.exec_bg(vec![]), 0);
}
#[test]
fn glob_default_returns_paths_for_literal_pattern() {
let mut h = DefaultHost;
let tmp = std::env::temp_dir();
let tmp_str = tmp.to_string_lossy().to_string();
let result = h.glob(&tmp_str, false);
assert_eq!(result.len(), 1, "literal existing path matches itself");
assert!(!result[0].is_empty());
}
#[test]
fn glob_default_returns_empty_for_nonmatching_pattern() {
let mut h = DefaultHost;
let result = h.glob(
"/this/path/definitely/does/not/exist/anywhere_xyz_*.tmp",
false,
);
assert!(result.is_empty(), "no match → empty, got: {:?}", result);
}
#[test]
fn glob_default_ignores_recursive_flag() {
let mut h = DefaultHost;
let tmp = std::env::temp_dir();
let tmp_str = tmp.to_string_lossy().to_string();
let r1 = h.glob(&tmp_str, false);
let r2 = h.glob(&tmp_str, true);
assert_eq!(r1, r2);
}
#[test]
fn expand_param_default_ignores_modifier_and_args() {
let mut h = DefaultHost;
assert_eq!(h.expand_param("ANY", 0, &[]), Value::str(""));
assert_eq!(h.expand_param("ANY", 255, &[]), Value::str(""));
assert_eq!(
h.expand_param("ANY", 7, &[Value::Int(42), Value::str("x")]),
Value::str("")
);
}
#[test]
fn word_split_collapses_consecutive_whitespace() {
let mut h = DefaultHost;
assert_eq!(
h.word_split(" a\t b\n\nc \t d "),
vec!["a", "b", "c", "d"]
);
}
#[test]
fn array_index_default_returns_undef_for_any_index_type() {
let mut h = DefaultHost;
assert_eq!(h.array_index("a", &Value::Int(-1)), Value::Undef);
assert_eq!(h.array_index("", &Value::str("key")), Value::Undef);
assert_eq!(h.array_index("a", &Value::Undef), Value::Undef);
}
#[test]
fn pipeline_lifecycle_does_not_drift_status_in_default_impl() {
let mut h = DefaultHost;
for _ in 0..5 {
h.pipeline_begin(3);
h.pipeline_stage();
h.pipeline_stage();
assert_eq!(h.pipeline_end(), 0);
}
}
#[test]
fn trap_set_default_accepts_any_signal_name_without_panic() {
let mut h = DefaultHost;
let c = Chunk::new();
h.trap_set("", &c);
h.trap_set("SIGINT", &c);
h.trap_set("EXIT", &c);
h.trap_set("\0nonsense", &c);
}
}