command_stream/commands/
cd.rs1use crate::commands::CommandContext;
4use crate::utils::{trace, CommandResult};
5use std::env;
6use std::path::PathBuf;
7
8pub async fn cd(ctx: CommandContext) -> CommandResult {
21 let home = env::var("HOME")
22 .or_else(|_| env::var("USERPROFILE"))
23 .unwrap_or_else(|_| "/".to_string());
24
25 let previous_dir = env::current_dir().ok();
26 let base = ctx.get_cwd();
27
28 let mut print_dir = false;
29 let target: String = match ctx.args.first().map(|s| s.as_str()) {
30 None | Some("") => home.clone(),
32 Some("-") => match env::var("OLDPWD") {
34 Ok(oldpwd) if !oldpwd.is_empty() => {
35 print_dir = true;
36 oldpwd
37 }
38 _ => {
39 trace("VirtualCommand", "cd: OLDPWD not set");
40 return CommandResult::error("cd: OLDPWD not set\n");
41 }
42 },
43 Some("~") => home.clone(),
44 Some(t) if t.starts_with("~/") => PathBuf::from(&home).join(&t[2..]).display().to_string(),
45 Some(t) => t.to_string(),
46 };
47
48 let target_path = PathBuf::from(&target);
51 let resolved = if target_path.is_absolute() {
52 target_path
53 } else {
54 base.join(&target_path)
55 };
56
57 trace(
58 "VirtualCommand",
59 &format!("cd: changing directory to {:?}", resolved),
60 );
61
62 match env::set_current_dir(&resolved) {
63 Ok(()) => {
64 let new_dir = env::current_dir()
65 .map(|p| p.display().to_string())
66 .unwrap_or_default();
67 if let Some(prev) = previous_dir {
70 env::set_var("OLDPWD", prev);
71 }
72 env::set_var("PWD", &new_dir);
73 trace(
74 "VirtualCommand",
75 &format!("cd: success, new dir: {}", new_dir),
76 );
77 if print_dir {
79 CommandResult::success(format!("{}\n", new_dir))
80 } else {
81 CommandResult::success_empty()
82 }
83 }
84 Err(e) => {
85 trace("VirtualCommand", &format!("cd: failed: {}", e));
86 CommandResult::error(format!("cd: {}\n", e))
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use std::path::Path;
95 use tempfile::tempdir;
96 use tokio::sync::Mutex;
97
98 static CD_TEST_LOCK: Mutex<()> = Mutex::const_new(());
104
105 fn normalize(p: &Path) -> PathBuf {
108 std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
109 }
110
111 #[tokio::test]
112 async fn test_cd_to_temp() {
113 let _guard = CD_TEST_LOCK.lock().await;
114 let temp = tempdir().unwrap();
115 let temp_path = temp.path().to_string_lossy().to_string();
116 let original_dir = env::current_dir().unwrap();
117
118 let ctx = CommandContext::new(vec![temp_path.clone()]);
119 let result = cd(ctx).await;
120 assert!(result.is_success());
121 assert_eq!(result.stdout, "");
122 assert_eq!(
123 normalize(&env::current_dir().unwrap()),
124 normalize(temp.path())
125 );
126
127 env::set_current_dir(original_dir).unwrap();
129 }
130
131 #[tokio::test]
132 async fn test_cd_to_nonexistent() {
133 let _guard = CD_TEST_LOCK.lock().await;
134 let original_dir = env::current_dir().unwrap();
135 let ctx = CommandContext::new(vec!["/nonexistent/path/12345".to_string()]);
136 let result = cd(ctx).await;
137 assert!(!result.is_success());
138 assert_eq!(result.code, 1);
139 assert!(result.stderr.contains("cd:"));
140 assert_eq!(env::current_dir().unwrap(), original_dir);
142 }
143
144 #[tokio::test]
145 async fn test_cd_no_arg_goes_home() {
146 let _guard = CD_TEST_LOCK.lock().await;
147 let temp = tempdir().unwrap();
148 let original_dir = env::current_dir().unwrap();
149 env::set_var("HOME", temp.path());
150
151 let ctx = CommandContext::new(vec![]);
152 let result = cd(ctx).await;
153 assert!(result.is_success());
154 assert_eq!(
155 normalize(&env::current_dir().unwrap()),
156 normalize(temp.path())
157 );
158
159 env::set_current_dir(original_dir).unwrap();
160 }
161
162 #[tokio::test]
163 async fn test_cd_tilde_expands_home() {
164 let _guard = CD_TEST_LOCK.lock().await;
165 let temp = tempdir().unwrap();
166 let original_dir = env::current_dir().unwrap();
167 env::set_var("HOME", temp.path());
168
169 let ctx = CommandContext::new(vec!["~".to_string()]);
170 let result = cd(ctx).await;
171 assert!(result.is_success());
172 assert_eq!(
173 normalize(&env::current_dir().unwrap()),
174 normalize(temp.path())
175 );
176
177 env::set_current_dir(original_dir).unwrap();
178 }
179
180 #[tokio::test]
181 async fn test_cd_tilde_subpath_expands() {
182 let _guard = CD_TEST_LOCK.lock().await;
183 let temp = tempdir().unwrap();
184 std::fs::create_dir(temp.path().join("sub")).unwrap();
185 let original_dir = env::current_dir().unwrap();
186 env::set_var("HOME", temp.path());
187
188 let ctx = CommandContext::new(vec!["~/sub".to_string()]);
189 let result = cd(ctx).await;
190 assert!(result.is_success());
191 assert_eq!(
192 normalize(&env::current_dir().unwrap()),
193 normalize(&temp.path().join("sub"))
194 );
195
196 env::set_current_dir(original_dir).unwrap();
197 }
198
199 #[tokio::test]
200 async fn test_cd_dash_switches_and_prints() {
201 let _guard = CD_TEST_LOCK.lock().await;
202 let dir_a = tempdir().unwrap();
203 let dir_b = tempdir().unwrap();
204 let original_dir = env::current_dir().unwrap();
205
206 let _ = cd(CommandContext::new(vec![dir_a
207 .path()
208 .to_string_lossy()
209 .to_string()]))
210 .await;
211 let _ = cd(CommandContext::new(vec![dir_b
212 .path()
213 .to_string_lossy()
214 .to_string()]))
215 .await;
216
217 let result = cd(CommandContext::new(vec!["-".to_string()])).await;
218 assert!(result.is_success());
219 assert_eq!(
221 normalize(Path::new(result.stdout.trim())),
222 normalize(dir_a.path())
223 );
224 assert_eq!(
225 normalize(&env::current_dir().unwrap()),
226 normalize(dir_a.path())
227 );
228
229 env::set_current_dir(original_dir).unwrap();
230 }
231
232 #[tokio::test]
233 async fn test_cd_updates_pwd_and_oldpwd() {
234 let _guard = CD_TEST_LOCK.lock().await;
235 let dir_a = tempdir().unwrap();
236 let dir_b = tempdir().unwrap();
237 let original_dir = env::current_dir().unwrap();
238
239 let _ = cd(CommandContext::new(vec![dir_a
240 .path()
241 .to_string_lossy()
242 .to_string()]))
243 .await;
244 assert_eq!(
245 normalize(Path::new(&env::var("PWD").unwrap())),
246 normalize(dir_a.path())
247 );
248
249 let _ = cd(CommandContext::new(vec![dir_b
250 .path()
251 .to_string_lossy()
252 .to_string()]))
253 .await;
254 assert_eq!(
255 normalize(Path::new(&env::var("PWD").unwrap())),
256 normalize(dir_b.path())
257 );
258 assert_eq!(
259 normalize(Path::new(&env::var("OLDPWD").unwrap())),
260 normalize(dir_a.path())
261 );
262
263 env::set_current_dir(original_dir).unwrap();
264 }
265
266 #[tokio::test]
267 async fn test_cd_relative_resolves_against_cwd_option() {
268 let _guard = CD_TEST_LOCK.lock().await;
269 let temp = tempdir().unwrap();
270 std::fs::create_dir(temp.path().join("sub")).unwrap();
271 let original_dir = env::current_dir().unwrap();
272
273 let mut ctx = CommandContext::new(vec!["sub".to_string()]);
274 ctx.cwd = Some(temp.path().to_path_buf());
275 let result = cd(ctx).await;
276 assert!(result.is_success());
277 assert_eq!(
278 normalize(&env::current_dir().unwrap()),
279 normalize(&temp.path().join("sub"))
280 );
281
282 env::set_current_dir(original_dir).unwrap();
283 }
284}