Combinando Python y Bash
INTRODUCCIÓN
Vamos a ver algunas cosas interesantes que puedes hacer combinando Python y Bash. En esta web nos empeñamos en llevar a python a todas partes y por eso escribo sobre PythonNET, Kivy, Brython, etc. Pero no debemos olvidar que el hábitat original de python es Linux y los scripts en python para administrar el sistema se mueven como pez en el agua, por lo que tiene sentido hablar de Python en términos de para lo que fue creado aunque hoy en día su utilidad va mucho mas allá.
Todo lo que leerás aquí puede aplicarse a cualquier CLI + Python, pero he decidico hablar de Bash porque es el CLI por defecto de la mayoría de distribuciones Linux, y no he querido hablar de Powershell porque aunque powershell es el rey en windows, python no está instalado por defecto en este sistema operativo.
NOTAS:
- Mi apartado favorito en este artículo es Trolear redefiniendo comandos, pero te recomiento leer todo el artículo
- Si no conoces bash o te preguntas cuales son las diferencias entre un lenguaje de programación como tal y un CLI - Command Line Interface, puede que aquí salgas de dudas.
- Este artículo no pretende ser una guia sobre bash, pero veremos cosas interesantes para los programadores python.
UN POCO SOBRE BASH:
Bash es el CLI por defecto en la mayoría de distribuciones Linux. Se le conoce como terminal, línea de comandos o shell. Normalmente un usuario abre un terminal bash y ejecuta comandos para obtener información del sistema, navegar por el sistema de archivos, administrar procesos, instalar programas, etc.
Pero además Bash puede ejecutar conjuntos de comandos definidos en scripts para automatizar tareas. Por ejemplo, puedes crear un archivo donde-estoy.sh y editarlo así:
#!/bin/bash
echo Estás en `pwd`
echo que contiene los siguientes archivos/carpetas:
echo ""
for i in *; do
echo " $i"
done
Puedes abrir un terminal y ejecutar este script mediante:
bash donde-estoy.sh
O puedes hacer que sea un archivo ejecutable mediante chmod 755 donde-estoy.sh
y ejecutarlo mediante:
./donde-estoy-sh
Si quieres omitir el ./
o si quieres ejecutar el script desde cualquier directorio en el sistema de archivos debes añadir el directorio que contiene el script al path del sistema:
export PATH=$PATH:.
Esto hace que puedas ejecutar donde-estoy.sh desde donde sea aunque navegues mediante el comando cd
. Has modificado el path del sistema para el shell actual y sus hijos, pero si quieres aplicar este cambio para cualquier nuevo shell puedes añadir export PATH=$PATH:.
al archivo ~/.bashrc
(y ejecutar source ~/.bashrc
para aplicar los cambios, además, en el shell actual).
Supongo que sabes como ejecutar un script python desde bash. Si tienes un archivo program.py abres el terminal y ejecutas:
python3 program.py
Puedes añadir el shebang #!/bin/python3
a program.py y ejecutar chmod 755 program.py
para ejecutar el script así:
./program.py
Así tienes un script ejecutable sin tener que invocar explicitamente a python. Seguramente ya sabías esto, pero había que mencionarlo. Ahora toca leer cosas realmente interesantes.
INCRUSTAR PYTHON EN BASH
Probablemente sabías que puedes abrir un terminal y ejecutar esto:
python -c "print('Hola')"
Con lo que se imprime el texto Hola
.
Pero también es muy probable que no te hayas dado cuenta de lo útil que puede ser la posibilidad de ejecutar python de esa forma: puedes utilizar python -c en scripts bash para ejecutar programas mas complejos. Observa:
#!/bin/bash
python3 -c "
i = 0
while i < 10:
print(i, end=' ')
i += 1
"
El resultado es el siguiente:
0 1 2 3 4 5 6 7 8 9
¿te parece interesante? es posible que no, ya que esta posibilidad no aporta nada nuevo, de momento. Podrías haber hecho lo mismo en un script python ¿no?.
Mira esto:
#!/bin/bash
i=0
max=10
step=1
python3 -c "
i = $i
while i < $max:
print(i, end=' ')
i += $step
"
Admítelo, eso ha sido un poco sexy...
Hemos sustituido el valor de las variables bash i
, max
y step
en el texto que es pasado como argumento al intérprete de python.
Vale, es verdad que puedes crear un script python que acepte argumentos que determinen su funcionalidad. Yo admito que esto es una forma bizarra de hacer las cosas y estamos pasándonos al lado oscuro de las malas prácticas pero ¿núnca quisiste mas ser un Sith que un Jedy?
Si quieres hacer travesuras con python y bash continúa leyendo...
CREA TUS PROPIOS COMANDOS CON PYTHON
Las funciones en bash son comandos definidos por el usuario. Observa el siguiente ejemplo:
#!/bin/bash
function saludar { echo Hola; }
saludar
Al ejecutar la función saludar
se imprime el texto Hola
.
La función saludar
se invoca como cualquier comando (es un comando), y no como se habría hecho en python: saludar()
. Al crear un comando en bash no definimos la firma de la función, es decir, no especificamos los argumentos que recibe la función. Vamos a mejorar el código anterior:
#!/bin/bash
function saludar { echo Hola $1; }
saludar Jose
saludar Maria
saludar Pepito
Al ejecutar este script se imprime lo siguiente en pantalla:
Hola Jose Hola Maria Hola Pepito
En la definición de un comando/función puede manejarse el primer argumento que se le pasa al comando mediante $1
. Si hubiese un segundo comando $2
, para un tercero $3
, y así sucesivamente.
¿te das cuenta de que estás definiendo comandos que pueden ejecutarse en el terminal? se invocan precisamente como cualquier otro comando. Por ejemplo, en cd ..
, los dos puntos son $1
.
UN COMANDO QUE EJECUTA CÓDIGO PYTHON
Vamos a crear un comando py
que ejecutará el primer argumento que se le pase:
function py { python3 -c "$1"; }
"$1"
tiene que ir entrecomillado porque de lo contrario el primer argumento ($1
) sería la primera palabra del código pasado.
Mira como podemos utilizar el comando py
:
#!/bin/bash
function py { python3 -c "$1"; }
py "
for i in range(5):
print(i, end=' ')
"
El resultado es:
0 1 2 3 4
Como vimos al principio, podemos sustituir partes del código pasado por el valor de variables bash:
#!/bin/bash
function py { python3 -c "$1"; }
range=7
py "
for i in range($range):
print(i, end=' ')
"
En este caso el resultado es:
0 1 2 3 4 5 6
COMUNICACIÓN ENTRE PYTHON Y BASH
Hasta ahora hemos ejecutado python desde bash de una forma diferente a la habitual, pero en ningún momento ha habido comunicación entre ambos, simplemente hemos ejecutado uno dentro del otro.
Hay dos formas de comunicar python y bash:
print
- Imprimiendo el resultado de python en la salida estándar, siendo esta una variable de bash.sys.exit
- Notificando el código de finalización al sistema (éxito o fracaso).
1. Imprimiendo el resultado: print
En bash todo lo que se maneja es texto, es decir, strings. No hay clases, objetos, etc. Todo es texto y si por ejemplo quieres manipularlo como si fuesen datos numéricos tienes que utilizar un comando diseñado para ello. Son los comandos los que deciden las operaciones que pueden realizarse sobre los argumentos, no como en python donde cada tipo de dato tiene una serie de métodos.
Observa la siguiente definición de un comando en el shell bash:
function x2 { python3 -c "print($1*2)"; }
La función/comando x2
multiplica el primer argumento pasado al comando ($1
) por dos e imprime el resultado en la salida estándar. La salida estándar es el shell desde el que se ha ejecutado a python. Puedes utilizarlo así:
x2 5
Y sucede exactamente lo que cabría esperar, se imprime en pantalla el resultado de multiplicar 5 por 2:
10
En bash puedes ejecutar un comando en un shell hijo para asignar el resultado a una variable en el shell actual. Por ejemplo:
number=$(x2 5)
Al hacer esto no se imprime el resultado en pantalla, ya que la salida estándar es el shell hijo (que se ejecuta en segundo plano) y lo que se imprima en el shell hijo es asignado a la variable number
.
Ahora puedes utilizar el valor de number
en el shell actual:
Puedes imprimirlo en pantalla:
echo $number
O puedes hacer cualquier otra cosa:
while (( $x > 1 )); do echo $x; ((x--)); done
2. Notificando el código de finalización al sistema: sys.exit
Esta forma de comunicación es útil para la toma de decisiones, es decir, para evaluar la ejecución del código python en términos de verdadero/falso o éxito/fracaso. Por ejemplo:
function isdigit { python3 -c "
import sys
if '$1'.isdigit():
sys.exit(0)
else:
sys.exit(1)
"; }
isdigit 1
echo $?
isdigit A
echo $?
Al ejecutar este código se imprime lo siguiente en pantalla:
0 1
El comando isdigit
ejecuta un script python que finaliza con código 0 (éxito) si el argumento pasado es una representación numérica. Si no lo es finaliza con código 1 (fracaso).
En bash, después de ejecutar un comando/función/programa puede leerse su código de finalización mediante la variable $?
. En el ejemplo hemos imprimido (echo
) el código para los argumentos 1
y A
, siendo sus correspondientes códigos de finalización 0
y 1
. El código de finalización puede evaluarse.
Comprueba tú mismo lo que sucede al ejecutar el siguiente script bash:
function isdigit { python3 -c "import sys; sys.exit(0) if '$1'.isdigit() else sys.exit(1)"; }
echo INDICA UN NÚMERO:
read value
isdigit $value
if [ $? == 0 ]; then
echo CORRECTO: $value es un número
else
echo INCORRECTO: $value no es un número
fi
APRENDER BASH DISFRUTANDO DE PYTHON
Desde el punto de vista de un programador python puede parecer que bash no merece la pena. Con bash estamos manejando texto todo el tiempo, no hay tipos de datos, no puede hacerse programación orientada a objetos, no hay módulos ni librería estándar, etc. Utilizar Bash en lugar de Python no supone un aumento de rendimiento. Entonces: ¿por qué aprender bash?
Bash ideal para administrar el sistema, se mueve por el sistema de archivos de manera natural y es extremadamente sencillo realizar determinadas tareas:
- Manejo de archivos: crear, borrar, editar, mover...
- Administrar procesos: ejecutar programas, cambiar su prioridad, detenerlos...
- Texto: Es especialmente bueno manejando archivos de texto plano, ya que en bash todo es texto.
Las dudas sobre la utilidad de bash se disipan cuando te das cuenta de que puedes ejecutar python3 mi_script.py
gracias a bash. La invocación tan sencilla de ejecutables es una de las características de cualquier CLI. Para hacer esto desde python la cosa se complica un poco: import subprocess;subprocess.run('python3 mi_cript.py')
No obstante, empezar a estudiar un nuevo lenguaje puede resultar frustrante porque las tareas sencillas que realizas en python ahora son complicadas en bash, ya que no tienes ni idea de bash. Es fácil tirar la toalla y volver a la pyzona de confort.
APROVECHA LO QUE YA SABES HACER
Pero no estás solo en este criptico viaje, puedes ir armado con todo tu kit de habilidades en python. Por ejemplo, considera el siguiente script bash:
#!/bin/bash
texto="SoY uN tExTo En mAyUsCuLaS"
echo $texto
Quieres que un texto se muestre en mayúsculas ¿cómo hago esto en bash?
En situaciones como esta hay riesgo de que te rindas, porque tú sabes que esto es báscio y podrías hacerlo en python así:
texto = "SoY uN tExTo En mAyUsCuLaS"
print(texto.upper())
¿Por qué no aprovechar lo que ya sabes? puedes crear tu propio comando upper
:
#!/bin/bash
function upper { python3 -c "print('$1'.upper())"; }
texto=$(upper "SoY uN tExTo En mAyUsCuLaS")
echo $texto
Has conseguido el resultado que deseabas:
SOY UN TEXTO EN MAYUSCULAS
Puedes invocar a python desde bash para no perder la motivación mientras aprendes mas.
Cuando sepas mas podrás resolver el problema sin tener que usar el comodín python:
#!/bin/bash
texto="SoY uN tExTo En mAyUsCuLaS"
texto=${texto^^} # para convertirlo a minúsculas: texto=${texto,,}
echo $texto
OPERACIONES MATEMÁTICAS EN UN LENGUAJE EN EL QUE TODO ES TEXTO
Como dije anteriormente, bash solamente maneja texto. Para realizar operaciones matemáticas hay que usar algún comando. Los comandos en bash pueden ser internos (definidos en el programa bash escrito en C), externos (otros programas que son invocados) o definidos por el usuario (por ejemplo, las funciones que estamos creando para ejecutar código python).
Antes de ver cómo realizar operaciones matemáticas en bash date cuenta de que ya sabes cómo hacerlo, ya que tú sabes hacerlo en python y puedes invocar a python desde bash.
Observa el siguiente código:
x=5+5
echo $x
El resultado de este código no es el que nos gustaría:
5+5
En bash todo es texto, esté o no entrecomillado.
Puedes implementar un comando que haga calculos así:
function calc { python3 -c "print($1)"; }
Recuerda que con print
podemos comunicar ambos lenguajes, ya que no hay problema en pasar texto a un lenguaje que solo maneja texto.
Como vimos anteriormente, puede pasarse aquello que un comando imprime en la salida estándar a una variable envolviéndola en $()
:
#!/bin/bash
function calc { python3 -c "print($1)"; }
x=$(calc 5+5)
echo El valor de x es $x
Si ejecutas este código el resultado es el siguiente:
El valor de x es 10
En realidad bash dispone de un comando para realizar operaciones matemáticas. El comando let
:
#!/bin/bash
let x=5+5
echo El valor de x es $x
Ahora bien, imagina que has creado un script bash del que te sientes orgulloso. Durante el desarrollo de este has realizado operaciones matemáticas utilizándo el comando calc
que habíamos definido previamente para que python hiciese el trabajo, ya que no sabías de la existencia del comando let
. Seguro que no te apetece sustituir calc
por let
cuando has invocado a calc
muchas veces en un script de cientos de líneas.
¡no tienes por que hacerlo! simplemente redefine tu comando calc:
#!/bin/bash
function calc {
r=0
let r=$1
echo $r
}
x=$(calc 5+5)
echo El valor de x es $x
Como ves, puedes utilizar python en bash sin ningún problema. Si mas adelante quieres optimizar tus scripts bash y que estos no dependan de python solamente tienes que redefinir los comandos que invocaban a python. Tu trabajo no habrá sido en vano.
COLORES EN EL TERMINAL
Permiteme decirte que si conoces el módulo colorama de python tú ya sabes como imprimir texto de colores en bash.
Observa el siguiente script:
#!/bin/bash
function color {
python -c "
from colorama import *
init()
print(repr($1)[1:-1])
"
}
red=$(color Fore.RED)
yellow=$(color Fore.YELLOW)
cyan=$(color Fore.CYAN)
reset=$(color Fore.RESET)
echo -e "${red}HOLA"
echo -e "${yellow}MUNDO"
echo -e "${cyan}BASH${reset}"
Ponte cómodo y ejecútalo:
¿no te parece bonito? quizás soy un friki loco pero a mi si :P
Para imprimir texto que incluya secuencias de escape hay que usar echo -e
. Hemos definido un comando color
cuyo primer parámetro formatea un script python para que nos devuelva la secuencia de escape correspondiente al color que deseamos.
Nos hemos referido directamente a Fore.RED
y los demás colores en colorama directamente desde bash, has calificado los colores igual que lo harías en python, ¡pero en bash! :D
Podrías haber asignado directamente a las variables que representan el color (red
, yellow
, etc) su correspondiente secuencia de escape, pero tú no tienes por qué sabertela de memoria.
En realidad, para obtener el código de 3 o 4 colores podrías abrir el REPL de Python, verlos en el terminal y copiarlos, pero estamos aquí para hacer travesuras :P.
TIEMPOS MUERTOS: sleep
Voy a exponer un último ejemplo antes de entrar al siguiente apartado.
En python podrías hacer un programa como el siguiente:
#!/bin/python3
import time
for i in "Hola", "Mundo", "Python":
print(i)
time.sleep(1)
Y en bash lo implementarías así:
#!/bin/bash
for i in Hola Mundo Bash; do
sleep 1
echo $i
done
Si tú quisieses crear este programa en bash pero desconoces la existencia del comando sleep
lo podrías haber implementado tú mismo:
#!/bin/bash
function sleep { python -c "import time;time.sleep($1)"; }
for i in Hola Mundo Bash; do
sleep 1
echo $i
done
Habrías implementado un comando que ya existía, pero lo que has hecho no es inútil. Por si no te has dado cuenta has redefinido un comando de bash que ya existía.
Si redefines el comando sleep
en el archivo ~/.bashrc ¿sabes lo que estás haciendo? en el ordenador en el que hagas esto estás modificando la funcionalidad de todos los scripts bash que utilizan el comando sleep
. Esta acción es propia de un hacker hecho y derecho.
Si en el archivo ~/.bashrc defines la siguiente función:
function sleep { shutdown $1; }
Y reinicias el ordenador (o ejecutas source ~/.bashrc
para aplicar los cambios en el shell actual), ¿sabes lo que estás haciendo? has hackeado el ordenador, y cada vez que un script bash utilice el comando sleep en ese ordenador, en lugar de esperar y continuar esperará y se apagará.
TROLEAR REDEFINIENDO COMANDOS
Puedes redefinir comandos de bash para que estos invoquen a tu código python. Recuerda que si redefines comandos en el archivo ~/.bashrc y ejecutas source ~/.bashrc
estás modificando la funcionalidad de estos comandos en el shell actual y en todos aquellos que se ejecuten posteriormente (en esta y en las sucesivas sesiones). Vamos a ver algunos ejemplos:
EJEMPLO 1: pwd
Puedes reimplementar pwd
para que funcione como cabría esperar (pero usando python) así:
function pwd { python3 -c "import os;print(os.getcwd())"; }
pwd
En mi caso el resultado es este:
/home/tecnobillo
Pero si eres un friki de El Señor de los Anillos y un troll profesional:
function pwd { echo NO TIENES PODER AQUÍ; }
Si en el mismo shell donde has hecho esta definición ejecutas pwd
:
NO TIENES PODER AQUÍ
Utilizando python puedes crear una versión fake de pwd
que mejore el troleo. Puedes hacer que se visualize un texto aleatorio en pantalla:
function pwd { python3 -c "
import random
outputs = [
'NO TIENES PODER AQUÍ',
'El comando \"pwd\" ha salido a pasear. Vuelve mas tarde.',
'Se requieren conocimientos de C++ para ejecutar \"pwd\"',
'VUELVE A WINDOWS NOOB'
]
random_i = random.randrange(0, len(outputs))
print(outputs[random_i])
"; }
Al ejecutar pwd
en el shell donde has hecho esta definición el resultado puede ser cualquiera de los siguientes:
NO TIENES PODER AQUÍ
El comando "pwd" ha salido a pasear. Vuelve mas tarde.
Se requieren conocimientos de C++ para ejecutar "pwd"
VUELVE A WINDOWS NOOB
EJEMPLO 2: curl
EL comando curl
permite obtener recursos web. Por ejemplo si ejecutas:
curl https://api.ipify.org/?format=text
Se imprime en pantalla la dirección ip pública. En mi caso:
188.93.38.163
Podrías redefinirlo así y funcionaría igual:
function curl {
python -c "
import urllib.request
with urllib.request.urlopen('$1') as response:
print(response.read().decode('utf-8'))
"
}
curl https://api.ipify.org/?format=text
En este caso funciona exactamente igual, aunque no hemos definido el comportamiento de curl
cuando recibe ciertos parámetros. Nuestro curl solamente acepta un parámetro.
En cualquier caso, puedes modificar completamente el funcionamiento del comando curl
para trolear como un pro. Por ejemplo, podrías hacer que este comando se comporte aparentemente con normalidad, pero que además envíe información a un servidor de tu propiedad de manera silenciosa. Espero que tu moralidad te impida hacer cosas así, ya que los trolls solamente gastamos bromas.
En vez de ser tan malvado puedes limitarte a trolear a tu amigo Juan:
function curl {
python -c "
import urllib.request
with urllib.request.urlopen('$1') as response:
print('Tu dirección ip es ' + response.read().decode('utf-8') + ' Juan, te estoy vigilando y hoy estás muy guapo :D')
"
}
curl https://api.ipify.org/?format=text
El resultado (con mi ip mientras escribo esto) es:
Tu dirección ip es 188.93.38.163 Juan, te estoy vigilando y hoy estás muy guapo :D
NOTAS FINALES
En este artículo he querido mostrar que se pueden hacer cosas muy interesantes combinando python y bash. Espero haberte motivado a aprender un poco mas sobre la línea de comandos, porque hacerlo no significa reemplazar python por bash, sino aprender formas creativas de hacer las cosas con python extendiendo su funcionalidad desde bash.
Si hay algo que me gusta del mundo de la programación es que hay muchas formas de hacer las cosas y es imposible saberlo todo.
- programar es divertido -
INFROMACIÓN ADICIONAL
A continuación hablaré sobre algunas cosas mas que pueden hacerse con python y bash, pero las agrupo bajo el título "Información adicional* porque lo mejor y mas divertido de este post ha sucedido en el apartado anterior. Pero si estás interesado continúa leyendo:
Batería de programas python en un script
Puedes crear un script bash que sea un contenedor de scripts python, por ejemplo:
function program1 {
python -c "
print('Soy el Programa 1')
"; }
function program2 {
python -c "
print('Soy el Programa 2')
"; }
function program3 {
python -c "
print('Soy el Programa 3')
"; }
program1
program2
program3
Soy el Programa 1 Soy el Programa 2 Soy el Programa 3
Puedes hacer que se ejecute un determinado programa embebido manejando los argumentos que se pasan al script bash:
function program1 {
python -c "
print('Soy el Programa 1')
"; }
function program2 {
python -c "
print('Soy el Programa 2')
"; }
function program3 {
python -c "
print('Soy el Programa 3')
"; }
program=$1
$program
Puedes hacer que se ejecute un programa u otro, que se ejecuten todos, que se ejecuten algunos según ciertas condiciones, que se ejecuten en distinto orden, que para que se ejecute uno primero tenga que haberse ejecutado otro con éxito, etc.
Por ejemplo, puedes ejecutar un programa python que pregunte la contraseña al usuario, y si la contraseña es correcta que se ejecute el programa principal:
function ask_pass {
password=123
python3 -c "
import sys
password = input('PASSWORD: ')
if password == '$password':
sys.exit(0)
else:
sys.exit(1)
"; }
function main_program {
python3 -c "
print('Has indicado la contraseña correcta')
print('Se ha ejecutado el programa principal')
"; }
ask_pass
if [ $? == 0 ]; then
main_program
exit 0
else
echo "Contraseña incorrecta, fin del programa..."
exit 1
fi
Aquí estamos usando bash como código pegamento.
Puedes combinar Python y Bash para hacer scripts realmente interesantes.
Agrupar varios programas en un solo script bash también puede ser interesante para distribuir muchos programas en un solo archivo. Por ejemplo puedes crear un programas.sh que contenga tus programas python, y publicarlo en Internet para que pueda descargarse mediante wget http:/tudominio.com/programas.sh
. Así un usuario habrá todos tus programas accediendo únicamente a un recurso web. Esto puedes verlo como una forma de comprimir tus programas para distribuirlos: podrías añadir funcionalidad para exportar los programas internos a script externos y cosas por el estilo.
Envolver para determinar funcionalidad
Imagina que tienes un program.py que realiza una determinada tarea y quieres que dicha tarea se ejecute obligatoriamente en segundo plano.
Por ejemplo:
#!/bin/python3
import time
import datetime
for i in range(100):
with open('log.txt', 'a') as f:
f.write(str(datetime.datetime.now())+'\n')
Si el programa es ejecutable, el usuario puede ejecutarlo así:
./program.py
Y para ejecutarlo en segundo plano podría hacerse así:
./program.py &
Pero el usuario tiene la opción de ejecutarlo de las dos formasa. Si tú quieres que siempre sea ejecutado en segundo plano puedes crear un script en bash llamado program.sh:
#!/bin/bash
./program.py &
Ejecutando ./program.sh
el programa trabajará en segundo plano. Pero en este caso tendrías que distribuir dos archivos implicados en la ejecución de tu programa: program.py y program.sh.
Lo que puedes hacer es crear un wrapper en bash que contiene tu código python y lo ejecuta en segundo plano usando &
:
#!/bin/bash
function main { python -c "
import time
import datetime
for i in range(100):
with open('log.txt', 'a') as f:
f.write(str(datetime.datetime.now())+'\n')
"; }
main &
De esta manera solamente tienes que distribuir un archivo: program.sh, que contiene tu programa python y lo ejecuta en segundo plano de manera predeterminada.