Ejecutar comandos del sistema desde nuestros propios scripts escritos en Python, es una de las tareas más comunes y solicitadas. Resolvemos la pregunta, ¿Cómo podemos ejecutar un comando del Sistema, desde un Script escrito en Python?
Ejecutar comandos de Sistema con Python
Supongamos que necesitamos obtener la información, para saber si un host responde correctamente. Para realizar esta tarea podemos hacer uso de un módulo externo o también podemos ejecutar el comando directamente.
Python nos ofrece una forma de ejecutar comandos de Sistema en nuestros scripts, a través del módulo subprocess
. Este nos devuelve un Objeto llamado CompletedProcess que incluye información del comando ejecutado.
1 2 3 4 5 |
#!/usr/bin/env python3 import subprocess result = subprocess.run(["uptime"]) print(result) # CompletedProcess(args=['uptime'], returncode=0) |
En este ejemplo, el comando uptime nos devuelve el tiempo que lleva encendido el sistema, usuarios conectados, y carga del sistema. El objeto CompletedProcess contiene los argumentos y el código de retorno devuelto del comando. Hasta que un subproceso no termina, este no le devuelve el control a nuestro script para que pueda terminar de ejecutarse.
Programa Python → Subproceso → Programa Python (Fin programa)
Para entenderlo, veamos cómo el intérprete se bloquea, cuando usamos el comando sleep. Nuestro script permanecerá bloqueado hasta que el subproceso termine de ejecutarse.
1 2 |
subprocess.run(["sleep","2"]) # CompletedProcess(args=['sleep', '2'], returncode=0) |
Para poder acceder al valor de retorno que devuelve el proceso, accedemos al valor del atributo returncode, lo que nos será realmente útil para saber si nuestros scripts han sido ejecutados correctamente. El valor 0 indicará que nuestro comando se ha ejecutado correctamente.
1 2 |
result = subprocess.run(["ls", "LEEME.md"]) print(result.returncode) # retorna 0, 1 o 2 |
¿Cómo obtener la salida de un Comando del Sistema?
Para que nuestros scripts escritos en Python, puedan manipular la información devuelta por un comando del sistema, necesitamos especificárselo y así este pueda capturar la información . Será útil cuando queremos extraer información de un comando y utilizarla más adelante.
Supongamos que queremos obtener la información de los servidores de correo de un determinado dominio. Para capturar la salida de este comando establecemos el parámetro capture_output de subprocess.run a True.
1 2 3 4 |
#!/usr/bin/env python3 import subprocess result = subprocess.run(["dig", "MX", "google.es"], capture_output=True) print(result) |
Toda la información devuelta por el comando “dig”, se guarda en el atributo stdout.
1 |
print(result.stdout) |
Obtenemos una salida algo confusa, ¿Qué significa esa b al inicio de la cadena de salida?, nos indica que no es una cadena escrita por Python, sino que es un array de bytes.
Los datos en los ordenadores, son guardados y transmitidos en bytes y cada uno de ellos puede representar 256 caracteres. Existen miles de caracteres usados por diferentes lenguajes (el lenguaje chino dispone aproximadamente de 10000 caracteres).
Para ser capaz de guardar los datos, las aplicaciones usan lo que llamamos codificación y de esta forma saben a qué corresponden cada secuencia de bytes. Actualmente la codificación más usada es la UTF-8, que es parte del estándar Unicode y lista todos los caracteres posibles que pueden ser representados.
Cuando ejecutamos un comando usando la función run, Python no reconoce la codificación de salida del comando, por lo que representa su salida como un conjunto de bytes. Si deseamos que esta salida use una codificación correcta, necesitamos usar la función decode(), que transforma el array de bytes en una cadena legible, usando la codificación UTF-8.
1 |
print(result.stdout.decode()) |
Para poder capturar errores, lo haremos a través del atributo stderr, por lo que cuando existan errores el atributo stdout, este estará vacío.
1 2 3 |
result = subprocess.run(["rm", "fichero"], capture_output=True) print(result.stderr.decode()) # rm: cannot remove 'fichero': No such file or directory |
Gestión avanzada de Subprocesos
El módulo subprocess, nos ofrece gran cantidad de opciones que podemos usar en nuestros scripts escritos en Python. Una de ellas es poder modificar las variables de entorno de un subproceso.
Vamos a copiar el entorno de nuestro script escrito en Python, modificarlo y usarlo en el entorno al subproceso. Para realizar esta tarea, usamos el método copy() del módulo os.environ, que crea una copia de todas las variables del entorno y de esta forma las podemos modificar sin necesidad de tocar las variables del entorno del sistema.
1 2 3 4 5 6 7 |
#!/usr/bin/env python3 import os import subprocess nuevo_env = os.environ.copy() print(nuevo_env) |
Como vemos, todas las variables se han copiado en un diccionario, al que hemos llamado «nuevo_env». Supongamos que necesitamos incluir un nuevo PATH a la variable PATH. Para ello usamos el método join() del módulo os.pathsep.
A este le pasamos dos parámetros, uno con el nuevo PATH que queremos incluir y otro la KEY del diccionario donde está guardado el PATH. De esta forma le decimos que busque ejecutables en el directorio especificado, porque nuestro proceso lo necesita.
1 2 |
nuevo_env['PATH'] = os.pathsep.join(["/home/jose/backups/", nuevo_env['PATH']]) print(nuevo_env['PATH']) |
Para ejecutar el proceso usando las variables de entorno ya modificadas, usamos el módulo subprocess.run, incluyendo el parámetro env, con el entorno que hemos creado específicamente para ello.
1 |
result = subprocess.run(["app_backups"], env=nuevo_env) |
El método run se encargará de matar un proceso, si este no responde durante un tiempo.
Otro parámetro que podemos usar es Shell, que nos permite especificar en que Shell debe ser ejecutado el subproceso. En caso de no ser usado, utiliza el que tenga por defecto nuestro usuario.
El problema de usar comandos nativos del sistema, es que puede provocar fallos cuando migramos la aplicación a otros Sistemas Operativos. Por lo que es importante hacer uso de módulos específicos para realizar este tipo de tareas.