Si es un programador de Python, es muy probable que tenga experiencia en secuencias de comandos de shell. No es raro enfrentarse a una tarea que parece trivial de resolver con un comando de shell. Por lo tanto, es útil familiarizarse con cómo llamar a estos comandos de manera eficiente desde su código Python y conocer sus limitaciones.
En este breve artículo, discuto cómo usar el antiguo (aunque aún relativamente común)
os.system
comando y el más nuevo subprocess
dominio. Mostraré algunos de sus riesgos potenciales, limitaciones y proporcionaré ejemplos completos de su uso.Los comandos externos pueden ser una opción atractiva para tareas pequeñas si está familiarizado con ellos y la alternativa de Python implicaría más tiempo de aprendizaje/implementación. Sin embargo, las funciones de Python le ahorrarán tiempo y problemas a largo plazo.
Muestro a continuación un ejemplo sencillo de
os.system
para imprimir la primera línea de un archivo proporcionado por el usuario, usando head -n 1
. #!/usr/bin/env python3 import os # Define command and options wanted command = "head -n 1 " # Ask user for file name(s) - SECURITY RISK: susceptible to shell injection filename = input( "Please introduce name of file of interest:\n" ) # Run os.system and save return_value return_value = os.system(command+filename) print( '###############' ) print( 'Return Value:' , return_value)
los
os.system
La función es fácil de usar e interpretar: simplemente use como entrada de la función el mismo comando que usaría en un shell. Sin embargo, está en desuso y se recomienda su uso subprocess
ahora.Tenga en cuenta que esta función simplemente ejecutará el comando de shell y el resultado se imprimirá en la salida estándar, pero la salida que devuelve la función es el valor de retorno (0 si se ejecutó correctamente y diferente de 0 en caso contrario).
Entre otros inconvenientes,
os.system
ejecuta directamente el comando en un shell, lo que significa que es susceptible a la inyección de shell (también conocida como inyección de comando). Puede leer más sobre esto en 10 problemas de seguridad comunes en Python y cómo evitarlos por Anthony Shaw).La inyección de Shell es un problema cada vez que
os.system
está recibiendo una entrada sin formato, como por ejemplo cuando un usuario puede introducir un nombre de archivo, como en el ejemplo anterior. Puede intentar ejecutar el script anterior y dar la siguiente entrada: dummy; touch harmful_file
Esto dará como resultado que el shell haga:
head -n 1 dummy; touch harmful_file
Como resultado, el programa ejecutará primero
head -n 1 dummy
, como se esperaba, pero luego ejecutará el comando touch harmful_file
para crear un archivo llamado 'archivo_dañino'.De acuerdo, este archivo vacío no es una gran amenaza, pero puede imaginar a un usuario agregando comandos adicionales para crear un archivo con propósitos nefastos reales. Esta inyección de shell también se puede usar para simplemente transferir o eliminar información. Por ejemplo, uno podría usar
;rm -rf ~
, ;rm -rf /
o cualquier otro comando potencialmente peligroso (¡por favor , no los intente!).Una alternativa preferible es
subprocess.call
. Como os.sys, la función subprocess.call devuelve el valor devuelto como salida. Un primer enfoque ingenuo para el subproceso es usar el shell=True
opción. De esta forma, el comando deseado también se ejecutará en una subcapa. Tenga en cuenta que esto no se recomienda, ya que tiene los mismos riesgos de seguridad potenciales que os.system
. Por ejemplo, el siguiente código sería equivalente al anterior os.system
ejemplo. #!/usr/bin/env python3 import subprocess # Define command and options wanted command = "head -n 1 " # Ask user for file name(s) - SECURITY RISK: susceptible to shell injection filename = input( "Please introduce name of file of interest:\n" ) # Run subprocess.call and save return_value return_value = subprocess.call(command+filename, shell= True ) print( '###############' ) print( 'Return value:' , return_value)
Una forma preferible de correr
subprocess.call
es usando shell=False
(es la opción por defecto, por lo que no es necesario especificarla). Entonces, simplemente podemos llamar subprocess.call(args)
, dónde agrs[0]
contiene el comando, y args[1:]
contiene todas las opciones adicionales al comando. #!/usr/bin/env python3 import subprocess # Define command and options wanted command = "head" options = "-n 1" # Ask user for file name(s) - now it's safe from shell injection filename = input( "Please introduce name(s) of file(s) of interest:\n" ) # Create list with arguments for subprocess.call args=[] args.append(command) args.append(options) for i in filename.split(): args.append(i) # Run subprocess.call and save return_value return_value = subprocess.call(args) print( '###############' ) print( 'Return value:' , return_value)
subprocess.check_output
Como se mencionó antes,
subprocess.call
devuelve el valor devuelto . Sin embargo, a veces nos puede interesar la salida estándar devuelta por el comando de shell. En este caso, podemos hacer uso de subprocess.check_output
.Para hacer que nuestro script sea más sólido, podemos agregar una declaración de prueba/exclusión para tratar situaciones en las que
check_output
genera un error (en este caso, por ejemplo, cuando no se encuentra ningún archivo).Como se muestra a continuación, podemos usar
subprocess.CalledProcessError
en la cláusula de excepción para tratar el error de forma controlada y obtener información al respecto. #!/usr/bin/env python3 import subprocess # Define command and options wanted command = "head" options = "-n 1" # Ask user for file name(s) - now it's safe from shell injection filename = input( "Please introduce name(s) of file(s) of interest:\n" ) # Create list with arguments for subprocess.check_output args=[] args.append(command) args.append(options) for i in filename.split(): args.append(i) # Run subprocess.check_output and save command output try : output = subprocess.check_output(args) # use decode function to convert to string print( '###############' ) print( 'Output:' , output.decode( "utf-8" )) # If check_output returns an error: except subprocess.CalledProcessError as error: print( 'Error code:' , error.returncode, '. Output:' , error.output.decode( "utf-8" ))
La forma recomendada de ejecutar comandos de shell externos , desde Python 3.5, es con el
subprocess.run
función. Es más seguro y fácil de usar que las opciones anteriores discutidas.De forma predeterminada, esta función devuelve un objeto con el comando de entrada y el código de retorno. También se puede obtener fácilmente la salida estándar usando la opción
capture_output=True
y finalmente recuperar el código de retorno y la salida del comando usando: output.returncode
y output.stdout
, respectivamente. #!/usr/bin/env python3 import subprocess # Define command and options wanted command = "head" options = "-n 1" # Ask user for file name(s) - now it's safe from shell injection filename = input( "Please introduce name(s) of file(s) of interest:\n" ) # Create list with arguments for subprocess.run args=[] args.append(command) args.append(options) for i in filename.split(): args.append(i) # Run subprocess.run and save output object output = subprocess.run(args,capture_output= True ) print( '###############' ) print( 'Return code:' , output.returncode) # use decode function to convert to string print( 'Output:' ,output.stdout.decode( "utf-8" ))
Si está pensando en usar cualquiera de los métodos discutidos aquí para llamar a un comando externo, podría valer la pena considerar si existe una función estándar de Python que le permita realizar la misma tarea y evitará la creación de un nuevo proceso. Tenga en cuenta que el uso de comandos externos también hace que su programa sea menos compatible con varias plataformas.
Los comandos externos pueden ser una opción atractiva para tareas pequeñas si está familiarizado con ellos y la alternativa de Python implicaría más tiempo de aprendizaje/implementación.
Sin embargo, apegarse a las funciones de Python le ahorrará tiempo de cálculo y problemas a largo plazo, por lo que los comandos externos deben guardarse para tareas que no se pueden lograr con las bibliotecas estándar de Python.
¡Espero que este artículo te haya ayudado a llamar comandos externos desde tu código python!