Stratus3D

A blog on software engineering by Trevor Brown

What I Learned Building a CLI App in Rust

It’s been 8 years since I learned Elixir and since then I have not tried to learn another programming language. I have now decided to learn a systems programming language, and it seems like a good time to be learning about them. New languages like Vale and Rust combine the memory safety of a high-level language with the performance of C. Rust is very popular today but there are many other new systems languages with similar characteristics. Zig, Vlang, Nim are a few that come to mind.

They all seemed very interesting, but I had to choose one, so knowing very little about systems programming languages I chose Rust. I’ve watched Rust develop and grow for years, and several months ago decided it was time to give it a try. I chose Rust over the others because of its memory safety guarantees, performance, and its large community. Another big thing that pushed me towards Rust was the quality of open-source software written in Rust. CLI tools like hyperfine and ripgrep are excellent and I noticed they were written in Rust.

I wanted to try out Rust by building a command line application because I had a CLI tool I needed, and because I thought Rust would be well suited for it. I read the Command Line Applications in Rust book and began writing code. This article documents some of the things I didn’t comprehend when I started.

Think About Error Handling First

A lot of example code shows the unhappy paths in the code invoking Rust’s panic! macro, but this isn’t a good pattern for application code. In practice panic! should only be used for situations that should never happen. Instead of using panic!, it’s usually better to return a Result and let the calling code figure out how to handle it. Rust makes you handle any errors that a function might return, and will fail to compile your code if you don’t handle an error. This makes it easy to write robust tools - it’s impossible to forget to handle an error!

Since you have to handle all errors that could occur you’ll quickly end up needing to handle a lot of different error types. If you define a function that can return different errors you either have to define an enum for the return type with all possible error types in it, which can get very verbose, or use some sort of trait-based error handling. For CLI applications the Error trait or a package like Anyhow or Eyre keeps things simpler. I settled on using the Eyre package and its trait object for unifying error handling and found it to be a nice improvement upon the Result enum in the standard library. Error handling was something I struggled with until I read "More than you’ve ever wanted to know about errors in Rust" on shuttle.rs. It’s worth a read if you want more details on this.

Also, use human-panic to set up user-friendly panic messages so your users know how to report bugs when things do go wrong.

Write Tests as You Go

Rust’s built-in test framework allows you to define tests almost anywhere and run them. Adding unit tests is easy enough that you can do test-driven development and end up writing better code to begin with.

Integration Testing can be Easy

It depends on the type of application you are building but in my case I found integration testing with assert_cmd to be easy. assert_fs and predicates were also nice packages that helped make my integration tests simpler. The CLI book covers this in some detail.

Beware of External Libraries

External libraries make compiling the application for multiple architectures harder. I used a package that relied on OpenSSL for the tool I wrote, and it caused me some headaches when trying to compile it for a different operating system.

Conclusion

The books and documentation make the learning curve easier, but Rust can be a challenging language to learn. There were definitely a few surprises along the way, but Rust lived up to my expectations. I recommend it for CLI apps in particular. I’m already planning on using Rust for future projects!