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: