← Table of contents

5.4 The Direct Word

A verbal instruction and a written order are not the same thing. The written one stands until revoked. The verbal one applies once, because that is exactly what was said. A command-line flag is a verbal instruction: arrived, spoke, left. An environment variable is a note on the wall that is still there.

What You Need

--data-dir is a flag that tells tq where to look for this specific run. The config file stays untouched; the environment variable too. The flag acts once and leaves no trace.

With this flag the chain will be complete: the flag overrides the environment variable, the variable overrides the file, the file overrides the built-in default. Each source has its own scope; the flag is the narrowest, and therefore the loudest.

The Build

The flag appears in two places: in the Cli struct, where clap will parse it from the command line, and in Config::load, where it is applied last - after the file and the environment variable.

Add the data_dir field to src/cli/mod.rs, before the command field. The #[arg(long)] attribute tells clap: this is a named flag, --data-dir. Where a field is declared determines its scope: a field in Cli is a global flag; a field inside a Commands variant is a flag only for that subcommand.

// src/cli/mod.rs - CHANGED
use std::path::PathBuf;                 // NEW

#[derive(Parser)]
#[command(about = "Task queue manager", version)]
pub struct Cli {
    #[arg(long)]                        // NEW
    pub data_dir: Option<PathBuf>,      // NEW
    #[command(subcommand)]
    command: Commands,
}

Option<PathBuf>:

clap generates --data-dir <PATH> in --help automatically.

Now src/config/mod.rs. The Config::load signature takes a second argument, and after the environment variable check another layer is added:

// src/config/mod.rs - CHANGED
pub fn load(path: &Path, data_dir_flag: Option<PathBuf>) -> TqResult<Config> {  // CHANGED
    let mut config = if !path.exists() {
        Config::default()
    } else {
        let content = fs::read_to_string(path).map_err(|e| TqError::Parse(e.to_string()))?;
        toml::from_str(&content).map_err(|e| TqError::Parse(e.to_string()))?
    };

    if let Ok(dir) = std::env::var("TQ_DATA_DIR") {
        config.data_dir = PathBuf::from(dir);
    }

    if let Some(dir) = data_dir_flag {      // NEW
        config.data_dir = dir;              // NEW
    }                                       // NEW

    if !config.data_dir.exists() {
        return Err(TqError::Parse(format!(
            "data_dir '{}' does not exist; create it with: mkdir -p {}",
            config.data_dir.display(),
            config.data_dir.display()
        )));
    }
    Ok(config)
}

The order of assignments defines priority: the file is read first, the environment variable overwrites it, the flag overwrites everything. Each layer is silent when not set; only if all are silent will default be used.

The existing tests in src/config/tests.rs call Config::load with one argument - the compiler will point to every one of them. Add None as the second argument to each call.

One change remains in src/main.rs - pass the flag value through:

// src/main.rs - CHANGED
fn run() -> TqResult<()> {
    let cli = Cli::parse();
    let config = Config::load(Path::new("config.toml"), cli.data_dir.clone())?;  // CHANGED
    cli::run(cli, config)
}

cli.data_dir.clone() is needed because cli moves into cli::run on the next line.

The test for the new behaviour lives in src/config/tests.rs. It sets TQ_DATA_DIR to /tmp but passes a different directory as the flag. The flag must win:

#[test]
fn flag_overrides_env_var() {
    let tmp = std::env::temp_dir().join("tq-m54-flag");
    std::fs::create_dir_all(&tmp).unwrap();
    unsafe { std::env::set_var("TQ_DATA_DIR", "/tmp") };
    let result = Config::load(Path::new("nonexistent.toml"), Some(tmp.clone()));
    unsafe { std::env::remove_var("TQ_DATA_DIR") };
    assert_eq!(result.unwrap().data_dir, tmp);
}

The Result

$ mkdir -p /tmp/tq-single
$ cargo run -- --data-dir /tmp/tq-single add "Fix the latch"
#1: Fix the latch [Todo] (2026-06-05)

The flag comes before the subcommand: --data-dir is global - it belongs to tq, not to add.

$ cargo run -- --data-dir /nonexistent list
tq: parse error: data_dir '/nonexistent' does not exist; create it with: mkdir -p /nonexistent

Neither the config file nor the environment was touched. The chain is complete: flag > env > file > default.

make ci passes. The full test suite is in tq/.

The complete tq code for this chapter is in 5-interface/04-the-direct-word/.


Lore: The Priority Chain

tq reads the data path from four sources. In descending order of weight:

--data-dir /path     ← flag: stated right now, for this run
TQ_DATA_DIR=/path    ← environment variable: session or CI job
data_dir = "/path"   ← config.toml: permanent setting
"."                  ← built-in default: when nothing was said

The more specific the intention, the higher the priority. A flag requires typing an argument by hand - it is used deliberately, right now. An environment variable is set before the run and covers the entire session. The file describes permanent conditions. The built-in default is the code’s last word when everything else was silent.

This hierarchy appears in many command-line tools. The principle is one: the more specific the source, the louder its voice.