Introduction
To learn a new language, I find it quite interesting to build something. Having simple features at first to get the hang of the language, then adding more stuff to it, making it more complex, faster, cleaner...
So this is what we'll try to do together in this series. We will build a password manager in Rust. The first article will start with simple features. By the end, we'll have a command line application with:
- a way to display our passwords
- a way to save a new password
No encryption, password generations, user interfaces... These will come later. Ok, let's go!
First step: The command line
For this project, we will use the Clap crate to interact easily with the command line. We already used it in this article.
So, let's recap what we want from our command line arguments:
- an argument to list our existing passwords
- an argument to save a new password
Here's how we can do it:
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
cmd: Commands,
}
#[derive(Subcommand)]
enum Commands {
List,
Add {
service: String,
username: String,
password: String,
},
}
What's going on here? Using Clap, we define a Cli structure with a cmd. This can take two values, List or Add (in our enum Commands). Note that the #[derive(...)] syntax tells Rust that we want our enum or struc to implement certains traits. In our case, we want our enum Commands to implement the Subcommand trait from Clap.
Our List subcommand doesn't take any arguments. But our Add subcommand will take three, the name of the service, a username and a password.
Great! Let's add some code in our main function to see how we interact with all of this:
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
cmd: Commands,
}
#[derive(Subcommand)]
enum Commands {
List,
Add {
service: String,
username: String,
password: String,
},
}
fn main() -> std::io::Result<()> {
let args = Cli::parse();
match args.cmd {
Commands::List => display_passwords(),
Commands::Add {
service,
username,
password,
} => add_new_password(service, username, password),
}
Ok(())
}
fn display_passwords() {
println!("Will display passwords")
}
fn add_new_password(service: String, username: String, password: String) {
println!("Will add new password.");
println!("Service:{}", service);
println!("Username:{}", username);
println!("Password:{}", password);
}
We add a match in the main function to handle the different commands we can handle. We add two functions that will only print out something for now. Let's run it!
What happens if we run the add command without the correct number of arguments?
It's another nice thing about Clap, it gives us helpful information about the arguments we can use. It also gives us the help command by default:
Step 2: Having a way to store our passwords
So, this works great, but our functions don't do anything yet. We need to decide how we are going to store our passwords. For now, we are simply going to have a .txt file. So, at the root of our project, we are going to add a passwords.txt file. We'll define some rules for our very simple implementation:
- each password will have its own line in the file
- on each line, we'll separate the service, username and password with a special character
This is obviously far from being perfect, but it will do the job in our quest to learn the language. Let's do it!
Our passwords.txt will have some entries already for testing purposes:
dev.to|Damien|my dev password
twitter|Bobby|123Awesome456
Our List command will read from this file and display its contents. Let's update our display_passwords function:
//All our imports
use clap::{Parser, Subcommand};
use std::{
fs::{self},
path::Path,
};
// Rest of the code ...
fn display_passwords() {
let path = Path::new("./passwords.txt");
let contents = fs::read_to_string(path).expect("Could not read the passwords file");
println!("{}", contents)
}
We create a new Path with our relative file path and we pass it to the read_to_string function provided by the fs module.
Note: If you put the txt file in the src folder, your relative path will be ./src/passwords.txt.
The .expect() will print out the message we provided if we encounter an error. Let's check the results:
Nice! On to the next step.
Step 3: Adding a new password to our file
We're almost there! Now, we need to implement our add_new_password function.
Step 4:
- Writing to the new file
fn add_new_password(service: String, username: String, password: String) -> std::io::Result<()> {
let path = Path::new("./passwords.txt");
let password_infos = format!("{}|{}|{}\n", service, username, password);
let mut file = OpenOptions::new().append(true).open(path)?;
file.write_all(password_infos.as_bytes())?;
Ok(())
}
After creating our Path, we use format! to format our String we want to append to our passwords.txt file. Then, we use OpenOptions. Provided by the fs module, it defines how we want to open a file. In our case, we want to append data to the contents, so we use append(true). Notice that we use the mut keyword, to tell Rust that this variable is mutable.
Then, we use the write_all function to append our password informations. Careful, the write_all function expects a u8 primitive type for its argument. That's why we use as_bytes().
Finally, we return Ok(()).
And we should be done! Our full code looks like this:
use clap::{Parser, Subcommand};
use std::io::Write;
use std::{
fs::{self, OpenOptions},
path::Path,
};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
cmd: Commands,
}
#[derive(Subcommand)]
enum Commands {
List,
Add {
service: String,
username: String,
password: String,
},
}
fn main() -> std::io::Result<()> {
let args = Cli::parse();
match args.cmd {
Commands::List => display_passwords(),
Commands::Add {
service,
username,
password,
} => add_new_password(service, username, password)?,
}
Ok(())
}
fn display_passwords() {
let path = Path::new(".passwords.txt");
let contents = fs::read_to_string(path).expect("Could not read the passwords file");
println!("{}", contents);
}
fn add_new_password(service: String, username: String, password: String) -> std::io::Result<()> {
let path = Path::new("./passwords.txt");
let password_infos = format!("{}|{}|{}\n", service, username, password);
let mut file = OpenOptions::new().append(true).open(path)?;
file.write_all(password_infos.as_bytes())?;
Ok(())
}
Time to run this and test it out!
Adding a new password:
Listing our passwords list:
Yay! It works! Congratulations!
In this article, we explored a bit deeper the Clap crate to create subcommands. Then, we learned how to interact with files thanks to the fs module. In the next articles, we'll keep making this little project better. You can find the code here
Have fun ❤️
Top comments (2)
Congrats on your progress in Rust!
By the way, running your program via
cargo
isn't much fun. Why not just run a compiled version directly from CLI? It's in /target/release or similar folder and have the name of your project from .toml file.Also, I would recommend to use serialization using the
serde
instead of parsing file manually. By the way, you use CSV file format with | as a delimiter. Rust has an awesome package calledcsv
for saving and loading data from this file format.I'm thrilling to read your next post on Rust! Keep up doing!
Thank you for the tips and the kind words! I'll make sure to use your suggestions in the next articles to keep improving the code. ❤️