Build a Python CLI Tool with Rust
February 25, 2023
Introduction
In this tutorial, we will build a simple Python CLI tool with Rust. The tool will search for a given string in the given directory and print the file names that contain the string.
Prerequisites
To follow this tutorial, you need to have the following installed:
- Rust
- Cargo
- Python 3.7 or later
Create a New Rust project
First, we need to create a new Rust project that will contain the code for our CLI tool.
We will use the cargo to create the project and we will call it findrs:
The directory structure of the project will look like this:
Parse Command-Line Arguments
Add the clap Crate to the Project
The clap crate is a command-line argument parser for Rust. We can use this crate to parse the command-line arguments for findrs.
To add the clap to our project, we can run the following command:
- This will add the latest version of
clapcrate to theCargo.tomlfile under the[dependencies]section.
Add clap Argument Parser to the Project
Now, we can add the clap argument parser our the project. Let's add the following code to the main.rs file:
use std::path::PathBuf;
use clap::Parser; // (1)!
#[derive(Debug, Parser)] // (2)!
#[command(about = "Find all files containing a given name.")] // (3)!
pub struct Arguments {
/// Name to find. // (4)!
#[arg(short, long)] // (5)!
pub name: String, // (6)!
/// Path to to check.
#[arg(default_value = ".")] // (7)!
pub path: PathBuf, // (8)!
}
fn main() {
let args = Arguments::parse(); // (9)!
println!("{:?}", args);
}
- Import
calp'sParsertrait. - Derive the
DebugandParsertraits for theArgumentsstruct. - Add the
aboutattributes to theArgumentsstruct. This will show-up in the help message. - Add help message to the
namefield. - Add the
argattributes to thenamefield. This will add the--nameand-nflags to thefindrsCLI tool. - Add the
namefield to theArgumentsstruct. This field will be used to store the name that we want to match with the file names. - Add the
default_valueattribute to thepathfield. This will set the default value of thepathfield to.(current directory). - Add the
pathfield to theArgumentsstruct. This field will be used to store the path to the directory that we want to search for the given name. - Parse the command-line arguments and store them in the
argsvariable.
Now, we can run the findrs CLI tool with the --help flag to see the help message:
Output:
Find all files containing a given name.
Usage: findrs --name <NAME> [PATH]
Arguments:
[PATH] Path to to check [default: .]
Options:
-n, --name <NAME> Name to find
-h, --help Print help
You can run the findrs CLI tool with cargo by running the following command:
Output:
Here you can see that the name and path fields of the Arguments struct are set to the values that we passed to the findrs CLI tool.
Search for the Files that Contain the Given Name
Add the walkdir Crate to the Project
The walkdir crate is a Rust library for walking a directory tree. We can use this crate to search for the files that contain the given name.
To add the walkdir to our project, we can run the following command:
- This will add the latest version of
workdircrate to theCargo.tomlfile under the[dependencies]section.
Update the main.rs File
Now, we can update the main.rs file to search for the files that contain the given name. Let's add the following code to the main.rs file:
use std::ffi::OsStr; // (1)!
use std::path::PathBuf;
use clap::Parser;
use walkdir::WalkDir; // (2)!
#[derive(Debug, Parser)]
#[command(author, about = "Find all files containing a given name.")]
pub struct Arguments {
/// Name to find.
#[arg(short, long)]
pub name: String,
/// Path to to check.
#[arg(default_value = ".")]
pub path: PathBuf,
}
fn main() {
let args = Arguments::parse();
for entry in WalkDir::new(&args.path).into_iter().filter_map(|e| e.ok()) { // (3)!
let path = entry.path();
if path.is_file() { // (4)!
match &path.file_name().and_then(OsStr::to_str) { // (5)!
Some(name) if name.contains(&args.name) => println!("{}", path.display()), // (6)!
_ => (), // (7)!
}
}
}
}
- Import
OsStrstruct. - Import
walkdir'sWalkDirstruct. - Iterate over all entries in the given directory and ignore any errors that may arise.
- Check if the entry is a file.
- Get the file name of the entry and convert it to a
&str. - If the file name contains the given name, print the path to the file.
- If the file name doesn't contain the given name or is
Nonevariant, do nothing.
Now, we can run the findrs CLI tool with the --name flag and the directory to search for the files that contain the given name:
Output:
Prepare the Project to Run with Python
Install maturin
Install maturin inside a virtualenv by running the following command:
Add pyproject.toml File
Now, we can add the pyproject.toml file at the root of the project.
Let's add the following code to the pyproject.toml file:
[build-system]
requires = ["maturin>=0.14,<0.15"] # (1)!
build-backend = "maturin" # (2)!
[project]
name = "findrs" # (3)!
description = "Find all files containing a given name." # (4)!
requires-python = ">=3.7" # (5)!
[tool.maturin]
bindings = "bin" # (6)!
strip = true # (7)!
- This specifies the version of
maturinthat we want to use. - We need to use
maturinas the build backend for the project. - Specify the name of the package as
findrs. - Add the description of the package.
- Specify the minimum version of Python that we want to support.
- In our case, we want to generate bindings for a binary because it is a CLI tool.
- Strip the library for minimum file size.
Build and install the module with maturin
For Development
For local development, the maturin can be used to build the package and install it to virtualenv.
We can also run pip install directly from project root directory:
Now we can run the findrs CLI tool directly from the terminal:
Create a wheel for distribution
Now, we can create wheels for distribution:
This will create a wheel file in the target/wheels directory.
We can install the wheel file using pip:
Publish the Package to PyPI
Now, we can publish the package to PyPI. First, we need to create an account on PyPI. Then run the following command to upload the package to PyPI:
Integrate GitHub Actions
We can use GitHub actions to run the tests and publish the package to PyPI automatically. To generate the GitHub actions workflow file, we can run the following command:
Useful References
Install Rust: https://www.rust-lang.org/tools/install
Rust Book: https://doc.rust-lang.org/book/
Maturin Documentation: https://www.maturin.rs/index.html
Conclusion
maturin has made it very easy to build and publish Python packages built with Rust.
This tutorial showcases a simple example of that. I hope you found this useful.