After reaching a part where I personally felt like I had a grasp on CS theory I decided that it would be best to start a project in a brand new language that I’m interested in. For a long time ive know that language to be Rust. Rust fills a lot of personal issues that I have in programming and at the end of the day it actually makes me excited to work with the language rather then feeling like im working with the language.
To start simple I wanted to create a project that would use a library and try to extend it to a useful program. I finally decided on what is now rmc, a program that could copy and move files. You can checkout and download rmc on either crates.io or github.
Overview
My initial moments with the project required me to learn some basic Rust, I read the first three chapters with ease and then I got to chapter four and have since been taking my time to understand the concepts while reading the fourth chapter. Thankfully while I take my time with this fourth chapter, the first three gave me enough of an intro to be able to do some work. Rust is vaguely similar to JavaScript so im not completely lost but rather when it comes to the quirks of Rust I just need to figure them out. And a majority of these quirks are the type system, but thankfully the rust compiler is your friend. Even in vscode it’s pretty quick to tell you when something is wrong in your program. This can be annoying at first but I came to quickly love it because it gradually helped me realize why what I was doing was wrong.
clap
I wanted to use a library in order to help reduce the initial workload that I had to do and let me focus on the core feature of the program. The clap crate was a vital help to me in achieving this. Clap is and stands for a Command Line Argument Parser, this allows me with very minimal code to get a polished out of the box CLI experience that I don’t have to make myself. Here is a snippet from the code of the “choice” option in my program that prompts the user to enter a choice to move or copy.
Arg::new("choice")
.short('c')
.long("choice")
.help("m to move, c to copy, rc to recursively copy, rm to recursively move")
.num_args(1)
.required(true)
Although simple it allows my program to accept multiple options in my CLI program and specifies the help tooltip to be displayed when –help is issued.
To move or to copy
The move and copy functions were rather easy to figure out the solution for and move is more so an extension of copy. The copy function takes an input and output &str and passes them to the fs::copy() function which performs the copy or produces an error.
match fs::copy(input, output) {
Ok(bytes) => println!("{} bytes copied", bytes),
Err(err) => println!("Error: {}", err),
}
The move function can be thought of as an extension of the copy function because it performs the same work but one additional delete. It works the same way as copy taking in two &str for input and output but this time passing them to copy_file. But since we’re moving we need to take care of the original file.
match fs::remove_file(input) {
Ok(()) => println!("original deleted"),
Err(err) => println!("Error: {}", err),
}
With the original deleted our files move is complete!
Recursion, Recursion, Recursion
With the basic versions of move and copy out of the way the more advanced version can begin to be created.
This begins with the recursive copy function that takes two inputs just the same as the basic copy. But inside it functions very differently, the main workhorse of the function is the fs::read_dir()
function which returns an iterator that will iterate over a directory of files and folders. Using this iterator we can check if each element is either a file or folder. If the element is a file we copy the file. Where as if the element is a folder we create the new folder in the output argument and then recursively call our function on the folder that was copied from the input argument.
The recursive move function is funny enough pretty similar to its non recursive counterpart. It calls the recursive copy function and then calls fs::remove_dir_all()
function called on the input argument deletes the folder and everything inside it.
Tests
I developed the tests at the end of the project, but I really learned the importance of them in this and how useful they can be during any stage of development. Im sure how useful is based on the scope of the project, but at least for mine it appeared to be very useful.
Heres an example of a test I designed after the fact that probably would have been very helpful early on.
#[test]
fn copy_file_test() {
if let Ok(_) = fs::write("test.txt", "to be copied") {
println!("file created");
}
//copy_file("test.txt","test-copy.txt");
let mut cmd = Command::cargo_bin("rmc").unwrap();
//cmd.assert().success();
cmd
.arg("-c")
.arg("c")
.arg("test.txt")
.arg("test_copy.txt");
cmd.assert();
let mut file1 = match fs::File::open("test.txt") {
Ok(f) => f,
Err(e) => panic!("{}", e),
};
let mut file2 = match fs::File::open("test_copy.txt") {
Ok(f) => f,
Err(e) => panic!("{}", e),
};
assert!(diff_files(&mut file1, &mut file2), "files not the same");
match fs::remove_file("test.txt") {
Ok(()) => println!("original deleted"),
Err(err) => println!("Error: {}", err),
}
match fs::remove_file("test_copy.txt") {
Ok(()) => println!("original deleted"),
Err(err) => println!("Error: {}", err),
}
}
In this code example I use the fs module again to create a file with fs::write()
. This gives me a file to test, then with with the assert_cmd
crate I can call my program on the newly created file. With fs:File::open()
paired with the file_diff crate I can compare
the two files with an assert!()
expression. If either of the two files aren’t exactly the same then assert will present an error.
Bugs
Weather intentional or intentional, programs are not without their bugs and my program certainly had a few.
One of the bigger issues I was faced with early on was in the recursive_copy()
function. Early on I was doing some string manipulation in order to try and get the final path to copy files. This lead to everything being created in the root directory as one long file name instead of it being the correct path. To resolve this I simply had to treat it as a path instead of a string. The std::path::Path
struct has the ability to join two instances of itself with the .join()
function. This allows me to preserve the path regardless of OS.
The second was learning how to implement tests. Im sure theres some proper way to go about the issue I was experiencing but I couldn’t find a resolution for it. When I moved my tests to their own file in their own folder they could no longer locate the main.rs file. Any attempt I made at getting it imported appeared futile as nothing seemed to work. The solution I found was using the assert_cmd
crate to call the program and its arguments.
Conclusion
Overall this was a great project that was easy enough for me to figure out the solutions for but complex enough that I couldn’t do everything immediately off the bat and had to problem solve. Additionally working in rust was pretty enjoyable. Its the same feeling I get when I use my apple products, “It just works!”. I can develop everything on my mac but still have the ability to build binaries for my friends that use Windows. It makes me want to create my future projects in Rust but I wonder if that time would be better spent in learning C++.