7

CLI profesional en Rust para Linuxeros

Vistas: 0
CLI profesional en Rust para Linuxeros

Te voy a contar una cosa que me pasó no hace mucho. Llevaba meses acumulando scripts sueltos en Rust. Un parser de logs aquí, un grep recursivo allá, un dashboard del sistema, un administrador interactivo… cada uno en su propio proyecto, con su propio cargo new, su propio main.rs, su propia manera de recibir argumentos. Y claro, llegó el día en que ya no recordaba cuál era cuál. ¿El de estadísticas se llamaba file-stats o archivo-info? ¿Y el grep era rust-grep o buscar? Menudo fenómeno estoy hecho. Lo peor es que cada vez que quería usar uno, tenía que acordarme no solo del nombre, sino también de si los argumentos eran posicionales o con flags, si había que usar --file o pasar la ruta directamente, si el autocompletado lo había instalado o no. Vamos, un despropósito.

Fue entonces cuando me di cuenta de que necesitaba hacer lo mismo que hacen git, docker o cargo: un solo binario con subcomandos. Un punto de entrada único que agrupara todas mis herramientas. Y no solo por orden, sino porque cuando tienes seis binarios distintos, el autocompletado de cada uno vive en su propio fichero, la documentación está dispersa y, seamos sinceros, tener que recordar seis nombres diferentes es una faena. Además, piensa en la experiencia del usuario: si le pasas una herramienta a un compañero, es mucho más fácil que recuerde un solo comando con subcomandos que seis binarios distintos con nombres crípticos.

En este capítulo vas a aprender a hacer exactamente eso. Vas a unificar todas las herramientas que hemos construido hasta ahora en una suite CLI profesional llamada crustaceo-tools. Y de paso, vas a descubrir cómo generar autocompletado para Bash, Fish y Zsh sin escribir ni una línea de scripting de shell, cómo publicar tu binario en GitHub Releases y cómo automatizar todo el proceso con GitHub Actions. Vamos allá 😊

El proyecto: crustaceo-tools

Arrancamos como siempre, creando un proyecto nuevo con Cargo.

cargo new crustaceo-tools
cd crustaceo-tools

Ahora abre Cargo.toml y pégalo con estos metadatos. Fíjate bien, porque aquí hay más miga de la que parece a simple vista.

[package]
name = "crustaceo-tools"
version = "0.1.0"
edition = "2024"
description = "Suite de herramientas CLI para sysadmins: stats, logparse, sysinfo, grep, dashboard"
authors = ["Tu Nombre <tu@email.com>"]
license = "MIT"
repository = "https://github.com/tuusuario/crustaceo-tools"
homepage = "https://atareao.es/tutorial/rust/"
keywords = ["cli", "sysadmin", "tools", "rust", "terminal"]
categories = ["command-line-utilities"]

[dependencies]
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
colored = "2"
anyhow = "1"
regex = "1"
walkdir = "2"
sysinfo = "0.33"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }

[[bin]]
name = "crustaceo-tools"
path = "src/main.rs"

¿Ves todos esos campos de metadatos? description, authors, license, repository, homepage, keywords, categories. No son relleno. Cada uno de ellos se incrusta en el binario cuando compilas, de forma que cuando alguien ejecute crustaceo-tools --version vea algo con sentido. Y si algún día decides publicar la crate en crates.io, estos metadatos determinan cómo aparece tu herramienta en el buscador. En mi caso particular, siempre que empiezo un proyecto nuevo me tomo un minuto para rellenarlos bien, porque luego me ahorro dolores de cabeza cuando quiero compartir el binario.

Las dependencias son las que ya conoces de capítulos anteriores, con dos incorporaciones nuevas: clap con la feature derive para la CLI profesional, y clap_complete para el autocompletado. El resto (colored, anyhow, regex, walkdir, sysinfo, serde, serde_json, chrono) son las crates que ya usaste para construir cada herramienta individual.

Estructura del proyecto

Vas a organizar el código en módulos, uno por subcomando. Así el main.rs queda limpio como una patena y cada funcionalidad vive en su propio archivo, independiente y testeable.

src/
├── main.rs          # Punto de entrada, definición de subcomandos
├── cli.rs           # Struct Args con clap derive
├── stats.rs         # Subcomando stats
├── logparse.rs      # Subcomando logparse
├── sysinfo_mod.rs   # Subcomando sysinfo
├── grep.rs          # Subcomando grep
├── dashboard.rs     # Subcomando dashboard
└── completions.rs   # Subcomando completions

Crea los archivos de una tacada:

touch src/cli.rs src/stats.rs src/logparse.rs src/sysinfo_mod.rs src/grep.rs src/dashboard.rs src/completions.rs

Fíjate en que sysinfo_mod.rs se llama así y no sysinfo.rs porque sysinfo es también el nombre de la crate, y Rust se lía con los nombres duplicados. Pequeño truco que aprendí después de que el compilador me mirase mal un par de veces. Y no es el único caso: si alguna vez has intentado llamar a un módulo mod.rs dentro de un directorio, sabes de lo que hablo. Las convenciones de Rust tienen sus cosillas, pero una vez que las conoces, todo fluye.

Subcomandos con clap derive

La clave de los subcomandos en clap 4 es usar un enum con #[derive(Subcommand)]. Cada variante del enum es un subcomando, y los campos de cada variante son sus argumentos. Es tan elegante que cuando vuelves a Bash y ves el case "$1" in te entran escalofríos.

Abre src/cli.rs y escribe esto:

Vamos por partes, que esto tiene miga.

#[derive(Parser)] en la struct Cli le dice a clap que esta struct es el punto de entrada de tu CLI. El atributo #[command(...)] configura el nombre, la versión, el autor y la descripción que aparecerán en --help.

Luego viene el campo comando con #[command(subcommand)]. Esto le indica a clap que aquí dentro va un subcomando. Y el enum Comandos con #[derive(Subcommand)] define cada subcomando como una variante.

Cada variante puede tener sus propios campos con #[arg(...)]. Mira el subcomando Logparse, por ejemplo. Tiene un flag --level con valor por defecto info, un flag --quiet booleano y un argumento posicional archivo. Todo tipado, todo documentado, todo validado automáticamente.

Lo que más me gusta es el value_parser en el campo formato de Sysinfo y en shell de Completions. Mira esta línea:

#[arg(short = 's', long = "shell", value_parser = ["bash", "zsh", "fish"])]

Si el usuario escribe crustaceo-tools completions --shell python, clap devuelve un error automático sin que hayas escrito ni una línea de validación manual:

error: invalid value 'python' for '--shell <SHELL>'
  [possible values: bash, zsh, fish]

¿Te imaginas tener que escribir eso a mano en Bash con un case y un if para cada flag? Yo lo he hecho durante años y te aseguro que no le echo de menos.

Fíjate también en default_value = "0" para watch en el subcomando Dashboard. Cuando no se pasa --watch, el dashboard se ejecuta una vez y termina. Cuando se pasa --watch 5, se actualiza cada 5 segundos. Un detalle pequeño pero que marca la diferencia entre una herramienta usable y una que no.

El main.rs

Ahora conecta todo en src/main.rs. Este archivo es el cerebro de la operación, aunque su trabajo es sorprendentemente simple: parsear la CLI y delegar en el módulo correspondiente.

Fíjate en el patrón. Cada variante del enum Comandos se corresponde con una llamada a módulo::ejecutar(). El main.rs no sabe cómo se hacen las estadísticas ni cómo se parsean los logs. Solo sabe a quién llamar. Esto se llama el patrón router y es la misma arquitectura que usan frameworks web como Axum o Actix. Separación de responsabilidades pura y dura.

Y ojo al detalle de los asteriscos: *quiet, *ignorar_mayusculas, *watch. Cuando clap hace el pattern matching, los campos booleanos y numéricos se toman por referencia (&bool, &u64). El asterisco los desreferencia para pasarlos por valor a las funciones. Es un detalle que a mí me costó un par de errores de compilación hasta que lo interioricé.

Subcomando stats

Empezamos por el más sencillo, que ya construimos en el capítulo 01. src/stats.rs:

use anyhow::{Context, Result};
use std::fs;

pub fn ejecutar(archivo: &str) -> Result<()> {
    let contenido =
        fs::read_to_string(archivo).with_context(|| format!("No se pudo leer '{}'", archivo))?;

    let lineas = contenido.lines().count();
    let palabras = contenido.split_whitespace().count();
    let caracteres = contenido.chars().count();

    println!("── Estadísticas de: {} ──", archivo);
    println!("  Líneas:    {}", lineas);
    println!("  Palabras:  {}", palabras);
    println!("  Caracteres: {}", caracteres);
    println!("────────────────────────────");

    Ok(())
}

Este es el subcomando más corto y directo. Lee un archivo, cuenta líneas, palabras y caracteres, y lo muestra. No tiene secretos. Pero fíjate en que usa with_context de anyhow para dar un mensaje de error descriptivo si el archivo no existe. Son esos pequeños detalles los que marcan la diferencia entre una herramienta que usas una vez y una que guardas en tu PATH para siempre.

Subcomando logparse

El siguiente es el parseador de logs del capítulo 02, ahora con colores y filtrado por nivel.

La función recibe tres parámetros: la ruta del archivo, el nivel de log a filtrar y un booleano quiet. Si quiet es true, solo imprime las líneas que coinciden, sin metadatos ni etiquetas de color. Esto es útil cuando quieres pasar la salida a otro comando con un pipe.

Si quiet es false, en cambio, cada línea se muestra con una etiqueta coloreada: ERROR en rojo y negrita, WARN en amarillo y negrita, INFO en verde, DEBUG en cian. Y al final, un resumen con el número de líneas encontradas.

Es un ejemplo perfecto de cómo una herramienta simple puede ser mucho más útil con un par de flags bien pensados.

Subcomando sysinfo

La información del sistema del capítulo 03, ahora con dos modos de salida: texto legible y JSON.

El modo JSON es especialmente interesante. Si ejecutas crustaceo-tools sysinfo --format json, obtienes un JSON que puedes pasar a jq, a un script de Python o a cualquier otra herramienta. Esto es lo que se llama componibilidad: tu herramienta no solo sirve para que un humano la lea, sino que puede integrarse en pipelines automatizados.

Fíjate en el std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL). Es necesario porque sysinfo necesita un pequeño intervalo para calcular el uso de CPU correctamente. Sin esa pausa, el porcentaje de CPU siempre sería cero. Te puedes imaginar el susto la primera vez que lo probé y veía que la CPU estaba al 0% con el navegador abierto y 20 pestañas de Chromium devorando RAM. Lo peor es que estuve un buen rato pensando que había roto algo, hasta que caí en que era un problema de tiempos de muestreo.

Subcomando grep

El grep recursivo con regex del capítulo 04.

La función ejecutar compila una expresión regular (con la opción (?i) si el usuario ha pedido ignorar mayúsculas) y luego decide si el path es un archivo o un directorio. Si es un directorio, lo recorre recursivamente con walkdir. Si es un archivo, lo busca directamente.

La función auxiliar buscar_en_archivo es interesante porque maneja silenciosamente los errores de lectura. Si un archivo es binario o no tienes permisos, simplemente lo saltas y sigues con el siguiente. En una búsqueda recursiva por todo el sistema de archivos, esto es esencial: no quieres que tu herramienta se detenga porque encontró un socket o un pipe.

Subcomando dashboard

El dashboard del sistema del capítulo 05, ahora con opción --watch.

Este subcomando es especial porque ejecuta un bucle infinito. Cada iteración limpia la pantalla con el código de escape \x1B[2J\x1B[H, refresca la información del sistema y la muestra en un formato tabular con bordes de caja.

Si watch es 0, el bucle se ejecuta una vez y termina. Si watch es 5, por ejemplo, espera 5 segundos entre cada actualización. Es como tener un htop minimalista escrito por ti mismo.

Una cosa curiosa: cuando escribí este subcomando por primera vez, me olvidé de limpiar la pantalla antes de cada iteración. El resultado era que el dashboard se imprimía una y otra vez sobre sí mismo, creando un rastro de información que ocupaba toda la terminal. Quedaba psicodélico, pero no era precisamente útil. Tuve que leer la documentación de los códigos de escape ANSI un par de veces hasta que dí con la combinación correcta. Son esas pequeñas victorias las que hacen que merezca la pena.

Subcomando completions: la joya de la corona

Y llegamos al subcomando estrella del capítulo. El que genera autocompletado para Bash, Fish y Zsh a partir de la definición de Cli. El usuario simplemente ejecuta:

crustaceo-tools completions --shell bash > crustaceo-tools.bash
source crustaceo-tools.bash

Y desde ese momento, al escribir crustaceo-tools y pulsar TAB, el shell completa subcomandos, flags, argumentos… todo lo que definiste en la struct. Sin una sola línea de scripting de shell.

Fíjate en que no importamos std::env (lo he eliminado expresamente porque no lo usamos). Es un detalle que en la primera versión del código se me coló y no hacía nada. Qué torpeza la mía.

La función ejecutar recibe el nombre del shell como string, lo convierte al enum Shell de clap_complete, obtiene el comando de clap con Cli::command(), y llama a generate_to para generar el script de autocompletado en el directorio actual.

El resultado son archivos como estos:

Bash (crustaceo-tools.bash):

_crustaceo-tools() {
    local i cur prev opts cmd
    ...
}
complete -F _crustaceo-tools crustaceo-tools

Fish (crustaceo-tools.fish):

complete -c crustaceo-tools -n "__fish_use_subcommand" -l help -d 'Print help'
...

Zsh (_crustaceo-tools):

#compdef crustaceo-tools
...

No escribes ni una línea de autocompletado manual. clap_complete lo genera todo a partir de tu definición de CLI. Es una de esas cosas que cuando las descubres te preguntas cómo has podido vivir sin ellas hasta ahora 💡

Cómo instalarlo permanentemente

Generar el autocompletado está bien, pero si tienes que hacer source cada vez que abres una terminal, no sirve de mucho. Aquí tienes cómo instalarlo de forma permanente en cada shell.

Bash

Si tienes bash-completion instalado (lo más probable en cualquier distribución Linux moderna), puedes guardar el script en el directorio de completions de Bash:

mkdir -p ~/.local/share/bash-completion/completions/
crustaceo-tools completions --shell bash > ~/.local/share/bash-completion/completions/crustaceo-tools

Bash lo carga automáticamente la próxima vez que abras una terminal. Si no funciona, añade esto a tu ~/.bashrc:

source /ruta/a/crustaceo-tools.bash

Fish

Fish es el más sencillo de todos. Solo tienes que copiar el script al directorio de completions:

mkdir -p ~/.config/fish/completions/
crustaceo-tools completions --shell fish > ~/.config/fish/completions/crustaceo-tools.fish

Fish lo carga automáticamente al arrancar. Sin configuración adicional, sin source, sin nada. Por eso cada vez me gusta más Fish.

Zsh

Zsh requiere un par de pasos más, pero sigue siendo sencillo:

mkdir -p ~/.zsh/completions/
crustaceo-tools completions --shell zsh > ~/.zsh/completions/_crustaceo-tools

Y añade esto a tu ~/.zshrc:

fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit

Compilar y probar

Compila el proyecto para asegurarte de que todo está en orden:

cargo build

Si compila sin errores (y debería), prueba el help generado:

./target/debug/crustaceo-tools --help

La salida debería ser algo así:

Suite de herramientas CLI para sysadmins

Usage: crustaceo-tools <COMMAND>

Commands:
  stats        Estadísticas básicas de un archivo (líneas, palabras, caracteres)
  logparse     Parsea un archivo de logs y filtra por nivel
  sysinfo      Muestra información del sistema (CPU, memoria, disco)
  grep         Busca un patrón en archivos (grep recursivo con regex)
  dashboard    Muestra un dashboard del sistema en tiempo real
  completions  Genera scripts de autocompletado para shells
  help         Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Cada subcomando tiene su propio --help. Prueba, por ejemplo:

./target/debug/crustaceo-tools logparse --help
Parsea un archivo de logs y filtra por nivel

Usage: crustaceo-tools logparse [OPTIONS] <ARCHIVO>

Arguments:
  <ARCHIVO>  Archivo de log a procesar

Options:
  -l, --level <NIVEL>  Nivel de log a filtrar (error, warn, info, debug) [default: info]
  -q, --quiet          Mostrar solo las líneas que coinciden, sin metadatos
  -h, --help           Print help
  -V, --version        Print version

Y la versión:

./target/debug/crustaceo-tools --version
crustaceo-tools 0.1.0

Ahora vamos a probar cada subcomando para asegurarnos de que funciona correctamente.

Probar stats

echo -e "hola mundo\nadios mundo" > /tmp/test.txt
cargo run -- stats /tmp/test.txt

Salida esperada:

── Estadísticas de: /tmp/test.txt ──
  Líneas:    2
  Palabras:  4
  Caracteres: 21
────────────────────────────

Probar logparse

cat > /tmp/test.log << EOF
2024-01-15 INFO: Servicio iniciado
2024-01-15 ERROR: Fallo en la conexión
2024-01-15 WARN: Reintentando...
EOF

cargo run -- logparse /tmp/test.log
cargo run -- logparse --level error /tmp/test.log
cargo run -- logparse --level warn --quiet /tmp/test.log

Probar sysinfo

cargo run -- sysinfo

Debe mostrar hostname, kernel, CPU, memoria, swap y uptime.

cargo run -- sysinfo --format json

Debe mostrar la misma información en JSON, lista para ser consumida por otras herramientas.

Probar grep

cargo run -- grep ERROR /tmp/test.log
cargo run -- grep "servicio" --ignore-case /tmp/test.log

Probar dashboard

cargo run -- dashboard

Debe mostrar el dashboard y salir inmediatamente.

cargo run -- dashboard --watch 2

Debe mostrar el dashboard cada 2 segundos. Pulsa Ctrl+C para salir.

Probar error en shell no soportada

cargo run -- completions --shell python 2>&1

Debe mostrar un error de validación indicando que los valores válidos son bash, zsh o fish.

Probar autocompletado en Fish (si usas Fish)

mkdir -p ~/.config/fish/completions/
./target/debug/crustaceo-tools completions --shell fish > ~/.config/fish/completions/crustaceo-tools.fish

Abre una nueva terminal Fish y prueba el autocompletado:

crustaceo-tools <TAB>
crustaceo-tools l<TAB>   → logparse
crustaceo-tools logparse --<TAB>   → --level --quiet --help

Esa sensación de ver cómo tu propio programa completa los subcomandos con solo pulsar TAB… no tiene precio 🔥

Publicar en GitHub Releases

Una vez que tu proyecto compila y funciona, el siguiente paso natural es distribuirlo. GitHub Releases te permite publicar binarios compilados para que cualquier usuario los descargue y ejecute sin necesidad de Rust ni Cargo. Sin cargo install, sin rustup, sin nada. Un solo binario estático que funciona en cualquier Linux.

1. Sube el código a GitHub

git init
git add .
git commit -m "Primera versión de crustaceo-tools"
gh repo create crustaceo-tools --public --source=. --push

Si no tienes gh instalado, puedes crear el repositorio desde la interfaz web de GitHub y luego añadir el remote manualmente.

2. Compila en release

cargo build --release

El binario está en target/release/crustaceo-tools. Es un ejecutable independiente, sin dependencias externas más allá de las librerías del sistema (glibc, básicamente).

file target/release/crustaceo-tools
# crustaceo-tools: ELF 64-bit LSB executable, x86-64, dynamically linked, ...

strip target/release/crustaceo-tools
# Reduce el tamaño eliminando símbolos de depuración

ls -lh target/release/crustaceo-tools
# -rwxr-xr-x  ~4.5 MB (o menos tras strip)

El comando strip es importante. Elimina los símbolos de depuración del binario, reduciendo su tamaño considerablemente. En mi caso, pasé de 6.2 MB a 4.5 MB. No está mal para un solo comando.

3. Crea un release en GitHub

Con el CLI de GitHub:

gh release create v0.1.0 \
  --title "v0.1.0 - Primera release" \
  --notes "Primera versión de crustaceo-tools. Ver capítulo 07 del tutorial." \
  target/release/crustaceo-tools

O desde la interfaz web: ve a tu repositorio, haz clic en Releases, luego en Create a new release, sube el binario y publica.

Cualquier usuario puede descargarlo con una simple línea:

curl -LO https://github.com/tuusuario/crustaceo-tools/releases/download/v0.1.0/crustaceo-tools
chmod +x crustaceo-tools
./crustaceo-tools --help

Sin Rust, sin Cargo, sin dependencias. Un solo comando curl y ya tienes la herramienta funcionando.

GitHub Actions: CI/CD automático

Hacer releases manualmente está bien para la primera versión, pero si tienes que hacerlo cada vez que corriges un error o añades una funcionalidad, se vuelve tedioso muy rápido. La solución es automatizar todo con GitHub Actions.

Crea el directorio y el archivo del workflow:

mkdir -p .github/workflows

Y dentro, crea .github/workflows/ci.yml con este contenido:

name: CI/CD

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Cache cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry/
            ~/.cargo/git/
            target/
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Compilar
        run: cargo build --release --verbose

      - name: Test
        run: cargo test --verbose

      - name: Mostrar binario
        run: ls -lh target/release/crustaceo-tools

      - name: Subir artefacto
        uses: actions/upload-artifact@v4
        with:
          name: crustaceo-tools
          path: target/release/crustaceo-tools

  release:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: build
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Descargar artefacto
        uses: actions/download-artifact@v4
        with:
          name: crustaceo-tools

      - name: Crear release
        uses: softprops/action-gh-release@v2
        with:
          files: crustaceo-tools
          generate_release_notes: true

Este workflow hace dos cosas. Primero, en cada push a main y en cada pull request, compila el proyecto en modo release, ejecuta los tests y sube el binario como artefacto. Segundo, cuando etiquetas un commit con v* (por ejemplo v0.2.0), además de compilar, crea un Release en GitHub con el binario adjunto y notas de release generadas automáticamente.

Para crear un nuevo release, solo tienes que hacer:

git tag v0.2.0
git push origin v0.2.0

GitHub Actions lo compila, lo empaqueta y lo publica automáticamente. Sin intervención manual. Es una de esas cosas que cuando las configuras por primera vez y ves cómo funciona, te preguntas por qué no lo hiciste antes.

El caching de Cargo es especialmente importante. Sin él, cada ejecución del workflow tendría que descargar y compilar todas las dependencias desde cero, lo que llevaría varios minutos. Con el caché, las ejecuciones subsiguientes son mucho más rápidas. En mi caso, la primera ejecución tarda unos 3 minutos, pero las siguientes bajan a 30 segundos si no han cambiado las dependencias. Merece mucho la pena configurarlo bien.

Una cosa que aprendí con la práctica: el workflow que te he mostrado compila solo para Linux (el runner de GitHub Actions usa Ubuntu). Si quieres distribuir para macOS y Windows, tendrías que añadir más jobs en la matriz de compilación. Pero eso lo veremos en un capítulo posterior, cuando hablemos de compilación cruzada y distribución multi-plataforma. De momento, con Linux tenemos suficiente, que para eso somos sysadmins y linuxeros de pro.

Trucos y detalles finos

Antes de cerrar el capítulo, déjame compartir algunos trucos que he aprendido en el proceso.

Usa subcommand_negates_reqs = true. Este atributo en #[command(...)] permite que los subcomandos se usen sin necesidad de argumentos adicionales. Sin él, clap podría requerir argumentos a nivel raíz que no tienen sentido cuando usas subcomandos.

Los nombres de los campos en el enum se convierten automáticamente en flags. Si llamas a un campo ignorar_mayusculas, clap genera automáticamente --ignorar-mayusculas. Puedes personalizarlo con long = "ignore-case" si prefieres inglés.

value_parser acepta closures. No solo arrays de strings. Puedes pasar una función de validación personalizada:

#[arg(value_parser = |s: &str| -> Result<MiTipo, String> {
    // validación personalizada
})]

El autocompletado también funciona para tus propios argumentos. Si defines un argumento con value_parser = ["bash", "zsh", "fish"], el autocompletado de Zsh te mostrará esos valores como opciones al pulsar TAB. Todo integrado.

Puedes generar el autocompletado en tiempo de compilación. Si quieres distribuir el binario con el autocompletado incluido, puedes usar include_str! en un build.rs para incrustar los scripts generados en el propio binario. Es un truco avanzado, pero muy útil si quieres que el autocompletado funcione desde el primer momento sin necesidad de ejecutar el subcomando completions.

Los errores de clap son informativos por defecto. Si el usuario se equivoca al escribir un subcomando, clap no solo muestra el error, sino que sugiere el subcomando correcto más parecido. Por ejemplo, si escribes crustaceo-tools stat (sin la s), clap te dirá algo como «error: unrecognized subcommand ‘stat’. Did you mean ‘stats’?». Eso es calidad de vida para el usuario final.

No olvides el archivo .gitignore. Cuando trabajas con Rust, target/ entero debe estar en .gitignore. Y si usas el autocompletado, también deberías ignorar los archivos generados (*.bash, *.fish, _*). Nadie quiere commits accidentales de scripts de autocompletado generados automáticamente.

Por qué esto cambia tu flujo de trabajo

Antes de pasar al resumen, quiero contarte cómo ha cambiado mi día a día tener una suite CLI unificada. Cuando tenía seis binarios separados, mi flujo era más o menos así: abría una terminal, escribía file-stats, me acordaba de que se llamaba archivo-info, lo ejecutaba, luego rust-grep, luego sys-dash… y así hasta que perdía la cuenta. Cada herramienta tenía su propia sintaxis, sus propios flags, su propia manera de hacer las cosas. Era un caos.

Ahora, con crustaceo-tools, todo está en un solo sitio. La sintaxis es coherente: todos los subcomandos usan --help de la misma manera, todos los flags siguen el mismo patrón, todo el autocompletado funciona de forma consistente. Y lo mejor es que cuando quiero añadir una herramienta nueva, solo tengo que crear un nuevo módulo, añadir una variante al enum y una línea en el match del main.rs. El autocompletado se actualiza solo, la documentación se genera sola, todo encaja.

Esto no es solo una cuestión de orden. Es una cuestión de productividad. Cuando tus herramientas comparten una interfaz común, tu cerebro deja de gastar energía recordando sintaxis diferentes y se centra en lo que realmente importa: resolver el problema que tienes delante.

Resumen del capítulo

En este capítulo has aprendido a estructurar una suite CLI con subcomandos usando clap 4 y #[derive(Subcommand)], a definir argumentos posicionales y flags con valores por defecto y validación automática con value_parser, a generar autocompletado para Bash, Fish y Zsh con clap_complete, a instalar el autocompletado de forma permanente en cada shell, a añadir metadatos al binario como nombre, versión, autor y descripción, a publicar en GitHub Releases y distribuir como binario único, y a automatizar compilación y publicación con GitHub Actions CI/CD.

Y lo más importante: has unificado todas tus herramientas en un solo binario profesional, con autocompletado nativo, documentación automática y un pipeline de distribución listo para producción. Dejas de tener scripts sueltos y pasas a tener una suite CLI como las que usas cada día: git, docker, cargo, podman

En el próximo bloque empezaremos con un tema nuevo: Rust en el backend. Construirás un servidor HTTP, servirás APIs, manejarás peticiones concurrentes y conectarás tus herramientas CLI con servicios web. Todo desde cero, con Rust, y con el mismo enfoque práctico de siempre.

Artículos relacionados


Más información,

Deja una respuesta