Skip to content

Commit d685481

Browse files
authored
Support to add -r to cp-utility tool (#843)
* Cp utility to support -r * Add unit tests * nits to add nested loop * add Parser_correct unit test * use match to simplify
1 parent a359284 commit d685481

File tree

1 file changed

+126
-4
lines changed

1 file changed

+126
-4
lines changed

tools/cp-utility/src/main.rs

Lines changed: 126 additions & 4 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,7 +75,48 @@ 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)?,
79+
};
80+
Ok(())
81+
}
82+
83+
fn copy_recursive(source: &Path, dest: &Path) -> io::Result<()> {
84+
// This will cover the case in which the destination exists
85+
let sanitized_dest: PathBuf = if dest.exists() {
86+
// If the path is a normal file, this is the file name. If it’s the path of a directory, this is the directory name.s
87+
dest.to_path_buf()
88+
.join(source.file_name().ok_or(io::Error::new(
89+
io::ErrorKind::InvalidInput,
90+
"Invalid source file",
91+
))?)
92+
} else {
93+
dest.to_path_buf()
7294
};
95+
96+
let mut stack = VecDeque::new();
97+
stack.push_back((source.to_path_buf(), sanitized_dest));
98+
99+
while let Some((current_source, current_dest)) = stack.pop_back() {
100+
if current_source.is_dir() {
101+
fs::create_dir(&current_dest)?;
102+
for entry in fs::read_dir(current_source)? {
103+
let next_source = entry?.path();
104+
let next_dest =
105+
current_dest
106+
.clone()
107+
.join(next_source.file_name().ok_or(io::Error::new(
108+
io::ErrorKind::InvalidInput,
109+
"Invalid source file",
110+
))?);
111+
stack.push_back((next_source, next_dest));
112+
}
113+
} else if current_source.is_symlink() {
114+
// Follow symbolic links as regular files
115+
fs::copy(current_source, current_dest)?;
116+
} else if current_source.is_file() {
117+
fs::copy(current_source, current_dest)?;
118+
}
119+
}
73120
Ok(())
74121
}
75122

@@ -172,7 +219,7 @@ mod tests {
172219
fn parser_failure() {
173220
// prepare
174221
let inputs = vec![
175-
vec!["cp", "-r", "foo.txt", "bar.txt"],
222+
vec!["cp", "-r", "foo.txt", "bar.txt", "foo1.txt"],
176223
vec!["cp", "-a", "param1", "param2", "param3"],
177224
vec!["cp", "param1", "param2", "param3"],
178225
];
@@ -186,6 +233,24 @@ mod tests {
186233
}
187234
}
188235

236+
#[test]
237+
fn parser_correct() {
238+
// prepare
239+
let inputs = vec![
240+
vec!["cp", "-r", "foo.txt", "bar.txt"],
241+
vec!["cp", "-a", "param1", "param2"],
242+
vec!["cp", "param1", "param2"],
243+
];
244+
245+
for input in inputs.into_iter() {
246+
// act
247+
let result = parse_args(input.clone());
248+
249+
// assert
250+
assert!(result.is_ok(), "input should fail {:?}", input);
251+
}
252+
}
253+
189254
#[test]
190255
fn test_copy_single() {
191256
// prepare
@@ -229,6 +294,51 @@ mod tests {
229294
assert!(result.is_err());
230295
}
231296

297+
#[test]
298+
fn test_copy_recursive() {
299+
// prepare
300+
let tempdir = tempfile::tempdir().unwrap();
301+
let test_base = tempdir.path().to_path_buf();
302+
["foo", "foo/foo0", "foo/foo1", "foo/bar"]
303+
.iter()
304+
.for_each(|x| create_dir(&test_base, x));
305+
let files = [
306+
"foo/file1.txt",
307+
"foo/file2.txt",
308+
"foo/foo1/file3.txt",
309+
"foo/bar/file4.txt",
310+
];
311+
files.iter().for_each(|x| create_file(&test_base, x));
312+
[("foo/symlink1.txt", "./file1.txt")]
313+
.iter()
314+
.for_each(|(x, y)| create_symlink(&test_base, x, y));
315+
316+
// act
317+
let recursive_copy = CopyOperation {
318+
copy_type: CopyType::Recursive,
319+
source: test_base.join("foo"),
320+
destination: test_base.join("bar"),
321+
};
322+
do_copy(recursive_copy).unwrap();
323+
324+
// assert
325+
files.iter().for_each(|x| {
326+
assert_same_file(
327+
&test_base.join(x),
328+
&test_base.join(x.replace("foo/", "bar/")),
329+
)
330+
});
331+
assert_same_file(
332+
&test_base.join("foo/symlink1.txt"),
333+
&test_base.join("bar/symlink1.txt"),
334+
);
335+
// recursive copy will treat symlink as a file
336+
assert_recursive_same_link(
337+
&test_base.join("foo/symlink1.txt"),
338+
&test_base.join("bar/symlink1.txt"),
339+
)
340+
}
341+
232342
#[test]
233343
fn test_copy_archive() {
234344
// prepare
@@ -351,4 +461,16 @@ mod tests {
351461

352462
assert_eq!(fs::read_link(source).unwrap(), fs::read_link(dest).unwrap());
353463
}
464+
465+
fn assert_recursive_same_link(source: &Path, dest: &Path) {
466+
assert!(source.exists());
467+
assert!(dest.exists());
468+
assert!(source.is_symlink());
469+
assert!(dest.is_file());
470+
471+
assert_eq!(
472+
fs::read_to_string(source).unwrap(),
473+
fs::read_to_string(dest).unwrap()
474+
);
475+
}
354476
}

0 commit comments

Comments
 (0)