Parseo de argumentos CLI en Python, JS, Java, Go y Rust
Construí herramientas de línea de comandos que manejen flags, argumentos posicionales, subcomandos y validación en Python, JS, Java, Go y Rust.
Las herramientas de línea de comandos siguen corriendo la mayoría de flujos de desarrollo, automatización DevOps y pipelines de procesamiento de datos. Una buena CLI te da subcomandos claros, defaults razonables, errores útiles y ayuda que se genera sola.
Construí y mantuve herramientas CLI en Python, Go y Rust para automatización de deploys, pipelines
de datos y tooling interno de desarrollo. La lección más grande que aprendí: agarrá un framework desde el inicio.
Parsear argumentos a mano se siente rápido el día uno, pero para cuando necesitás texto de --help,
subcomandos o shell completion, una librería como argparse,
commander.js o clap ya lo resuelve.
Para secrets y config, combiná tu CLI con variables de entorno
y parseo de archivos de config — nunca hardcodees credenciales
en flags.
A continuación hay ejemplos concretos del mismo CLI deploy en Python, JavaScript, Java, Go y Rust,
usando las librerías que los equipos usan en la práctica.
Cuándo Usarlo
- Estás construyendo herramientas internas, scripts de deploy o utilidades de automatización.
- Necesitás un pipeline de procesamiento o ETL que los operadores lancen desde el terminal.
- Querés exponer funcionalidad de la app a sysadmins o pipelines de CI/CD.
- Tu script creció más allá de un par de argumentos, así que un parser lo mantiene manejable.
- Necesitás feature flags toggled via CLI switches en diferentes entornos.
Cuándo NO Usarlo
- El script es de una sola vez con una o dos flags; los argumentos de shell podrían alcanzar.
- Una web UI o dashboard tiene más sentido para usuarios no técnicos que no deberían tocar un terminal.
- La herramienta depende de prompts interactivos en entornos sin TTY.
Solución
Python (argparse)
import argparse
def main():
parser = argparse.ArgumentParser(description="Deploy CLI tool")
parser.add_argument("environment", choices=["dev", "staging", "prod"],
help="Target environment")
parser.add_argument("--version", default="latest",
help="App version to deploy")
parser.add_argument("--dry-run", action="store_true",
help="Simulate without changes")
parser.add_argument("-v", "--verbose", action="store_true",
help="Enable verbose output")
args = parser.parse_args()
print(f"Deploying {args.version} to {args.environment}")
if args.dry_run:
print("(dry run mode)")
if __name__ == "__main__":
main()
Python (Typer)
import typer
app = typer.Typer()
@app.command()
def deploy(environment: str, version: str = "latest",
dry_run: bool = False, verbose: bool = False):
typer.echo(f"Deploying {version} to {environment}")
if dry_run:
typer.echo("(dry run mode)")
if __name__ == "__main__":
app()
JavaScript (commander.js)
const { Command } = require("commander");
const program = new Command();
program.name("deploy-cli").description("CLI for app deployments").version("1.0.0");
program
.command("deploy <environment>")
.description("Deploy to an environment")
.option("-v, --version <ver>", "App version", "latest")
.option("--dry-run", "Simulate without changes", false)
.option("--verbose", "Verbose output", false)
.action((environment, options) => {
console.log(`Deploying ${options.version} to ${environment}`);
if (options.dryRun) console.log("(dry run mode)");
});
program.parse();
JavaScript (yargs)
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
yargs(hideBin(process.argv))
.command("deploy <env>", "Deploy to environment", (yargs) => {
return yargs
.positional("env", { describe: "Target environment",
choices: ["dev", "staging", "prod"] })
.option("version", { alias: "v", default: "latest" })
.option("dry-run", { type: "boolean", default: false });
}, (argv) => {
console.log(`Deploying ${argv.version} to ${argv.env}`);
})
.demandCommand(1, "You need at least one command")
.help()
.argv;
Java (picocli)
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.util.concurrent.Callable;
@Command(name = "deploy-cli",
description = "CLI for app deployments",
version = "1.0.0",
mixinStandardHelpOptions = true)
public class DeployCli implements Callable<Integer> {
@Parameters(index = "0", description = "Target environment")
private String environment;
@Option(names = {"-v", "--version"}, defaultValue = "latest",
description = "App version")
private String version;
@Option(names = "--dry-run", description = "Simulate without changes")
private boolean dryRun;
@Option(names = {"-V", "--verbose"}, description = "Verbose output")
private boolean verbose;
@Override
public Integer call() {
System.out.printf("Deploying %s to %s%n", version, environment);
if (dryRun) System.out.println("(dry run mode)");
return 0;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new DeployCli()).execute(args);
System.exit(exitCode);
}
}
Go (cobra)
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
version string
dryRun bool
verbose bool
)
func main() {
rootCmd := &cobra.Command{
Use: "deploy-cli",
Short: "CLI for app deployments",
Version: "1.0.0",
}
deployCmd := &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy to an environment",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
env := args[0]
fmt.Printf("Deploying %s to %s\n", version, env)
if dryRun {
fmt.Println("(dry run mode)")
}
},
}
deployCmd.Flags().StringVarP(&version, "version", "v", "latest", "App version")
deployCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Simulate without changes")
deployCmd.Flags().BoolVarP(&verbose, "verbose", "V", false, "Verbose output")
rootCmd.AddCommand(deployCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
Rust (clap)
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "deploy-cli", version = "1.0.0",
about = "CLI for app deployments")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Deploy to an environment
Deploy {
/// Target environment
#[arg(value_enum)]
environment: Environment,
/// App version
#[arg(short, long, default_value = "latest")]
version: String,
/// Simulate without changes
#[arg(long)]
dry_run: bool,
},
}
#[derive(clap::ValueEnum, Clone)]
enum Environment {
Dev,
Staging,
Prod,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Deploy { environment, version, dry_run } => {
println!("Deploying {} to {:?}", version, environment);
if dry_run {
println!("(dry run mode)");
}
}
}
}
Explicación
Un framework de CLI se encarga de lo aburrido para que te concentres en la lógica de la herramienta:
- Parsing separa
deploy prod --version 2.1.0 --dry-runen un objeto estructurado. - Validación atrapa choices inválidos, flags requeridos faltantes y type mismatches antes de que tu handler se ejecute.
- Generación de ayuda construye
--helpa partir de las definiciones. - Subcomandos organizan herramientas complejas (
git push,git pull,git log). - Códigos de salida devuelven
0en éxito y distinto de cero en error, para que CI/CD y scripts de shell reaccionen.
Referencia de exit codes
Códigos de salida estándar que la mayoría de frameworks CLI siguen:
| Código | Significado | Cuándo usarlo |
|---|---|---|
| 0 | Éxito | El comando completó sin errores |
| 1 | Fallo general | Error de lógica de la app, excepción inesperada |
| 2 | Mal uso | Argumentos inválidos, flags requeridos faltantes, choices inválidos |
| 126 | Comando no ejecutable | Problema de permisos |
| 127 | Comando no encontrado | Typo, binario faltante |
| 130 | Interrumpido por SIGINT | El usuario presionó Ctrl+C |
Una vez pasé dos horas debuggeando un pipeline de CI que “tenía éxito” incluso cuando el CLI de deploy fallaba — la herramienta devolvía exit code 0 en errores. Siempre devolvé non-zero en fallos; tu sistema de CI/CD depende de eso.
Debuggear y testear CLIs
Para debuggear, agregá un flag --verbose que imprima estado intermedio a stderr. No contamines
stdout con diagnósticos — otras herramientas pueden pipear tu output. Para testear:
- Separá lógica del cableado CLI — mantené la lógica de negocio en funciones puras que no
toquen
argvostdout. Testealas directamente. - Tests de integración vía subprocess — lanzá el binario compilado con
subprocess.run()en Python oexec.Command()en Go. Assertá sobre exit code y stdout/stderr. - Snapshot test de
--help— como el texto de ayuda funciona como docs para el usuario, un snapshot test atrapa cambios accidentales cuando agregás o quitás flags. --dry-runen CI — cableá--dry-runcomo step de CI. Si sale non-zero, atrapás una config mala antes de que llegue a producción.
Shell completion
Cobra (Go) y clap (Rust) generan scripts de shell completion automáticamente:
# Cobra: generar bash completion
deploy-cli completion bash > /etc/bash_completion.d/deploy-cli
# clap: generar completion vía clap_complete
deploy-cli completions --shell bash
picocli (Java) soporta completion vía picocli.AutoComplete. Commander.js y yargs tienen plugins
de la comunidad. Shell completion es un esfuerzo chico que se paga cada vez que un usuario tipea
el nombre de tu herramienta.
Variantes
| Lenguaje | Librería | Estilo | Ideal para |
|---|---|---|---|
| Python | argparse | Stdlib, imperativo | Sin dependencias, scripts simples |
| Python | typer | Type hints, moderno | Desarrollo rápido, docs automáticas |
| JavaScript | commander.js | Cadena fluida | CLI Node.js, middleware |
| JavaScript | yargs | Declarativo, validación | CLIs complejos, subcomandos anidados |
| Java | picocli | Anotaciones, GraalVM | Enterprise, imágenes nativas |
| Go | cobra | Estilo stdlib, subcomandos | CLIs Go, shell completion |
| Rust | clap | Macros derive | Type-safe, binarios rápidos |
Buenas Prácticas
- Proveé
--helpy--versionpara que los usuarios no necesiten leer el código fuente. - Devolvé códigos de salida correctos:
0éxito,1error general,2mal uso,130para SIGINT. - Soportá
-para stdin/stdout para que los pipes funcionen:cat data.csv | mytool process - > output.json. - Validá temprano y fallá rápido; imprimir un mensaje claro con lo esperado y lo recibido.
- Mantené secretos en variables de entorno, no en argumentos
--api-key.
Errores Comunes
- Imprimir
Error: invalid argumentsin contexto. Decile al usuario qué se esperaba y qué pasó. - Hacer una herramienta con 20 flags en vez de varios subcomandos.
- Hardcodear rutas y asumir el entorno de desarrollo local.
- Enviar progreso y diagnósticos a
stdouten lugar destderr. - Permitir valores inválidos como
--replicas=-5y que lleguen a la lógica de la app.
Ver También
- argparse docs — Python standard library
- commander.js docs — CLI framework para Node.js
- picocli docs — CLI Java con anotaciones y soporte GraalVM
- cobra docs — CLI Go con subcomandos
- clap docs — CLI Rust con derive macros
- Typer docs — CLI Python moderno basado en type hints
- yargs docs — CLI Node.js declarativo con validación
- Receta de variables de entorno — Combiná flags CLI con env vars
- Parseo de archivos de config — Cargá config files como defaults CLI
Preguntas frecuentes
¿Uso un framework o parseo a mano?
Usá un framework. argparse, commander.js, picocli, cobra y clap manejan comillas, escapes,
flags desconocidos y formateo de ayuda. El tiempo que ahorrás supera con creces el costo de la
dependencia.
¿Cómo combino archivos de configuración con argumentos CLI?
Cargá un archivo de configuración como default y dejá que los argumentos de CLI sobrescriban valores específicos. El orden de precedencia es: args CLI > vars de entorno > archivo de config > defaults hardcodeados.
¿Cómo testeo una herramienta CLI?
Mantené la lógica de negocio separada del cableado de la CLI. Testeá las funciones core
directamente, luego agregá algunos tests de integración que corran el binario con subprocess. En
Java, testeá el método call() de la clase picocli; en Rust, la lógica del match de Commands.
¿Cómo distribuyo mi herramienta CLI?
La distribución depende del lenguaje. Python se distribuye vía pip o pipx desde PyPI.
JavaScript va por npm install -g o npx. Go produce un binario único — distribuílo con
go install o GitHub Releases. Rust usa cargo install desde crates.io. Java puede
compilar a una imagen nativa de GraalVM para startup rápido, o distribuirse como JAR.
¿Por qué mi CLI sale con exit code 2?
Exit code 2 significa que el usuario invocó la herramienta mal — un argumento inválido, un flag requerido faltante o un choice inválido. La mayoría de los frameworks (argparse, cobra, clap) devuelven 2 automáticamente cuando el parseo falla. Si estás devolviendo 1 para argumentos malos, cambiá a 2 para que los scripts de shell puedan distinguir "error del usuario" de "error de la aplicación".
¿Cuál es la diferencia entre flags y argumentos posicionales?
Los argumentos posicionales se identifican por su posición en la línea de comandos (deploy prod),
mientras que los flags son nombrados (deploy --env prod). Usá argumentos posicionales para el
sujeto principal del comando (el entorno, el path del archivo) y flags para modificadores
opcionales (version, verbose, dry-run). Si tenés más de dos argumentos posicionales, considerá
reestructurar en subcomandos.
¿Puedo usar variables de entorno junto con flags CLI?
Sí. La precedencia estándar es flags CLI > env vars > archivo de config > defaults hardcodeados. Esto te permite setear defaults sensatos en un archivo de config, sobrescribir por entorno vía env vars, y sobrescribir por invocación vía flags. Mirá la receta de variables de entorno para patrones de carga y validación de env vars.
Recursos Relacionados
Tareas en Segundo Plano (Background Jobs)
Cómo programar y ejecutar tareas en segundo plano usando cron, colas de trabajo y workers.
RecipeVariables de Entorno
Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.
RecipeTareas programadas con Cron
Cómo programar y gestionar tareas recurrentes usando sintaxis cron en Linux, Python y Node.js.
RecipeEndpoint de Health Check
Cómo implementar un endpoint de health check listo para producción para monitoreo y load balancers.
RecipeFeature Flags: Rollout, Segmentación y Rollback Seguro
Implementá feature toggles para desplegar, probar y revertir funcionalidad de forma segura sin volver a desplegar código.
RecipeParsear y Validar Configuración YAML/JSON
Cómo parsear y validar archivos de configuración de aplicaciones en YAML y JSON en Python, JavaScript, Java y Go.