Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
clap, what's the recommended pattern for subcommands that share common arguments?Command-line interfaces frequently need arguments that apply across multiple subcommandsâglobal flags like verbosity, configuration file paths, or output format specifiers. Clap provides several mechanisms for sharing arguments between subcommands, each with different trade-offs in ergonomics, type safety, and code organization.
Without a sharing mechanism, common arguments must be repeated across subcommands:
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Build {
#[arg(long)]
verbose: bool,
#[arg(long, default_value = "release")]
profile: String,
#[arg(long)]
output: String,
},
Test {
#[arg(long)] // Duplicated!
verbose: bool,
#[arg(long, default_value = "release")] // Duplicated!
profile: String,
#[arg(long)]
filter: String,
},
Run {
#[arg(long)] // Duplicated again!
verbose: bool,
#[arg(long, default_value = "release")] // And again!
profile: String,
#[arg(long)]
args: Vec<String>,
},
}This violates DRY principles and becomes a maintenance burden when adding new flags or changing defaults.
The most straightforward approach places shared arguments at the top level of the CLI structure:
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "myapp", about = "A CLI tool")]
struct Cli {
/// Increase verbosity
#[arg(short, long, global = true)]
verbose: bool,
/// Build profile
#[arg(long, default_value = "release", global = true)]
profile: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Build {
#[arg(long)]
output: String,
},
Test {
#[arg(long)]
filter: String,
},
Run {
#[arg(long)]
args: Vec<String>,
},
}
fn main() {
let cli = Cli::parse();
// Global arguments are accessible here
if cli.verbose {
println!("Running in verbose mode with profile: {}", cli.profile);
}
match cli.command {
Commands::Build { output } => {
println!("Building to {} with profile {}", output, cli.profile);
}
Commands::Test { filter } => {
println!("Testing with filter: {}", filter);
}
Commands::Run { args } => {
println!("Running with args: {:?}", args);
}
}
}The global = true attribute allows these arguments to appear after the subcommand on the command line:
myapp build --output ./dist --verbose
myapp --verbose build --output ./dist # Also valid
myapp test --filter unit --profile debugGlobal arguments defined at the top level are accessible on the parent struct, not within subcommand variants. This creates a separation between the argument and where it's used:
match cli.command {
Commands::Build { output } => {
// cli.verbose and cli.profile are in scope
// but not destructured with the subcommand
if cli.verbose {
eprintln!("Building with profile: {}", cli.profile);
}
}
// ...
}For subcommands that need many local arguments, this can feel disjointed.
When multiple subcommands need the same set of arguments, flattening provides a more integrated approach:
use clap::{Parser, Subcommand, Args};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Args)]
struct CommonArgs {
/// Increase verbosity
#[arg(short, long)]
verbose: bool,
/// Build profile
#[arg(long, default_value = "release")]
profile: String,
}
#[derive(Subcommand)]
enum Commands {
Build {
#[command(flatten)]
common: CommonArgs,
#[arg(long)]
output: String,
},
Test {
#[command(flatten)]
common: CommonArgs,
#[arg(long)]
filter: String,
},
Run {
#[command(flatten)]
common: CommonArgs,
#[arg(long)]
args: Vec<String>,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Build { common, output } => {
if common.verbose {
println!("Building to {} with profile {}", output, common.profile);
}
}
Commands::Test { common, filter } => {
if common.verbose {
println!("Testing with filter {} (profile: {})", filter, common.profile);
}
}
Commands::Run { common, args } => {
if common.verbose {
println!("Running {:?} with profile {}", args, common.profile);
}
}
}
}The #[command(flatten)] attribute embeds the CommonArgs fields directly into the subcommand's argument namespace. The command line accepts:
myapp build --output ./dist --verbose --profile debug
myapp test --filter integration --verboseFor truly global options that apply to every subcommand, use top-level globals. For shared but not universal options, use flattening:
#[derive(Parser)]
struct Cli {
/// Global verbosity (applies to all commands)
#[arg(short, long, global = true)]
verbose: bool,
/// Config file path (global)
#[arg(long, global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Commands,
}
#[derive(Args)]
struct DatabaseArgs {
/// Database connection URL
#[arg(long)]
database_url: String,
/// Connection timeout in seconds
#[arg(long, default_value = "30")]
timeout: u64,
}
#[derive(Subcommand)]
enum Commands {
/// Query the database
Query {
#[command(flatten)]
db: DatabaseArgs,
#[arg(long)]
query: String,
},
/// Migrate the database
Migrate {
#[command(flatten)]
db: DatabaseArgs,
#[arg(long)]
version: Option<u32>,
},
/// Validate configuration (no database needed)
Validate {
#[arg(long)]
strict: bool,
},
}Here --verbose and --config apply to all commands (including validate), while DatabaseArgs only applies to query and migrate.
Clap 4.x introduced the #[command(subcommand_required = false)] pattern and improved #[command(flatten)] handling for nested structures:
#[derive(Parser)]
struct Cli {
#[command(flatten)]
global: GlobalArgs,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Args)]
struct GlobalArgs {
#[arg(short, long)]
verbose: bool,
}
#[derive(Subcommand)]
enum Commands {
Build {
#[arg(long)]
output: String,
},
}
fn main() {
let cli = Cli::parse();
// Can run without subcommand
if cli.command.is_none() {
println!("No command specified, verbose: {}", cli.global.verbose);
return;
}
// Handle subcommands
match cli.command.unwrap() {
Commands::Build { output } => {
println!("Building to {}, verbose: {}", output, cli.global.verbose);
}
}
}For larger applications, you can define a trait that extracts common arguments:
trait HasCommonArgs {
fn common(&self) -> &CommonArgs;
fn verbose(&self) -> bool {
self.common().verbose
}
fn profile(&self) -> &str {
&self.common().profile
}
}
impl HasCommonArgs for Commands {
fn common(&self) -> &CommonArgs {
match self {
Commands::Build { common, .. } => common,
Commands::Test { common, .. } => common,
Commands::Run { common, .. } => common,
}
}
}
fn execute_command(cmd: Commands) {
if cmd.verbose() {
println!("Verbose mode enabled");
}
match cmd {
Commands::Build { output, .. } => {
println!("Building with profile: {}", cmd.profile());
}
Commands::Test { filter, .. } => {
println!("Testing: {}", filter);
}
Commands::Run { args, .. } => {
println!("Running: {:?}", args);
}
}
}When using flattened arguments, watch for name conflicts:
#[derive(Args)]
struct CommonArgs {
#[arg(long)]
output: String, // This will conflict with Build's --output
}
#[derive(Subcommand)]
enum Commands {
Build {
#[command(flatten)]
common: CommonArgs,
#[arg(long)]
output: String, // ERROR: duplicate argument name
},
}Use #[arg(name = "...")] to rename or restructure to avoid conflicts:
#[derive(Args)]
struct CommonArgs {
#[arg(long = "output-dir")] // Different flag name
output: String,
}Sometimes a subcommand needs different defaults for shared arguments:
#[derive(Args)]
struct BuildArgs {
#[arg(long, default_value = "release")]
profile: String,
#[command(flatten)]
common: CommonArgs,
}
#[derive(Args)]
struct TestArgs {
#[arg(long, default_value = "test")] // Different default!
profile: String,
#[command(flatten)]
common: CommonArgs,
}However, this creates duplicate fields. A cleaner approach uses #[arg(default_value = ...)] with environment variable fallback:
#[derive(Args)]
struct CommonArgs {
#[arg(long, default_value = "release", env = "PROFILE")]
profile: String,
}Now callers can override per-invocation or via environment:
PROFILE=test myapp build --output ./distuse clap::{Parser, Subcommand, Args};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "containerctl", version, about)]
struct Cli {
/// Enable debug logging
#[arg(short, long, global = true)]
debug: bool,
/// Configuration file
#[arg(short, long, global = true, env = "CONTAINER_CONFIG")]
config: Option<PathBuf>,
#[command(subcommand)]
command: Commands,
}
#[derive(Args)]
struct ContainerSelector {
/// Container name or ID
#[arg(short, long)]
name: String,
/// Namespace
#[arg(short, long, default_value = "default")]
namespace: String,
}
#[derive(Subcommand)]
enum Commands {
/// Create a new container
Create {
#[command(flatten)]
selector: ContainerSelector,
/// Container image
#[arg(short, long)]
image: String,
/// Container arguments
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// Start a container
Start {
#[command(flatten)]
selector: ContainerSelector,
/// Attach to container
#[arg(short, long)]
attach: bool,
},
/// Stop a container
Stop {
#[command(flatten)]
selector: ContainerSelector,
/// Force stop (SIGKILL)
#[arg(short, long)]
force: bool,
/// Timeout in seconds
#[arg(short, long, default_value = "10")]
timeout: u64,
},
/// List containers
List {
/// Show all containers (including stopped)
#[arg(short, long)]
all: bool,
/// Output format
#[arg(short, long, default_value = "table")]
format: String,
},
}
fn main() {
let cli = Cli::parse();
if cli.debug {
eprintln!("Debug mode enabled");
if let Some(config) = &cli.config {
eprintln!("Using config: {:?}", config);
}
}
match cli.command {
Commands::Create { selector, image, args } => {
println!("Creating container '{}' in namespace '{}'",
selector.name, selector.namespace);
println!("Image: {}, Args: {:?}", image, args);
}
Commands::Start { selector, attach } => {
println!("Starting '{}' (attach: {})", selector.name, attach);
}
Commands::Stop { selector, force, timeout } => {
println!("Stopping '{}' (force: {}, timeout: {}s)",
selector.name, force, timeout);
}
Commands::List { all, format } => {
println!("Listing containers (all: {}, format: {})", all, format);
}
}
}Clap offers three primary patterns for sharing arguments across subcommands:
Global arguments at the CLI root with global = true for options that apply universally. These appear on the parent struct and work for all commands.
Flattened argument groups with #[command(flatten)] for options shared by multiple (but not all) subcommands. These integrate directly into the subcommand's argument namespace.
Trait-based extraction for complex applications where you want type-safe access to shared arguments across command variants.
The choice depends on your specific needs: global arguments are simplest for truly universal flags, while flattened groups provide better locality for subcommand-specific handling. For large CLIs, combining both approachesâwith global flags like --verbose and --config at the root, and command-group-specific arguments via flatteningâyields clean, maintainable code.