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>:
Optionфлаг необязательныйPathBuf- тип пути в файловой системе; он умеет склеивать компоненты и работает с разделителями платформы. Clap генерирует--data-dir <PATH>в--helpавтоматически.
Теперь 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: постоянная настройка
"." ← встроенный путь: когда не сказано ничего
Чем конкретнее намерение, тем выше приоритет. Флаг требует напечатать аргумент вручную - и используется осознанно, именно сейчас. Переменная среды задаётся до запуска и охватывает весь сеанс. Файл описывает постоянные условия. Встроенный путь - последнее слово кода, когда всё остальное промолчало.
Такая иерархия встречается во многих инструментах командной строки. Принцип один: чем специфичнее источник, тем выше его голос.