← Table of contents

5.3 What the Air Carries

A good instruction describes the general; the details belong to the workshop. Where to store finished work is not a question for the tool - it is a question of place. In one shop, one corner; in another, another; in a temporary one, nowhere and not for long. A configuration file cannot speak for itself. It has to be changed before a move. Moves happen more often than planned.

What You Need

tq reads config.toml - takes data_dir from there, or uses the current directory by default. That works for one location. A developer with data in ~/tq-data and a CI job with data in /tmp/ci-tq must either edit the file before each run or maintain two files and swap them in as needed. Both options require manual work.

An environment variable solves the problem differently: it does not touch the file, does not require versioning, and lives exactly as long as needed - one run, one session, one CI job.

The Build

std::env::var("TQ_DATA_DIR") returns Ok(String) if the variable is set, and Err if not. if let Ok(dir) takes the value only on success; if the variable is not set, the block is skipped.

Two changes to src/config/mod.rs - config becomes mut, and after loading the file a check for the environment variable is added:

// src/config/mod.rs - CHANGED
pub fn load(path: &Path) -> TqResult<Config> {
    let mut config = if !path.exists() {           // CHANGED: let -> let mut
        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") {  // NEW
        config.data_dir = PathBuf::from(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 file is loaded first (or the default taken), then - if the variable is set - data_dir is overwritten. The existence check comes last, after all sources have been consulted. Priority: TQ_DATA_DIR > config.toml > default.

Uppercase letters and the TQ_ prefix are a Unix convention: environment variables are readable by any tool, and the name must be distinctive enough not to collide with someone else’s.

The test for the new path lives in src/config/tests.rs. In Rust 2024, set_var requires unsafe (details in M6.x), and the variable is removed before unwrap() - if the assertion fails, remove_var has already run:

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

Note. env_var_overrides_data_dir sets TQ_DATA_DIR - a process-wide environment variable shared across all threads. When tests run in parallel, two tests can set different values simultaneously and interfere with each other. In this book the simple path is chosen: make ci runs tests with --test-threads=1, which eliminates the potential problem.

# Makefile - CHANGED
ci:
	@cargo fmt --check && cargo clippy -- -D warnings && cargo test -- --test-threads=1

The Result

$ mkdir -p /tmp/tq-test
$ TQ_DATA_DIR=/tmp/tq-test cargo run -- list

The list is empty: /tmp/tq-test contains no tasks.json. That is expected - the variable changes where tq looks, not what is there.

An invalid path:

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

make ci passes.

The complete tq code for this chapter is in 5-interface/03-what-the-air-carries/.