El BUCLE FOR

El bucle for permite iterar sobre una serie de elementos de una cadena, lista, range, etc..

en la segunda linea del codigo mostrado mas abajo declaramos i como la variable que recibirá los diferentes valores contenidos en $(ls). La tercera linea puede contener todas las instancias que sean necesarias sobre la variable $i. Por último en la cuarta linea se da por cerrado el ciclo for con done, después de done se cierra el ciclo for.

for i in $(ls); do
  echo $i
done

# tambien se puede usar sin identación
for i in $(ls)
do
echo $i
done

# o como una linea de código en el terminal
for i in $(ls);do echo $i;done

En el siguiente ejemplo se itera sobre todos los archivos presentes en el directorio de ejecucion del script que acaben en .fa, imprimirá en pantalla el nombre del archivo y contará el numero de lineas que presentan.

get_fasta_count.sh
for i in *.fa; do
    echo $i 
    grep -c "^>" $i
done

C-like for loop

De forma similar al bucle for usado en C# o awk, generamos un bucle for que estará iterando hasta que la variable que inicialicemos ( en el ejemplo $i) alcance un determinado valor i = 100. Es util si queremos realizar una accion un numero determinado de veces.

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

Ranges

Utilizando la función de Bash Sequence Expression, podemos generar rangos de integers o characters, definiendo un start y un end. Esta funcion se suele usar en conjunción con los bucles for.

{START..END..INCREMENT}

for i in {1..5};do
    echo "hola jugador $i"
done    
$ bash script.sh
hola jugador 1
hola jugador 2
hola jugador 3
hola jugador 4
hola jugador 5
for i in {0..50..5};do
    echo "mostrar decenas $i"
done    
$ bash script.sh
mostrar decenas 0
mostrar decenas 5
mostrar decenas 10
...
mostrar decenas 45
mostrar decenas 50

tambien los podemos hacer con caracteres.

for i in {A..Z};do
    echo "mostrar caracter $i"
done    
$ bash script.sh

mostrar caracter A
mostrar caracter B
mostrar caracter C
mostrar caracter D
mostrar caracter E
mostrar caracter F
mostrar caracter G
...
mostrar caracter Z

EL BUCLE WHILE

El bucle for no es la única forma de hacer ciclos en scripts de bash. El ciclo while hace el mismo trabajo, pero verifica una condición antes de cada iteración.

El comando while tiene la siguiente estructura:

while [condition];do
    commands
done

aqui un ejemplo:

#!/bin/bash

number=10

while [ $number -gt 5 ];do

    echo $number
    number=$(($number-1))

done

El script es muy simple; inicializamos primero la variable number y le asignamos el valor de 10. Luego el comando while verifica si el número es mayor que 5 (greater than -gt; lo veremos mas adelante), en caso afirmativo, se ejecutará el ciclo y el valor numérico disminuirá cada vez en 1 y en cada iteración de ciclo imprimirá el valor del número, una vez que el valor sea 5 el ciclo terminará.

Forever while loop

Si queremos generar un script que no termine y que esté siempre haciendo una tarea, podemos meter los comandos dentro de un bucle while tal como sigue:

while true; do
    ....
done

Se puede utilizar en programas que controlan las comunicaciones de entrada y salida por los puertos de conexion del ordenador para que estén siempre escuchando. Para controlar la actividad en procesos que tengamos corriendo en servidores tambien es muy útil, aunque de nivel mas avanzado.

Anidación: bucles dentro de otro bucle

Cuando ejecutamos ciclos dentro de otro ciclo se denomina afilamiento. No es aconsejable abusar mucho de esta técnica pues dificulta la comprensión del código.

En el siguiente ejemplo se ejecutan dos ciclos for uno superior que va desde 1 hasta 4, y en cada una de estas 4 iteraciones se realizará el ciclo interno que va desde 1 hasta 3.

#!/bin/bash

for ((variable_1 = 1; variable_1 < 5; variable_1++)); do

    echo "Iteración superior: $variable_1:"

    for ((variable_2 = 1; variable_2 <= 3; variable_2++)); do

        echo "ciclo interno: $variable_2"

    done

done
$ bash script.sh
Iteración superior: 1:
ciclo interno: 1
ciclo interno: 2
ciclo interno: 3
Iteración superior: 2:
ciclo interno: 1
ciclo interno: 2
ciclo interno: 3
Iteración superior: 3:
ciclo interno: 1
ciclo interno: 2
ciclo interno: 3
Iteración superior: 4:
ciclo interno: 1
ciclo interno: 2
ciclo interno: 3

El separador de campo

De forma predeterminada, los siguientes caracteres se tratan como campos.

  • Espacio
  • Tabulación
  • Nueva línea

Si tu texto incluye alguno de estos caracteres, el intérprete de comandos asumirá que es un campo nuevo.

Puedes cambiar el separador de campo interno , INTERNAL FIELD SEPARATOR (IFS).

IFS="\n"

IFS determina como deben ser separados los diferentes string que vayan saliendo durante la iteración. Lo enteremos mejor con un ejemplo:

imaginad el siguiente string:

file="casa armario compra codo euro"

si iteramos con un bucle for sobre dicho string obtendremos una serie lineas con cada uno de los elementos que contiene el string:

#!/bin/bash

file="casa armario compra codo euro"

for var in $file; do
    echo "$var"
done
$ bash script.sh
casa
armario
compra
codo
euro

Asi que si queremos recuperar el string tal cual estaba almacenado tenemos que cambiar el IFS, asi shell considerará el carácter nueva línea (“\n”) como un separador interno en lugar de espacios.

#!/bin/bash

file="casa armario compra codo euro"
IFS=$"\n"
for var in $file; do
    echo "$var"
done
$ bash script.sh
casa armario compra codo euro

Leyendo archivos usando bucles

Aunque el bucle for es muy util para iterar sobre archivos en directorios o llevar cuentas numéricas, no nos permite iterar de manera clasica sobre las lineas de un archivo de texto. Si intentamos leer un archivo línea por línea y utilizamos bucle “for”, (for line in $(cat file.txt); do …) lo que realizará es una evaluación de cada palabra y no de cada línea, que es lo que realmente buscamos

$ cat file.txt
esta es la linea 1
esta es la linea 2

Usando el archivo de texto file.txt mostrado arriba

for line in $(cat file.txt);do
    echo ${line}
done
$ bash script.sh
Esta
Es
La
Línea
1
Esta
Es
La
Línea
2

Sin embargo, si que podemos leer un archivo de texto con un bucle “for” con la condición de que cambiemos el valor de la variable “$IFS” (Internal Field Separator, separador de campo interno) antes de ejecutar el bucle. Es lo que veremos a continuación.

oldIFS=$IFS # conserva el separador de campo

IFS=$'\n' # nuevo separador de campo, el caracter fin de línea

for line in $(cat file.txt);do
    echo "$line"
done

IFS=$old_IFS # restablece el separador de campo predeterminado para el resto del script

De todas formas, esto es algo engorroso y no se suele proceder así. La solución mas usada consiste en utilizar un bucle “while” asociado al comando interno “read”.

cat file.txt | while read line; do 
    echo  "$line"
done 

# tambien se puede usar esta otra forma

while read line; do 
    echo "$line"
done < file.txt

en ambos casos el output sera el siguiente:

$ bash script.sh
esta es la linea 1
esta es la linea 2

CONDICIONALES CON IF

A la hora de escribir scripts en BASH son necesarios poder establecer acciones sujetas a que se cumplan ciertas condiciones. Si el valor es menor que 2 haz esto, si el fichero no existe, créalo, etc.. Esto se lleva a cabo a mediante la sentencia if-then-elif-else.

if [[ -z "$string" ]]; then
  echo "String is empty"
elif [[ -n "$string" ]]; then
  echo "String is not empty"
fi

empecemos por la mas simple, la sentencia If-then:

if comand; then

hacer algo

fi

aqui un ejemplo:

#!/bin/bash
set -e

echo ' Adivina el valor numerico de la variable'
read variable #entrada de datos

if [ $variable = 1 ]; then
    echo 'Acertaste'
    exit 0
fi

echo 'No acertaste'

El script nos pregunta que adivinemos el valor, el valor se ingresa usando un comando nuevo, read, que directamente almacena la entrada de datos del standard input en la variable . Luego la condicion es verificada (variable igual a 1), si es asi, ejecutará las lineas de comando encapsuladas en la estructura if-then, si no es asi, el script simplemente se salta la estructura.

Podemos ganar un poco más de control añadiendo la sentencia else:

#!/bin/bash

echo 'Adivina el valor numerico de la variable'
read variable #entrada de datos

if [ $variable = 1 ];then
    echo 'Acertaste'
    exit 0
else
    echo 'No acertaste'
    exit
fi

Utilizando la sentencia else tenemos un mayor control dentro de la sentencia if-then, ya que tanto si se da la condición como si no todos los comandos que se ejectuen estaran dentro de dicha estructura.

Ahora pongamos que quereremos controlar aun mas parámetros, de tal forma que queremos indicarle al jugador si se ha quedado cerca del resultado cierto. Lo hariamos utilizando la sentencia if-then-elif-else:

#!/bin/bash

echo ' Adivina el valor numerico de la variable'
read variable

if [ $variable = 1 ]; then
    echo 'Acertaste'
    exit 0
elif [ $variable = 2 ]; then
    echo 'casi!'
else
    echo 'No acertaste'
fi

exit 0

Podemos introducir todas las condiciones que queramos usando elif:

#!/bin/bash

#echo 'Adivina el valor numerico de la variable [1-10]'

read variable

if [ $variable = 3 ]; then
    echo 'Acertaste'
    exit 0
elif [ $variable = 2 ]; then
    echo 'casi!'
elif [ $variable = 4 ]; then
    echo 'casi!'        
else
    echo 'No acertaste'
fi

exit 0

Una de las cosas que convierte a Bash un lenguaje un poco especial respecto a otros lenguajes es que las estructuras de control True/False se rigen por el estado del exit status de los comandos utilizados dentro de las condiciones if-elif (Recuerda: contariamente a otros lenguajes, 0 representa True/success y cualquier otra cosa False/error) Es decir cualquier nonzero exit stauts que se obtenga en la condicion if provocará el salto de la sentencia if, continuando hacia la siguiente sentencia, ya sea un elif-else u otros comandos fuera de la estructura if-then-elif-else. Para comprender esto último mejor echemos un vistazo a la linea de código escrita al principio de este bloque.

string="hola"

if [[ -z "$string" ]]; then
  echo "String is empty"
elif [[ -n "$string" ]]; then
  echo "String is not empty"
fi

Lo primero que hace este bucle es comprobar si la variable string de entrada está vacía. En caso afirmativo el comando test con la opcion -z (lo veremos a continuación) utilizado en la instancia if devuelve un nonzero exit status (1; False) ya que el string no está vacío, asi que se salta dicha sentencia y continua hacia la siguiente, que contiene la sentencia elif que en este caso devuelve un zero exit status (0; True) pues la variable string no está vacia contiene el string “hola”.

Sentencia test

El ultimo elemento que necesitamos para terminar de comprender el funcionamiento de la sentencia if es el comando test. Como otros comandos la salida del comando test puede ser 0 o 1. Pero en este caso el exit status indica el valor de retorno del test mediante el argumento utilizado (verdadero o falso). test soporta el uso de un gran numero de operadores de comparación (son dos strings iguales, son dos integers diferentes, es el primer integer mayor que el segundo,…) Veamos algunos ejemplos directamente usando la terminal y comprobando el exit status imprimiendo la variable $? ( echo “$?”):

$ test "ATG" = "ATG" ; echo "$?" 
0 # True
$ test "ATG" = "atg" ; echo "$?" 
1 # False
$ test 3 -lt 1; echo"$?" 
1 # False
$ test 3 -le 3; echo"$?" 
0 # True

Sin embargo, podemos usar una sintaxis mas sencilla en Bash para usar la sentencia test: [ “ATG ” = “ATG” ]. Es importante dejar el hueco alrededor de los corchetes. Esto hace más facil el uso de test en las sentencias if-then cuando usamos comparaciones:

if test "ATG" = "ATG"; then
    [...] 
fi

if [ "ATG" = "ATG" ]; then
    [...] 
fi
# Arguments for Test and [ ] expressions:

# STRINGS
[ -z STRING ]                        # Empty string
[ -n STRING ]                        # Not empty string
[ STRING == STRING ]        # Equal
[ STRING != STRING ]        # Not Equal
[ STRING =~ STRING ]        # Regular expression

# INTEGERS
[ NUM -eq NUM ]                    # Equal
[ NUM -ne NUM ]                    # Not equal
[ NUM -lt NUM ]                    # Less than
[ NUM -le NUM ]                    # Less than or equal
[ NUM -gt NUM ]                    # Greater than
[ NUM -ge NUM ]                    # Greater than or equal

# NUMERIC CONDITIONS
(( NUM > NUM ))                    # Greater than
(( NUM < NUM ))                    # Less than
(( NUM == NUM ))                # Equal

# File conditions
[ -f FILE ]                            # File
[ -d FILE ]                            # Directory
[ -h FILE ]                            # symLink
[ -e FILE ]                            # Exists
[ -x FILE ]                            # Executable
[ -r FILE ]                            # Readable
[ -w FILE ]                            # Writable
[ -s FILE ]                            # Size is > 0 bytes
[ FILE1 -ef FILE2 ]            # Same files

un ejemplo práctico de File conditions usando la sentencia if:

comprobar si un directorio existe o no:

#!/bin/bash

mydir=~/Documents

if [[ -e $mydir ]]; then
    echo "Directory $mydir exists"
    ls  $mydir
else
    echo "NO such file or directory $mydir"

fi

Si el directorio almacenado en $mydir existe se mostrará en pantalla “Directory $mydir exists” y a continuacion realizará un list directory.

$ bash script.sh
/Users/JGL/Documents:
Cosas_chachis      trabajo        investigacion 

Combinando pruebas

Se pueden combinar varias pruebas/test dentro de una misma condicion if usando los comando booleanos && (and), II (or), and ! (not).

&& False si una de ellas es falsa:

#!/bin/bash

variable=5

if [[ $variable > 1 ]] && [[ $variable < 6 ]]; then
    echo "es cierto que 1 < variable < 6 "
fi

||** False si ambas afirmaciones son falsas:

#!/bin/bash

variable=5

if [[ $variable > 1 ]] || [[ $variable < 6 ]]; then
    echo "es cierto que variable < 6 o que la variable > 1"
fi

!** convierte en false una afirmación positiva:

#!/bin/bash

variable=5

if [[ ! $variable < 1 ]] ; then
    echo " la variable no es menor que 1 "
fi

output:
" la variable no es menor que 1 "

Controlando los ciclos

Tal vez después de que el ciclo comience quieres que se detenga en un valor específico, ¿esperarás hasta que termine el ciclo? Por supuesto que no, hay dos comandos que nos ayudan en esto:

  • El comando break
  • El comando continue

El comando break

El comando break se utiliza para salir de cualquier ciclo o bucle.

#!/bin/bash

for number in {10..15}; do
#for number in 10 11 12 13 14 15; do  # tambien vale

    if [ $number -eq 13 ]; then
        break

    fi
    echo "Number is: $number"

done
echo "number is: $number, end of cycle!"

El ciclo for continuará hasta que el valor de la variable number sea 13, en ese momento, se ejecutarán las lineas encapsuladas en la sentencia if, en este caso, el comando break provoca la salida del ciclo for.

$ bash script.sh

number is: 10
number is: 11
number is: 12
number is 13, end of cycle!

El comando continue

Puede utilizar el comando continue para detener la ejecución de los comandos restantes dentro de un ciclo pero sin salir del mismo y pasar inmediatamente a la siguiente iteración.

#!/bin/bash

for ((number = 1; number < 8; number++)); do # inicializamos ciclo

    if [ $number -lt 5 ]; then 
        continue # saltamos las  lineas de comando del ciclo for de la presente iteracion

    fi
    echo "number is: $number"

done
$ bash script.sh
number is: 5
number is: 6
number is: 7
number is: 8

Si la condicion establecida por if-then es verdadera la actual iteracion del ciclo **for ** se interrumpe y no imprime “number is: $number”, pasando directamente a la siguiente iteración, esto continuará hasta que el number sea igual a 5.

Combinando bucles FOR con sentencias IF

Cuando realmente alcanzan un alto potencial los bucles for y las sentencias if es cuando se combinan juntas. A continuación mostraremos un ejemplo para iterar sobre los elementos alamcenados en un directorio usando el bucle for, e indicar si dicho elemento es un archivo o un directorio usando la sentencia IF.

#!/bin/bash

for element in /home/Documents/*; do # utilizando * iteramos sobre todo el folder

    if [[ -d "$element" ]]; then
        echo "$element es un directorio"
    elif [[ -f "$element" ]]; then
        echo "$element es un archivo"
    fi

done
$ bash script.sh
/Users/JGL 1/Documents/Augustus es un directorio
/Users/JGL 1/Documents/Coches es un directorio
/Users/JGL 1/Documents/Datos de usuario de Microsoft es un directorio
....
/Users/JGL 1/Documents/genes.fa es un archivo