Skip to main content

slash_lib/builtins/
exec.rs

1use std::process::Command as Proc;
2use std::process::Stdio;
3
4use slash_lang::parser::ast::Arg;
5
6use crate::command::{MethodDef, SlashCommand};
7use crate::executor::{CommandOutput, ExecutionError, PipeValue};
8
9/// `/exec(command)` — run a shell command and capture output.
10///
11/// `/exec(cargo test)` — run, capture stdout/stderr.
12/// `/exec(cargo test).verbose` — forward stderr to terminal in real time.
13///
14/// This is the only builtin that spawns a subprocess.
15pub struct Exec;
16
17impl SlashCommand for Exec {
18    fn name(&self) -> &str {
19        "exec"
20    }
21
22    fn methods(&self) -> &[MethodDef] {
23        static METHODS: [MethodDef; 1] = [MethodDef::flag("verbose")];
24        &METHODS
25    }
26
27    fn execute(
28        &self,
29        primary: Option<&str>,
30        args: &[Arg],
31        input: Option<&PipeValue>,
32    ) -> Result<CommandOutput, ExecutionError> {
33        let cmd_str = primary.ok_or_else(|| {
34            ExecutionError::Runner("/exec requires a command: /exec(cargo test)".into())
35        })?;
36
37        let stdin_bytes: Option<&[u8]> = match input {
38            Some(PipeValue::Bytes(b)) => Some(b),
39            Some(PipeValue::Context(ctx)) => {
40                // Context will be serialized below; we can't borrow it here
41                // because we need the json string to outlive this match.
42                let _ = ctx;
43                None
44            }
45            None => None,
46        };
47
48        // Serialize context input if present.
49        let context_json: Option<String> = match input {
50            Some(PipeValue::Context(ctx)) => Some(ctx.to_json()),
51            _ => None,
52        };
53
54        let effective_stdin = stdin_bytes.or(context_json.as_deref().map(|s| s.as_bytes()));
55
56        let stdin_cfg = if effective_stdin.is_some() {
57            Stdio::piped()
58        } else {
59            Stdio::null()
60        };
61
62        let mut child = Proc::new("sh")
63            .arg("-c")
64            .arg(cmd_str)
65            .stdin(stdin_cfg)
66            .stdout(Stdio::piped())
67            .stderr(Stdio::piped())
68            .spawn()
69            .map_err(|e| ExecutionError::Runner(format!("/exec({}): {}", cmd_str, e)))?;
70
71        if let Some(data) = effective_stdin {
72            if let Some(mut stdin) = child.stdin.take() {
73                use std::io::Write as _;
74                let _ = stdin.write_all(data);
75            }
76        }
77
78        let output = child
79            .wait_with_output()
80            .map_err(|e| ExecutionError::Runner(format!("/exec({}): {}", cmd_str, e)))?;
81
82        let verbose = args.iter().any(|a| a.name == "verbose");
83        if verbose && !output.stderr.is_empty() {
84            use std::io::Write as _;
85            let _ = std::io::stderr().write_all(&output.stderr);
86        }
87
88        let success = output.status.success();
89        let stdout = if output.stdout.is_empty() {
90            None
91        } else {
92            Some(output.stdout)
93        };
94        let stderr = if output.stderr.is_empty() {
95            None
96        } else {
97            Some(output.stderr)
98        };
99
100        Ok(CommandOutput {
101            stdout,
102            stderr,
103            success,
104        })
105    }
106}