Try   HackMD
tags: ASO-GIT

Programación de shell scripts para la BASH

Introducción

La shell es 1)una potente interfaz con el sistema operativo basada en línea de comandos y 2)un interprete de scripts. Un shell script es un programa realizado en el lenguaje de la shell. Dicho de una forma sencilla y generalizando, un shell script es un fichero de texto que contiene órdenes que también podrían ser ejecutadas desde la línea de comandos.

Los shell script son muy utilizados en la administración de los sistemas ya que permiten, con una sola orden, ejecutar múltiples comandos, generalmente para automatizar alguna tarea. Ejemplos de tareas son la realización de copias de seguridad, la comprobación del espacio libre, la rotación de ficheros de log, etc. Cuando estas tareas se realizan de forma periódica su ejecución además se suele automatizar utilizando cron.

La shell

Hay múltiples implementaciones de shell: Bourne shell (sh), Bourne-Again shell (bash), Debian Almquist shell (dash), C shell (csh), etc. En Ubuntu, sh era la shell por defecto para el arranque del sistema, aunque desde hace años esa labor ha pasado a manos de dash (para minimizar los cambios, ahora en Ubuntu /bin/sh es un enlace simbólico a dash). Para los usuarios, la shell por defecto es bash (la shell de inicio de un usuario concreto se encuentra en /etc/passwd).

Para saber qué shell se está usando (ya sea en un terminal, ya sea en un script) se puede consultar la variable de entorno $SHELL

echo $SHELL

Ejecución de un shell script

Como se ha dicho, los shell script son ficheros de texto, por lo cual se pueden crear con cualquier editor: vi, nano, gedit, etc.

Empecemos con el siguiente ejemplo:

#!/bin/bash
#  Programa: escribe3.sh
# Propósito: Escribe tres veces el parámetro pasado
#     Autor: Javier Macías
#     Fecha: 12/11/2019

echo $1 $1 $1
exit 0

El símbolo # indica el comienzo de comentario (puede ir en cualquier parte de la línea, pero si va precedido de una orden debe ir separado por un blanco).

En el ejemplo, la primera línea, aunque parece un comentario, tiene un significado especial. #!/bin/bash se llama shebang (el nombre viene del sonido de las dos primeras letras de #, SHarp, y de ! que se denomina bang en la cultura hacker). Esta línea dice a la shell que lo ejecuta qué intérprete de comandos usar de no especificarse uno concreto para su ejecución. En este primer caso, es la bash quien ejecutaría ese script, pero podría haber sido cualquier otro intérprete, por ejemplo:

#!/usr/bin/python
#  Programa: holapy.py
# Proposito: Hola mundo en python
#     Autor: Oscar Garcia
#     Fecha: 12/11/2019

print("Hola mundo, soy Python!")

Nota: El shebang es optativo (pero muy recomendable) y debe aparecer siempre en la primera línea. Por otro lado, Linux no usa las extensiones de los ficheros para saber su tipo, aunque es recomendable su utilización (así además evitamos que el nombre del script coincida con alguna orden de Linux). Para saber el tipo de fichero Linux utiliza los "números mágicos". En el caso de los script, que son ficheros de texto, el shebang ayuda a reconocerlos.

Ahora podemos ejecutar el primer programa, por ejemplo, especificando que sea con bash:

$ bash escribe3.sh Hola
Hola Hola Hola

También podríamos haber utilizado dash:

$ dash escribe3.sh Hola
Hola Hola Hola

Se puede hacer que sea la propia shell actual la que ejecute el script (con lo cual, por ejemplo, se puede hacer que el script varíe las variables de entorno de la shell actual) mediante la orden source (que también se puede abreviar como . ):

$ source escribe3.sh Hola  #comentar antes la línea exit 0 ¿sabes por qué?
Hola Hola Hola

Para hacer que el script se ejecute con la shell especificada en el shebang, el fichero tiene que tener como mínimo permisos de lectura y ejecución:

$ chmod 755 escribe3.sh

Ahora ya podemos llamarlo directamente desde la línea de comando:

$ ./escribe3.sh Hola
Hola Hola Hola

Es necesario especificar la ruta del fichero porque, por razones de seguridad, Linux no busca por defecto el fichero en el directorio actual (solo lo hace en los directorios que se encuentran en la variable de entorno PATH).

Ejercicio: Explica qué ocurre aquí:

$ chmod 755 holapy.py 
$ bash holapy.py
h.py: line 7: syntax error near unexpected token `"Hola mundo, soy Python!"'
h.py: line 7: `print("Hola mundo, soy Python!")'
$ ./holapy.py
Hola mundo, soy Python!
(solución)

El error que aparece en la primera llamada al script es debido a que estamos intentando ejecutar un script Python con bash. En la segunda, como no especificamos ninguna shell y existe un shebang, se utiliza éste (/usr/bin/python) para su ejecución, obteniéndose el resultado esperado.

Para depurar scripts, las shell disponen de diversas opciones. Por ejemplo, bash tiene entre otras -x (Print commands and their arguments as they are executed) y -v (Print shell input lines as they are read). Estas opciones se pueden incluir en el shebang y/o en la ejecución explícita con bash. Ejemplo

$ bash -x escribe3.sh Hola
+ echo Hola Hola Hola
Hola Hola Hola

Paso de argumentos a script

Se utilizan los siguientes identificadores:

  • $0 nombre del script. Si no se quiere el path, se puede usar $(basename $0)
  • $i (con i entre 1 y 9), i-esimo parámetro
  • ${i} (obligatorio para i a partir de 10, aunque se puede usar para cualquier valor de i), i-ésimo parámetro
  • $# Número de argumentos
  • $* Todos los argumentos

Devolución del resultado de la ejecución del script

Cuando se ejecuta una orden en la shell, va a devolver un valor (exit status) que indica si el resultado de la ejecución ha sido correcto (0) o ha habido algún problema (>0). Este valor se almacena en la variable $? como se ve en el siguiente ejemplo:

$ cd esteDirectorioNoExiste
bash: cd: esteDirectorioNoExiste: No such file or directory
$ echo $?
1
$ echo $? #EJERCICIO: explica por qué ahora aparece el valor 0
0
(Solución)

El segundo echo $? lo que muestra es el resultado de la ejecución de la orden anterior, que es echo $? y que se ha ejecutado correctamente, por lo cual se muestra 0.

De igual forma, los script pueden devolver un valor de salida determinado mediante la orden bash exit (que además hace que termine la ejecución del script) seguida de un número que es el que se desea que se retorne de exit status. Si la última orden ejecutada antes de terminar el script no indica explícitamente el valor retornado (esto es, se termine en una instrucción distinta de exit seguida del valor a retornar), el valor de salida es el exit status de dicha última orden.

Declaración de variables

Al igual que en la línea de comando, en un script se pueden definir variables. Éstas pueden ser "locales" (existen solo en la shell donde se han declarado) o "globales" o de entorno, que son además accesibles a todas las subshell que se ejecuten desde la shell en la que se han declarado.

Por ejemplo, sean los siguientes script:

#!/bin/bash
#  Programa: p1.sh
# Propósito: Escribe tres veces el parámetro pasado
#     Autor: Javier Macías
#     Fecha: 12/11/2019

varLocal=1
export VAR_ENTORNO=2

echo "p1.sh: varLocal=$varLocal   VAR_ENTORNO=$VAR_ENTORNO"
./p2.sh



#!/bin/bash
#  Programa: p2.sh
# Propósito: Escribe tres veces el parámetro pasado
#     Autor: Javier Macías
#     Fecha: 12/11/2019

echo "p2.sh: varLocal=$varLocal   VAR_ENTORNO=$VAR_ENTORNO"

Razónese el resultado obtenido por las siguientes órdenes:

$ ./p1.sh
p1.sh: varLocal=1   VAR_ENTORNO=2
p2.sh: varLocal=   VAR_ENTORNO=2
$ echo $varLocal $VAR_ENTORNO

$ source ./p1.sh
p1.sh: varLocal=1   VAR_ENTORNO=2
p2.sh: varLocal=   VAR_ENTORNO=2
$ echo $varLocal $VAR_ENTORNO
1 2
(Solución)

Al invocar p1.sh con source, dicho script se ejecuta en la propia shell por lo cual se puede acceder al contenido de ambas variables.

Lecturas interactivas

En bash se pueden hacer lecturas interactivas usando la orden interna read, que además puede escribir, con la opción -p, un prompt o mensaje previo. Además se puede especificar la variable donde se guardará la entrada del usuario (de no especificarse, se guarda en la variable REPLY)

Por ejemplo:

$ read -p "¿Edad? " edad
¿Edad? 20
$ echo $edad
20

Hay otras opciones interesantes, como -s (de silence que hace que la entrada de teclado no se muestre por pantalla) y -nX que hace que la lectura sea como máximo de X caracteres (si se llega al tope sin pulsar intro, se termina la lectura y se pasa a la siguiente orden). Un uso común se muestra a continuación:

$ read -sn1 -p "Pulsa una tecla para terminar"

Condicionales con if

En shell scripts es posible alterar el camino de ejecución en función del valor de retorno de una instrucción cualquiera. Por ejemplo:

if mkdir carpeta
then
    echo Acabo de crear una carpeta
else
    echo No puedo puedo crear carpeta porque ya existe; la borraré
    rm -Rf carpeta
fi

Es importante tener esto en cuenta: lo que evalúa if no es una condición booleana sino el resultado (exit status) de ejecutar una instrucción. Es fácil olvidarse de esto dado que en la mayoría de los lenguajes de programación, la clausula if evalúa una expresión lógica que puede ser true o false.

Ejercicio: escribir un script que reciba como argumento un nombre de usuario. El script debe verificar si dicho usuario existe (si existe en el archivo /etc/passwd) y emitir un mensaje por stdout indicando si existe o no.

(solución)

Se puede hacer evaluando en un if el resultado de hacer grep ^$USUARIO: /etc/passwd . Nótese que se pone ^ para especificar que el nombre de usuario debe encontrarse a principio de línea y : es el separador de campos en el fichero passwd.

Si realmente necesitamos hacer comprobaciones lógicas entonces necesitamos una orden que las haga por nosotros: la orden test

Orden test

test es una orden de la shell que evalúa expresiones booleanas simples de ficheros, números y cadenas, devolviendo (exit status) 0 en caso verdadero y 1 en caso falso.

$ test 10 -lt 5   #¿es 10<5?
$ echo $?
1

Nota: se pueden usar la notación de corchetes, por ejemplo [ 10 -lt 5 ], donde el corchete de apertura equivale a test (por eso es necesario que tras el corchete vaya un espacio en blanco).

Algunos argumentos comunes (nótese que cada uno es un argumento y que por tanto deben estar separados por espacios):

Comprobación de test con ficheros
-d nombre nombre es un directorio
-f nombre nombre es un fichero regular
-r nombre nombre existe y se puede leer
-w nombre nombre existe y se puede escribir
-x nombre nombre existe y es ejecutable
-s nombre nombre existe y su tamaño no es cero
f1 -nt f2 f1 es más reciente que f2
f1 -ot f2 f1 es más antiguo que f2

Comprobaciones de test con cadenas
s1 = s1 s1 igual a s2
s1 != s2 s1 distinta a s2
-z s s tiene longitud 0
-n s s tiene longitud mayor que cero

test numéricos (números enteros)
a -eq b a igual a b
a -ne b a distinto de b
a -gt b a mayor que b
a -ge b a mayor o igual a b
a -lt b a menor que b
a -le b a menor o igual que b

Combinación y negación de test
t1 -a t2 And: los test t1 y t2 son ciertos
t1 -o t2 Or: t1 o t2 es cierto
!t Niega t
\( \) Agrupa test

Órdenes if / if else / if elif

La sintaxis de la orden if es:

if condición
then
  orden(es)
fi

Es muy habitual poner el then en la misma línea que el if, lo cual requiere un ;

if condición; then
  orden(es)
fi

Como ya se ha visto, es posible también utilizar un if-else:

if condición; then
  orden(es)
else
  orden(es)
fi

Cuando intervienen varias condiciones, es más cómodo usar el if-elif:

if condición1; then
  orden(es)
elif condición2; then
  orden(es)
elif condición3; then
  orden(es)
...
else
  orden(es)
fi

Ejemplo:

#!/bin/bash
#  Programa: ordenados3.sh
# Propósito: Indica si los tres argumentos pasados están en orden de
#            menor a mayor.
#     Autor: Javier Macías
#     Fecha: 12/11/2019

if test $# -ne 3; then
  echo "Número de parámetros erróneo. Uso: $0 arg1 arg2 arg3"
  exit 1
fi

if test $1 -le $2 -a $2 -le $3; then
  echo "Están ordenados: $1<=$2<=$3"
else
  echo "No están ordenados"
fi
exit 0

Orden case

Cuando dependiendo del resultado de una expresión haya múltiples opciones, es mejor utilizar case en vez de if. Su sintaxis es:

case expresion in
    caso1)
        orden(es)
        ;;
    caso2)
        orden(es)
        ;;
    ...
    *)
        orden(es)
        ;;
esac

Es importante que la última orden de cada caso termine en ;; y por ello, para que resalte y sea fácil descubrir un olvido, se suele poner en la línea siguiente.

Ejemplo (nótese que en los casos se pueden usar patrones como los que se utilizan en la expansión de nombres de fichero):

case $1 in
    [Yy]es)
        echo "Sí"
        ;;
    [Nn]o)
        echo "No"
        ;;
    *)
        echo "Opción inválida"
        ;;
esac

Iteración (bucles)

Hay varías instrucciones en la shell que permiten realizar bucles. Se muestran a continuación mediante ejemplos.

blucle while

i=0
while test $i -lt 10; do
  echo $i
  i=$((i+1))  #POSIX shell arithmetic substitution 
done

blucle until

c=0
until test $c -ge 10; do
  echo $c
  c=$((c+1))
done      

bucle for

La primera sintaxis del for es muy similar a C:

for ((i=0; i<10; i++)); do
  echo $i
done

En su segunda sintaxis, permite el recorrido de listas. Por ejemplo:

#!/bin/bash
#  Programa: colores.sh
# Proposito: Ej. recorrido lista de argumentos
#     Autor: Oscar Garcia
#     Fecha: 12/11/2019

for color in $*
do
    echo "Me gusta el $color"
done

Un ejemplo de ejecución sería:

$ ./colores.sh azul rojo naranja
Me gusta el azul
Me gusta el rojo
Me gusta el naranja

Una de las posibles aplicaciones es el recorrido de listas de argumentos cuando dicha lista es muy larga, como por ejemplo por el resultado de la expansión del metacaracter *. ¿Qué ocurriría si el script anterior lo invocamos haciendo ./colores.sh /etc/*?

Bibliografía:

  • The Linux Command Line. Willian Shotts. 2ª ed. No Starch Press.
  • Linux Pocket Guide. Daniel J. Barrett. 3ª ed. O’Reilly.
  • Mastering Linux Shell Scripting. Mokhtar Ebrahim, Andrew Mallett. 2º ed. Packt.