Skip to content

Commit 4d5707e

Browse files
committed
Add cp -r support to cp-utility tool
1 parent 2907367 commit 4d5707e

File tree

1 file changed

+115
-5
lines changed

1 file changed

+115
-5
lines changed

tools/cp-utility/src/main.rs

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ enum CopyType {
2828
SingleFile,
2929
/// equivalent to cp -a <source> <dest>
3030
Archive,
31+
/// equivalent to cp -r <source> <dest>
32+
Recursive,
3133
}
3234

3335
/// Encapsulate a copy operation
@@ -42,18 +44,22 @@ struct CopyOperation {
4244

4345
/// Parse command line arguments and transform into `CopyOperation`
4446
fn parse_args(args: Vec<&str>) -> io::Result<CopyOperation> {
45-
if !(args.len() == 3 || args.len() == 4 && args[1].eq("-a")) {
47+
if !(args.len() == 3 || (args.len() == 4 && (args[1] == "-a" || args[1] == "-r"))) {
4648
return Err(io::Error::new(
4749
io::ErrorKind::InvalidInput,
48-
"Invalid parameters. Expected cp [-a] <source> <destination>",
50+
"Invalid parameters. Expected cp [-a | -r] <source> <destination>",
4951
));
5052
}
5153

5254
if args.len() == 4 {
5355
return Ok(CopyOperation {
5456
source: PathBuf::from(args[2]),
5557
destination: PathBuf::from(args[3]),
56-
copy_type: CopyType::Archive,
58+
copy_type: match args[1] {
59+
"-a" => CopyType::Archive,
60+
"-r" => CopyType::Recursive,
61+
_ => panic!("Invalid option. Expected -a or -r"),
62+
},
5763
});
5864
}
5965

@@ -69,10 +75,40 @@ fn do_copy(operation: CopyOperation) -> io::Result<()> {
6975
match operation.copy_type {
7076
CopyType::Archive => copy_archive(&operation.source, &operation.destination)?,
7177
CopyType::SingleFile => fs::copy(&operation.source, &operation.destination).map(|_| ())?,
78+
CopyType::Recursive => copy_recursive(&operation.source, &operation.destination)?,
7279
};
7380
Ok(())
7481
}
7582

83+
fn copy_recursive(source: &Path, dest: &Path) -> io::Result<()> {
84+
let mut stack = VecDeque::new();
85+
stack.push_back((source.to_path_buf(), dest.to_path_buf()));
86+
while let Some((current_source, current_dest)) = stack.pop_back() {
87+
if current_source.is_dir() {
88+
if !current_dest.exists() {
89+
fs::create_dir(&current_dest)?;
90+
}
91+
for entry in fs::read_dir(current_source)? {
92+
let next_source = entry?.path();
93+
let next_dest =
94+
current_dest
95+
.clone()
96+
.join(next_source.file_name().ok_or(io::Error::new(
97+
io::ErrorKind::InvalidInput,
98+
"Invalid source file",
99+
))?);
100+
stack.push_back((next_source, next_dest));
101+
}
102+
} else if current_source.is_symlink() {
103+
// Follow symbolic links as regular files
104+
fs::copy(current_source, current_dest)?;
105+
} else if current_source.is_file() {
106+
fs::copy(current_source, current_dest)?;
107+
}
108+
}
109+
Ok(())
110+
}
111+
76112
// Execute the recursive type of copy operation
77113
fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> {
78114
let mut stack = VecDeque::new();
@@ -100,7 +136,6 @@ fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> {
100136
fs::copy(current_source, current_dest)?;
101137
}
102138
}
103-
104139
Ok(())
105140
}
106141

@@ -163,7 +198,7 @@ mod tests {
163198
fn parser_failure() {
164199
// prepare
165200
let inputs = vec![
166-
vec!["cp", "-r", "foo.txt", "bar.txt"],
201+
vec!["cp", "-r", "foo.txt", "bar.txt", "foo1.txt"],
167202
vec!["cp", "-a", "param1", "param2", "param3"],
168203
vec!["cp", "param1", "param2", "param3"],
169204
];
@@ -177,6 +212,24 @@ mod tests {
177212
}
178213
}
179214

215+
#[test]
216+
fn parser_correct() {
217+
// prepare
218+
let inputs = vec![
219+
vec!["cp", "-r", "foo.txt", "bar.txt"],
220+
vec!["cp", "-a", "param1", "param2"],
221+
vec!["cp", "param1", "param2"],
222+
];
223+
224+
for input in inputs.into_iter() {
225+
// act
226+
let result = parse_args(input.clone());
227+
228+
// assert
229+
assert!(result.is_ok(), "input should fail {:?}", input);
230+
}
231+
}
232+
180233
#[test]
181234
fn test_copy_single() {
182235
// prepare
@@ -220,6 +273,51 @@ mod tests {
220273
assert!(result.is_err());
221274
}
222275

276+
#[test]
277+
fn test_copy_recursive() {
278+
// prepare
279+
let tempdir = tempfile::tempdir().unwrap();
280+
let test_base = tempdir.path().to_path_buf();
281+
["foo", "foo/foo0", "foo/foo1", "foo/bar"]
282+
.iter()
283+
.for_each(|x| create_dir(&test_base, x));
284+
let files = [
285+
"foo/file1.txt",
286+
"foo/file2.txt",
287+
"foo/foo1/file3.txt",
288+
"foo/bar/file4.txt",
289+
];
290+
files.iter().for_each(|x| create_file(&test_base, x));
291+
[("foo/symlink1.txt", "./file1.txt")]
292+
.iter()
293+
.for_each(|(x, y)| create_symlink(&test_base, x, y));
294+
295+
// act
296+
let recursive_copy = CopyOperation {
297+
copy_type: CopyType::Recursive,
298+
source: test_base.join("foo"),
299+
destination: test_base.join("bar"),
300+
};
301+
do_copy(recursive_copy).unwrap();
302+
303+
// assert
304+
files.iter().for_each(|x| {
305+
assert_same_file(
306+
&test_base.join(x),
307+
&test_base.join(x.replace("foo/", "bar/")),
308+
)
309+
});
310+
assert_same_file(
311+
&test_base.join("foo/symlink1.txt"),
312+
&test_base.join("bar/symlink1.txt"),
313+
);
314+
// recursive copy will treat symlink as a file
315+
assert_recursive_same_link(
316+
&test_base.join("foo/symlink1.txt"),
317+
&test_base.join("bar/symlink1.txt"),
318+
)
319+
}
320+
223321
#[test]
224322
fn test_copy_archive() {
225323
// prepare
@@ -342,4 +440,16 @@ mod tests {
342440

343441
assert_eq!(fs::read_link(source).unwrap(), fs::read_link(dest).unwrap());
344442
}
443+
444+
fn assert_recursive_same_link(source: &Path, dest: &Path) {
445+
assert!(source.exists());
446+
assert!(dest.exists());
447+
assert!(source.is_symlink());
448+
assert!(dest.is_file());
449+
450+
assert_eq!(
451+
fs::read_to_string(source).unwrap(),
452+
fs::read_to_string(dest).unwrap()
453+
);
454+
}
345455
}

0 commit comments

Comments
 (0)