How does clap::Arg::default_value_if set conditional defaults based on other argument values?

default_value_if allows an argument's default value to depend on the presence or value of another argument, enabling context-sensitive defaults that adapt to user input without requiring manual validation code. This declarative approach simplifies command-line argument handling by encoding conditional logic directly into the argument definition.

Basic Conditional Defaults

use clap::{Arg, Command};
 
fn basic_conditional() -> Command {
    Command::new("example")
        .arg(
            Arg::new("format")
                .long("format")
                .value_parser(["json", "yaml", "toml"])
                .default_value("json")
        )
        .arg(
            Arg::new("output")
                .long("output")
                .default_value_if("format", "json", "output.json")
                .default_value_if("format", "yaml", "output.yaml")
                .default_value_if("format", "toml", "output.toml")
        )
}

The output argument gets a different default depending on the format argument's value.

Simple Condition Syntax

use clap::{Arg, Command};
 
fn condition_syntax() -> Command {
    Command::new("example")
        .arg(
            Arg::new("mode")
                .long("mode")
                .value_parser(["fast", "thorough"])
        )
        .arg(
            Arg::new("threads")
                .long("threads")
                // If mode is "fast", default threads to "4"
                .default_value_if("mode", "fast", "4")
                // If mode is "thorough", default threads to "16"
                .default_value_if("mode", "thorough", "16")
        )
}

Each default_value_if call specifies: argument name, expected value, and default to apply.

Presence-Based Conditions

use clap::{Arg, Command};
 
fn presence_condition() -> Command {
    Command::new("example")
        .arg(
            Arg::new("verbose")
                .long("verbose")
                .action(clap::ArgAction::SetTrue)
        )
        .arg(
            Arg::new("log-level")
                .long("log-level")
                // If "verbose" is present, default to "debug"
                .default_value_if("verbose", "true", "debug")
                // Otherwise use this default
                .default_value("info")
        )
}

When verbose flag is present, log-level defaults to "debug" instead of "info".

Multiple Conditions with Fallbacks

use clap::{Arg, Command};
 
fn multiple_conditions() -> Command {
    Command::new("builder")
        .arg(
            Arg::new("compiler")
                .long("compiler")
                .value_parser(["gcc", "clang", "msvc"])
                .default_value("gcc")
        )
        .arg(
            Arg::new("flags")
                .long("flags")
                // Chain of conditions evaluated in order
                .default_value_if("compiler", "gcc", "-O2 -Wall")
                .default_value_if("compiler", "clang", "-O2 -Weverything")
                .default_value_if("compiler", "msvc", "/O2 /W4")
        )
}

Conditions are evaluated in order; the first matching condition provides the default.

Condition with Any Value

use clap::{Arg, Command};
 
fn any_value_condition() -> Command {
    Command::new("example")
        .arg(
            Arg::new("config")
                .long("config")
        )
        .arg(
            Arg::new("validate")
                .long("validate")
                .action(clap::ArgAction::SetTrue)
                // If config has any value, set validate to true
                .default_value_if("config", None, "true")
        )
}

Using None as the condition value matches any value of the referenced argument.

Condition Negation

use clap::{Arg, Command};
 
fn negated_condition() -> Command {
    Command::new("example")
        .arg(
            Arg::new("input")
                .long("input")
                .required(true)
        )
        .arg(
            Arg::new("output")
                .long("output")
                // Default output to stdout if no explicit output
                .default_value_if("input", None, "-")
        )
        .arg(
            Arg::new("compress")
                .long("compress")
                .action(clap::ArgAction::SetTrue)
        )
        .arg(
            Arg::new("level")
                .long("level")
                // Only set default if compress is NOT present
                // (default_value_if only sets when condition matches)
                .default_value("6")
        )
}

The condition checks for presence or specific values; absence leaves the argument unset.

Complex Conditional Logic

use clap::{Arg, Command};
 
fn complex_conditions() -> Command {
    Command::new("server")
        .arg(
            Arg::new("protocol")
                .long("protocol")
                .value_parser(["http", "https"])
                .default_value("http")
        )
        .arg(
            Arg::new("port")
                .long("port")
                .default_value_if("protocol", "http", "80")
                .default_value_if("protocol", "https", "443")
        )
        .arg(
            Arg::new("timeout")
                .long("timeout")
                // Default timeout depends on protocol
                .default_value_if("protocol", "http", "30")
                .default_value_if("protocol", "https", "60")
        )
        .arg(
            Arg::new("retries")
                .long("retries")
                .default_value("3")
        )
}

Multiple arguments can have conditional defaults based on the same parent argument.

Priority Ordering

use clap::{Arg, Command};
 
fn priority_ordering() -> Command {
    Command::new("example")
        .arg(
            Arg::new("env")
                .long("env")
                .value_parser(["dev", "staging", "prod"])
        )
        .arg(
            Arg::new("debug")
                .long("debug")
                // First matching condition wins
                .default_value_if("env", "dev", "true")
                .default_value_if("env", "staging", "true")
                .default_value_if("env", "prod", "false")
                // Fallback if no env specified
                .default_value("false")
        )
}

Conditions are evaluated in definition order; first match wins.

Interacting with Required Arguments

use clap::{Arg, Command};
 
fn with_required() -> Command {
    Command::new("example")
        .arg(
            Arg::new("input")
                .required(true)
        )
        .arg(
            Arg::new("format")
                .long("format")
                .value_parser(["auto", "json", "csv"])
                .default_value("auto")
        )
        .arg(
            Arg::new("delimiter")
                .long("delimiter")
                // Only relevant when format is csv
                .default_value_if("format", "csv", ",")
                // If format is auto, try to detect
                .default_value_if("format", "auto", "detect")
        )
}

Conditional defaults work alongside required arguments.

Integration with ArgAction

use clap::{Arg, ArgAction, Command};
 
fn with_actions() -> Command {
    Command::new("example")
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(ArgAction::Count)
        )
        .arg(
            Arg::new("log-level")
                .long("log-level")
                .value_parser(["error", "warn", "info", "debug", "trace"])
                // Different defaults based on verbosity count
                .default_value_if("verbose", None, "info")
        )
        .arg(
            Arg::new("progress")
                .long("progress")
                .action(ArgAction::SetTrue)
                // Show progress bar if verbose
                .default_value_if("verbose", None, "true")
        )
}

Conditional defaults work with ArgAction values, checking presence via None.

Real-World Example: Build Tool

use clap::{Arg, Command};
 
fn build_tool() -> Command {
    Command::new("build")
        .about("Build tool with smart defaults")
        .arg(
            Arg::new("target")
                .long("target")
                .short('t')
                .value_parser(["debug", "release", "test"])
                .default_value("debug")
        )
        .arg(
            Arg::new("optimization")
                .long("opt")
                .short('O')
                .default_value_if("target", "debug", "0")
                .default_value_if("target", "release", "3")
                .default_value_if("target", "test", "0")
        )
        .arg(
            Arg::new("symbols")
                .long("symbols")
                .default_value_if("target", "debug", "full")
                .default_value_if("target", "release", "none")
                .default_value_if("target", "test", "full")
        )
        .arg(
            Arg::new("lto")
                .long("lto")
                .default_value_if("target", "release", "thin")
                // No LTO for debug/test targets
        )
        .arg(
            Arg::new("parallel")
                .long("parallel")
                .default_value_if("target", "debug", "1")
                .default_value_if("target", "release", "auto")
                .default_value_if("target", "test", "auto")
        )
}

Build targets automatically configure appropriate defaults for optimization, symbols, LTO, and parallelism.

Real-World Example: Database Client

use clap::{Arg, Command};
 
fn database_client() -> Command {
    Command::new("db-cli")
        .about("Database client with connection-aware defaults")
        .arg(
            Arg::new("database")
                .long("database")
                .value_parser(["postgres", "mysql", "sqlite"])
                .required(true)
        )
        .arg(
            Arg::new("port")
                .long("port")
                .default_value_if("database", "postgres", "5432")
                .default_value_if("database", "mysql", "3306")
                .default_value_if("database", "sqlite", "")
        )
        .arg(
            Arg::new("host")
                .long("host")
                .default_value_if("database", "postgres", "localhost")
                .default_value_if("database", "mysql", "localhost")
                .default_value_if("database", "sqlite", "")
        )
        .arg(
            Arg::new("timeout")
                .long("timeout")
                .default_value_if("database", "postgres", "30")
                .default_value_if("database", "mysql", "30")
                .default_value_if("database", "sqlite", "0")
        )
}

Database type determines sensible defaults for port, host, and timeout.

Debugging Conditional Defaults

use clap::{Arg, Command};
 
fn debug_conditions() {
    let matches = Command::new("example")
        .arg(
            Arg::new("mode")
                .long("mode")
                .value_parser(["local", "remote"])
                .default_value("local")
        )
        .arg(
            Arg::new("path")
                .long("path")
                .default_value_if("mode", "local", "/var/local")
                .default_value_if("mode", "remote", "/var/remote")
        )
        .get_matches_from(&["example", "--mode", "local"]);
    
    // Check what values were resolved
    let mode = matches.get_one::<String>("mode").unwrap();
    let path = matches.get_one::<String>("path").unwrap();
    
    println!("Mode: {}", mode);  // "local"
    println!("Path: {}", path);  // "/var/local"
    
    // The path was set by default_value_if
    // User didn't provide --path, but it has a value
}

Arguments with conditional defaults appear to have values even when not explicitly provided.

Conditional vs Explicit Values

use clap::{Arg, Command};
 
fn conditional_vs_explicit() {
    let matches = Command::new("example")
        .arg(
            Arg::new("format")
                .long("format")
                .value_parser(["json", "xml"])
                .default_value("json")
        )
        .arg(
            Arg::new("indent")
                .long("indent")
                .default_value_if("format", "json", "2")
                .default_value_if("format", "xml", "4")
        )
        .get_matches_from(&["example", "--format", "json", "--indent", "4"]);
    
    // Explicit --indent overrides the conditional default
    let indent = matches.get_one::<String>("indent").unwrap();
    assert_eq!(indent, "4");  // Explicit, not "2"
    
    // Conditional default only applies when no explicit value
}

User-provided values always take precedence over conditional defaults.

Combining with Validation

use clap::{Arg, Command};
 
fn with_validation() -> Command {
    Command::new("example")
        .arg(
            Arg::new("level")
                .long("level")
                .value_parser(["1", "2", "3"])
                .default_value("1")
        )
        .arg(
            Arg::new("workers")
                .long("workers")
                .default_value_if("level", "1", "1")
                .default_value_if("level", "2", "4")
                .default_value_if("level", "3", "16")
                // Validate that workers is reasonable
                .value_parser(clap::value_parser!(u32).range(1..=32))
        )
        .arg(
            Arg::new("timeout")
                .long("timeout")
                // Defaults depend on level
                .default_value_if("level", "1", "60")
                .default_value_if("level", "2", "120")
                .default_value_if("level", "3", "300")
                .value_parser(clap::value_parser!(u64))
        )
}

Conditional defaults work with value parsers for validation.

Multiple Argument Dependencies

use clap::{Arg, Command};
 
fn multiple_dependencies() -> Command {
    Command::new("example")
        .arg(
            Arg::new("input-format")
                .long("input-format")
                .value_parser(["json", "yaml"])
                .default_value("json")
        )
        .arg(
            Arg::new("output-format")
                .long("output-format")
                .value_parser(["json", "yaml", "csv"])
                .default_value_if("input-format", "json", "json")
                .default_value_if("input-format", "yaml", "yaml")
        )
        .arg(
            Arg::new("pretty")
                .long("pretty")
                .action(clap::ArgAction::SetTrue)
        )
        .arg(
            Arg::new("indent")
                .long("indent")
                // Only applies if pretty is set
                .default_value_if("pretty", "true", "2")
        )
}

Conditions can chain: output-format depends on input-format, indent depends on pretty.

Checking Conditional vs Unconditional Defaults

use clap::{Arg, Command};
 
fn default_types() {
    // Unconditional default: always applied
    let cmd = Command::new("example")
        .arg(
            Arg::new("value")
                .long("value")
                .default_value("42")  // Always defaults to 42
        );
    
    // Conditional default: depends on condition
    let cmd = Command::new("example")
        .arg(
            Arg::new("mode")
                .long("mode")
                .value_parser(["a", "b"])
        )
        .arg(
            Arg::new("value")
                .long("value")
                .default_value_if("mode", "a", "10")  // Only if mode is "a"
                .default_value_if("mode", "b", "20")  // Only if mode is "b"
        );
    
    // If mode is not specified, value has no default!
    // Combine with unconditional default for fallback:
    let cmd = Command::new("example")
        .arg(
            Arg::new("mode")
                .long("mode")
                .value_parser(["a", "b"])
        )
        .arg(
            Arg::new("value")
                .long("value")
                .default_value_if("mode", "a", "10")
                .default_value_if("mode", "b", "20")
                .default_value("0")  // Fallback for no mode
        );
}

Combine conditional and unconditional defaults to ensure a value is always present.

Edge Cases and Behavior

use clap::{Arg, Command};
 
fn edge_cases() {
    // Case 1: Condition argument not provided
    let matches = Command::new("example")
        .arg(Arg::new("mode").long("mode"))
        .arg(
            Arg::new("value")
                .long("value")
                .default_value_if("mode", "fast", "100")
        )
        .get_matches_from(&["example"]);
    
    // mode not provided -> value has no default (None unless unconditional default)
    assert!(matches.get_one::<String>("value").is_none());
    
    // Case 2: Condition argument provided but value doesn't match
    let matches = Command::new("example")
        .arg(Arg::new("mode").long("mode").default_value("slow"))
        .arg(
            Arg::new("value")
                .long("value")
                .default_value_if("mode", "fast", "100")
        )
        .get_matches_from(&["example"]);
    
    // mode is "slow", not "fast" -> value has no default
    assert!(matches.get_one::<String>("value").is_none());
    
    // Case 3: Multiple conditions, some match
    let matches = Command::new("example")
        .arg(Arg::new("mode").long("mode").default_value("medium"))
        .arg(
            Arg::new("value")
                .long("value")
                .default_value_if("mode", "fast", "10")
                .default_value_if("mode", "medium", "50")
                .default_value_if("mode", "slow", "100")
        )
        .get_matches_from(&["example"]);
    
    // First matching condition wins: "medium" matches second
    assert_eq!(matches.get_one::<String>("value").unwrap(), "50");
}

Understanding behavior when conditions don't match is crucial for correct usage.

Summary Table

fn summary_table() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Pattern                              β”‚ Behavior                        β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ default_value_if("arg", "val", "x")  β”‚ Default "x" if arg equals "val" β”‚
    // β”‚ default_value_if("arg", None, "x")   β”‚ Default "x" if arg has any valueβ”‚
    // β”‚ Multiple conditions                  β”‚ First match wins                β”‚
    // β”‚ + default_value("x")                 β”‚ Fallback if no conditions matchβ”‚
    // β”‚ User provides value                  β”‚ Always overrides default        β”‚
    // β”‚ Condition arg not provided           β”‚ No conditional default applied  β”‚
    // β”‚ Condition value doesn't match        β”‚ No conditional default applied  β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}

Key Points Summary

fn key_points() {
    // 1. default_value_if sets defaults based on other argument values
    // 2. Format: default_value_if("arg_name", "expected_value", "default")
    // 3. Use None as expected_value to match any value presence
    // 4. Multiple conditions are evaluated in order; first match wins
    // 5. Combine with default_value for unconditional fallback
    // 6. User-provided values always override conditional defaults
    // 7. No match = no default (argument may be None)
    // 8. Works with ArgAction for flag presence detection
    // 9. Integrates with value_parser for validation
    // 10. Conditions can check presence or specific values
    // 11. Useful for context-sensitive configuration
    // 12. Reduces need for manual default resolution in code
}

Key insight: default_value_if enables declarative conditional logic directly in argument definitions, eliminating boilerplate code that would otherwise check argument values and set defaults manually. The pattern scales well: a single configuration argument can influence multiple other arguments' defaults, and the conditions compose naturally with explicit user values taking precedence. This keeps the argument parsing logic in one placeβ€”the command definitionβ€”rather than scattered through the application code. Use unconditional default_value as a fallback when no conditions match to ensure arguments always have sensible values.