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:


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:

  1. print - Imprimiendo el resultado de python en la salida estándar, siendo esta una variable de bash.
  2. 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:

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:

HOLA
MUNDO
BASH

¿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:


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.