ai_agent/utils/shell/
bash_provider.rs1use super::shell_provider::{ShellError, ShellExecCommand};
4use super::shell_tool_utils::ShellType;
5use crate::constants::env::{ai, system};
6use std::collections::HashMap;
7
8fn get_disable_extglob_command(shell_path: &str) -> Option<String> {
12 if std::env::var(ai::SHELL_PREFIX).is_ok() {
15 return Some(
18 "{ shopt -u extglob || setopt NO_EXTENDED_GLOB; } >/dev/null 2>&1 || true".to_string(),
19 );
20 }
21
22 if shell_path.contains("bash") {
24 Some("shopt -u extglob 2>/dev/null || true".to_string())
25 } else if shell_path.contains("zsh") {
26 Some("setopt NO_EXTENDED_GLOB 2>/dev/null || true".to_string())
27 } else {
28 None
30 }
31}
32
33pub struct BashShellProvider {
36 shell_path: String,
37}
38
39impl BashShellProvider {
40 pub fn new(shell_path: &str) -> Self {
42 Self {
43 shell_path: shell_path.to_string(),
44 }
45 }
46
47 pub fn get_type(&self) -> ShellType {
49 ShellType::Bash
50 }
51
52 pub fn get_shell_path(&self) -> &str {
54 &self.shell_path
55 }
56
57 pub fn is_detached(&self) -> bool {
59 true
60 }
61
62 pub async fn build_exec_command(
65 &self,
66 command: &str,
67 id: usize,
68 sandbox_tmp_dir: Option<&str>,
69 use_sandbox: bool,
70 ) -> Result<ShellExecCommand, ShellError> {
71 let tmpdir = std::env::temp_dir();
72
73 let (shell_cwd_file_path, cwd_file_path) = if use_sandbox {
76 let sandbox = sandbox_tmp_dir.ok_or_else(|| {
77 ShellError::BuildError(
78 "sandbox_tmp_dir required when use_sandbox is true".to_string(),
79 )
80 })?;
81 (
82 format!("{}/cwd-{}", sandbox, id),
83 format!("{}/cwd-{}", sandbox, id),
84 )
85 } else {
86 let cwd_file = format!("ai-{}-cwd", id);
87 (
88 tmpdir.join(&cwd_file).to_string_lossy().to_string(),
89 tmpdir.join(&cwd_file).to_string_lossy().to_string(),
90 )
91 };
92
93 let mut command_parts: Vec<String> = Vec::new();
97
98 if let Some(disable_extglob_cmd) = get_disable_extglob_command(&self.shell_path) {
107 command_parts.push(disable_extglob_cmd);
108 }
109
110 command_parts.push(format!("eval {}", self.quote_command(command)));
113 command_parts.push(format!(
114 "pwd -P >| {}",
115 self.quote_path(&shell_cwd_file_path)
116 ));
117
118 let command_string = command_parts.join(" && ");
119
120 let command_string = if let Ok(prefix) = std::env::var(ai::SHELL_PREFIX) {
122 format!("{} -c '{}'", prefix, command_string)
123 } else {
124 command_string
125 };
126
127 Ok(ShellExecCommand {
128 command_string,
129 cwd_file_path,
130 })
131 }
132
133 pub fn get_spawn_args(&self, command_string: &str) -> Vec<String> {
135 vec![
138 "-c".to_string(),
139 "-l".to_string(),
140 command_string.to_string(),
141 ]
142 }
143
144 pub async fn get_environment_overrides(&self, _command: &str) -> HashMap<String, String> {
146 let mut env = HashMap::new();
147
148 env
155 }
156
157 fn quote_command(&self, command: &str) -> String {
159 format!("'{}'", command.replace('\'', "'\\''"))
162 }
163
164 fn quote_path(&self, path: &str) -> String {
166 self.quote_command(path)
167 }
168}
169
170impl Default for BashShellProvider {
171 fn default() -> Self {
172 #[cfg(target_os = "windows")]
173 let shell = "bash".to_string();
174 #[cfg(not(target_os = "windows"))]
175 let shell = std::env::var(system::SHELL).unwrap_or_else(|_| "/bin/bash".to_string());
176
177 Self::new(&shell)
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[tokio::test]
186 async fn test_build_exec_command() {
187 let provider = BashShellProvider::new("/bin/bash");
188 let result = provider
189 .build_exec_command("echo hello", 1, None, false)
190 .await;
191 assert!(result.is_ok());
192 let cmd = result.unwrap();
193 assert!(cmd.command_string.contains("echo hello"));
194 }
195
196 #[test]
197 fn test_get_spawn_args() {
198 let provider = BashShellProvider::new("/bin/bash");
199 let args = provider.get_spawn_args("echo hello");
200 assert_eq!(args[0], "-c");
201 }
202
203 #[test]
204 fn test_disable_extglob_command() {
205 let cmd = get_disable_extglob_command("/bin/bash");
206 assert!(cmd.is_some());
207
208 let cmd = get_disable_extglob_command("/bin/zsh");
209 assert!(cmd.is_some());
210
211 let cmd = get_disable_extglob_command("/bin/sh");
212 assert!(cmd.is_none());
213 }
214}