Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:52:52.245Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

shell_builder.rs

341 lines · 11.6 KB · rust
1use std::borrow::Cow;
2
3use crate::shell::get_system_shell;
4use crate::shell::{Shell, ShellKind};
5
6/// ShellBuilder is used to turn a user-requested task into a
7/// program that can be executed by the shell.
8pub struct ShellBuilder {
9    /// The shell to run
10    program: String,
11    args: Vec<String>,
12    interactive: bool,
13    /// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
14    redirect_stdin: bool,
15    kind: ShellKind,
16}
17
18impl ShellBuilder {
19    /// Create a new ShellBuilder as configured.
20    pub fn new(shell: &Shell, is_windows: bool) -> Self {
21        let (program, args) = match shell {
22            Shell::System => (get_system_shell(), Vec::new()),
23            Shell::Program(shell) => (shell.clone(), Vec::new()),
24            Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
25        };
26
27        let kind = ShellKind::new(&program, is_windows);
28        Self {
29            program,
30            args,
31            interactive: true,
32            kind,
33            redirect_stdin: false,
34        }
35    }
36    pub fn non_interactive(mut self) -> Self {
37        self.interactive = false;
38        self
39    }
40
41    /// Returns the label to show in the terminal tab
42    pub fn command_label(&self, command_to_use_in_label: &str) -> String {
43        if command_to_use_in_label.trim().is_empty() {
44            self.program.clone()
45        } else {
46            match self.kind {
47                ShellKind::PowerShell | ShellKind::Pwsh => {
48                    format!("{} -C '{}'", self.program, command_to_use_in_label)
49                }
50                ShellKind::Cmd => {
51                    format!("{} /C \"{}\"", self.program, command_to_use_in_label)
52                }
53                ShellKind::Posix
54                | ShellKind::Nushell
55                | ShellKind::Fish
56                | ShellKind::Csh
57                | ShellKind::Tcsh
58                | ShellKind::Rc
59                | ShellKind::Xonsh
60                | ShellKind::Elvish => {
61                    let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
62                    format!(
63                        "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
64                        PROGRAM = self.program
65                    )
66                }
67            }
68        }
69    }
70
71    pub fn redirect_stdin_to_dev_null(mut self) -> Self {
72        self.redirect_stdin = true;
73        self
74    }
75
76    /// Returns the program and arguments to run this task in a shell.
77    pub fn build(
78        mut self,
79        task_command: Option<String>,
80        task_args: &[String],
81    ) -> (String, Vec<String>) {
82        if let Some(task_command) = task_command {
83            let task_command = if !task_args.is_empty() {
84                match self.kind.try_quote_prefix_aware(&task_command) {
85                    Some(task_command) => task_command.into_owned(),
86                    None => task_command,
87                }
88            } else {
89                task_command
90            };
91            let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
92                command.push(' ');
93                let shell_variable = self.kind.to_shell_variable(arg);
94                command.push_str(&match self.kind.try_quote(&shell_variable) {
95                    Some(shell_variable) => shell_variable,
96                    None => Cow::Owned(shell_variable),
97                });
98                command
99            });
100            if self.redirect_stdin {
101                match self.kind {
102                    ShellKind::Fish | ShellKind::Posix => {
103                        combined_command.insert_str(0, "exec </dev/null; ");
104                    }
105                    ShellKind::Nushell
106                    | ShellKind::Csh
107                    | ShellKind::Tcsh
108                    | ShellKind::Rc
109                    | ShellKind::Xonsh
110                    | ShellKind::Elvish => {
111                        combined_command.insert(0, '(');
112                        combined_command.push_str("\n) </dev/null");
113                    }
114                    ShellKind::PowerShell | ShellKind::Pwsh => {
115                        combined_command.insert_str(0, "$null | & {");
116                        combined_command.push_str("}");
117                    }
118                    ShellKind::Cmd => {
119                        combined_command.push_str("< NUL");
120                    }
121                }
122            }
123
124            self.args
125                .extend(self.kind.args_for_shell(self.interactive, combined_command));
126        }
127
128        (self.program, self.args)
129    }
130
131    // This should not exist, but our task infra is broken beyond repair right now
132    #[doc(hidden)]
133    pub fn build_no_quote(
134        mut self,
135        task_command: Option<String>,
136        task_args: &[String],
137    ) -> (String, Vec<String>) {
138        if let Some(task_command) = task_command {
139            let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
140                command.push(' ');
141                command.push_str(&self.kind.to_shell_variable(arg));
142                command
143            });
144            if self.redirect_stdin {
145                match self.kind {
146                    ShellKind::Fish | ShellKind::Posix => {
147                        combined_command.insert_str(0, "exec </dev/null; ");
148                    }
149                    ShellKind::Nushell
150                    | ShellKind::Csh
151                    | ShellKind::Tcsh
152                    | ShellKind::Rc
153                    | ShellKind::Xonsh
154                    | ShellKind::Elvish => {
155                        combined_command.insert(0, '(');
156                        combined_command.push_str("\n) </dev/null");
157                    }
158                    ShellKind::PowerShell | ShellKind::Pwsh => {
159                        combined_command.insert_str(0, "$null | & {");
160                        combined_command.push_str("}");
161                    }
162                    ShellKind::Cmd => {
163                        combined_command.push_str("< NUL");
164                    }
165                }
166            }
167
168            self.args
169                .extend(self.kind.args_for_shell(self.interactive, combined_command));
170        }
171
172        (self.program, self.args)
173    }
174
175    /// Builds a `smol::process::Command` with the given task command and arguments.
176    ///
177    /// Prefer this over manually constructing a command with the output of `Self::build`,
178    /// as this method handles `cmd` weirdness on windows correctly.
179    pub fn build_smol_command(
180        self,
181        task_command: Option<String>,
182        task_args: &[String],
183    ) -> smol::process::Command {
184        smol::process::Command::from(self.build_std_command(task_command, task_args))
185    }
186
187    /// Builds a `std::process::Command` with the given task command and arguments.
188    ///
189    /// Prefer this over manually constructing a command with the output of `Self::build`,
190    /// as this method handles `cmd` weirdness on windows correctly.
191    pub fn build_std_command(
192        self,
193        mut task_command: Option<String>,
194        task_args: &[String],
195    ) -> std::process::Command {
196        #[cfg(windows)]
197        let kind = self.kind;
198        if task_args.is_empty() {
199            task_command = task_command
200                .as_ref()
201                .map(|cmd| self.kind.try_quote_prefix_aware(&cmd).map(Cow::into_owned))
202                .unwrap_or(task_command);
203        }
204        let (program, args) = self.build(task_command, task_args);
205
206        let mut child = crate::command::new_std_command(program);
207
208        #[cfg(windows)]
209        if kind == ShellKind::Cmd {
210            use std::os::windows::process::CommandExt;
211
212            for arg in args {
213                child.raw_arg(arg);
214            }
215        } else {
216            child.args(args);
217        }
218
219        #[cfg(not(windows))]
220        child.args(args);
221
222        child
223    }
224
225    pub fn kind(&self) -> ShellKind {
226        self.kind
227    }
228}
229
230#[cfg(test)]
231mod test {
232    use super::*;
233
234    #[test]
235    fn test_nu_shell_variable_substitution() {
236        let shell = Shell::Program("nu".to_owned());
237        let shell_builder = ShellBuilder::new(&shell, false);
238
239        let (program, args) = shell_builder.build(
240            Some("echo".into()),
241            &[
242                "${hello}".to_string(),
243                "$world".to_string(),
244                "nothing".to_string(),
245                "--$something".to_string(),
246                "$".to_string(),
247                "${test".to_string(),
248            ],
249        );
250
251        assert_eq!(program, "nu");
252        assert_eq!(
253            args,
254            vec![
255                "-i",
256                "-c",
257                "echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
258            ]
259        );
260    }
261
262    #[test]
263    fn redirect_stdin_to_dev_null_precedence() {
264        let shell = Shell::Program("nu".to_owned());
265        let shell_builder = ShellBuilder::new(&shell, false);
266
267        let (program, args) = shell_builder
268            .redirect_stdin_to_dev_null()
269            .build(Some("echo".into()), &["nothing".to_string()]);
270
271        assert_eq!(program, "nu");
272        assert_eq!(args, vec!["-i", "-c", "(echo nothing\n) </dev/null"]);
273    }
274
275    #[test]
276    fn redirect_stdin_to_dev_null_fish() {
277        let shell = Shell::Program("fish".to_owned());
278        let shell_builder = ShellBuilder::new(&shell, false);
279
280        let (program, args) = shell_builder
281            .redirect_stdin_to_dev_null()
282            .build(Some("echo".into()), &["test".to_string()]);
283
284        assert_eq!(program, "fish");
285        assert_eq!(args, vec!["-i", "-c", "exec </dev/null; echo test"]);
286    }
287
288    #[test]
289    fn redirect_stdin_to_dev_null_preserves_heredoc() {
290        let shell = Shell::Program("sh".to_owned());
291        let shell_builder = ShellBuilder::new(&shell, false);
292
293        let command = "cat <<EOF\nhello\nEOF";
294        let (program, args) = shell_builder
295            .redirect_stdin_to_dev_null()
296            .build(Some(command.into()), &[]);
297
298        assert_eq!(program, "sh");
299        assert_eq!(
300            args,
301            vec!["-i", "-c", "exec </dev/null; cat <<EOF\nhello\nEOF"]
302        );
303    }
304
305    #[test]
306    fn non_interactive_omits_interactive_flag() {
307        // Headless hosts (e.g. the eval CLI) build the agent's shell command
308        // non-interactively so it works without a controlling TTY.
309        let shell = Shell::Program("sh".to_owned());
310        let shell_builder = ShellBuilder::new(&shell, false).non_interactive();
311
312        let (program, args) = shell_builder.build(Some("echo hello".into()), &[]);
313
314        assert_eq!(program, "sh");
315        assert_eq!(args, vec!["-c", "echo hello"]);
316        assert!(
317            !args.iter().any(|arg| arg == "-i"),
318            "non-interactive shell command must not include `-i`"
319        );
320    }
321
322    #[test]
323    fn does_not_quote_sole_command_only() {
324        let shell = Shell::Program("fish".to_owned());
325        let shell_builder = ShellBuilder::new(&shell, false);
326
327        let (program, args) = shell_builder.build(Some("echo".into()), &[]);
328
329        assert_eq!(program, "fish");
330        assert_eq!(args, vec!["-i", "-c", "echo"]);
331
332        let shell = Shell::Program("fish".to_owned());
333        let shell_builder = ShellBuilder::new(&shell, false);
334
335        let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
336
337        assert_eq!(program, "fish");
338        assert_eq!(args, vec!["-i", "-c", "echo oo"]);
339    }
340}
341
Served at tenant.openagents/omega Member data and write actions are omitted.