1use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8
9#[derive(Debug)]
11pub struct ExecutionResult {
12 pub status: std::process::ExitStatus,
14 pub new_cwd: Option<PathBuf>,
16}
17
18fn parse_cd_command(command_text: &str) -> Option<&str> {
23 let trimmed = command_text.trim();
24
25 if !trimmed.starts_with("cd ") {
27 return None;
28 }
29
30 if trimmed.contains("&&")
33 || trimmed.contains("||")
34 || trimmed.contains(';')
35 || trimmed.contains('|')
36 {
37 return None;
38 }
39
40 let path = trimmed.strip_prefix("cd ")?.trim();
42
43 let path = path
45 .strip_prefix('"')
46 .and_then(|p| p.strip_suffix('"'))
47 .or_else(|| path.strip_prefix('\'').and_then(|p| p.strip_suffix('\'')))
48 .unwrap_or(path);
49
50 if path.is_empty() {
51 return None;
52 }
53
54 Some(path)
55}
56
57fn resolve_cd_path(target: &str, current_dir: &Path) -> Option<PathBuf> {
65 if target == "-" {
66 return None;
68 }
69
70 let expanded = if target.starts_with('~') {
71 if let Some(home) = std::env::var_os("HOME") {
72 let home_path = PathBuf::from(home);
73 if target == "~" {
74 home_path
75 } else if let Some(rest) = target.strip_prefix("~/") {
76 home_path.join(rest)
77 } else {
78 return None;
80 }
81 } else {
82 return None;
83 }
84 } else if Path::new(target).is_absolute() {
85 PathBuf::from(target)
86 } else {
87 current_dir.join(target)
88 };
89
90 expanded.canonicalize().ok()
92}
93
94pub fn execute_command(command_text: &str, cwd: Option<&Path>) -> std::io::Result<ExecutionResult> {
112 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
113
114 let mut cmd = Command::new(&shell);
115 cmd.arg("-c").arg(command_text);
116
117 let effective_cwd = if let Some(dir) = cwd {
118 if dir.exists() {
119 cmd.current_dir(dir);
120 dir.to_path_buf()
121 } else {
122 eprintln!(
123 "\x1b[33m Warning: directory '{}' does not exist, using current directory\x1b[0m",
124 dir.display()
125 );
126 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
127 }
128 } else {
129 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
130 };
131
132 cmd.stdin(Stdio::inherit())
133 .stdout(Stdio::inherit())
134 .stderr(Stdio::inherit());
135
136 let status = cmd.status()?;
137
138 let new_cwd = if status.success() {
140 if let Some(target) = parse_cd_command(command_text) {
141 resolve_cd_path(target, &effective_cwd)
142 } else {
143 None
144 }
145 } else {
146 None
147 };
148
149 Ok(ExecutionResult { status, new_cwd })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_parse_cd_simple() {
158 assert_eq!(parse_cd_command("cd foo"), Some("foo"));
159 assert_eq!(parse_cd_command("cd /tmp"), Some("/tmp"));
160 assert_eq!(parse_cd_command("cd ~/Code"), Some("~/Code"));
161 assert_eq!(parse_cd_command("cd .."), Some(".."));
162 }
163
164 #[test]
165 fn test_parse_cd_quoted() {
166 assert_eq!(parse_cd_command("cd \"my folder\""), Some("my folder"));
167 assert_eq!(parse_cd_command("cd 'my folder'"), Some("my folder"));
168 }
169
170 #[test]
171 fn test_parse_cd_chained_returns_none() {
172 assert_eq!(parse_cd_command("cd foo && ls"), None);
173 assert_eq!(parse_cd_command("cd foo; echo"), None);
174 assert_eq!(parse_cd_command("cd foo || true"), None);
175 assert_eq!(parse_cd_command("cd foo | cat"), None);
176 }
177
178 #[test]
179 fn test_parse_cd_not_cd() {
180 assert_eq!(parse_cd_command("echo cd foo"), None);
181 assert_eq!(parse_cd_command("ls"), None);
182 assert_eq!(parse_cd_command("cdfoo"), None);
183 }
184
185 #[test]
186 fn test_resolve_cd_path_absolute() {
187 let current = PathBuf::from("/home/user");
188 let resolved = resolve_cd_path("/tmp", ¤t);
190 assert!(resolved.is_some());
191 assert!(resolved.unwrap().is_absolute());
192 }
193
194 #[test]
195 fn test_resolve_cd_path_home() {
196 let current = PathBuf::from("/tmp");
197 if std::env::var("HOME").is_ok() {
198 let resolved = resolve_cd_path("~", ¤t);
199 assert!(resolved.is_some());
200 }
201 }
202
203 #[test]
204 fn test_resolve_cd_path_dash_returns_none() {
205 let current = PathBuf::from("/tmp");
206 assert_eq!(resolve_cd_path("-", ¤t), None);
207 }
208
209 #[test]
210 fn test_execute_echo_command() {
211 let result = execute_command("echo hello", None).unwrap();
212 assert!(result.status.success());
213 assert!(result.new_cwd.is_none());
214 }
215
216 #[test]
217 fn test_execute_cd_returns_new_cwd() {
218 let result = execute_command("cd /tmp", None).unwrap();
219 assert!(result.status.success());
220 assert!(result.new_cwd.is_some());
221 assert_eq!(result.new_cwd.unwrap(), PathBuf::from("/tmp"));
222 }
223
224 #[test]
225 fn test_execute_cd_nonexistent_no_new_cwd() {
226 let result = execute_command("cd /nonexistent_dir_12345", None).unwrap();
227 assert!(!result.status.success());
228 assert!(result.new_cwd.is_none());
229 }
230}