Skip to main content

gammaloop_api/commands/
shell.rs

1use clap::Args;
2use color_eyre::Result;
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq, Default)]
8pub struct Shell {
9    // Capture everything after '!' as raw tokens
10    /// Command and arguments passed to the user's login shell.
11    #[arg(
12        trailing_var_arg = true,
13        allow_hyphen_values = true,
14        value_name = "CMD",
15        num_args = 1..,
16    )]
17    cmd: Vec<std::ffi::OsString>,
18}
19
20impl Shell {
21    pub fn run(self) -> Result<()> {
22        use std::process::Command;
23
24        // Pick a shell. macOS typically has /bin/zsh; fallback to /bin/sh
25        let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
26
27        // Turn args back into a safe shell string: 'abc' -> 'abc', foo bar -> 'foo' 'bar'
28        fn sh_quote(arg: &std::ffi::OsString) -> String {
29            // Convert OsStr lossily and single-quote. Replace ' with '\'' (close-quote + escaped quote + reopen)
30            let s = arg.to_string_lossy();
31            if s.is_empty() {
32                "''".to_string()
33            } else {
34                format!("'{}'", s.replace('\'', "'\"'\"'"))
35            }
36        }
37
38        let cmd_string = self.cmd.iter().map(sh_quote).collect::<Vec<_>>().join(" ");
39
40        let status = Command::new(shell)
41            .arg("-lc") // login + run command; drop -l if you don’t want login semantics
42            .arg(cmd_string)
43            .status()?;
44
45        if !status.success() {
46            return Err(eyre::eyre!(
47                "Shell command exited with status {:?}",
48                status.code()
49            ));
50        }
51        Ok(())
52    }
53}