use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)]
#[command(name = "tasks", version, about = "A tiny task manager")]
pub struct Cli {
#[arg(short, long, global = true)]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Add(AddArgs),
Remove(RemoveArgs),
List(ListArgs),
}
#[derive(Args, Debug)]
pub struct AddArgs {
pub title: String,
#[arg(short, long, default_value_t = 3)]
pub priority: u8,
#[arg(short, long)]
pub tags: Vec<String>,
}
#[derive(Args, Debug)]
pub struct RemoveArgs {
pub id: u64,
#[arg(short, long)]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct ListArgs {
#[arg(short, long, value_enum, default_value_t = SortOrder::Created)]
pub sort: SortOrder,
#[arg(short, long, default_value_t = 20)]
pub limit: usize,
}
#[derive(ValueEnum, Clone, Copy, Debug)]
pub enum SortOrder {
Created,
Priority,
Title,
}
use anyhow::{ensure, Result};
use crate::cli::{AddArgs, ListArgs, RemoveArgs, SortOrder};
pub struct Context {
pub verbose: bool,
}
impl Context {
pub fn log(&self, msg: &str) {
if self.verbose {
eprintln!("[debug] {msg}");
}
}
}
pub trait Command {
fn run(&self, ctx: &Context) -> Result<()>;
}
impl Command for AddArgs {
fn run(&self, ctx: &Context) -> Result<()> {
ensure!((1..=5).contains(&self.priority), "priority must be between 1 and 5");
ctx.log(&format!("adding task with {} tags", self.tags.len()));
println!("added \"{}\" (priority {})", self.title, self.priority);
Ok(())
}
}
impl Command for RemoveArgs {
fn run(&self, ctx: &Context) -> Result<()> {
if !self.force {
ctx.log("remove called without --force");
}
println!("removed task #{}", self.id);
Ok(())
}
}
impl Command for ListArgs {
fn run(&self, ctx: &Context) -> Result<()> {
let order = match self.sort {
SortOrder::Created => "created",
SortOrder::Priority => "priority",
SortOrder::Title => "title",
};
ctx.log(&format!("listing up to {} tasks", self.limit));
println!("listing {} tasks sorted by {order}", self.limit);
Ok(())
}
}
mod cli;
mod command;
use anyhow::Result;
use clap::Parser;
use cli::{Cli, Commands};
use command::{Command, Context};
fn main() -> Result<()> {
let cli = Cli::parse();
let ctx = Context { verbose: cli.verbose };
match cli.command {
Commands::Add(args) => args.run(&ctx),
Commands::Remove(args) => args.run(&ctx),
Commands::List(args) => args.run(&ctx),
}
}
This snippet shows a common Rust CLI pattern: parse arguments with clap's derive API into a strongly-typed enum of subcommands, then dispatch each variant to its own handler through a shared trait. The goal is to keep argument definitions declarative while keeping the execution logic modular, so adding a new subcommand touches one enum variant and one struct rather than a growing match full of inline logic.
In cli.rs, the top-level Cli struct derives Parser and carries a global --verbose flag plus a command field of type Commands. The Commands enum derives Subcommand, and each variant wraps a dedicated args struct — AddArgs, RemoveArgs, ListArgs. Attributes like #[arg(short, long)], default_value_t, and value_enum show how clap maps flags and positional arguments onto typed fields, including an enum-valued --sort option backed by SortOrder deriving ValueEnum. Keeping each variant's data in its own struct means the parsing surface for one command stays isolated and self-documenting.
In command.rs, the Command trait defines a single run method returning anyhow::Result<()>, and a Context struct threads shared state (here just the verbose flag) into every handler. Each args struct implements Command, so the behavior lives next to the data it operates on. This is the trait-object-free version of a plugin system: dispatch is static, the compiler verifies every variant has a handler, and there is no runtime downcasting.
In main.rs, Cli::parse() does the whole parse-or-exit dance — on bad input clap prints usage and exits with a non-zero code automatically. The parsed cli.command is matched once, and each arm simply calls args.run(&ctx), forwarding the shared Context. The match is exhaustive, so a forgotten handler is a compile error rather than a silent no-op.
The trade-off is a little boilerplate per command, but it scales cleanly: the enum stays a flat routing table while logic grows in separate impls. A developer reaches for this when a tool has several verbs that each need distinct options, and wants clap's generated help, validation, and error messages without letting main balloon into a monolith. Returning anyhow::Result from main lets errors propagate with context and a tidy exit code.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
Share this code
Here's the card — post it anywhere.