use std::collections::HashMap;
use std::io::Write as _;
use slash_lang::parser::ast::{Command, Op, Program, Redirection};
pub struct Context {
pub values: HashMap<String, Option<String>>,
}
impl Context {
pub fn new() -> Self {
Self {
values: HashMap::new(),
}
}
pub fn insert(&mut self, key: impl Into<String>, value: Option<String>) {
self.values.insert(key.into(), value);
}
pub fn to_json(&self) -> String {
let pairs: Vec<String> = self
.values
.iter()
.map(|(k, v)| match v {
Some(s) => format!("\"{}\":\"{}\"", k, s.replace('"', "\\\"")),
None => format!("\"{}\":null", k),
})
.collect();
format!("{{{}}}", pairs.join(","))
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
pub enum PipeValue {
Bytes(Vec<u8>),
Context(Context),
}
pub struct CommandOutput {
pub stdout: Option<Vec<u8>>,
pub stderr: Option<Vec<u8>>,
pub success: bool,
}
#[derive(Debug)]
pub enum ExecutionError {
Runner(String),
Redirect(String),
}
pub trait CommandRunner {
fn run(
&self,
cmd: &Command,
input: Option<&PipeValue>,
) -> Result<CommandOutput, ExecutionError>;
}
pub trait Execute {
fn execute(&self, program: &Program) -> Result<Option<PipeValue>, ExecutionError>;
}
pub struct Executor<R: CommandRunner> {
runner: R,
}
impl<R: CommandRunner> Executor<R> {
pub fn new(runner: R) -> Self {
Self { runner }
}
pub fn into_runner(self) -> R {
self.runner
}
}
impl<R: CommandRunner> Execute for Executor<R> {
fn execute(&self, program: &Program) -> Result<Option<PipeValue>, ExecutionError> {
let mut last_output: Option<PipeValue> = None;
let mut last_success = true;
let mut skip_reason: Option<&Op> = None;
for pipeline in &program.pipelines {
if let Some(op) = skip_reason {
let skip = match op {
Op::And => !last_success, Op::Or => last_success, _ => false,
};
if skip {
skip_reason = pipeline.operator.as_ref();
continue;
}
}
let (output, success) =
run_pipeline(&self.runner, &pipeline.commands, last_output.take())?;
last_output = output;
last_success = success;
skip_reason = pipeline.operator.as_ref();
}
Ok(last_output)
}
}
fn run_pipeline<R: CommandRunner>(
runner: &R,
commands: &[Command],
initial_input: Option<PipeValue>,
) -> Result<(Option<PipeValue>, bool), ExecutionError> {
let mut pipe_input: Option<PipeValue> = initial_input;
let mut context = Context::new();
let mut in_optional_chain = false;
let mut last_success = true;
for cmd in commands {
if cmd.optional {
in_optional_chain = true;
let result = runner.run(cmd, None)?;
last_success = result.success;
let stdout_str = result.stdout.and_then(|b| String::from_utf8(b).ok());
context.insert(&cmd.name, stdout_str);
} else {
let input = if in_optional_chain {
in_optional_chain = false;
let json = {
let mut ctx = Context::new();
std::mem::swap(&mut ctx, &mut context);
ctx.to_json()
};
Some(PipeValue::Bytes(json.into_bytes()))
} else {
pipe_input.take()
};
let result = runner.run(cmd, input.as_ref())?;
last_success = result.success;
if let Some(redirect) = &cmd.redirect {
if let Some(stdout) = result.stdout {
write_redirect(redirect, &stdout)?;
}
pipe_input = None;
} else {
pipe_input = match &cmd.pipe {
Some(Op::PipeErr) => {
let mut combined = result.stdout.unwrap_or_default();
combined.extend(result.stderr.unwrap_or_default());
if combined.is_empty() {
None
} else {
Some(PipeValue::Bytes(combined))
}
}
_ => result.stdout.map(PipeValue::Bytes),
};
}
}
}
Ok((pipe_input, last_success))
}
fn write_redirect(redirect: &Redirection, data: &[u8]) -> Result<(), ExecutionError> {
match redirect {
Redirection::Truncate(path) => {
std::fs::write(path, data).map_err(|e| ExecutionError::Redirect(e.to_string()))
}
Redirection::Append(path) => {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| ExecutionError::Redirect(e.to_string()))?;
file.write_all(data)
.map_err(|e| ExecutionError::Redirect(e.to_string()))
}
}
}