Bash: Teoría del intérprete de órdenes (scripts)

Si bien lo usamos generalmente para operaciones administrativas o de gestión de archivos, la consola de Linux extiende su funcionalidad mucho más allá de ese propósito, permitiéndonos programar scripts acorde a nuestras necesidades.

bash

Este artículo no pretende ser una referencia completa sobre la programación en Bash, sino una introducción a los comandos y estructuras básicas, lo cual nos permitirá ampliar el poder de nuestro sistema GNU/Linux.

¿Qué es un “Script”?

Básicamente decimos que es un archivo que contiene código escrito en determinado lenguaje de programación que el sistema usa para determinada tarea. No es necesario que tenga una entrada externa ni interfaz gráfica, pero sí que provoque una salida de datos procesados (por más de que el usuario no los vea).

El lenguaje usado por Bash está definido por su propio intérprete y combina la sintaxis de otros Shells, como el Korn Shell (ksh) o el C Shell (csh). Muchos de los comandos que usualmente se usan en la consola también pueden usarse en los scripts, salvo aquellos que pertenecen estrictamente a una distribución en particular.

1. Cosas que se le pasan a la mayoría de la gente

/bin/bash o /bin/sh

Una de las primeras cosas que hace la máquina al ejecutar nuestro script es mirar con qué shell debe hacerlo. En la mayoría de los sistemas linux actuales /bin/sh es un link a /bin/bash, pero no siempre es así, por ejemplo en las distribuciones que utilizan busybox traen Sh y normalmente también traen Bash, pero si se utiliza /bin/sh, no se ejecutará con Bash. Por eso recomiendo utilizar siempre /bin/bash.

Unicode vs ASCII

¿Os habéis preguntado por qué no podéis utilizar “¿” o “ñ” en vuestros scripts? ¿O utilizar tildes? Puede llegar a ser bastante molesto en scripts interactivos. Eso es porque la codificación por defecto de Bash es ASCII, o lo que es lo mismo, el set de caracteres del inglés. Para cambiarlo solo tenemos que indicarle a nuestro script que queremos utilizar Unicode. Para eso hay que añadir una línea justo después del intérprete de comandos:

# -*- ENCODING: UTF-8 -*-

Ojo, es importante que esta línea esté al principio del script.

Hacer el script ejecutable

Es curioso la cantidad de gente que ejecuta los scripts con “$ bash script.sh” en vez de “$ ./script.sh” al fin y al cabo es para eso que hemos definido un intérprete de comandos.

Para añadirle permisos de ejecución hay que ejecutar:

chmod +x script.sh

Si nuestro script es ejecutable, lo podemos añadir a nuestro PATH y hacer que sea ejecutable desde cualquier lugar/ carpeta de nuestro equipo. Para eso hay que añadir ya sea al .bashrc de nuestro usuario o a /etc/bashrc la línea

BIN="carpeta donde tengamos los scripts"
PATH="$BIN$PATH"

Es una norma de Bash escribir los nombres de variables todo en mayúsculas. Mucha gente no sigue esta norma, pero para scripts largos se agradece porque los hacen mucho mas legibles

2. Estructura de un script

  1. Cabecera
  2. Definición de variables globales
  3. Ayuda
  4. Funciones
  5. Cuerpo principal

La cabecera es donde se indica qué shell queremos utilizar y la codificación. La ventaja de las funciones es reutilizar código que se repite escribiéndolo una sola vez y hacer mas fácil el entendimiento del script, para código que supera las 100 líneas es muy útil.

Para poder utilizar funciones, estas deben estar definidas con antes del cuerpo principal de nuestro script. Y si queremos utilizar variables a nivel global de todo nuestro script, tanto en el cuerpo principal como en las funciones, debemos definirlas al principio de todo, justo después de la cabecera.

Por último, es de buena práctica escribir una función de ayuda para cuando nuestro script se ejecute mal o con malos parámetros. Obviamente, es esos casos queremos salir del script inmediatamente, sin leer las funciones. Para eso podemos utilizar:

function help(){
echo """Nuestro texto de ayuda bien formateado."""
exit
if [[ -z $1 || $1 == "-h" || $1 == "--help" ]]; then
help
fi

Si añadimos “exit” a la función de ayuda, saldremos del script cada vez que ejecutemos ayuda, por ejemplo después de mensajes de errores, etc. Nos ahorramos unas cuantas líneas de código.

La condición indica mostrar ayuda en pantalla y salir si se ejecuta el script sin parámetros o si se indica -h/–help. Si os fijáis ese es el comportamiento estándar de la mayoría de programas linux.

El uso de las 3 comillas con echo permite utilizar saltos de línea sin salir del mensaje a mostrar por echo. Para mensajes multilínea es mucho mas cómodo utilizar echo una sola vez.

3. Imprimir en pantalla

Hay 2 comandos principales para imprimir en pantalla en bash: “echo” y “printf“. Los dos son igual de rápidos y los dos forman parte de bash. La principal diferencia para un principiante es que echo añade un salto de línea al final, mientras que “printf” no lo hace.

Echo está muy bien y es lo que usa la mayoría de la gente, sin embargo cuando se leen el INPUT del usuario, o cuando se quieren imprimir variables sacadas de archivos mediante procesado de texto, pueden pasar cosas raras. Normalmente se solucionan fácilmente, tan fácil como cambiar las dobles comillas a simples o viceversa, o sacar las referencias a variables fuera de las comillas. “Echo” hace cosas raras también dependiendo de cómo se ha compilado, si utilizamos siempre Ubuntu o siempre Fedora, no nos afecta, pero si cambiamos de distribución si.

Por eso utilizo “printf“, que no me da dolores de cabeza y además se comporta mas como el “printf” de C o el “print” de Python, eso es muy importante si algún día queréis portar vuestro script a otro lenguaje de programación.

Para una discusión mas extensa podéis visitar esta pregunta de Unix & Linux en Stack Exchange.

4. Leer el INPUT del usuario

Todo lo que escribamos después del nombre de nuestro script y antes de darle a la tecla ENTER queda automáticamente guardado en unas variables especiales. Dichas variables son del tipo $X donde la X es un número.

$0” indica el nombre de nuestro script y desde “$1” hasta el infinito son variables todo lo que hemos escrito después. Por ejemplo:

cat << EOF >> test.sh
#!/bin/bash
# -*- ENCODING: UTF-8 -*-
printf "\$0 = $0\n"
printf "\$1 = $1\n"
printf "\$2 = $2\n"
EOF
chmod +x script.sh
./script.sh mi archivo.txt

Creamos un script de prueba, lo hacemos ejecutable y lo ejecutamos con 2 parámetros. Obtenemos la salida en pantalla de:

$0 = ./script.sh
$1 = mi
$2 = archivo.txt

Utilizando comillas podríamos haber pasado “mi archivo.txt” a “$1″.

También podemos leer el INPUT de un usuario con el comando “read”, indicando directamente la variable donde queramos guardar el parámetro. Por ejemplo:

printf "¿Cómo te llamas?\n"
read NOMBRE
printf "Hola, $NOMBRE.\n"

Ojo con la asignación de variables. “$VAR = contenido” va a producir un error, no se pueden dejar espacios entre el signo igual, el nombre de la variable y el contenido. El uso correcto es “VAR=contenido”

5. Cálculos en Bash

Para eso podemos utilizar “expr“, siempre que no necesitemos hacer cáclculos complejos. Hay que destacar dos cosas, la primera es que “expr” sólo admite números enteros, la segunda es que la división devuelve el resultado entero, para ver el resto podemos utilizar “%“.

Normalmente querremos asignar el resultado de expr a una variable. Eso lo podremos hacer de dos formas:

VAR2=`expr $VAR1 / 10`
VAR2=$(expr $VAR1 / 100)

También se puede obviar “expr” utilizando doble paréntesis:

VAR2=$(($VAR1 / 100))

Para una mayor explicación de “expr” o una alternativa que utilice números enteros, podéis mirar esta entrada de KZKG^gaara.

6. Condiciones

Se ha escrito ya de una manera muy extensa sobre “if“, “else“, “elif” y las condiciones. Podéis leer acerca de eso en:

Sólo quiero destacar la diferencia entre el uso de simples corchetes, “[ ]“, y dobles corchetes, “[[ ]]“, para la condiciones. Con dobles corchetes podemos utilizar condiciones adicionales:

  • &&” para y
  • ||” para o

Para utilizar “&&” y “||” con simples corchetes habría que separar cada parte entre corchetes separados. El ejemplo utilizado para la parte del script que mira si hay que ejecutar ayuda quedaría:

if [ -z "$1" ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]]; then
help
fi

También nos evita el tener que escribir los nombres de variables dentro de comillas para prevenir errores. Por ejemplo:

if [ $1 = 1 ]; then printf "El parámetro es igual a 1."; fi
if [ "$1" = 1 ]; then printf "El parámetro es igual a 1."; fi
if [[ $1 = 1 ]]; then printf "El parámetro es igual a 1."; fi

Si se ejecuta script.sh sin ningún parámetro, el primer caso daría un error:

bash: [: =: se esperaba un operador unario

En Bash “=” y “==” se interpretan ambos de la misma manera. Esto no pasa en otros lenguajes de programación donde “=” se utiliza sólo para asignar variables.

De lo que no se ha hablado es de “case“, que se utiliza para simplificar “if“. Comencemos por el principio, cuando no tenemos ningún “if” se ejecutará todo el código. Si añadimos una condición “if” tendremos dos casos, uno en el que se ejecuta el bloque de código que está dentro del “if” y el otro caso donde no se ejecuta dicho bloque.

Si añadimos un “else“, también tendremos dos casos, pero estos dos casos son diferentes a los anteriores. Porque ahora habrá dos bloques de código condicionales, A y B, y un bloque C, que es el resto del programa. Se ejecutará A o B, y C. En el caso anterior era A y C o sólo C.

Para evitar escribir condiciones “if/else” dentro de “else” y simplificar la lectura del código, se creo “elif“. Cuando tenemos muchas condiciones que dependen de la anterior, por ejemplo rango de números o del tipo:

VAR1=$1
if [[ $VAR1 = 1 ]]; then
printf "1\n"
elif [[ $VAR1 = 2 ]]; then
printf "2\n"
elif [[ $VAR1 = 3 ]]; then
printf "3\n"
else
printf "ninguno\n"
fi

En el caso del último “elif” se leerán muchas condiciones. Con case se agiliza este proceso:

VAR1=$1
case $VAR in
1)
printf "1\n"
;;
2)
printf "2\n"
;;
3|4)
printf "3 o 4, depende\n"
;;
*)
printf "ninguno\n"
;;
esac

Se leerá una variable, en este caso VAR1, y se mirará si equivale a alguno de los casos, si no, se ejecutará el caso por defecto “*”. Los dobles punto y coma equivalen a “break“, le indican a “case” que tiene que acabar.

Case” se puede utilizar también como una sucesión de “if“, para eso hay que utilizar “;;&” (continuar) en vez de “;;” (parar).

7. Bucles

Son muy pocos los bucles a saberse en cualquier lenguaje de programación. En Bash son “while“, “until” y “for“. Se ha escrito ya en el blog acerca de estos:

Hay dos tipos de bucles “for“, los que son del tipo “$ for VAR in LOQUESEA” y lo que son del tipo C “$ for ((I=0; I<=10; I++))“. El segundo tipo de bucles “for” son muy útiles, tiene 3 partes en el inicio del bucle:

  • Declaración e iniciación de variables (En este caso una variable auxiliar “I=0″).
  • Condición de ejecución (hasta que I sea menor o igual que 10).
  • Incremento de la variable auxiliar

En mi opinión es el bucle más potente de todos. Un ejemplo, que imprime todos los números desde el 0 hasta el 10, ambos inclusive:

#!/bin/bash
for ((I=0; I<=10; I++)); do
printf "$I\n"
done

8. Funciones

Hay algunas cosas que Bash no nos permite hacer, ¿o si? En un primer vistazo, las funciones de bash impiden hacer 3 cosas: declarar variables locales en las funciones, pasarle parámetros a las funciones y devolver parámetros. Todo tiene solución.

No hacer nada por el estilo de:

#!/bin/bash
VAR=1
printc "$VAR\n"
function hola() {
VAR=2
printf "$VAR\n"
}
hola
printf "$VAR\n"

Esto imprime en pantalla 1, 2 y 2.

Para declarar variables locales hay añadir “local” cuando se declara:

#!/bin/bash
VAR=1
printf "$VAR1\n"
function foo() {
local VAR1=2
printf "$VAR1\n"
}
printf "$VAR1\n"
foo
printf "$VAR1\n"

Esto imprime en pantalla 1, 1, 2, 1.

¿Cómo se le pasan parámetros a una función?

#!/bin/bash
# -*- ENCODING: UTF-8 -*-
function hola() {
printf "Hola $1\n"
}

printf “¿Cómo te llamas?\n”
read VAR1
hola $VAR1

¿Cómo se devuelven parámetros?

#!/bin/bash
# -*- ENCODING: UTF-8 -*-
function hola() {
printf "Hola holita"
}
printf "¿Cómo te llamas?\n"
read VAR1
VAR1=$(hola) # AQUI ESTÁ
printf "$VAR1 $VAR2\n"

Como veis esto tiene dos pegas, sólo se puede devolver un parámetro, que puede ser un vector, y si se quiere devolver un parámetro, ya no se puede imprimir en pantalla desde esa función.
Podéis encontrar más cosas de funciones en este artículo de Usemoslinux.

9. Getops

Una de las últimas cosas de Bash que hay que conocer para crear scripts complejos es “getops“. Sirve para pasar opciones al script sin importar el orden. La única pega es que sólo afecta opciones cortas:

#!/bin/bash
# -*- ENCODING: UTF-8 -*-
VARC=0
function help() {
printf "Mensaje de ayuda\n"
exit
}
if [[ -z $1 ]]; then
help
fi
while getopts :ha:b:c OPT; do
case $OPT in
h)
help
;;
:)
help
;;
a)
VARA=$OPTARG
;;
b)
VARB=$OPTARG
;;
c)
VARC=1
;;
\?)
help
;;
esac
done
# Bloque principal del script que
# hace cosas con VARA, VARB y VARC

Getopts” lee las opciones una a una, por eso se necesita de un bucle.

Hay 2 tipos de opciones que se pueden pasar usando “getopts“:

  • Los parámetros llamados flags, en este caso -c o -h. Se especifican con la letra que queramos usar. Son como variables booleanas, “true” (están) o “false” (no están).
  • Parámetros con argumentos asociados, -a cualquiercosa, -b cualquiercosa. Se especifican con la letra que queramos con dos puntos a continuación. El argumento es guardado en OPTARG (este nombre es inalterable).

Los dobles puntos iniciales son para no mostrar errores.

¿Qué hace este script?

Muestra el mensaje de ayuda cuando no se pasa ninguna opción, cuando se pasa el parámetro “-h”, cuando se pasa un parámetro no válido (por ejemplo “-x”, esto lo hace “\?”) o cuando se pasa un parámetro válido sin argumento (“:”). En el resto de los casos guarda la presencia de “-c” como un 1 en VARC, y los valores pasados con “-a” y “-b” en VARA y VARB.

Artículo original en desdelinux.net