Paw
— 2019-05-06

Today we're announcing paw, a first step by the CLI Working Group to make command line applications more first class in Rust.

We introduce a procedural macro paw::main that allows passing arguments to fn main, and a new trait ParseArgs that must be implemented by the arguments to main. This allows passing not only the classic std::env::Args to main but also, for example, structopt instances.

print_args

fn main(args: std::env::Args) {
    for arg in args {
        println!("{:?}", arg);
    }
}
$ cargo run --example print_args -- hello world
"hello"
"world"

structopt

#[derive(structopt::StructOpt)]
struct Args {
    address: String,
    port: u16,
}

async fn main(args: Args) -> Result<(), failure::Error> {
    let mut app = tide::App::new(());
    app.at("/").get(async |_| "Hello, world!");
    app.serve((args.address, args.port)).await?;
}
$ cargo run --example structopt -- localhost 8080

Our hope is that by allowing flexible passing of arguments to fn main we can make command line parsing feel more intuitive for new Rustaceans and seasoned experts alike.

Paw today

What paw brings is a stepping stone to enable our vision of first-class command line parsing in Rust. We envision a place where ParseArgs is included in stdlib, and std::env::Args implements it out of the box. However, as a precursor to an RFC we want to polish the ergonomics and test the usability of paw to get it right.

With paw today the print example can be written as:

#[paw::main]
fn main(args: paw::Args) {
    for arg in args {
        println!("{:?}", arg);
    }
}

This should provide a comparable experience to what a potential std experience would be like.

How does the trait work?

The trait has 1 method: parse_args which returns a Result<Self>. Because this must be known at compile time, Self needs to be Sized, and we have an associated Error type too. In total the declaration is about 4 lines, which means as far as traits go it's quite small.

pub trait ParseArgs: Sized {
    type Error;
    fn parse_args() -> Result<Self, Self::Error>;
}

The paw::main macro detects if the trait is implemented for the argument passed in at runtime, and then replaces (args: Args) with let args = Args::parse_args() inside the function body. We suspect that adding similar functionality to std would be a (relatively) small change too.

Conclusion

We've introduced paw, a crate to enable arguments in main. It consists of a proc macro, trait, and wrappers around stdlib's std::env::Args and std::env::ArgsOs types.

paw is available on GitHub as rust-cli/paw, and on crates.io as paw. Happy hacking!

Thanks

Thanks to stjepang and Dylan-DPC for help with the implementation. And all of the CLI WG for help and feedback on the API and this post.