Writing a Command Line Tool in Rust

Aug. 29, 2017 · Matt

cli-help-menu

What

Rust is a systems programming language that enables you to write fast, safe and concurrent code. It fits in the same niche as C and C++ but provides a fresh breath of features and convenience that makes writing programs in it fun.

Command Line Tools are programs designed to be executed in a terminal (command line) interface. They are synonymous with Unix programming where they are often called shell tools. An example is the ls command used to list directories.

We are going to cover how to write a command line tool using Rust by writing a simple clone of the popular wget tool used for file downloads.

Why

The aim here is to get started writing command line tools in Rust programming language also use some wonderful crates (community libraries) that make writing CLI programs a breeze.

How

Our simple wget clone will have the following features which a desirable in a command line tool:

  • Argument parsing
  • Colored Output
  • Progress bar

Project Setup

We use rust's build tool (and package manager) Cargo to setup our project skeleton.

cargo new rget --bin

This creates a new project called rget and the --bin option tells cargo we are building an executable and not a library. A folder is generated with the following structure.

$ cd rget
$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

Cargo.toml is a manifest file and our code will live under the src directory, in main.rs

Argument Parsing

We will use the clap crate for parsing command line arguments. We add to our project by updating cargo's manifest file dependecies section.

[package]
name = "rget"
version = "0.1.0"
authors = ["Matt Gathu <mattgathu@gmail.com>"]

[dependencies]
clap = "2.26.0"

We then update our main function in main.rs to perform argument parsing.

extern crate clap;

use clap::{Arg, App};

fn main() {
    let matches = App::new("Rget")
        .version("0.1.0")
        .author("Matt Gathu <mattgathu@gmail.com>")
        .about("wget clone written in Rust")
        .arg(Arg::with_name("URL")
                 .required(true)
                 .takes_value(true)
                 .index(1)
                 .help("url to download"))
        .get_matches();
    let url = matches.value_of("URL").unwrap();
    println!("{}", url);
}

We can now test our argument parser using Cargo.

cargo run

    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rget`
error: The following required arguments were not provided:
    <URL>

USAGE:
    rget <URL>

For more information try --help

We can pass arguments to our program by adding -- when calling cargo run

cargo run -- -h

    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rget -h`
Rget 0.1.0
Matt Gathu <mattgathu@gmail.com>
wget clone written in Rust

USAGE:
    rget <URL>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

ARGS:
    <URL>    url to download

Progress bar and colored output

indicatif is a rust library for indicating progress in command line applications. We use it to implement a progress bar and a spinner for our wget clone.

indicatif relies on another crate, console and uses it for colored output. we'll always leverage console and use it to print out colored text.

Below is the function for creating the progress bar:

fn create_progress_bar(quiet_mode: bool, msg: &str, length: Option<u64>) -> ProgressBar {
    let bar = match quiet_mode {
        true => ProgressBar::hidden(),
        false => {
            match length {
                Some(len) => ProgressBar::new(len),
                None => ProgressBar::new_spinner(),
            }
        }
    };

    bar.set_message(msg);
    match length.is_some() {
        true => bar
            .set_style(ProgressStyle::default_bar()
                .template("{msg} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} eta: {eta}")
                .progress_chars("=> ")),
        false => bar.set_style(ProgressStyle::default_spinner()),
    };

    bar
}

The function has several arguments and creates a progress bar based on their value. We use Rust's pattern matching feature to match the arguments to the desired progress bar.

Cloning wget's logic

We use the reqwest crate for implement file download function that receives a url and downloads it into a local file.

The download function will also update the progress bar when each chunk of the file is downloaded and also print out colored text with the download's metadata. This behaviour will be similar to wget's.

Here's a code snippet showing the download function:

fn download(target: &str, quiet_mode: bool) -> Result<(), Box<::std::error::Error>> {

    // parse url
    let url = parse_url(target)?;
    let client = Client::new().unwrap();
    let mut resp = client.get(url)?
        .send()
        .unwrap();
    print(format!("HTTP request sent... {}",
                  style(format!("{}", resp.status())).green()),
          quiet_mode);
    if resp.status().is_success() {

        let headers = resp.headers().clone();
        let ct_len = headers.get::<ContentLength>().map(|ct_len| **ct_len);

        let ct_type = headers.get::<ContentType>().unwrap();

        match ct_len {
            Some(len) => {
                print(format!("Length: {} ({})",
                      style(len).green(),
                      style(format!("{}", HumanBytes(len))).red()),
                    quiet_mode);
            },
            None => {
                print(format!("Length: {}", style("unknown").red()), quiet_mode); 
            },
        }

        print(format!("Type: {}", style(ct_type).green()), quiet_mode);

        let fname = target.split("/").last().unwrap();

        print(format!("Saving to: {}", style(fname).green()), quiet_mode);

        let chunk_size = match ct_len {
            Some(x) => x as usize / 99,
            None => 1024usize, // default chunk size
        };

        let mut buf = Vec::new();

        let bar = create_progress_bar(quiet_mode, fname, ct_len);

        loop {
            let mut buffer = vec![0; chunk_size];
            let bcount = resp.read(&mut buffer[..]).unwrap();
            buffer.truncate(bcount);
            if !buffer.is_empty() {
                buf.extend(buffer.into_boxed_slice()
                               .into_vec()
                               .iter()
                               .cloned());
                bar.inc(bcount as u64);
            } else {
                break;
            }
        }

        bar.finish();

        save_to_file(&mut buf, fname)?;
    }

    Ok(())

}

The download function takes a target url, parses it to generate a filename and uses the Content-Length HTTP header to determine the size of the file. It generates a colored progress bar and downloads the file in chunks. Once each chunk is received, the progress bar is updated to show progress.

Once the full download is complete, the file contents are save to a file.

Recap

Writing CLI tools in Rust is quite easy. Argument parsing can be done using the clap crate, progress bars generated using the indicatif crate and colored output using the console crate. The cargo build tool also makes it a breeze to build and run our code.

You can find the full implementation of the wget clone my github.