Creating a QR Code CLI with Rust
- Authors
- Name
- Dhravya Shah
- @DhravyaShah
Rust is difficult. And like any difficult thing, I realized that the best way to learn is by getting my hands dirty and doing some project so that I can start learning and creating useful things that people can use in their everyday lives
Since Saumya, a good friend of mine was working on an API wrapper for api.dhravya.me, I thought that making a CLI with similar functionality would be a nice idea.
I decided to go only with QR code for now, because I’m just a beginner at rust, and let’s be honest, no one is actually going to use the cli, it’s more of a learning experience.
By the end of this project, I learnt:
- How to make a CLI in Rust (using
clap
) - How to make API request in rust (using
reqwest
) - How to deal with Bytes and save to file (using
std::io::Write
)
This is the end product:
Starting out
Since I had no idea how to make a cli using Rust, I searched it up and ended up at https://rust-cli.github.io/book/tutorial, a really good tutorial.
Start the project with
cargo new qrcode-cli
Added the following dependencies to Cargo.toml
[dependencies]
clap = { version = "3.0", features = ["derive"] }
reqwest = { version = "^0.10.0", features=["blocking"] }
Yeah, I used blocking reqwest
because I have no idea how the async stuff works in rust yet, so not to get ahead of myself, I just used the blocking version
For cli, I’m using this really useful package called clap - It does all the CLI part for me, all I had to do is define a structure
Here’s the code for the structure:
#[derive(Parser, Debug)]
struct Cli {
#[clap(short = 'd', long = "data")]
// The information contained by the QR code
data: String,
#[clap(short = 'o', long = "output", default_value = ".")]
#[clap(parse(from_os_str))]
output: std::path::PathBuf,
}
std::path::PathBuf
is just a way to store aString
-like value but for file paths
I learnt that the #[]
is kinda just like a decorator in python
#[clap(parse(from_os_str))]
is to tell clap to support file autocomplete in the -o
output flag
Finally, I defined the main function, and got the args
fn main() {
let args = Cli::parse();
This provides a really good interface so I can get args.data
and args.output
easily
So, the CLI part was done, that was easy, huh. The tutorial made it simple for me. But I had no idea what came next.
First, I used Hyper to deal with all the request part, however, I soon realised that all the async stuff will make me crazy if i go without any knowledge, so I switched to reqwest
- An easier way of doing the same thing
Even in reqwest i used the Blocking feature and not the async part, I’ll switch when I learn how to
Now, I made a simple API GET
request and handled any errors that may come
// Using blocking request
let res = reqwest::blocking::get(format!("https://api.dhravya.me/qrcode?query={}", args.data).as_str());
// Check if the request was successful
if res.is_err() {
println!("Error: {}", res.err().unwrap());
return;
}
Errors are inevitable here, it could be because of my server not responding, or internet issue, or a 500 error, I handled all of them using the is_err()
function
So now I had to do the following:
- Make a file in the user-specified
output
directory - Get the body from the
res
response (as bytes) - Write the bytes to the
.png
file
I looked into how to make a new file, and came to know that I have to use the standard library std::fs::File
to create a qrcode.png
// Create a file in the output directory
let mut file_ = std::fs::File::create(args.output.join("qrcode.png")).unwrap();
I added the underscore because i thought it could mess with the standard library. Fortunately, I don’t need that because Rust handles it really well
Now I simply got the response body in bytes,
I first tries to_bytes()
but that didn’t seem to work, then I saw the docs and I had to get the Body
struct from response, but turns out I didn’t need that either.
// Get the response body in bytes
let body = res.unwrap().bytes().unwrap();
This seemed to work pretty well it basically just checks if res is valid, converts to bytes and checks again. The unwrap might be unnecessary but meh
and finally, I tried to use file.write
but again, that didn’t work 💀 fortunately the VS code autocomplete suggested write_all
so I just used that and it worked perfectly!
// Save the body to file
file_.write_all(&body).unwrap();
Finally I just printed a line telling them that the qr code has been saved successfully
println!("QR code saved to {}", args.output.join("qrcode.png").display());
More stuff I want to do with this project
There are some more things I want to do in this project, I’ll list them out here for future me to handle
Add help for everything
Currently it just uses the help stuff that clap provides out of the box i would like to customise that
Add more arguments to the CLI
my API provides so many customizations for the QR code (check it out at dhravya.dev/qrcode) That could be utilised in this CLI as more args, and taking the input in such a way that it’s easily understandable for the user
Async I would like this program to be async (probably using
tokio
andhyper
orreqwest
). I tried to make it async this time but I didn’t know stuff about future in rust (and the syntax) so I avoided going that routePublish on crates.io maybe? I could make this into a full fledged QR wrapper for my API and publish it on Crates.io for others to use
This project is open source on github.
Leave a ⭐ if you liked the project, and try out the CLI here
https://github.com/Dhravya/rust-qrcode-cli
Thanks for reading. You made it!
Thanks for reading so far! Not everyone comes here.
If you liked the blog and would like to know whenever I post one, make sure to subscribe to the newsletter at newsletter.dhravya.dev.
And follow me on Twitter, where I try to post interesting stuff every day.
Comment your thoughts and ideas down below! Let’s start a conversation.
Anyways, that’s it. Bye!