command_stream/commands/
mv.rs1use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult, VirtualUtils};
5use std::fs;
6
7pub async fn mv(ctx: CommandContext) -> CommandResult {
11 if ctx.args.len() < 2 {
12 return VirtualUtils::invalid_argument_error("mv", "missing file operand");
13 }
14
15 let mut paths = Vec::new();
17
18 for arg in &ctx.args {
19 if !arg.starts_with('-') {
20 paths.push(arg.clone());
21 }
22 }
23
24 if paths.len() < 2 {
25 return VirtualUtils::invalid_argument_error("mv", "missing destination file operand");
26 }
27
28 let cwd = ctx.get_cwd();
29 let dest = paths.pop().unwrap();
30 let dest_path = VirtualUtils::resolve_path(&dest, Some(&cwd));
31
32 let dest_is_dir = dest_path.is_dir();
34 let multiple_sources = paths.len() > 1;
35
36 if multiple_sources && !dest_is_dir {
37 return CommandResult::error(format!("mv: target '{}' is not a directory\n", dest));
38 }
39
40 for source in paths {
41 let source_path = VirtualUtils::resolve_path(&source, Some(&cwd));
42
43 trace_lazy("VirtualCommand", || {
44 format!("mv: moving {:?} to {:?}", source_path, dest_path)
45 });
46
47 if !source_path.exists() {
48 return CommandResult::error(format!(
49 "mv: cannot stat '{}': No such file or directory\n",
50 source
51 ));
52 }
53
54 let final_dest = if dest_is_dir {
55 dest_path.join(source_path.file_name().unwrap_or_default())
56 } else {
57 dest_path.clone()
58 };
59
60 match fs::rename(&source_path, &final_dest) {
62 Ok(()) => continue,
63 Err(e) => {
64 if e.kind() == std::io::ErrorKind::CrossesDevices
66 || e.kind() == std::io::ErrorKind::Other
67 {
68 if source_path.is_dir() {
69 if let Err(e) = copy_and_remove_dir(&source_path, &final_dest) {
70 return CommandResult::error(format!(
71 "mv: cannot move '{}': {}\n",
72 source, e
73 ));
74 }
75 } else {
76 if let Err(e) = fs::copy(&source_path, &final_dest) {
77 return CommandResult::error(format!(
78 "mv: cannot move '{}': {}\n",
79 source, e
80 ));
81 }
82 if let Err(e) = fs::remove_file(&source_path) {
83 return CommandResult::error(format!(
84 "mv: cannot remove '{}': {}\n",
85 source, e
86 ));
87 }
88 }
89 } else {
90 return CommandResult::error(format!("mv: cannot move '{}': {}\n", source, e));
91 }
92 }
93 }
94 }
95
96 CommandResult::success_empty()
97}
98
99fn copy_and_remove_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
100 fs::create_dir_all(dst)?;
101
102 for entry in fs::read_dir(src)? {
103 let entry = entry?;
104 let entry_path = entry.path();
105 let dest_path = dst.join(entry.file_name());
106
107 if entry_path.is_dir() {
108 copy_and_remove_dir(&entry_path, &dest_path)?;
109 } else {
110 fs::copy(&entry_path, &dest_path)?;
111 }
112 }
113
114 fs::remove_dir_all(src)?;
115 Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use tempfile::tempdir;
122
123 #[tokio::test]
124 async fn test_mv_file() {
125 let temp = tempdir().unwrap();
126 let src = temp.path().join("source.txt");
127 let dst = temp.path().join("dest.txt");
128 fs::write(&src, "test content").unwrap();
129
130 let ctx = CommandContext::new(vec![
131 src.to_string_lossy().to_string(),
132 dst.to_string_lossy().to_string(),
133 ]);
134 let result = mv(ctx).await;
135
136 assert!(result.is_success());
137 assert!(!src.exists());
138 assert!(dst.exists());
139 assert_eq!(fs::read_to_string(&dst).unwrap(), "test content");
140 }
141
142 #[tokio::test]
143 async fn test_mv_directory() {
144 let temp = tempdir().unwrap();
145 let src_dir = temp.path().join("src_dir");
146 let dst_dir = temp.path().join("dst_dir");
147
148 fs::create_dir(&src_dir).unwrap();
149 fs::write(src_dir.join("file.txt"), "test").unwrap();
150
151 let ctx = CommandContext::new(vec![
152 src_dir.to_string_lossy().to_string(),
153 dst_dir.to_string_lossy().to_string(),
154 ]);
155 let result = mv(ctx).await;
156
157 assert!(result.is_success());
158 assert!(!src_dir.exists());
159 assert!(dst_dir.join("file.txt").exists());
160 }
161
162 #[tokio::test]
163 async fn test_mv_nonexistent() {
164 let temp = tempdir().unwrap();
165
166 let ctx = CommandContext::new(vec![
167 "/nonexistent/file".to_string(),
168 temp.path().join("dest").to_string_lossy().to_string(),
169 ]);
170 let result = mv(ctx).await;
171
172 assert!(!result.is_success());
173 }
174}