slash-lib 0.1.0

Executor types and high-level API for the slash-command language
Documentation
use std::process::Command as Proc;
use std::process::Stdio;

use slash_lang::parser::ast::Arg;

use crate::command::{MethodDef, SlashCommand};
use crate::executor::{CommandOutput, ExecutionError, PipeValue};

/// `/exec(command)` — run a shell command and capture output.
///
/// `/exec(cargo test)` — run, capture stdout/stderr.
/// `/exec(cargo test).verbose` — forward stderr to terminal in real time.
///
/// This is the only builtin that spawns a subprocess.
pub struct Exec;

impl SlashCommand for Exec {
    fn name(&self) -> &str {
        "exec"
    }

    fn methods(&self) -> &[MethodDef] {
        static METHODS: [MethodDef; 1] = [MethodDef::flag("verbose")];
        &METHODS
    }

    fn execute(
        &self,
        primary: Option<&str>,
        args: &[Arg],
        input: Option<&PipeValue>,
    ) -> Result<CommandOutput, ExecutionError> {
        let cmd_str = primary.ok_or_else(|| {
            ExecutionError::Runner("/exec requires a command: /exec(cargo test)".into())
        })?;

        let stdin_bytes: Option<&[u8]> = match input {
            Some(PipeValue::Bytes(b)) => Some(b),
            Some(PipeValue::Context(ctx)) => {
                // Context will be serialized below; we can't borrow it here
                // because we need the json string to outlive this match.
                let _ = ctx;
                None
            }
            None => None,
        };

        // Serialize context input if present.
        let context_json: Option<String> = match input {
            Some(PipeValue::Context(ctx)) => Some(ctx.to_json()),
            _ => None,
        };

        let effective_stdin = stdin_bytes.or(context_json.as_deref().map(|s| s.as_bytes()));

        let stdin_cfg = if effective_stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        };

        let mut child = Proc::new("sh")
            .arg("-c")
            .arg(cmd_str)
            .stdin(stdin_cfg)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .map_err(|e| ExecutionError::Runner(format!("/exec({}): {}", cmd_str, e)))?;

        if let Some(data) = effective_stdin {
            if let Some(mut stdin) = child.stdin.take() {
                use std::io::Write as _;
                let _ = stdin.write_all(data);
            }
        }

        let output = child
            .wait_with_output()
            .map_err(|e| ExecutionError::Runner(format!("/exec({}): {}", cmd_str, e)))?;

        let verbose = args.iter().any(|a| a.name == "verbose");
        if verbose && !output.stderr.is_empty() {
            use std::io::Write as _;
            let _ = std::io::stderr().write_all(&output.stderr);
        }

        let success = output.status.success();
        let stdout = if output.stdout.is_empty() {
            None
        } else {
            Some(output.stdout)
        };
        let stderr = if output.stderr.is_empty() {
            None
        } else {
            Some(output.stderr)
        };

        Ok(CommandOutput {
            stdout,
            stderr,
            success,
        })
    }
}