Skip to repository content341 lines · 11.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:49:13.954Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
shell_builder.rs
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