Ejecutar comandos de Shell con Python

En lugar de secuencias de comandos de shell, que pueden volverse complejas y tediosas, podemos usar Python para automatizar los comandos de shell. En este tutorial, aprenderemos cómo ejecutarlos en aras de la escalabilidad y la mantenibilidad de los proyectos de Python.

Introducción

Las tareas repetitivas están maduras para la automatización. Es común que los desarrolladores y administradores de sistemas automaticen tareas rutinarias como controles de salud y copias de seguridad de archivos con scripts de shell. Sin embargo, a medida que esas tareas se vuelven más complejas, los scripts de shell pueden volverse más difíciles de mantener.

Afortunadamente, podemos usar Python en lugar de scripts de shell para la automatización. Python proporciona métodos para ejecutar comandos de shell, brindándonos la misma funcionalidad de esos scripts de shell. Aprender a ejecutar comandos de shell en Python nos abre la puerta para automatizar tareas informáticas de forma estructurada y escalable.

En este artículo, veremos las diversas formas de ejecutar comandos de shell en Python y la situación ideal para usar cada método.

Usar os.system para ejecutar un comando

Python nos permite ejecutar inmediatamente un comando de shell que está almacenado en una cadena usando la función os.system().

Comencemos creando un nuevo archivo de Python llamado echo_adelle.py e ingrese lo siguiente:

1
2
3
import os

os.system("echo Hello from the other side!")

Lo primero que hacemos en nuestro archivo de Python es importar el módulo os, que contiene la función system que puede ejecutar comandos de shell. La siguiente línea hace exactamente eso, ejecuta el comando echo en nuestro shell a través de Python.

En su Terminal, ejecute este archivo con el siguiente comando, y debería ver el resultado correspondiente:

1
2
$ python3 echo_adelle.py
Hello from the other side!

Como los comandos echo se imprimen en nuestra stdout, os.system() también muestra la salida en nuestra secuencia stdout. Si bien no está visible en la consola, el comando os.system() devuelve el código de salida del comando de shell. Un código de salida de 0 significa que se ejecutó sin problemas y cualquier otro número significa un error.

Vamos a crear un nuevo archivo llamado cd_return_codes.py y escriba lo siguiente:

1
2
3
4
5
6
import os

home_dir = os.system("cd ~")
print("`cd ~` ran with exit code %d" % home_dir)
unknown_dir = os.system("cd doesnotexist")
print("`cd doesnotexis` ran with exit code %d" % unknown_dir)

En este script, creamos dos variables que almacenan el resultado de ejecutar comandos que cambian el directorio a la carpeta de inicio y a una carpeta que no existe. Ejecutando este archivo, veremos:

1
2
3
4
$ python3 cd_return_codes.py
`cd ~` ran with exit code 0
sh: line 0: cd: doesnotexist: No such file or directory
`cd doesnotexist` ran with exit code 256

El primer comando, que cambia el directorio al directorio de inicio, se ejecuta correctamente. Por lo tanto, os.system() devuelve su código de salida, cero, que se almacena en home_dir. Por otro lado, unknown_dir almacena el código de salida del comando bash fallido para cambiar el directorio a una carpeta que no existe.

La función os.system() ejecuta un comando, imprime cualquier resultado del comando en la consola y devuelve el código de salida del comando. Si quisiéramos un control más detallado de la entrada y salida de un comando de shell en Python, deberíamos usar el módulo subprocess.

Ejecutar un comando con subproceso

El módulo de subproceso es la forma recomendada de Python para ejecutar comandos de shell. Nos brinda la flexibilidad de suprimir la salida de los comandos de shell o encadenar entradas y salidas de varios comandos, al mismo tiempo que brinda una experiencia similar a os.system() para casos de uso básicos.

En un nuevo archivo llamado list_subprocess.py, escribe el siguiente código:

1
2
3
4
import subprocess

list_files = subprocess.run(["ls", "-l"])
print("The exit code was: %d" % list_files.returncode)

En la primera línea, importamos el módulo subprocess, que forma parte de la biblioteca estándar de Python. Luego usamos la función subprocess.run() para ejecutar el comando. Al igual que os.system(), el comando subprocess.run() devuelve el código de salida de lo que se ejecutó.

A diferencia de os.system(), observe cómo subprocess.run() requiere una lista de cadenas como entrada en lugar de una sola cadena. El primer elemento de la lista es el nombre del comando. Los elementos restantes de la lista son las banderas y los argumentos del comando.

Nota: Como regla general, debe separar los argumentos según el espacio, por ejemplo ls -alh sería ["ls", "-alh"], mientras que ls -a - l -h, sería ["ls", "-a", -"l", "-h"]. Como otro ejemplo, echo hello world sería ["echo", "hello", "world"], mientras que echo "hello world" o echo hello\ world sería ["echo", "hola mundo"].

Ejecute este archivo y la salida de su consola será similar a:

1
2
3
4
5
6
$ python3 list_subprocess.py
total 80
[correo electrónico protegido] 1 wikihtp  staff    216 Dec  6 10:29 cd_return_codes.py
[correo electrónico protegido] 1 wikihtp  staff     56 Dec  6 10:11 echo_adelle.py
[correo electrónico protegido] 1 wikihtp  staff    116 Dec  6 11:20 list_subprocess.py
The exit code was: 0

Ahora intentemos usar una de las funciones más avanzadas de subprocess.run(), es decir, ignorar la salida a stdout. En el mismo archivo list_subprocess.py, cambie:

1
list_files = subprocess.run(["ls", "-l"])

A esto:

1
list_files = subprocess.run(["ls", "-l"], stdout=subprocess.DEVNULL)

La salida estándar del comando ahora se canaliza al dispositivo especial /dev/null, lo que significa que la salida no aparecerá en nuestras consolas. Ejecute el archivo en su shell para ver el siguiente resultado:

1
2
$ python3 list_subprocess.py
The exit code was: 0

¿Qué pasaría si quisiéramos proporcionar entrada a un comando? El subprocess.run() facilita esto por su argumento input. Cree un nuevo archivo llamado cat_subprocess.py, escribiendo lo siguiente:

1
2
3
4
import subprocess

useless_cat_call = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True, input="Hello from the other side")
print(useless_cat_call.stdout)  # Hello from the other side

Usamos subprocess.run() con bastantes comandos, repasémoslos:

  • stdout=subprocess.PIPE le dice a Python que redirija la salida del comando a un objeto para que pueda leerse manualmente más tarde
  • text=True devuelve stdout y stderr como cadenas. El tipo de retorno predeterminado es bytes.
  • input="Hola desde el otro lado" le dice a Python que agregue la cadena como entrada al comando cat.

La ejecución de este archivo produce el siguiente resultado:

1
Hello from the other side

También podemos generar una Excepción sin verificar manualmente el valor de retorno. En un archivo nuevo, false_subprocess.py, agregue el siguiente código:

1
2
3
4
import subprocess

failed_command = subprocess.run(["false"], check=True)
print("The exit code was: %d" % failed_command.returncode)

En su terminal, ejecute este archivo. Verá el siguiente error:

1
2
3
4
5
6
7
$ python3 false_subprocess.py
Traceback (most recent call last):
  File "false_subprocess.py", line 4, in <module>
    failed_command = subprocess.run(["false"], check=True)
  File "/usr/local/python/3.7.5/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 512, in run
    output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['false']' returned non-zero exit status 1.

Al usar check=True, le decimos a Python que genere excepciones si se encuentra un error. Dado que encontramos un error, no se ejecutó la declaración imprimir en la línea final.

La función subprocess.run() nos da una inmensa flexibilidad que os.system() no tiene al ejecutar comandos de shell. Esta función es una abstracción simplificada de la clase subprocess.Popen, que proporciona una funcionalidad adicional que podemos explorar.

Ejecutar un comando con Popen

La clase subprocess.Popen expone más opciones al desarrollador cuando interactúa con el shell. Sin embargo, debemos ser más explícitos acerca de la recuperación de resultados y errores.

Por defecto, subprocess.Popen no detiene el procesamiento de un programa de Python si su comando no ha terminado de ejecutarse. En un nuevo archivo llamado list_popen.py, escriba lo siguiente:

1
2
3
4
import subprocess

list_dir = subprocess.Popen(["ls", "-l"])
list_dir.wait()

Este código es equivalente al de list_subprocess.py. Ejecuta un comando usando subprocess.Popen y espera a que se complete antes de ejecutar el resto del script de Python.

Digamos que no queremos esperar a que nuestro comando de shell complete la ejecución para que el programa pueda trabajar en otras cosas. ¿Cómo sabría cuándo el comando de shell ha terminado de ejecutarse?

El método poll() devuelve el código de salida si un comando ha terminado de ejecutarse, o Ninguno si aún se está ejecutando. Por ejemplo, si quisiéramos comprobar si list_dir está completo en lugar de esperarlo, tendríamos la siguiente línea de código:

1
list_dir.poll()

Para administrar la entrada y la salida con subprocess.Popen, necesitamos usar el método communicate().

En un nuevo archivo llamado cat_popen.py, agregue el siguiente fragmento de código:

1
2
3
4
5
6
7
import subprocess

useless_cat_call = subprocess.Popen(["cat"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, errors = useless_cat_call.communicate(input="Hello from the other side!")
useless_cat_call.wait()
print(output)
print(errors)

El método communicate() toma un argumento input que se usa para pasar la entrada al comando de shell. El método communicate también devuelve stdout y stderr cuando están configurados.

Habiendo visto las ideas centrales detrás de subprocess.Popen, ahora hemos cubierto tres formas de ejecutar comandos de shell en Python. Reexaminemos sus características para saber qué método se adapta mejor a los requisitos de un proyecto.

¿Cuál debo usar?

Si necesita ejecutar uno o algunos comandos simples y no le importa si su salida va a la consola, puede usar el comando os.system(). Si desea administrar la entrada y salida de un comando de shell, use subprocess.run(). Si desea ejecutar un comando y continuar con otros trabajos mientras se ejecuta, use subprocess.Popen.

Aquí hay una tabla con algunas diferencias de usabilidad que también puede usar para informar su decisión:

                                   os.system      subprocess.run   subprocess.Popen

Requiere argumentos analizados no sí sí Espera el comando si si no Se comunica con stdin y stdout no sí sí Devuelve objeto de valor de retorno objeto

Conclusión

Python le permite ejecutar comandos de shell, que puede usar para iniciar otros programas o administrar mejor los scripts de shell que usa para la automatización. Dependiendo de nuestro caso de uso, podemos usar os.system(), subprocess.run() o subprocess.Popen para ejecutar comandos bash.

Usando estas técnicas, ¿qué tarea externa ejecutaría a través de Python?