← Índice

5.4 La palabra directa

Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.

Una instrucción verbal y una orden escrita no son lo mismo. La escrita permanece hasta que se revoca. La verbal se aplica una vez, porque eso es exactamente lo que se dijo. Un flag de línea de comandos es una instrucción verbal: llegó, habló, se fue. Una variable de entorno es una nota en la pared que sigue ahí.

Lo que necesitas

--data-dir es un flag que le dice a tq dónde buscar para esta ejecución concreta. El archivo de configuración queda intacto; también la variable de entorno. El flag actúa una vez y no deja rastro.

Con este flag la cadena estará completa: el flag anula la variable de entorno, la variable anula el archivo, el archivo anula el valor por defecto incorporado. Cada fuente tiene su propio alcance; el flag es el más estrecho, y por lo tanto el más alto.

La construcción

El flag aparece en dos lugares: en el struct Cli, donde clap lo analizará desde la línea de comandos, y en Config::load, donde se aplica al final - después del archivo y la variable de entorno.

Añade el campo data_dir a src/cli/mod.rs, antes del campo command. El atributo #[arg(long)] le dice a clap: esto es un flag con nombre, --data-dir. Donde se declara un campo determina su alcance: un campo en Cli es un flag global; un campo dentro de una variante de Commands es un flag solo para ese subcomando.

// src/cli/mod.rs - CAMBIADO
use std::path::PathBuf;                 // NUEVO

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

Option<PathBuf>:

clap genera --data-dir <PATH> en --help automáticamente.

Ahora src/config/mod.rs. La firma de Config::load toma un segundo argumento, y después de la comprobación de la variable de entorno se añade otra capa:

// src/config/mod.rs - CAMBIADO
pub fn load(path: &Path, data_dir_flag: Option<PathBuf>) -> TqResult<Config> {  // CAMBIADO
    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 {      // NUEVO
        config.data_dir = dir;              // NUEVO
    }                                       // NUEVO

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

El orden de las asignaciones define la prioridad: primero se lee el archivo, la variable de entorno lo sobreescribe, el flag sobreescribe todo. Cada capa calla cuando no está establecida; solo si todas callan se usará el valor por defecto.

Los tests existentes en src/config/tests.rs llaman a Config::load con un argumento - el compilador señalará cada uno de ellos. Añade None como segundo argumento a cada llamada.

Queda un cambio en src/main.rs - pasar el valor del flag:

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

cli.data_dir.clone() es necesario porque cli se mueve a cli::run en la siguiente línea.

El test para el nuevo comportamiento vive en src/config/tests.rs. Establece TQ_DATA_DIR a /tmp pero pasa un directorio diferente como flag. El flag debe ganar:

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

El resultado

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

El flag va antes del subcomando: --data-dir es global - pertenece a tq, no a add.

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

Ni el archivo de configuración ni el entorno fueron tocados. La cadena está completa: flag > env > archivo > valor por defecto.

make ci pasa. La suite completa de tests está en tq/.

El código completo de tq para este capítulo está en 5-interface/04-the-direct-word/.


Lore: La cadena de prioridad

tq lee la ruta de datos de cuatro fuentes. En orden descendente de peso:

--data-dir /ruta     ← flag: declarado ahora mismo, para esta ejecución
TQ_DATA_DIR=/ruta    ← variable de entorno: sesión o job de CI
data_dir = "/ruta"   ← config.toml: ajuste permanente
"."                  ← valor por defecto: cuando nada se dijo

Cuanto más específica es la intención, mayor es la prioridad. Un flag requiere escribir un argumento a mano - se usa deliberadamente, ahora mismo. Una variable de entorno se establece antes de la ejecución y cubre toda la sesión. El archivo describe condiciones permanentes. El valor por defecto incorporado es la última palabra del código cuando todo lo demás calló.

Esta jerarquía aparece en muchas herramientas de línea de comandos. El principio es uno: cuanto más específica es la fuente, más alta es su voz.