subprocess e Interoperabilidad con Shell
subprocess ejecuta binarios externos desde Python con argumentos controlados, tiempos de espera y salida capturada. Evita shell=True a menos que aceptes el riesgo de inyección y la complejidad de las comillas.
Receta
import subprocess
proc = subprocess.run(
["git", "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
timeout=30,
)
print(proc.stdout.strip())Cuándo usar esto:
- Git, rsync, tar, ffmpeg y otras herramientas CLI maduras
- Scripts de shell heredados que estás envolviendo incrementalmente
- Capturar stdout para analizar en Python
- Limitar el tiempo de ejecución de comandos largos para evitar trabajos cron bloqueados
Ejemplo Funcional
Ejecuta un comando con tiempo de espera, mapea errores a códigos de salida, transmite grandes volúmenes de salida línea por línea.
import logging
import subprocess
from collections.abc import Sequence
log = logging.getLogger(__name__)
def run_command(cmd: Sequence[str], timeout: float = 60) -> subprocess.CompletedProcess[str]:
log.info("ejecutando %s", list(cmd))
try:
return subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired as exc:
log.error("tiempo de espera agotado después de %ss: %s", timeout, cmd)
raise
except subprocess.CalledProcessError as exc:
log.error("salida con código %s stderr=%s", exc.returncode, exc.stderr.strip())
raise
def stream_lines(cmd: Sequence[str]) -> int:
with subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) as proc:
assert proc.stdout is not None
for line in proc.stdout:
print(line.rstrip())
return proc.wait()
if __name__ == "__main__":
result = run_command(["python3", "--version"])
print(result.stdout)Lo que esto demuestra:
- Secuencia de argumentos, no una cadena de shell: sin superficie de inyección
check=TruelanzaCalledProcessErrorcon el código de retorno y stderrPopentransmite stdout sin almacenar en búfer toda la salida en memoria
Análisis Profundo
Elección de API
| API | Uso |
|---|---|
run() | La mayoría de los comandos: síncrono simple |
Popen() | Transmisión, tuberías, interactivo |
check_output() | Captura rápida cuando check=True siempre |
Riesgo de shell=True
- La entrada del usuario en la cadena de comandos permite la inyección de shell
- Prefiere
argvde lista; si se requiere shell, usashlex.quoteen segmentos de usuario
Notas de Python
import shlex
user_file = "report; rm -rf /"
# MAL: subprocess.run(f"cat {user_file}", shell=True)
subprocess.run(["cat", user_file], check=True)Errores Comunes
shell=Truecon variables - error clásico de inyección. Solución: listaargvoshlex.splitsolo en scripts confiables.- Sin tiempo de espera - trabajos cron zombis para siempre. Solución: siempre establece
timeoutproporcional al tiempo de ejecución esperado. - Capturar
stdoutenorme - explosión de memoria. Solución: transmitir conPopeno redirigir a un archivo temporal. - Ignorar
stderr- contexto de fallo oculto. Solución: registrarstderren caso de salida distinta de cero. - Codificación en modo texto - las herramientas binarias fallan. Solución:
text=Falsey decodificar explícitamente cuando sea necesario.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Librerías Python puras | La operación tiene una API Python madura | Ya existe una CLI probada en batalla |
asyncio.create_subprocess_exec | Orquestación de bucle de eventos asíncrono | Script cron síncrono simple |
| Fabric/SSH remoto | Hosts remotos | Comando solo local |
Preguntas Frecuentes
¿Cuándo está bien usar shell=True?
Scripts internos fijos sin entrada de usuario en máquinas confiables; aún así documenta la excepción y el ticket.
¿Cómo paso el entorno?
env={**os.environ, "FOO": "bar"} - no vuelques secretos en el entorno hijo innecesariamente.
¿Cómo ejecuto en un CWD específico?
cwd="/path/to/repo" en run() - las rutas en el comando deben ser absolutas o relativas al CWD.
¿Cómo hago una tubería de comandos?
Encadena dos Popen con salida estándar a entrada estándar, o usa una tubería de shell con extrema precaución y comandos fijos.
¿Qué código de salida significa fallo?
Distinto de cero; mapea códigos conocidos en la documentación del script (por ejemplo, 2 error de uso).
¿Cómo oculto contraseñas en argv?
Pasa a través de la variable de entorno o stdin; argv es visible en ps en muchos sistemas.
¿Diferencias en Windows?
Los archivos .cmd pueden necesitar shell=True solo en Windows; aísla las ramas de plataforma.
¿Cómo pruebo subprocess?
Simula subprocess.run o usa binarios de eco conocidos en pytest con tmp_path.
¿Fusionar stdout/stderr?
stderr=subprocess.STDOUT cuando un solo flujo simplifica el análisis.
¿Cómo se relaciona esto con SSH?
subprocess local en un host remoto a través de una sesión SSH; consulta la página de SSH para ver patrones de ejecución remota.
Relacionado
- Conceptos Básicos de Scripting - puntos de entrada CLI
- SSH y Ejecución Remota - comandos remotos
- Scripts Robustos - manejo de errores
- Análisis de Registros y Texto - analizar la salida del comando
- Mejores Prácticas de Scripting de Sistemas - ejecución segura
Versiones de la pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+ y uv 0.6+.