Build a Rust CLI Using Seahorse
Time to read: 8 minutes
The command line interface (CLI) still plays a key role in software development, despite the popularity of graphical user interfaces (GUIs).
Apart from the fact that the CLI is not as resource intensive as a GUI, it also allows the user to take advantage of access to privileged commands in the system. This makes it especially useful to software engineers, as the added flexibility and control it provides enables them carry out their tasks very effectively. In fact, building a CLI for your product can enhance the developer experience.
Previously, I have written on how to build a CLI with PHP and Go. In this tutorial, I will show you how to build one in Rust using Seahorse.
Memory safety, thread safety, and the absence of a garbage collector make Rust a strong candidate when building performance-critical software. Building complex CLIs require a mix of these features. Seahorse takes away some of the complexity by handling flag typing and parsing for you. This allows you to focus on the problem domain instead of trying to parse user input.
To get familiar with Seahorse, I will walk you through the process of building a CLI that encrypts and decrypts text based on a specified cipher. Additionally, if the user adds the appropriate flag, the CLI will send an encrypted message to someone.
Prerequisites
To follow this tutorial, you will need the following:
- A basic understanding of Rust
- Rust ≥ 1.67 and Cargo
- A Twilio account. If you don't have one, you can sign up for a free trial account
- You will also need to activate the Twilio Sandbox for WhatsApp
What you will build
In this tutorial, you will build a CLI that can generate secret messages using substitution ciphers. While such algorithms are not secure enough to save your passwords, they are certainly enough to share funny jokes with your friends without prying eyes spoiling the fun.
To keep things short, the application will only use two algorithms: Caesar’s cipher and Bacon’s cipher. In addition, your application will be able to directly send the encrypted text as a WhatsApp message to a specified phone number via Twilio.
Get started
Where you create your Rust projects, create a new Rust project and change into it using the following commands.
Add project dependencies
Update the dependencies
section of the Cargo.toml file to match the following.
Here’s what each added crate does:
- Colored: This crate will be used to colour the CLI output. Information from the CLI will be yellow, error messages will be red while a successful action is coloured green.
- Regex: This crate provides routines for searching strings for matches of a regular expression (aka “regex”).
- Reqwest: This package will simplify sending API requests to Twilio.
- rust-dotenv: This package helps keep secure information, such as API keys and tokens, out of code, by setting them as environment variables.
- Seahorse: Seahorse is a minimalistic Rust framework that simplifies the process of building a CLI, by providing helper structs and methods to parse user input (and flags) and make them available in the preferred data type.
- serde, serde_derive, and serde_json: These packages reduce the complexity of deserialising JSON responses from Twilio, so that they can be more easily used.
Set the required environment variables
Now, create a new file called .env in the project's top-level directory, and paste the following code into it.
After that, retrieve your Twilio Auth Token, Account SID, and phone number from the Twilio Console Dashboard and insert them in place of the respective placeholders.
Build the application
Now that you have your Twilio credentials, you’re set to start building the application. To make your code easy to follow, you’ll build separate modules to handle separate concerns.
Implement encryption and decryption algorithms
Next, let’s add the modules that will handle encryption and decryption for the selected algorithms.
The Bacon algorithm
In the src folder, create a new file named bacon.rs and add the following code to it.
For this article, the 26 character alphabet cipher is being used. This ensures that each alphabet has a unique cipher. The Bacon cipher replaces a character with a sequence of 5 characters. Since each character has a predefined replacement, a map named lookup
is used to store the replacement for each character.
In the Encrypt()
function, the string to be encrypted is provided as an argument. This function iterates over each character in the provided string and retrieves the associated replacement from the lookup
map. The replacements for each character are concatenated into a string and returned as the encryption version of the input string.
The Decrypt()
function works in reverse. A reverseLookup
map is declared which has the decrypted character for each five character sequence. Given an encrypted sequence, the function iterates through the sequence and generates five character chunks. For each chunk, the function checks for the decrypted value of the chunk in the reverseLookup
map. The decrypted values are concatenated and returned similar to the process in the Encrypt()
function.
The Caesar algorithm
In the src folder, create a new file named caesar.rs and add the following code to it.
To encrypt or decrypt a Caesarean cipher, you need the input text and the number of rotations for each encryption. For each character in the text, the position in the alphabet is calculated. Then using the number of rotations, the position of the replacement text is determined and the character in that position retrieved. This is done for all characters in the input sequence. All the replacement characters are concatenated and returned as the output string.
Bear in mind that the number of rotations provided can exceed the number of letters in the alphabet (26) and because of that, you need a function to determine the index of the character to be used for replacement. For encryption, the cipher_index()
function is used, while the plain_index()
function is used for decryption.
Add functionality for WhatsApp notifications
At the moment, there’s no Twilio SDK available for Rust. However, using Twilio’s Programmable Messaging API, it is possible to send a JSON request for the purpose of sending WhatsApp messages. The next module you will add will contain the relevant structs and functions to simplify the process of dispatching notifications from your CLI.
In the src folder, create a new file named notification.rs and add the following code to it.
Two structs are modelled after the possible responses from the Twilio API. The SuccessResponse
takes a subset of the information returned by the API for a successful request, while the ErrorResponse
struct is modelled after the response for an unsuccessful request.
The send_whats_app_message()
is used to send a specified message to the provided phone number. This function returns a tuple containing the outcome of the action (a String, and a boolean indicating success or failure).
To do this, a JSON request is sent containing the message, the recipient, and the requisite authentication for a request, retrieved from the environment variables. Using the error_message()
and success_message()
, the API response is parsed and a string message is generated. Depending on the HTTP response status code, a boolean is returned alongside the generated string.
Add validation functionality
To ensure that the application functions smoothly, you also need to filter some of the user input. There are so many other things your code could (and should) filter to ensure your application works smoothly. But, to keep things simple, this application will only filter the input for the number of rotations and the input for the recipient’s phone number.
In the src folder, create a new file named input_filter.rs and add the following code to it.
Both functions return an Option. If the provided value is valid, it is returned in the Some
variant. Otherwise, the None
variant is returned.
The number_of_rotations()
function takes an input integer and checks to ensure that it is greater than 0. This is to prevent unexpected behaviour during the encryption and decryption processes as a result of negative input.
The recipient_phone_number()
function takes a string slice and matches the value against the regular expression for a valid E.164 phone number. In the event that the provided phone number is invalid, the None
variant is returned.
Next, add a helper struct which will hold the CLI response message. In addition to the message, the struct will also have a boolean flag which indicates whether or not the response to be printed is for an error or not.
In the src folder, create a new file named cli_response.rs and add the following code to it.
The CLIResponse
struct has two fields, one for the message and another for whether or not the message is an error message. The struct also has four functions, namely:
new()
: This is used to create a new instance of the struct.success_update()
: This is used to add a success message to the struct.error_update()
: This is used to add an error message to the struct.print_result()
: This is used to print the saved response to the terminal. The output is coloured (based on whether or not the message being printed is an error message)is_success()
: This is used to determine whether the struct contains a success message. This function will be used to determine whether or not some additional actions can be carried out.
Putting it all together
With all the other components of the application in place, it’s time to update the application entry point to handle user input and perform accordingly. Update the code in src/main.rs to match the following.
The entry point for the application is the main()
function. In this function, the environment variables are loaded and the CLI arguments are collected. Then, an App struct is instantiated and the run()
function is called with the CLI arguments passed as a parameter.
The base_action()
function returns an anonymous function which is executed when no additional commands are called. To trigger this action, you can run the following command.
This command prints the following coloured output to the terminal
The App
struct is instantiated with two commands as defined in the decrypt_command()
and encrypt_command()
functions. These functions instantiate a Command struct with the relevant description, alias(es), and flags specified. Both commands also have a corresponding function: decrypt_action()
and encrypt_action()
, which parse the provided flags and execute accordingly.
The encrypt_action()
also checks if a recipient
flag is provided, indicating that the user wants to send the ciphertext as a WhatsApp message.
In the event that the user passed the flag, the handle_notification()
function is called. This function takes the ciphertext and the value passed to the flag, then, it filters the recipient to ensure that it is in the appropriate format, then makes a call to the send_whats_app_message()
function in the notification
module. This function returns a tuple, either from the send_whats_app_message()
function or an error tuple which is created when the user enters an invalid phone number.
You can take your shiny new CLI for a spin by running the following commands.
There you have it!
Using Seahorse, you were able to build an encryption/decryption CLI. You also learnt how to handle arguments and flags in order to add more flexibility to your commands.
There are still some other things you can do such as building and distributing a binary which you can distribute to your inner circle for easy communication and distribution of secrets.
The entire codebase is available on GitHub should you get stuck at any point. I’m excited to see what else you come up with. Until next time, make peace not war ✌🏾
Joseph Udonsak is a software engineer with a passion for solving challenges – be it building applications, or conquering new frontiers on Candy Crush. When he’s not staring at his screens, he enjoys a cold beer and laughs with his family and friends. Find him on LinkedIn, Medium, and Dev.to.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.