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
- Cómo instalar Rust en Linux — instala la toolchain de Rust, cargo y configura el autocompletado para rustup y cargo en Bash y Zsh
- Rust-Script: ejecuta Rust como script en Linux — ejecuta código Rust al vuelo sin necesidad de compilar un proyecto completo
- zoxide: el cd inteligente para Linux — navegador de directorios escrito en Rust con integración para Bash, Zsh y Fish
- lazyworktree: gestiona git worktrees desde la terminal — TUI para git worktrees con autocompletado en Bash, Zsh y Fish
- Cómo instalar Podman — gestor de contenedores con autocompletado nativo para Bash, Zsh y Fish
Más información,
- Tutorial de Rust en atareao.es — el índice del tutorial completo, con todos los capítulos de Rust. Diario de un crustáceo
- Crate clap en crates.io — la crate definitiva para CLI profesionales en Rust, con soporte de subcomandos, validación y derive
- Documentación de clap 4 derive — guía completa de clap con derive: subcomandos, argumentos, validación y personalización
- Crate clap_complete en crates.io — generación de autocompletado para Bash, Fish, Zsh y PowerShell
- Documentación de clap_complete — API para generar scripts de autocompletado a partir de definiciones clap
- Crate anyhow en crates.io — manejo de errores simplificado con contexto y encadenamiento
- Crate sysinfo en crates.io — información del sistema en Rust: CPU, memoria, discos, procesos y red
- Crate walkdir en crates.io — recorrido recursivo de directorios eficiente y configurable
- Crate colored en crates.io — coloración de terminal para strings con sintaxis sencilla
- GitHub Actions — Building and testing Rust — guía oficial para CI/CD de proyectos Rust con GitHub Actions
- softprops/action-gh-release — action de GitHub para crear releases automáticos desde workflows
- Bash completion — documentación del sistema de autocompletado programable de Bash
- Fish shell completions — cómo funcionan los completions en Fish, el shell más amigable
- Zsh completion system — el sistema de completado de Zsh, el más potente y configurable