· Zen HuiFer · Learn  · 3 min read

Quickly build powerful command-line tools using Rust

Command line tools (CLI) are indispensable tools for developers and system administrators. Rust is renowned for its performance and security, making it an excellent choice for building robust and efficient CLI applications. This guide will take you step by step through creating a command-line tool using Rust and fully utilizing the latest features of Rust 1.70+.

Command line tools (CLI) are indispensable tools for developers and system administrators. Rust is renowned for its performance and security, making it an excellent choice for building robust and efficient CLI applications. This guide will take you step by step through creating a command-line tool using Rust and fully utilizing the latest features of Rust 1.70+.

Quickly build powerful command-line tools using Rust

Command line tools (CLI) are indispensable tools for developers and system administrators. Rust is renowned for its performance and security, making it an excellent choice for building robust and efficient CLI applications. This guide will take you step by step through creating a command-line tool using Rust and fully utilizing the latest features of Rust 1.70+.

Why choose Rust to build CLI tools?

  • performance Rust compiles to native code without the need for runtime or garbage collector.

  • Security Memory security assurance can prevent common errors.

  • ecosystem Rich libraries and tools.

  • Concurrent Excellent support for asynchronous programming.

Development environment setup

Ensure that the latest version of Rust is installed. Rustup is a recommended tool for managing Rust versions.

Install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Update Rust

rustup update

Create a new Rust project

Create a new Rust project using Cargo (Rust’s package manager).

cargo new my_cli_tool
cd my_cli_tool

useclapAnalyze command-line parameters

clapIt is a powerful crate used for parsing command-line parameters and subcommands.

takeclapAdd to Dependency

stayCargo.tomlAdd:

[dependencies]
clap = { version = "4.3.10", features = ["derive"] }

Write parameter parser

staysrc/main.rsIn the middle:

use clap::{Arg, Command};fn main() {
    let matches = Command::new("my_cli_tool")
        .version("1.0")
        .author("Your Name <you@example.com>")
        .about("Does awesome things")
        .arg(
            Arg::new("input")
                .about("Input file")
                .required(true)
                .index(1),
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .about("Increases verbosity")
                .multiple_occurrences(true),
        )
        .get_matches();    // Access arguments
    let input = matches.value_of("input").unwrap();
    let verbosity = matches.occurrences_of("verbose");    println!("Input file: {}", input);
    println!("Verbosity level: {}", verbosity);
}

Run the application program

cargo run -- input.txt -vvv

Output:

Input file: input.txt
Verbosity level: 3

Implement core functions

Let’s add some features to the CLI tool. Assuming we are creating a tool to count the number of lines, words, and characters in a text file (similar towc)。

read file

staymain.rsAdd the following code:

use std::fs::File;
use std::io::{self, BufRead, BufReader};fn main() {
    // ... (previous code)    // Open the file
    let file = File::open(input).expect("Could not open file");
    let reader = BufReader::new(file);    // Initialize counters
    let mut lines = 0;
    let mut words = 0;
    let mut bytes = 0;    for line in reader.lines() {
        let line = line.expect("Could not read line");
        lines += 1;
        words += line.split_whitespace().count();
        bytes += line.len();
    }    println!("Lines: {}", lines);
    println!("Words: {}", words);
    println!("Bytes: {}", bytes);
}

Run the application program

Create a file namedinput.txtExample file:

echo "Hello world!" > input.txt
echo "This is a test file." >> input.txt

Run this tool:

cargo run -- input.txt

Output:

Input file: input.txt
Verbosity level: 0
Lines: 2
Words: 6
Bytes: 33

Add sub commands and advanced features

Subcommands allow your CLI tool to perform different operations.

Update using sub commandsclapParser

modifymain.rs

use clap::{Arg, Command};fn main() {
    let matches = Command::new("my_cli_tool")
        .version("1.0")
        .author("Your Name <you@example.com>")
        .about("Does awesome things")
        .subcommand_required(true)
        .arg_required_else_help(true)
        .subcommand(
            Command::new("count")
                .about("Counts characters, words, or lines")
                .arg(
                    Arg::new("chars")
                        .short('c')
                        .long("chars")
                        .about("Count characters"),
                )
                .arg(
                    Arg::new("words")
                        .short('w')
                        .long("words")
                        .about("Count words"),
                )
                .arg(
                    Arg::new("lines")
                        .short('l')
                        .long("lines")
                        .about("Count lines"),
                )
                .arg(
                    Arg::new("input")
                        .about("Input file")
                        .required(true)
                        .index(1),
                ),
        )
        .get_matches();    match matches.subcommand() {
        Some(("count", sub_m)) => {
            let input = sub_m.value_of("input").unwrap();
            let count_chars = sub_m.is_present("chars");
            let count_words = sub_m.is_present("words");
            let count_lines = sub_m.is_present("lines");            // Call function to perform counting
            perform_counting(input, count_chars, count_words, count_lines);
        }
        _ => unreachable!("Exhausted list of subcommands"),
    }
}fn perform_counting(input: &str, chars: bool, words: bool, lines: bool) {
    // ... (implement counting logic)
}

realizationperform_counting

fn perform_counting(input: &str, chars: bool, words: bool, lines: bool) {
    let file = File::open(input).expect("Could not open file");
    let reader = BufReader::new(file);    let mut line_count = 0;
    let mut word_count = 0;
    let mut char_count = 0;    for line in reader.lines() {
        let line = line.expect("Could not read line");
        if lines {
            line_count += 1;
        }
        if words {
            word_count += line.split_whitespace().count();
        }
        if chars {
            char_count += line.len();
        }
    }    if lines {
        println!("Lines: {}", line_count);
    }
    if words {
        println!("Words: {}", word_count);
    }
    if chars {
        println!("Characters: {}", char_count);
    }
}

Use subcommands to run tools

cargo run -- count -l -w input.txt

Output:

Lines: 2
Words: 6

Error handling and logging

For CLI tools, robust error handling and logging are crucial.

useanyhowPerform error handling

stayCargo.tomlAdd inanyhow

[dependencies]
anyhow = "1.0"

Update function to returnResult

use anyhow::{Context, Result};fn main() -> Result<()> {
    // ... (previous code)    Ok(())
}fn perform_counting(input: &str, chars: bool, words: bool, lines: bool) -> Result<()> {
    let file = File::open(input).with_context(|| format!("Could not open file '{}'", input))?;
    // ... (rest of the code)    Ok(())
}

useenv_loggerAdd log records

stayCargo.tomlAdd inenv_loggerandlog

[dependencies]
log = "0.4"
env_logger = "0.10"

staymainInitialize the logger:

use log::{debug, error, info, trace, warn};fn main() -> Result<()> {
    env_logger::init();    // ... (rest of the code)
}

Using log macros in code:

fn perform_counting(input: &str, chars: bool, words: bool, lines: bool) -> Result<()> {
    info!("Starting counting for file: {}", input);
    // ... (rest of the code)
    Ok(())
}

Set log level when running applications:

RUST_LOG=info cargo run -- count -l -w input.txt

Enhance user experience

usecoloredAdd color output

stayCargo.tomlAdd incolored

[dependencies]
colored = "2.0"

Use in outputcolored

use colored::*;println!("Lines: {}", line_count.to_string().green());
println!("Words: {}", word_count.to_string().yellow());
println!("Characters: {}", char_count.to_string().blue());

useindicatifAdd progress bar

stayCargo.tomlAdd inindicatif

[dependencies]
indicatif = "0.17"

Use progress bar:

use indicatif::ProgressBar;fn perform_counting(input: &str, chars: bool, words: bool, lines: bool) -> Result<()> {
    let file = File::open(input).with_context(|| format!("Could not open file '{}'", input))?;
    let metadata = file.metadata()?;
    let total_size = metadata.len();    let reader = BufReader::new(file);
    let pb = ProgressBar::new(total_size);    // ... (read the file and update pb)    pb.finish_with_message("Done");
    Ok(())
}

Build and distribute your CLI tools

Build and publish binary files

cargo build --release

The binary file will be located attarget/release/my_cli_tool

usecrossPerform cross compilation

installcross

cargo install cross

Build for different goals:

cross build --target x86_64-unknown-linux-musl --release

usecargo-bundlepack

For macOS applications or Windows installers, please consider usingcargo-bundle

Publish to Crate.io

stayCargo.tomlNew data from China and Singapore:

[package]
name = "my_cli_tool"
version = "1.0.0"
authors = ["Your Name <you@example.com>"]
description = "A CLI tool for counting lines, words, and characters."
homepage = "https://github.com/yourusername/my_cli_tool"
repository = "https://github.com/yourusername/my_cli_tool"
license = "MIT"

Login and publish:

cargo login
cargo publish

summary

You have now created a feature rich command-line tool using Rust, including parameter parsing, subcommands, error handling, logging, as well as color output and progress bars to enhance user experience.

Key points:

  • Rust’s performance and security make it an ideal choice for CLI tools.

  • clapCrate simplifies parameter parsing and sub command management.

  • useanyhowPerform error handling and usageenv_loggerLogging improves robustness.

  • usecoloredandindicatifEnhancing UX makes your tools easier to use.

  • Using Cargo and its related tools makes it easy to build and distribute your tools.

Back to Blog

Related Posts

View All Posts »
Tauri2.0 has been released! Not just on the desktop

Tauri2.0 has been released! Not just on the desktop

Tauri 2.0 offers cross-platform development with mobile support, enhancing its appeal to developers seeking efficient, lightweight solutions for both desktop and mobile apps. Its performance and ease of use make it a strong contender in the market, potentially rivaling traditional frameworks. Ideal for projects requiring rapid development and multi-platform support.

Some Go Web Development Notes

Some Go Web Development Notes

Developing a website with Go 1.22 has become more streamlined with its improved routing features, making it simpler to handle HTTP requests. Go's standard library now supports method matching and wildcards, reducing reliance on third-party routing libraries.

Lightweight Rust Asynchronous Runtime

Lightweight Rust Asynchronous Runtime

Smol is a lightweight and fast asynchronous runtime for Rust, perfect for enhancing I/O operations and network communication efficiency. It supports native async/await, requires minimal dependencies, and is easy to use with its clear API. Ideal for both beginners and experienced Rust developers seeking high-performance async solutions. Learn how to implement Smol with detailed examples.

Significant changes to impl trap in Rust 2024

Significant changes to impl trap in Rust 2024

Rust 2024 introduces significant updates to `impl Trait`, making it more intuitive and flexible. The new version allows hidden types in `impl Trait` to utilize any generic parameters by default, aligning with common developer expectations. For finer control, a `use<>` bound syntax is introduced, specifying exactly which types and lifetimes can be used by the hidden types. These changes simplify code while enhancing expressiveness and flexibility, catering to both new and experienced Rust developers.