published on in development Rust

Accepting Command line Parameters in Rust

Accepting command line parameters can be done in essentially two ways: manually by using the args() function from the standard library, or by integrating a more or less de-facto standard package for just this purpose named clap. I’m all for using other people’s wheels instead of reinventing them myself so I’m going with clap. At the time of this writing the Clap package, or ‘crate’ in Rust parlance, is at version 3.1.1 so I add it to my project’s Config.toml file under the [dependencies] section.:

[dependencies]
clap = "3.1.1"

Now when you run cargo run the Cargo package manager will pull in the index of all crates, download all dependencies and compile them all ready for use with your own project.

Be sure to have a good look at Crates.io for thousands upon thousands of other crates that you can use.

Of course the resulting executable won’t do anything with the newly included package yet. That’s because we still need to write some code, and that’s where a whole load of Rust-specific idiom comes pouring out.

Clap offers two ways of implementing command line parsing: one is called the builder API, the other is called the derive API. Neither of these mean very much to me at present, but the derive API looks a bit simpler to me so that’s my first bet.

In order to use the derive API, I’m apparently supposed to enable a feature through Cargo.toml. As it turns out, Rust crates can have optional features that you enable by mentioning them in Cargo.toml like so:

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

This will enable the derive feature, but we’re not actually using Clap yet in our program. This needs an addition at the top of our main.rs source file:

use clap::Parser;

Compiling the application now will throw a large warning about unused_imports. That’s perfectly valid because we just told our program to use the Clap parser but we’re not doing anything with it yet so that’s wasteful. Good for the compiler to tell us about that.

Let’s write some code!

So why do I want my CLI tool to accept command line parameters in the first place? Well, I’m writing a utility that should wrangle virtual disk images for use with retro computer emulators. The first parameter I want to feed into my tool is the name/location of the disk image file in question. Other stuff will also be needed I’m sure, but let’s start small.

UNIX command lines accept two kinds of parameters. They’re either named or derived from their position. In this case I’m expecting growth in the very near future, so let’s go with a named argument called -p and for the GNU-afficionados it should also work when we call it --path. The value of the argument should be a valid location on our local filesystem that could house a virtual disk image at some point in time.

Borrowing liberally from Clap’s own documentation, I came up with this almost verbatim copy/paste from their examples:

use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
    /// Name of the person to greet
    #[clap(short, long)]
    path: String,

    /// Number of times to greet
    #[clap(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
  let args = Args::parse();
  println!("Value: {}", args.path);
}

It turns out that this does exactly what I described. The resulting binary accepts both -p and --path as parameters and it complains if you don’t provide it, which is also good. A nice added bonus is that you get usage instructions completely for free. Just build the code, run it and marvel at the result.

Please hold onto your chair though: the resulting binary with all the debug trimmings now weighs in at 22 megabytes. That’s huge for something that just takes a word on a command line and spits it right back at you. The reason for that is in the fact that cargo build by default creates binaries for debugging. When you build using cargo build --release the size of the binary nearly halves. But that’s still 12 megabytes for something that functionally does next to nothing. As a retro computing afficionado I’m used to software coming on floppy disks. This one utility would span around a dozen of those on a good day. Completely unacceptable, but that’s for another day.

So what’s with the derive bit?

Honestly? I don’t know yet. It has something to do with traits, which is a Rust-specific idiom that the official documentation introduces like so:

A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior.

I don’t know about you, but it tells me next to nothing so I’m leaving that subject for another time. For now I’m happy that I got my utility to the point where it accepts command line arguments. So sure, I’m being cargo-cultish here in just copy-pasting someone else’s code and being smug about the result without understanding every line. I did learn a few small things, though:

  • I learned about crates and where to find them
  • Got my first hands-on idea of how to use them, and it was easy!
  • I reached a functional goal for my project