← Оглавление

5.4 Прямое слово

Устная инструкция и письменное распоряжение - не одно и то же. Письменное действует пока его не отменили. Устное - один раз, потому что именно так сказали. Флаг командной строки - устная инструкция: пришёл, сказал, ушёл. Переменная окружения - листок на стене, который никуда не делся.

Что нужно

--data-dir - флаг, который говорит tq, куда смотреть в этот конкретный запуск. Файл с настройкой остаётся нетронутым, переменная среды - тоже. Флаг действует один раз и не оставляет следов.

С этим флагом цепочка замкнётся: флаг перекрывает переменную среды, переменная - файл, файл - встроенный путь по умолчанию. У каждого источника своя область действия; флаг - самый узкий, и потому самый громкий.

Сборка

Флаг появляется в двух местах: в структуре Cli, где clap разберёт его из командной строки, и в Config::load, где он применяется последним - после файла и переменной среды.

Добавьте поле data_dir в src/cli/mod.rs, до поля command. Атрибут #[arg(long)] говорит clap: это именованный флаг, --data-dir. Где объявлено поле - там и область действия: поле в Cli - флаг глобальный; поле внутри варианта Commands - флаг только той подкоманды.

// 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>:

Теперь src/config/mod.rs. Сигнатура Config::load принимает второй аргумент, и после проверки переменной среды добавляется ещё один слой:

// 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)
}

Порядок присваиваний задаёт приоритет: файл читается первым, переменная среды перекрывает его, флаг перекрывает всё. Каждый слой молчит, если не задан, как следствие только если все промолчали будет выбран default.

Существующие тесты в src/config/tests.rs вызывают Config::load с одним аргументом - компилятор укажет на каждый из них. Добавьте None вторым аргументом в каждый вызов.

В src/main.rs остаётся одно изменение - прокинуть значение флага:

// 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() нужен потому, что cli уходит в cli::run строкой ниже.

Тест для нового поведения - в src/config/tests.rs. Он устанавливает TQ_DATA_DIR в /tmp, но передаёт флагом другую папку. Флаг должен выиграть:

#[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);
}

Результат

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

Флаг стоит перед подкомандой: --data-dir - глобальный, он принадлежит tq, а не add.

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

Ни файл с настройкой, ни окружение не трогались. Цепочка замкнулась: флаг > env > файл > default.

make ci проходит. Полный набор тестов - в tq/.

Полный код tq для этой главы - 5-interface/04-the-direct-word/.


Lore: Цепочка приоритетов

tq читает путь к данным из четырёх источников. В порядке убывания веса:

--data-dir /path     ← флаг: сказано прямо сейчас, этому запуску
TQ_DATA_DIR=/path    ← переменная среды: сеанс или CI-задание
data_dir = "/path"   ← config.toml: постоянная настройка
"."                  ← встроенный путь: когда не сказано ничего

Чем конкретнее намерение, тем выше приоритет. Флаг требует напечатать аргумент вручную - и используется осознанно, именно сейчас. Переменная среды задаётся до запуска и охватывает весь сеанс. Файл описывает постоянные условия. Встроенный путь - последнее слово кода, когда всё остальное промолчало.

Такая иерархия встречается во многих инструментах командной строки. Принцип один: чем специфичнее источник, тем выше его голос.