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>:
Option- the flag is optionalPathBuf- a filesystem path type; it knows how to join components and handles platform separators.
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
tqcode for this chapter is in5-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.