Todo script de bash de más de 200 líneas pertenece a Python
Todo script de bash de más de doscientas líneas con condicionales debería reescribirse en Python. Más corto, y por fin testeable.
Bash es un pegamento excelente para una docena de líneas: ejecuta un comando, revisa el código de salida, canaliza hacia lo siguiente. Pero nunca fue diseñado como lenguaje de programación, y pasadas un par de cientos de líneas con ramificación real deja de ser pegamento y se vuelve un lastre que nadie en el equipo puede cambiar con seguridad.
Lo que bash no tiene
Los problemas no son de estilo, son huecos estructurales:
- Sin estructuras de datos reales. Los arreglos asociativos existen pero son torpes; cualquier cosa anidada (una lista de registros, JSON) implica invocar
jqy trucos con todo tratado como string. - Sin manejo de errores. No hay
try/except.set -euo pipefailayuda pero está lleno de bordes filosos: no se dispara dentro de las sustituciones de comandos como esperarías, y un solo$varsin comillas que esté vacío puede hacer glob o word-split en silencio hacia un comando destructivo. - Sin testeabilidad. No hay una costura natural para pruebas unitarias. Pruebas ejecutando todo contra efectos secundarios reales, lo que en un script de despliegue o migración significa probar en producción.
Un script de shell grande está a una comilla mal puesta de un rm -rf en la ruta equivocada, y como no se puede probar unitariamente, nadie lo refactoriza. Se calcifica.
La reescritura se paga sola
La misma lógica en Python suele ser más corta, no más larga, porque dejas de pelear con el lenguaje:
import subprocess, sys
def run(cmd: list[str]) -> str:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"{cmd[0]} failed: {result.stderr.strip()}")
return result.stdout
def deploy(env: str) -> None:
if env not in ("staging", "production"):
raise ValueError(f"unknown env: {env}")
artifacts = run(["build", "--env", env]).splitlines()
...
Un script de bash de ~200 líneas a menudo se convierte en ~150 líneas de Python con excepciones reales, datos tipados y funciones que puedes importar y probar. Y los argumentos de subprocess se pasan como una lista, así que no hay campo minado de word-splitting ni de comillas del shell.
| Aspecto | Bash | Python (o Go/Ruby) |
|---|---|---|
| Estructuras de datos | Strings + arreglos | Listas, dicts, dataclasses |
| Manejo de errores | Códigos de salida, traps set -e | Excepciones, stack traces |
| Pruebas unitarias | Ninguna práctica | pytest, mocks, fixtures |
| Seguridad al refactorizar | Manual, frágil | Pruebas + verificador de tipos |
Dónde está la línea
Esto no es "nunca uses bash". El pegamento corto y lineal se queda en bash. El disparador es longitud más ramificación: una vez que un script supera las ~200 líneas y carga condicionales, bucles y rutas de error significativas, el costo de las funciones que le faltan a bash supera el costo de una reescritura. En una ruta de despliegue o migración de datos, una suite de pruebas es la diferencia entre confianza y cruzar los dedos.
Movidas avanzadas
- Conserva la ergonomía de CLI. El
argparsede Python (o Typer/Click) te da la misma sensación de línea de comandos con validación gratis. - Invoca comandos con cuidado. Usa
subprocess.run([...], check=True)con listas de argumentos, nuncashell=Truesobre strings interpolados. - Lintea el bash que conserves. Para los scripts que se queden en shell, corre ShellCheck en CI para atrapar bugs de comillas y word-splitting.
- Porta incrementalmente. Envuelve el bash en un entrypoint de Python y luego mueve la lógica función por función para probar sobre la marcha.
Recursos
- ShellCheck — análisis estático para scripts de shell
- Google Shell Style Guide — "when to use shell"
- Módulo
subprocessde Python - Bash pitfalls — Greg's Wiki
¿Desarrollas sistemas de IA o en la nube? Yeda AI audita y refuerza pipelines de LLM y de despliegue en producción. Hablemos · Lee el blog