Getting Started: A Simple CLI Application

Let’s start with a basic command-line application that accepts arguments and prints output:

use std::env;

fn main() {
    // Collect command-line arguments
    let args: Vec<String> = env::args().collect();
    
    // The first argument is the program name
    println!("Program name: {}", args[0]);
    
    // Print the remaining arguments
    if args.len() > 1 {
        println!("Arguments:");
        for (i, arg) in args.iter().enumerate().skip(1) {
            println!("  {}: {}", i, arg);
        }
    } else {
        println!("No arguments provided");
    }
}

This simple example demonstrates how to access command-line arguments using the standard library. However, for more complex applications, we’ll want to use dedicated argument parsing libraries.


Argument Parsing with clap

The clap (Command Line Argument Parser) crate is the most popular choice for parsing command-line arguments in Rust. It provides a rich set of features for defining and parsing arguments, with helpful error messages and automatic help text generation.

Basic Usage with Builder Pattern

use clap::{App, Arg};

fn main() {
    let matches = App::new("greet")
        .version("1.0")
        .author("Your Name <[email protected]>")
        .about("A friendly greeting program")
        .arg(
            Arg::new("name")
                .short('n')
                .long("name")
                .value_name("NAME")
                .help("Sets the name to greet")
                .takes_value(true)
                .required(false),
        )
        .arg(
            Arg::new("count")
                .short('c')
                .long("count")
                .value_name("COUNT")
                .help("Number of times to greet")
                .takes_value(true)
                .default_value("1"),
        )
        .get_matches();
    
    // Get the values
    let name = matches.value_of("name").unwrap_or("World");
    let count: usize = matches.value_of("count").unwrap().parse().unwrap_or(1);
    
    for _ in 0..count {
        println!("Hello, {}!", name);
    }
}

This example defines a CLI application that accepts --name and --count options, with short forms -n and -c. It automatically generates help text (accessible via --help or -h) and version information (via --version or -V).

Using derive Macros with structopt

The structopt crate (now part of clap v3) provides a more declarative approach using derive macros:

use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "greet", about = "A friendly greeting program")]
struct Opt {
    /// Sets the name to greet
    #[structopt(short, long, default_value = "World")]
    name: String,
    
    /// Number of times to greet
    #[structopt(short, long, default_value = "1")]
    count: usize,
    
    /// Use fancy formatting
    #[structopt(short, long)]
    fancy: bool,
}

fn main() {
    let opt = Opt::from_args();
    
    for _ in 0..opt.count {
        if opt.fancy {
            println!("✨ Hello, {}! ✨", opt.name);
        } else {
            println!("Hello, {}!", opt.name);
        }
    }
}

This approach maps command-line arguments directly to a struct, making the code more concise and maintainable.

Subcommands

Many CLI applications have multiple subcommands, each with its own set of arguments. Both clap and structopt support this pattern:

use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "git", about = "A fictional version control system")]
enum Opt {
    #[structopt(about = "Clone a repository")]
    Clone {
        #[structopt(help = "Repository URL")]
        url: String,
        
        #[structopt(help = "Target directory", default_value = ".")]
        target: String,
    },
    
    #[structopt(about = "Commit changes")]
    Commit {
        #[structopt(short, long, help = "Commit message")]
        message: String,
        
        #[structopt(short, long, help = "Amend previous commit")]
        amend: bool,
    },
    
    #[structopt(about = "Push changes to remote")]
    Push {
        #[structopt(help = "Remote name", default_value = "origin")]
        remote: String,
        
        #[structopt(help = "Branch name", default_value = "master")]
        branch: String,
        
        #[structopt(short, long, help = "Force push")]
        force: bool,
    },
}

fn main() {
    let opt = Opt::from_args();
    
    match opt {
        Opt::Clone { url, target } => {
            println!("Cloning {} into {}", url, target);
        }
        Opt::Commit { message, amend } => {
            if amend {
                println!("Amending previous commit with message: {}", message);
            } else {
                println!("Creating new commit with message: {}", message);
            }
        }
        Opt::Push { remote, branch, force } => {
            if force {
                println!("Force pushing to {}/{}", remote, branch);
            } else {
                println!("Pushing to {}/{}", remote, branch);
            }
        }
    }
}

This example defines a fictional version control system with clone, commit, and push subcommands, each with its own set of arguments.


Input and Output

Command-line applications often need to read from standard input and write to standard output or error. Rust’s standard library provides several ways to do this: