Escritura de scripts de shell

Hemos llegado a un punto donde podemos realizar tareas más complejas a partir de los comandos aprendidos y es aquí donde radica el poder del intérprete de comandos bash. Como veremos a continuación, el intérprete de comandos es un poderoso lenguaje para realizar script que permitan unir varios comandos para realizar una tarea un poco más compleja (y es el este el poder principal de todo Un*x). El único requisito es tener nociones básicas de programación para poder sacar todo el provecho posible de esta característica del intérprete de comandos. En todo caso, con un poco de práctica y un buen sentido de la lógica se podrán hacer también script poderosos para desarrollar las tareas que requerimos.

Deberemos saber también que con la ayuda solamente de la conjunción de comandos no podremos hacer script verdaderamente interesantes. Por esto se incorporan las construcciones de shell. Estas son las construcciones while, for-in, if-then-fi y case-esac. Existen muchas más pero estas serán las más útiles para nosotros en estos momentos. Para mayor información sobre otro tipo de construcciones seria mejor revisar las páginas de manual del intérprete de comandos bash (man bash).

Empezaremos viendo un uso sencillo de la construcción for-in que tiene la siguiente sintaxis

 for var in word1 word2
 do
   commandos
 done

Para poder usar esto, podríamos realizar una lista de directorios que querramos nosotros de una sola vez

 for dir in /bin /etc /lib
 do
   ls -R $dir
 done

Esto hará un listado recursivo 3 veces. La primera vez que pase por el ciclo, la variable $dir tomará el valor /bin, la segunda será /etc y la tercera /lib.

Podríamos prescindir del par do-done con el uso de llaves ({})

 for dir in /bin /etc /lib
 {
   ls -R $dir
 }

Ya hemos visto anteriormente la idea de argumentos en la utilización de comandos y programas; pero deberemos ver como se realiza la codificación de un script para tomar estos argumentos. Como antes dijimos, los argumentos eran pasados a los programas para que estos lo utilizaran. En la construcción de script veremos lo que se llaman variables posicionales cuyo valor corresponde a la posición del argumento luego del nombre del script. Supongamos que tenemos un script que toma 3 argumentos. El primero será el nombre de un directorio, el segundo el nombre de un archivo y el tercero es una palabra a buscar. Este script buscará en todos los archivos del directorio, cuyo nombre incluya el nombre de archivo que le pasamos como argumento, la palabra que también le estamos pasando. El script se llamara miscript y estará compuesto del siguiente código

ls $1 | grep $2 | while read ARCHIVO
 do
   grep $3 ${1}/${ARCHIVO}
 done

La sintaxis será

miscript [directorio] [nombre_archivo] [palabra]

Aquí tenemos varias cosas para ver. Primero que nada, el uso de las variables posicionales. Como se podrá apreciar el número de la variable, que esta precedido por un signo $, indica la posición del argumento cuando el script es llamado. Solamente se podrán usar 9 variables de este tipo sin tener que emplear un pequeño truco de corrimiento que veremos luego, dado que el 0 representa al nombre del script mismo. Es decir que en este caso la variable posicional $0 valdrá "miscript". Como se puede ver se han utilizado canalizaciones para poner más de un comando junto. Al final de la construcción se esta usando una construcción while. Esta se usa para repetir un ciclo mientras una expresión sea cierta.

 while ($VARIABLE=valor)
 do
   commandos
 done

En este caso esta siendo usada al final de una canalización con la instrucción read ARCHIVO. Es decir, mientras pueda leer el contenido de la variable $ARCHIVO, continuar. Esta variable $ARCHIVO contiene el resultado de lo que arrojo la canalización del listado con la salvedad de que tenia que contener la palabra que le enviamos como argumento, es así que solo se imprimirán las líneas en las que coincida la palabra a buscar de los archivos que cumplan con los requisitos.

Otra cosa a tener en cuenta es una nueva construcción en este script, ${1}/${ARCHIVO}. Al encerrar un nombre de variable dentro de llaves podemos combinarlas. En este caso forman el nombre del directorio (${1}) y añadimos una / como separador del directorio, y seguido e nombre del archivo donde se aplicara el comando grep con la palabra a buscar $3.

Podríamos hacer que este script sea un poco más documentado. Para esto podríamos asignar las variables posicionales a otras variables para que se pueda entender mejor su uso.

 DIRECTORIO=$1
 ARCHIVO_BUS=$2
 PALABRA=$3

 ls $DIRECTORIO | grep $ARCHIVO_BUS | while read ARCHIVO
 do
   grep $PALABRA ${DIRECTRIO}/${ARCHIVO}
 done

El número de las variables posicionales que pueden usarse en un script, como antes dijimos, se encuentra restringido a 10. ¿Qué pasaría si tenemos más de 9 argumentos? Es aquí donde tenemos que usar la instrucción shift. Esta instrucción mueve los argumentos hacia abajo en la lista de parámetros posicionales. De esta manera podríamos tener una construcción con esta distribución de variables

 DIRECTORIO=$1
 shift
 ARCHIVO_BUS=$1

De esta manera podríamos asignar el valor de la primer variable posicional a la variable DIRECTORIO y luego el siguiente argumento que habíamos dado se tomara otra vez con el nombre de $1. Esto solo tiene sentido si asignamos las variables posicionales a otra variable. Si tuviéramos 10 argumentos, el décimo no estaría disponible. Sin embargo, una vez que hacemos el que las variables se corran de posición este se convertirá en el noveno y se accederá por la variable posicional $9. Existe una forma también de pasar como argumento a la instrucción shift el número de posiciones que queremos correr. Por lo cual podemos usar

 shift 9

y así se lograra que el décimo argumento sea el parámetro posicional 1.

Lo que ocurre con los anteriores 9 argumentos es que desaparecen si no se los asigno a una variable anteriormente. Podremos cambiar usar un nuevo parámetro que podrá contener mas de un parámetro pasado al script. Este se denomina $* y contendrá el resto de los argumentos que se pasen al script luego de que se haya realizado un corrimiento determinado. Por ejemplo, si quisiera buscar una frase en lugar de una única palabra el script podría ser

 DIRECTORIO=$1
 ARCHIVO_BUS=$2
 shift 2
 PALABRAS=$*

 ls $DIRECTORIO | grep $ARCHIVO_BUS | while read ARCHIVO
 do
   grep "$PALABRAS" ${DIRECTRIO}/${ARCHIVO}
 done

Lo que aquí cambio es que luego de haber asignado a variables los parámetros posicionales 1 y 2 las variables fueron desplazadas dos veces, eliminando los dos primeros argumentos. Luego asignamos los argumentos restantes a la variable PALABRAS. Para que sea tomado como una cadena, se lo encerró entre comillas para ser pasado al comando grep, si no lo hiciéramos el bash vería nuestra entrada como argumentos individuales para pasar al grep.

Otro parámetro que es de utilidad es el $# que lleva la cuenta de la cantidad de argumentos pasados al script. De esta forma podríamos realizar un script que identificara si se le están pasando la cantidad de parámetros que realmente necesita y anunciar el error si faltaran estos. Para ello utilizaremos la construcción if-then-fi que es muy parecida a la while-do-done, en donde el par if-fi marca el final de un bloque. La diferencia entre estas construcciones es que el if solo evaluara una vez la condición. La sintaxis es la siguiente

 if [ condición ]
 then
   hacer_algo
 fi

Las condiciones que puede usarse se encuentran en las man page test (man test). Nosotros usaremos una simple condición para contar argumentos, pero pueden ser usadas distintas condiciones como nombres de archivos, permisos, si son o no directorios, etc. Para saber si la cantidad de argumentos que se nos a pasado en el script es correcta, utilizaremos una opción aritmética que compare la cantidad de argumentos pasados ($#) con un número que nosotros estipularemos, en este caso 3. Pueden usarse diferentes opciones con el formato arg1 OP arg2, donde OP será alguno de los siguientes

  -eq     es igual
  -ne     no es igual
  -lt     menor que
  -le     menor que o igual
  -gt     mayor que
  -ge     mayor que o igual

Se usará en este caso el -ge (mayor o igual que) dado que si la cantidad de argumentos que siguen al segundo es mayor la tomaremos como una frase a buscar y si es igual como una palabra. Lo único que haremos en caso de que la cantidad de argumentos sea menor, será informar de esto y de la forma de usar el script.

 DIRECTORIO=$1
 ARCHIVO_BUS=$2
 shift 2
 PALABRAS=$*

 if [ $# -ge 3 ]
 then
   ls $DIRECTORIO | grep $ARCHIVO_BUS | while read ARCHIVO
   do
     grep "$PALABRAS" ${DIRECTRIO}/${ARCHIVO}
   done
   else
     echo "Número de argumentos insuficientes"
     echo "Use: $0 <directorio> <archivo_a_buscar> <palabras>"
   fi

Otra utilidad para del if, es la posibilidad de realizar lo que se denomina if anidados. De esta forma podríamos tener varias capas de if-then-else-fi. Como ejemplo podría ser esta una construcción válida

 if [ $condicion1 = "true" ]
 then
   if [ $condicion2 = "true" ]
   then
     if [ $condicion3 = "true" ]
     then
       echo "las condiciones 1, 2 y 3 son ciertas"
     else
       echo "solo son ciertas las condiciones 1 y 2"
     fi
   else
     echo "condición 1 es cierta, pero no la 2"
   fi
 else
   echo "la condición 1 no es cierta"
 fi

Podríamos también hacer que una sola variable tome diferente valores e interactuar con ella para ver si se cumple la condición buscada. De esta forma podríamos por ejemplo hacer un menú de usuario con distintas alternativas. Pero esta forma es útil solo para pocas condiciones. ¿Que pasaría si tuviéramos muchas condiciones mas que agregar? Se nos haría por demás de complicado seguir el esquema armado y sería demasiado código para lo que se trata de realizar. Es aquí es donde se necesita la estructura case-esac. Como se podrá ver, al igual que en el if-fi aquí el inverso de case (esac) cierra la construcción. Veamos un ejemplo de una construcción con case

 read ELECCION
 case $ELECCION in
   a) programa1;;
   b) programa2;;
   c) programa3;;
   *) echo "No eligió ninguna opción valida";;
 esac

Hay que tener en cuenta algunas cosas respecto a este tipo de construcción. Por ejemplo el mandato que le damos al principio read indica al bash que tiene que leer lo que se ingrese a continuación y lo guarde en una variable que se llamara ELECCION. Esto también será útil para el uso de otras construcciones ya que el read no es propiedad exclusiva de la construcción esac, sino que pertenece al mismo bash. Como se ve, se le indica que si el valor que la variable contiene es igual a alguna de las mostradas debajo se ejecute determinado programa. (case $ELECCION in). La elección se debe terminar con un paréntesis ")" para que se cierre las posibilidades. Podríamos poner más posibilidades para cada elección; lo único que hay que recordar es cerrar con un paréntesis. El punto y coma nos marca el final de un bloque, por lo que podríamos agregar otro comando y se cerrara con punto y coma doble al último. El asterisco del final nos indica que se hará en caso de que no coincida lo ingresado con ninguna de las posibilidades. Un ejemplo, sería que nuestra construcción reconozca mayúsculas y minúsculas y además ejecute más de un comando por bloque.

 read ELECCION
 case $ELECCION in
   a|A)
        programa1
        programa2
        programa3;;
   b|B)
        programa4
        programa5;;
   c|C)
        programa3;;
   *)
        echo "No eligió ninguna opción valida";;
 esac

También se podría haber incluído un rango de posibilidades

 echo "Ingrese un caracter: "
 read ELECCION
 case $ELECCION in
   [1-9]) echo "Usted ingreso un número";;
   [a-z]) echo "Usted ingreso una letra minúscula";;
   [A-Z]) echo "Usted ingreso una letra mayúscula";;
 esac

Hay que recordar que todos estos script podrán estar en un archivo, pero para que se ejecuten se le deberá primero dar los permisos pertinentes. Un par de cosas a tener en cuenta para la construcción de script son la forma en que se quiere que ejecute éste y la captura de teclas. Al ejecutarse un script de shell, se estará creando un bash hijo que lo ejecutará. Dado que las variables y funciones pertenecen al intérprete de comandos que las creó, al finalizar el script el proceso hijo del bash morirá y con el todos los seteos de variables y funciones. Por esto, si se quisiera que los cambios de las variables y las funciones que se definieron permanezcan para ser utilizables una vez que el script haya terminado, se deberá comenzar a ejecutar el script con un punto "." seguido por un espacio antes del nombre de éste. De esta forma el proceso del intérprete de comando actual sera quien ejecute el script con lo que se conservaran todas las variables y funciones.

[shrek@pantano:~]$ . miscript

Un script puede dejar cosas sueltas antes de terminar si éste es finalizado bruscamente enviándole una señal de finalización [1] ya sea con la combinación de teclas Ctrl-C o con un kill -15. Para esto se deberán capturar estas señales para poder hacer una limpieza, ya se de variables o archivos, antes de finalizar. La forma de hacerlo es con el uso del comando trap; de esta forma se capturará la señal que se le envíe al script y se podrá ya sea ignorar la misma o ejecutar otro comando de limpieza. Para demostrar esto haremos un pequeño script que servirá de menú. La llamada al script del menú podría estar en el archivo .profile del usuario o en el .bash_profile. Si lo que no queremos es que el usuario salga del script con usando la combinación de teclas Ctrl-C, lo que haremos es capturar la señal y hacer que se ejecute nuevamente el script que se llamará simplemente menu.

 trap './menu' 2
 while :
 do
   echo 'a) Listado de archivos'
   echo 'b) Día y hora actual'
   echo 'c) Mes actual'
   echo 'Seleccione: '
   read ELECCION
   case $ELECCION in
     a|A)      ls;;
     b|B)      date;;
     c|C)      cal;;
     *)      echo "No eligió ninguna opción valida";;
   esac
 done

Como se ve al principio del script se utiliza el comando trap que al captura la señal 2 (SIGINT) que produce el Ctrl-C relanza el script. Al final del script se ve que se llama nuevamente dado que al ejecutarse el comando de cada elección se quiere que el menú siga funcionando. Practicar con estas construcciones será de gran ayuda para entender el proceso de construcción de script y los preparara para script más complejos usando otros interpretes como el sed, awk y el lenguaje perl. Para mayor información respecto a la construcción de script, remitirse a las páginas de manual del intérprete de comandos, en este caso man bash.

Notas

[1]

Para saber el nombre y número de señales teclear kill -l