3.3. Estructuras de control

El epígrafe está dedicado a las estructuras de control disponibles es linux, excepto las funciones, que se verán mas adelante.

3.3.1. if

La sentencia condicional tiene la siguiente forma:

if ORDEN; then
   ORDENES
else
   ORDENES
fi

y, si quiere encadenarse varios:

if ORDEN; then
   ORDENES
elif ORDEN; then
   ORDENES
else
   ORDENES
fi

La orden1 que acompaña al if actúa como expresión evaluadora, de manera que si devuelve 0 (éxito) se evalúa el bloque del if; y, en caso contrario, el bloque del else. Por ejemplo:

if grep -q '^V' < fichero.txt; then
   echo "Al menos una línea empieza por V"
else
   echo "Ninguna línea empieza por V"
fi

Una de las órdenes que más se usa para construir expresiones evaluadoras es test, que permite inquirir sobre el valor de las variables y la existencia o inexistencia de ficheros, y es tanto una orden interna proporcionada por la propia shell como un comando independiente. Por ejemplo:

if test "$USER" = "root"; then
   echo "Es usted el administrador"
fi

o también:

if test -d "$HOME"; then
   echo "¡Felicidades, $USER! Su directorio personal existe"
fi

Para conocer todas las posibilidades que brinda test, consulte la ayuda interna o la página de manual del comando correspondiente:

$ help test
$ man test

No obstante lo anterior, existe una variante de la orden anterior llamada [ (sí, un corchete, así como se escribe), que funciona de la misma forma excepto por el hecho de que obliga siempre a que su argumento final sea ]. Por ese motivo, las condicionales arriba escritas se escriben más comunmente así:

if [ "$USER" = "root" ]; then
   echo "Es usted el administrador"
fi

if [ -d "$HOME" ]; then
   echo "¡Felicidades, $USER! Su directorio personal existe"
fi

Advertencia

Recuerde que [ es una orden y ], su último argumento. En consecuencia, siempre debe existir un espacio de separación entre entre ellos y el contenido (que son argumentos). De la misma forma, que no tiene sentido test-d "$HOME", no tiene sentido [-d "$HOME"].

Para construir una condición compuesta, basta con concatenar varias órdenes test, tal como se explicó con la concatenación de órdenes:

if [ -d "$HOME" ] || [ -z "$HOME" ]; fhen
   # Bla, bla, bla...
fi

Nota

La orden test según el estándar POSIX dispone de las opciones -a y -o, pero como extensiones XSI que no son obligatorias, por lo que por portabilidad es preferible usar varias órdenes test.

Advertencia

Recuerde que en la shell el operador lógico && tiene la misma precedencia que ||.

bash dispone, además, de la orden interna [[ que introduce algunas mejoras al test estándar. Una de sus ventajas es que permite el uso de expresiones regulares2, lo cual nos puede evitar el uso de grep, cuando lo que hacemos es comprobar valores de variables. Por ejemplo, esta evaluación:

if echo "$HOME" | grep -qw "$USER"; then
   echo "El directorio personal contiene el nombre de usuario"
fi

es equivalente a esta otra3:

if [[ $HOME =~ $USER ]]; then
   echo "El directorio personal contiene el nombre de usuario"
fi

También permite el uso de expansiones con las operaciones de igualdad y desigualdad (tal como hace case como veremos a continuación). Por ejemplo:

$ var="HOLA"
$ [ $var = H* ] && echo "Éxito"
$ [[ $var = H* ]] && echo "Éxito"
Éxito

3.3.2. case

Esta estructura se usa para expresar la sentencia condicional múltiple. Tiene esta forma:

case $VARIABLE in
   v1)
      # Bloque de instrucciones para el primer valor
      ;;
   v2)
      # Bloque de instrucciones para el segundo valor
      ;;
      .
      .
      .
   vn)
      # Bloque de instrucciones para el enésimo valor
      ;;
esac

donde v1, v2, …, vn son los distintos valores que puede tomar la variable VARIABLE cuyo valor se prueba al principio de la sentencia. El intérprete comprueba en orden si el valor de la variable coincide con los expresados y ejecutará el bloque del primero con el que coincida. Los bloques siempre deben acabar con dos puntos y coma. La potencia de esta estructura es que el intérprete expande los valores v1, v2, etc:

#!/bin/sh

read -p "Escriba un carácter: " CHAR
echo

case $CHAR in
   [0-9])
      echo "Ha introducido un número"
      ;;
   [a-zA-Z])
      echo "Ha introducido una letra"
      ;;
   *)
      echo "No tengo ni idea de lo que ha introducido"
      ;;
esac

También es posible usar el carácter | para permitir la coincidencia múltiple con un bloque:

case nombre in
   Luis|Manolo|Pablo)
      echo "$nombre es mi amigo."
      ;;
   *)
      echo "No tengo el gusto de conocer a $nombre"
      ;;
esac

3.3.3. while/until

Son los ordenes que permiten repetir un bucle mientras se cumpla (while) o no (until) una condición:

while ORDEN; do
   # Órdenes del bucle
done

y para until se tiene la misma estructura:

until ORDEN; do
   # Órdenes del bucle
done

La orden que expresa la condición se evalúa exactamente de la misma forma que en el caso de if. Por ejemplo:

#!/biun/sh

RANDOM=5

echo "Acabo de pensar un número entre 1 y 10."

while [ "$num" != "$RANDOM" ]; do
   read -p "Intente averiguarlo: " num
done

echo "¡¡¡Correcto!!!"

Sí, es cierto: el programa sólo sirve para jugar una vez, porque el número oculto siempre es el mismo. La idea es sólo mostrar cómo usar el bucle, pero podemos complicar el programa para que el número se obtenga al azar4:

#!/bin/sh

#
# Convierte código hexadecimal en binario
# (requiere bc, que puede no estar instalado)
# $1: El número en hexadecimal
#
hex2dec() {
   echo "ibase=16;$1" | bc
}


#
# Obtiene n bytes al azar expresados en hexadecimal
# $1: Número de bytes. 1, si no se especifica.
#
random_x() {
   # hexdump -n${1:-1} -ve '"%02X"' /dev/urandom
   # od debería ser más portable que hexdump,
   # aunque debemos eliminar espacios y pasar a mayúsculas.
   od -v -An -tx1 -N${1:-1} /dev/urandom | tr -d '\n ' | tr '[:lower:]' '[:upper:]'
}


#
# Obtiene un número decimal aleatorio entre 0 y 2**n-1.
# $1: El valor de n.
#
random() {
   hex2dec $(random_x "$1")
}


RANDOM=$((`random`%10+1))     # Número entre 1 y 10 (sí, no son equiprobables)

echo "Acabo de pensar un número entre 1 y 10."

while [ "$num" != "$RANDOM" ]; do
   read -p "Intente averiguarlo: " num
done

echo "¡¡¡Correcto!!!"

Como en otros lenguajes de programación, se dispone (y también en el próximo tipo de bucle) de break (romper el bucle) y continue (volver a comenzar el bucle sin completar la iteración actual).

Es bastante común usar el bucle while junto a la orden read para leer línea a línea el contenido de un fichero, aprovechando que ésta orden tienen éxito si lee algo y fracasa si no queda nada que leer o, visto de otro modo, si lee el carácter EOF:

while read linea; do
   echo $linea
done < fichero.txt

Si las líneas que deseamos leer se encuentran en una variable, también podemos utilizar esta construcción, bien con la redirección <<< (exclusiva de bash), o bien usando una tubería:

echo "$contenido" | while read linea; do
   echo $linea
done

Advertencia

La tubería provoca que el bucle se ejecute en una subshell y, en consecuencia, que toda definición o redefinición de variable que se haga dentro de él no exista fuera:

$ echo "AAA" | while read linea; do var="$linea"; done
$ echo "El valor de var es: $var"
El valor de var es:

A pesar de lo que podríamos esperar la variable var no vale «AAA», puesto que la asignación se hizo dentro del bucle y este se ejecuta en una subshell. Este es un error muy común, incluso entre programadores experimentados.

Soslayar este problema supone o cambiar la estructura (p.e. usando el for que se verá a continuación) o eliminar la tubería utilizando un Here Document (si programamos en bash podemos usar un Here String):

$ while read linea; do var="$linea"; done <<EOF
> AAA
EOF
$ echo "El valor de var es: $var"
El valor de var es: AAA

3.3.4. for

La shell dispone tambien de un bucle de tipo for, que está asociado a una variable que va tomando distintos valores5:

for var in a b c; do
   echo $var
done

En este caso, se imprimirán tres líneas con los valores a, b y c, respectivamente.

Nota

Aunque los caracteres separadores de campos vienen dado por el valor de la variable IFS, las expansiones de la shell se comportan de modo que cada campo es cada uno de los resultados individuales de la expansión. Por ejemplo, supongamos estos dos ficheros:

$ ls *.mp3
cancion1.mp3
cancion 2.mp3

Le expansión genera dos elementos, no tres, aunque el segundo nombre contenga un espacio:

$ for f in *.mp3; do
>    echo "$f"
> done
cancion1.mp3
cancion 2.mp3

Esto no es una característica exclusiva de for, sino que es aplicable a cualquier ocasión en que haya que interpretar distintos campos (p.e. cuando se pasan argumentos a una función).

bash, además, dispone de un for con la sintaxis de C6:

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

3.3.5. Bloque

Un bloque no es propiamente una estructura de control de flujo, sino simplemente un modo de agrupar lógicamente varias instrucciones:

{  # Bloque: encerrado entre llaves.
   orden1
   orden2
   orden3
}

Nota

Si queremos llevar la llave de cierre a la misma línea que la última orden, hay que separarla de esta con un «;» (punto y coma):

{ orden1; orden2; orden3; }

En principio, no hay diferencia alguna entre crear o no el bloque, esto es, encerrar las instrucciones entre llaves, si no es la de organizar con más claridad el código. Tiene, sin embargo, utilidad cuando se combina con redirecciones, puesto que permite redirigir la entrada o la salida de todas las instrucciones hacia el mismo sitio. Por ejemplo:

{
   read _  # Este read lee la primera línea.
   while read -r linea; do
      # Manipulamos la línea
   done
} < entrada.txt

El efecto del código anterior es desechar la primera línea del fichero, ya que no la tratamos. Con la redirección de salida podemos obrar igual:

{ echo "Hola"; echo "Adios"; } > salida.txt

O con la tubería:

$ echo "Hola"; echo "Adios" | tr '[:lower:]' '[:upper:]'
Hola
ADIOS
$ { echo "Hola"; echo "Adios"; } | tr '[:lower:]' '[:upper:]'
HOLA
ADIOS

Advertencia

Usar parentesis tiene el efecto aparente de agrupar, pero en realidad, lo que hace es crear una subshell y que todas las órdenes encerradas en él, se ejecuten en la subshell.

3.3.6. Bonus track: xargs

xargs no es propiamente un bucle, sino una orden externa que nos permite emular el comportamiento de un bucle7 y, bajo ciertas circunstancias, tomar ventaja sobre el uso de while o for. Básicamente es una orden que se alimenta de la entrada estándar y pasa los datos que recibe a la orden que se haya escrito como argumento suyo. Por ejemplo:

$ seq 1 5 | xargs printf "%05d\n"
00001
00002
00003
00004
00005

que equivale a8:

for x in $(seq 1 5); do
   printf "%05d\n" $x
done

Hay que tener presente, no obstante, varias particularidades:

  1. xargs considera separadores de argumentos el espacio, la tabulación y el cambio de línea. Esta consideración es independiente del valor de la variable de ambiente IFS.

    Ahora bien, como en la shell, el escapado o las comillas simples o dobles permiten construir argumentos que contienen estos caracteres separadores. Por ejemplo:

    $ echo "1 '2 3'" | xargs printf "%05d\n"
    00001
    printf: «2 3»: valor no completamente convertido
    00002
    

    que funciona mal, porque el segundo argumento que se pasa es el conjunto «2 3» y eso no es un número. Sin las comillas, 2 y 3 serían dos argumentos distintos y así es como xargs los habria pasado a printf.

  2. En principio, no pasa todos los argumentos uno a uno, sino todos de golpe9. En consecuencia, la orden del primer ejemplo es, en realidad, equivalente a:

    $ printf "%05d\n" $(seq 1 5)
    

    pero como tenemos la suerte de que printf interpreta que debe aplicar el mismo formato (%05d\n) a los cinco argumentos, el resultado de la orden es el mismo que si se hubiera ejecutado cinco veces repetidamente lo que nos ha permitido antes mentir al respecto. Para llegar a ver cómo puede comportarse xargs como un bucle, debemos avanzar un poco más.

  3. En ausencia de indicación alguna (como es el caso) añade los datos recibidos al final de la orden que se da como argumento.

  4. Cuando xargs propicia que la orden de la que se acompaña se ejecute varias veces, se ejecuta secuencialmente (no en paralelo), que es precisamente la forma en que se ejecutan las iteraciones de un bucle.

Para alterar el primer aspecto existe la opción:

-0

que provoca que sólo se considere como carácter separador de argumentos el caracter nulo. Por tanto:

$ printf "1\00002" | xargs -0 printf "%4.2f\n"
1,00
2,00

Nota

find tiene un argumento -print0 para hacer que los ficheros que encuentre se separen con un carácter nulo, en vez de con un cambio de línea, lo cual lo hace muy apto para usarlo en conjunción con xargs.

El segundo aspecto lo podemos alterar con algunas opciones:

-L

Permite indicar cada cuántas líneas queremos que xargs pare de pasar de un tirón argumentos y continúe pasando los siguientes en una nueva orden. Por ejemplo:

$ echo -e "1 2\n3 4" | xargs echo
1 2 3 4

hace que xargs pasa los cuatro números a echo por lo que todos se escribirán en una misma línea. En cambio:

$ echo -e "1 2\n3 4" | xargs -L1 echo
1 2
3 4

pasa los dos primeros argumentos (números) a un primer echo, mientras que los dos segundos se los pasa a un echo distinto. En consecuencia, aparecen en dos líneas distintas.

-n

Permite indicar cuántos argumentos como máximo pasa xargs a la orden expresada en su argumento. En consecuencia:

$ echo 1 2 3 4 | xargs -n1 echo
1
2
3
4

escribe los dos argumentos en líneas distintas, ya que son imprimidos por distinto echo.

-r

No ejecuta la orden si no tiene nada que pasar. Un ejemplo bastante estúpido es éste:

$ echo "" | xargs echo

$ echo "" | xargs -r echo

En el primer caso se imprime una línea en blanco, pues ejecuta el echo sin argumentos. En el segundo caso, al añadir la opción -r jamás se llega a ejecutar el echo derecho. Es bastamte útil, por ejemplo, cuando pasamos líneas completas, pero queremos saltar las líneas en blanco.

Para el tercer aspecto existe la opción:

-I

que permite indicar una cadena que se usará para indicar en qué punto de la orden deben incluirse los datos suministrados. Por ejemplo:

$ echo "eth0 eth1" | xargs -n1 -I {} arp-scan --interface {} --localnet

hará que el nombre de la interfaz aparezca como argumento de la opción --interface de arp-scan.

Nota

Tal como prescribe su página de manual, usar esta opción implica -L 1

El cuarto aspecto también se puede modificar, de manera que las órdenes se procesen en paralelo. De hecho, puede ser interesante si nos es indiferente el orden de procesamiento y, además, cada una se demora un tiempo. Para ello existe la opción:

-P

que permite indicar el número de órdenes que admitimos que se ejecuten en paralelo (0, si queremos que se ejecuten todas en paralelo). En este caso:

$ echo 192.168.1.{1..254} | xargs -n1 -P20 ping -q -W2 -c1

Hacemos ping a los 254 posibles dispositivos de la red, pero de 20 en 20, para ahorrar tiempo.

En conclusión, podemos usar xargs para emular bucles pero teniendo en cuenta lo siguiente:

  • Sólo vale para ejecución de una orden, que, además, no puede ser una función de la shell, ya que xargs es un programa externo.

  • Usarlo implica la ejecución en subshells y la ejecución de xargs como intermediario.

  • En compensación, es una sintaxis más compacta y permite hacer fácilmente ejecuciones en paralelo.

Notas al pie

1

En realidad, pueden escribirse varias ordenes, de manera que el resultado evaluable será el que devuelva la última.

2

bash tiene soporte limitado para ERE.

3

En honor a la verdad no son equivalentes. Para que lo fueran el patrón debería ser \b$USER\b. Sin embargo, usar estos caracteres dentro del corchete da problemas en bash por lo que nos veríamos obligados a usar una variable intermedia:

pattern=\\b$USER\\b
if [[ $HOME =~ $pattern ]]; then
   echo "El directorio personal contiene el nombre de usuario"
fi
4

bash dispone de una variable para generar número aleatorios entre 0 y 32767:

$ echo $RANDOM
28396
$ echo $RANDOM
1919

pero no existe en otras shell lo que hace la solución no portable. Para evitarlo, hemos accedido al dispositivo de bytes pseudo-aleatorios /dev/urandom.

5

Es decir, que el bucle es más bien del tipo foreach.

6

Podríamos escribir este bucle coon la sintaxis anterior usando las expansiones:

for i in {0..9}; do
   echo $i
done

pero, de nuevo, esta forma sólo es compatible con bash, porque en el estándar no existe la expansión de llaves. Preservando la compatibilidad, podría llevarse a cabo la acción, recurriendo a la orden seq:

for i in $(seq 1 10); do
   echo $i
done
7

Para el programador familiarizado medianamente con la programación funcional, podríamos considerar xargs una suerte de función map. Claro que, siendo tan laxos, también podríamos considerar grep el sucedáneo de la función filter.

8

Mentira, no equivale a tal bucle, sino a lo que se explicará a continuación, pero con el argumento apropiado sí podríamos hacer que equivaliera a ese bucle de cinco iteraciones. Tras la explicación quedará todo claro.

9

Esta afirmación no es complemente exacta: hay un tamaño máximo, superado el cual los restantes argumentos se reservan para una segunda ejecución de la orden. Esta cantidad máxima de caracteres que se pueden pasar como argumentos dependen del sistema. Para más información puede consultarse lo que se dice de la opción -s en su página de manual.