Reading from Standard Input

use std::io::{self, BufRead};

fn main() -> io::Result<()> {
    let stdin = io::stdin();
    let mut lines = 0;
    let mut words = 0;
    let mut chars = 0;
    
    for line in stdin.lock().lines() {
        let line = line?;
        lines += 1;
        words += line.split_whitespace().count();
        chars += line.chars().count();
    }
    
    println!("Lines: {}", lines);
    println!("Words: {}", words);
    println!("Characters: {}", chars);
    
    Ok(())
}

This example counts lines, words, and characters from standard input, similar to the wc command.

Writing to Standard Output and Error

use std::io::{self, Write};

fn main() -> io::Result<()> {
    // Write to stdout
    writeln!(io::stdout(), "This is a normal message")?;
    
    // Write to stderr
    writeln!(io::stderr(), "This is an error message")?;
    
    // Flush stdout to ensure the message is displayed immediately
    io::stdout().flush()?;
    
    Ok(())
}

For more complex output formatting, you can use crates like colored for terminal colors or indicatif for progress bars:

use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::{thread, time::Duration};

fn main() {
    // Colored output
    println!("{} {}", "Error:".red().bold(), "Something went wrong".red());
    println!("{} {}", "Success:".green().bold(), "Operation completed".green());
    println!("{} {}", "Warning:".yellow().bold(), "Proceed with caution".yellow());
    
    // Progress bar
    let pb = ProgressBar::new(100);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
            .unwrap()
            .progress_chars("#>-"),
    );
    
    for i in 0..100 {
        pb.inc(1);
        thread::sleep(Duration::from_millis(50));
    }
    
    pb.finish_with_message("Download complete");
}

Error Handling

Robust error handling is essential for command-line applications. Rust’s Result type and the ? operator make it easy to propagate errors:

use std::fs::File;
use std::io::{self, Read};
use std::path::PathBuf;
use structopt::StructOpt;
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("I/O error: {0}")]
    Io(#[from] io::Error),
    
    #[error("Invalid UTF-8: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),
    
    #[error("File is empty")]
    EmptyFile,
}

#[derive(StructOpt, Debug)]
struct Opt {
    #[structopt(parse(from_os_str))]
    file: PathBuf,
}

fn read_file(path: &PathBuf) -> Result<String, AppError> {
    let mut file = File::open(path)?;
    
    let mut contents = Vec::new();
    file.read_to_end(&mut contents)?;
    
    if contents.is_empty() {
        return Err(AppError::EmptyFile);
    }
    
    let text = String::from_utf8(contents)?;
    Ok(text)
}

fn main() {
    let opt = Opt::from_args();
    
    match read_file(&opt.file) {
        Ok(contents) => {
            println!("File contents:");
            println!("{}", contents);
        }
        Err(err) => {
            eprintln!("Error: {}", err);
            std::process::exit(1);
        }
    }
}

This example uses the thiserror crate to define custom error types and automatically implement the Error trait.

For more user-friendly error reporting, you can use the anyhow crate:

use anyhow::{Context, Result};
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
struct Opt {
    #[structopt(parse(from_os_str))]
    file: PathBuf,
}

fn read_file(path: &PathBuf) -> Result<String> {
    let mut file = File::open(path)
        .with_context(|| format!("Failed to open file: {}", path.display()))?;
    
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .with_context(|| format!("Failed to read file: {}", path.display()))?;
    
    if contents.is_empty() {
        anyhow::bail!("File is empty: {}", path.display());
    }
    
    Ok(contents)
}

fn main() -> Result<()> {
    let opt = Opt::from_args();
    
    let contents = read_file(&opt.file)?;
    println!("File contents:");
    println!("{}", contents);
    
    Ok(())
}

The anyhow crate provides the Context trait for adding context to errors and the bail! macro for early returns with custom error messages.


Configuration Files

Many command-line applications support configuration files to avoid repeating command-line arguments. The config crate makes it easy to load configuration from files, environment variables, and command-line arguments:

use config::{Config, ConfigError, File};
use serde::Deserialize;
use std::path::PathBuf;
use structopt::StructOpt;

#[derive(Debug, Deserialize)]
struct AppConfig {
    server: ServerConfig,
    database: DatabaseConfig,
}

#[derive(Debug, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
}

#[derive(Debug, Deserialize)]
struct DatabaseConfig {
    url: String,
    username: String,
    password: String,
}

#[derive(StructOpt, Debug)]
struct Opt {
    #[structopt(short, long, parse(from_os_str))]
    config: Option<PathBuf>,
    
    #[structopt(short, long)]
    host: Option<String>,
    
    #[structopt(short, long)]
    port: Option<u16>,
}

fn load_config(opt: &Opt) -> Result<AppConfig, ConfigError> {
    let mut builder = Config::builder();
    
    // Start with default values
    builder = builder.set_default("server.host", "127.0.0.1")?;
    builder = builder.set_default("server.port", 8080)?;
    
    // Load from file if specified
    if let Some(config_path) = &opt.config {
        builder = builder.add_source(File::from(config_path.clone()));
    }
    
    // Override with environment variables
    builder = builder.add_source(config::Environment::with_prefix("APP"));
    
    // Override with command-line arguments
    if let Some(host) = &opt.host {
        builder = builder.set_override("server.host", host.clone())?;
    }
    
    if let Some(port) = opt.port {
        builder = builder.set_override("server.port", port)?;
    }
    
    // Build and deserialize
    let config = builder.build()?;
    config.try_deserialize()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let opt = Opt::from_args();
    
    let config = load_config(&opt)?;
    println!("Configuration: {:#?}", config);
    
    // Use the configuration
    println!("Server: {}:{}", config.server.host, config.server.port);
    println!("Database URL: {}", config.database.url);
    
    Ok(())
}

This example loads configuration from a file (if specified), environment variables (prefixed with APP_), and command-line arguments, with each source overriding the previous ones.


Testing CLI Applications

Testing command-line applications involves verifying that they correctly parse arguments, produce the expected output, and handle errors gracefully. Rust’s testing framework makes this straightforward:

use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::tempdir;

#[test]
fn test_help() {
    let mut cmd = Command::cargo_bin("myapp").unwrap();
    cmd.arg("--help");
    cmd.assert().success().stdout(predicate::str::contains("USAGE"));
}

#[test]
fn test_version() {
    let mut cmd = Command::cargo_bin("myapp").unwrap();
    cmd.arg("--version");
    cmd.assert().success().stdout(predicate::str::contains("myapp"));
}

#[test]
fn test_file_not_found() {
    let mut cmd = Command::cargo_bin("myapp").unwrap();
    cmd.arg("nonexistent-file.txt");
    cmd.assert().failure().stderr(predicate::str::contains("No such file"));
}

#[test]
fn test_valid_file() {
    // Create a temporary directory
    let temp_dir = tempdir().unwrap();
    let file_path = temp_dir.path().join("test.txt");
    
    // Write test data to the file
    fs::write(&file_path, "Hello, world!").unwrap();
    
    // Run the command
    let mut cmd = Command::cargo_bin("myapp").unwrap();
    cmd.arg(file_path);
    cmd.assert().success().stdout(predicate::str::contains("Hello, world!"));
}

This example uses the assert_cmd crate to run the CLI application and verify its output, and the predicates crate to make assertions about the output.


Packaging and Distribution

Once your CLI application is ready, you’ll want to package and distribute it to users. Rust’s toolchain makes this relatively straightforward: