use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use mk_rs_core::shell::{Shell, ShellResult, ShellError};
#[derive(Debug, Clone)]
pub struct ShShell;
impl Shell for ShShell {
fn name(&self) -> &str {
"sh"
}
fn execute(
&self,
recipe: &str,
env: &HashMap<String, String>,
dir: &Path,
) -> Result<ShellResult, ShellError> {
let mut cmd = Command::new("/bin/sh");
cmd.arg("-e") .arg("-c") .arg(recipe)
.current_dir(dir);
cmd.env_clear();
for (k, v) in env {
cmd.env(k, v);
}
if !env.contains_key("PATH") {
cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin");
}
let status = cmd.status()?;
Ok(ShellResult {
exit_code: status.code().unwrap_or(-1),
stdout: String::new(),
stderr: String::new(),
})
}
fn find_unescaped(&self, input: &str, ch: char) -> Vec<usize> {
let mut positions = Vec::new();
let bytes = input.as_bytes();
let mut in_single = false;
let mut in_double = false;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' if !in_single => {
i += 2; continue;
}
b'\'' if !in_double => {
in_single = !in_single;
}
b'"' if !in_single => {
in_double = !in_double;
}
c if c == ch as u8 && !in_single && !in_double => {
positions.push(i);
}
_ => {}
}
i += 1;
}
positions
}
fn quote(&self, token: &str) -> String {
if token.is_empty() {
return "''".to_string();
}
if !token.contains('\'') {
return format!("'{}'", token);
}
let escaped = token.replace('\'', "'\\''");
format!("'{}'", escaped)
}
}
#[derive(Debug, Clone)]
pub struct CustomShell {
cmd: String,
}
impl CustomShell {
pub fn new(cmd: &str) -> Self {
Self { cmd: cmd.to_string() }
}
}
impl Shell for CustomShell {
fn name(&self) -> &str { &self.cmd }
fn execute(
&self,
recipe: &str,
env: &HashMap<String, String>,
dir: &Path,
) -> Result<ShellResult, ShellError> {
let parts: Vec<&str> = self.cmd.split_whitespace().collect();
if parts.is_empty() {
return Err(ShellError::ShellNotFound { name: "empty MKSHELL".into() });
}
let mut cmd = Command::new(parts[0]);
if parts.len() > 1 {
for arg in &parts[1..] {
cmd.arg(arg);
}
} else {
cmd.arg("-c");
}
cmd.arg(recipe).current_dir(dir);
cmd.env_clear();
for (k, v) in env {
cmd.env(k, v);
}
if !env.contains_key("PATH") {
cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin");
}
let status = cmd.status()?;
Ok(ShellResult {
exit_code: status.code().unwrap_or(-1),
stdout: String::new(),
stderr: String::new(),
})
}
fn find_unescaped(&self, input: &str, ch: char) -> Vec<usize> {
ShShell.find_unescaped(input, ch) }
fn quote(&self, token: &str) -> String {
ShShell.quote(token)
}
}
#[cfg(test)]
mod custom_shell_tests {
use super::*;
#[test]
fn custom_shell_bash() {
let shell = CustomShell::new("/bin/bash -c");
assert_eq!(shell.name(), "/bin/bash -c");
let result = shell.execute("echo hello", &HashMap::new(), Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
}
#[test]
#[ignore] fn custom_shell_node() {
let shell = CustomShell::new("node -e");
assert_eq!(shell.name(), "node -e");
let result = shell.execute("console.log('hello')", &HashMap::new(), Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
}
#[test]
fn custom_shell_no_flags_defaults_to_c() {
let shell = CustomShell::new("/bin/bash");
let result = shell.execute("echo hi", &HashMap::new(), Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
}
}
#[cfg(feature = "duckscript")]
#[derive(Debug, Clone)]
pub struct DuckShell;
#[cfg(feature = "duckscript")]
impl Shell for DuckShell {
fn name(&self) -> &str {
"duckscript"
}
fn execute(
&self,
recipe: &str,
env: &HashMap<String, String>,
dir: &Path,
) -> Result<ShellResult, ShellError> {
let mut context = duckscript::types::runtime::Context::new();
for (k, v) in env {
context.variables.insert(k.clone(), v.clone());
}
duckscriptsdk::load(&mut context.commands)
.map_err(|e| ShellError::Io(std::io::Error::other(e.to_string())))?;
std::env::set_current_dir(dir)
.map_err(ShellError::Io)?;
duckscript::runner::run_script(recipe, context, None)
.map_err(|e| ShellError::Io(std::io::Error::other(e.to_string())))?;
Ok(ShellResult {
exit_code: 0,
stdout: String::new(),
stderr: String::new(),
})
}
fn find_unescaped(&self, input: &str, ch: char) -> Vec<usize> {
input.match_indices(ch).map(|(i, _)| i).collect()
}
fn quote(&self, token: &str) -> String {
token.to_string() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sh_shell_name() {
assert_eq!(ShShell.name(), "sh");
}
#[test]
fn execute_echo() {
let shell = ShShell;
let env = HashMap::new();
let result = shell.execute("echo hello", &env, Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.exit_code, 0);
}
#[test]
fn execute_error() {
let shell = ShShell;
let env = HashMap::new();
let result = shell.execute("exit 1", &env, Path::new(".")).unwrap();
assert_eq!(result.exit_code, 1);
}
#[test]
fn execute_with_env() {
let shell = ShShell;
let mut env = HashMap::new();
env.insert("MYVAR".into(), "myval".into());
let result = shell.execute("echo $MYVAR", &env, Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
}
#[test]
fn find_unescaped_equal() {
let shell = ShShell;
let pos = shell.find_unescaped("CC=gcc", '=');
assert_eq!(pos, vec![2]);
}
#[test]
fn find_unescaped_ignores_quoted() {
let shell = ShShell;
let pos = shell.find_unescaped("foo '=' bar", '=');
assert!(pos.is_empty());
}
#[test]
fn find_unescaped_ignores_escaped() {
let shell = ShShell;
let pos = shell.find_unescaped("foo \\= bar", '=');
assert!(pos.is_empty());
}
#[test]
fn quote_simple() {
let shell = ShShell;
assert_eq!(shell.quote("hello"), "'hello'");
}
#[test]
fn quote_empty() {
assert_eq!(ShShell.quote(""), "''");
}
#[test]
fn quote_with_single_quote() {
let shell = ShShell;
assert_eq!(shell.quote("it's"), "'it'\\''s'");
}
#[test]
fn execute_stdout_inherited_not_captured() {
let shell = ShShell;
let env = HashMap::new();
let result = shell.execute("echo visible", &env, Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.is_empty());
assert!(result.stderr.is_empty());
}
#[cfg(feature = "duckscript")]
#[test]
fn duck_shell_name() {
assert_eq!(DuckShell.name(), "duckscript");
}
#[cfg(feature = "duckscript")]
#[test]
fn duck_shell_execute_simple() {
let shell = DuckShell;
let env = HashMap::new();
let result = shell.execute("echo hello", &env, Path::new(".")).unwrap();
assert_eq!(result.exit_code, 0);
}
}