Skip to main content

command_stream/commands/
cp.rs

1//! Virtual `cp` command implementation
2
3use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult, VirtualUtils};
5use std::fs;
6use std::path::Path;
7
8/// Execute the cp command
9///
10/// Copies files and directories.
11pub async fn cp(ctx: CommandContext) -> CommandResult {
12    if ctx.args.len() < 2 {
13        return VirtualUtils::invalid_argument_error("cp", "missing file operand");
14    }
15
16    // Parse flags
17    let mut recursive = false;
18    let mut paths = Vec::new();
19
20    for arg in &ctx.args {
21        if arg == "-r" || arg == "-R" || arg == "--recursive" {
22            recursive = true;
23        } else if arg.starts_with('-') {
24            if arg.contains('r') || arg.contains('R') {
25                recursive = true;
26            }
27        } else {
28            paths.push(arg.clone());
29        }
30    }
31
32    if paths.len() < 2 {
33        return VirtualUtils::invalid_argument_error("cp", "missing destination file operand");
34    }
35
36    let cwd = ctx.get_cwd();
37    let dest = paths.pop().unwrap();
38    let dest_path = VirtualUtils::resolve_path(&dest, Some(&cwd));
39
40    // If multiple sources or dest is a directory, copy into the directory
41    let dest_is_dir = dest_path.is_dir();
42    let multiple_sources = paths.len() > 1;
43
44    if multiple_sources && !dest_is_dir {
45        return CommandResult::error(format!("cp: target '{}' is not a directory\n", dest));
46    }
47
48    for source in paths {
49        let source_path = VirtualUtils::resolve_path(&source, Some(&cwd));
50
51        trace_lazy("VirtualCommand", || {
52            format!("cp: copying {:?} to {:?}", source_path, dest_path)
53        });
54
55        if !source_path.exists() {
56            return CommandResult::error(format!(
57                "cp: cannot stat '{}': No such file or directory\n",
58                source
59            ));
60        }
61
62        let final_dest = if dest_is_dir {
63            dest_path.join(source_path.file_name().unwrap_or_default())
64        } else {
65            dest_path.clone()
66        };
67
68        if source_path.is_dir() {
69            if !recursive {
70                return CommandResult::error(format!(
71                    "cp: -r not specified; omitting directory '{}'\n",
72                    source
73                ));
74            }
75
76            if let Err(e) = copy_dir_recursive(&source_path, &final_dest) {
77                return CommandResult::error(format!("cp: cannot copy '{}': {}\n", source, e));
78            }
79        } else {
80            if let Some(parent) = final_dest.parent() {
81                if !parent.exists() {
82                    if let Err(e) = fs::create_dir_all(parent) {
83                        return CommandResult::error(format!(
84                            "cp: cannot create directory '{}': {}\n",
85                            parent.display(),
86                            e
87                        ));
88                    }
89                }
90            }
91
92            if let Err(e) = fs::copy(&source_path, &final_dest) {
93                return CommandResult::error(format!("cp: cannot copy '{}': {}\n", source, e));
94            }
95        }
96    }
97
98    CommandResult::success_empty()
99}
100
101fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
102    fs::create_dir_all(dst)?;
103
104    for entry in fs::read_dir(src)? {
105        let entry = entry?;
106        let entry_path = entry.path();
107        let dest_path = dst.join(entry.file_name());
108
109        if entry_path.is_dir() {
110            copy_dir_recursive(&entry_path, &dest_path)?;
111        } else {
112            fs::copy(&entry_path, &dest_path)?;
113        }
114    }
115
116    Ok(())
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use tempfile::tempdir;
123
124    #[tokio::test]
125    async fn test_cp_file() {
126        let temp = tempdir().unwrap();
127        let src = temp.path().join("source.txt");
128        let dst = temp.path().join("dest.txt");
129        fs::write(&src, "test content").unwrap();
130
131        let ctx = CommandContext::new(vec![
132            src.to_string_lossy().to_string(),
133            dst.to_string_lossy().to_string(),
134        ]);
135        let result = cp(ctx).await;
136
137        assert!(result.is_success());
138        assert!(dst.exists());
139        assert_eq!(fs::read_to_string(&dst).unwrap(), "test content");
140    }
141
142    #[tokio::test]
143    async fn test_cp_directory_recursive() {
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            "-r".to_string(),
153            src_dir.to_string_lossy().to_string(),
154            dst_dir.to_string_lossy().to_string(),
155        ]);
156        let result = cp(ctx).await;
157
158        assert!(result.is_success());
159        assert!(dst_dir.join("file.txt").exists());
160    }
161
162    #[tokio::test]
163    async fn test_cp_directory_without_recursive() {
164        let temp = tempdir().unwrap();
165        let src_dir = temp.path().join("src_dir");
166        let dst_dir = temp.path().join("dst_dir");
167
168        fs::create_dir(&src_dir).unwrap();
169
170        let ctx = CommandContext::new(vec![
171            src_dir.to_string_lossy().to_string(),
172            dst_dir.to_string_lossy().to_string(),
173        ]);
174        let result = cp(ctx).await;
175
176        assert!(!result.is_success());
177        assert!(result.stderr.contains("-r not specified"));
178    }
179}