Scripting en Bash | Introducción
Documento escrito por: Alejandro Aranda
Puedes ver todos los documentos de este autor aquí.
Introducción
Hoy nos vamos a sumergir en el apasionante mundo del scripting, esta vez, en Bash. La idea es que este artículo sirva como una Introducción para cualquier persona que quiera aprender a hacer sus propios scripts en Bash.
Antes de empezar, es necesario que tengas conocimientos sobre Bash en general. Puedes consultar un artículo en el que ya hablé sobre este tema aqui.
Parámetros en bash
Cuando hablamos de parámetros en Bash, nos referimos a unas variables reservadas que utilizaremos principalmente para para recoger datos que introduzca el usuario.
./test.sh parámetro1 parámetro2 parámetro3
Tenemos varios tipos de parámetros en función de cómo recogen la información introducida a la hora de ejecutar el script:
Parámetros posicionales
Para llamar a los parámetros, pondremos un "$" y el número de parámetro, siendo el primer parámetro por ejemplo $1
. Si el parámetro está en una posición cuyo número tiene más de una cifra, lo indicaremos de la siguiente manera: ${10}
He creado este pequeño programa para mostrar el funcionamiento de los parámetros.
Como se puede ver, utilizamos un espacio en blanco para separar cada parámetro.
En caso de querer pasar un parámetro que contenga varias palabras, utilizaremos comillas:
./test.sh "Buenos días a todos" parámetro3 parámetro4
De esta forma, el texto Buenos días a todos
sería el primer parámetro.
Devolver todos los parámetros
Ya hemos visto como trabajar con los parámetros uno por uno, pero sería normal pensar, ¿Hay alguna forma de trabajar con todos de forma directa?
La respuesta es un rotundo si, en Bash contamos con dos expresiones para referirnos a todo el conjunto de parámetros, $*
y $@
.
He creado este pequeño programa para mostrar cómo funcionan estas expresiones.
Como vemos, nos devuelven en la misma fila todos los parámetros.
Aunque pueda parecerlo a primera vista, estas dos expresiones no funcionan de la misma forma. Podemos apreciar las diferencias cuando utilizamos bucles (que explicaré más adelante).
En este programa, iteraré con las variables uno
y dos
sobre cada parámetro. Lo realmente importante en este ejemplo es que, cuando utilizamos "$@"
con comillas Bash interpreta dos palabras separadas como si fuese un solo parámetro.
Contar número de parámetros
Algo muy útil cuando creamos scripts en Bash es saber cuántos parámetros ha introducido el usuario. Esto lo podemos hacer utilizando la expresión $#
Vemos que es capaz de interpretar como un solo parámetro dos palabras que cuentan con espacios entre sí.
Más adelante cuando veamos estructuras de control, veremos que esto es muy útil para comprobar si el usuario ha introducido parámetros y, por ejemplo, en caso de no haberlo hecho, no ejecutar el programa.
Variables en Bash
Una variable es un elemento capaz de almacenar datos para posteriormente recuperarlos para su uso. Esto nos será muy útil para hacer operaciones aritméticas, comparaciones y trabajar con los datos en general.
Una característica muy importante de las variables es que pueden tomar diferentes valores durante toda la ejecución del programa.
Variables comunes
Cuando hablo de este tipo de variables me refiero a aquellas que son definidas por el usuario, ya sea en el mismo script o recogidas de alguna forma.
Variables alfanuméricas
El ejemplo más simple del funcionamiento de una variable lo observamos directamente en la terminal:
Definiremos una variable escribiendo su nombre y recuperaremos su valor utilizando el símbolo $
.
Como vemos, una vez definidas las variables, podemos trabajar con ellas sin ninguna limitación.
Cuando definimos variables debemos tener en cuenta los espacios.
En caso de querer definir una variable que contenga espacios en blanco, debemos utilizar comillas dobles.
Recoger un valor por teclado
En Bash, tenemos la opción de pedirle al usuario que ejecuta el script que introduzca valores por teclado, algo realmente útil. Esto lo haremos de la siguiente forma:
Con read
le pedimos al usuario introducir datos por teclado.
Variables con operaciones aritméticas
También podemos crear variables que almacenen el resultado de operaciones aritméticas. Esto es muy útil por ejemplo en un bucle en el que queremos saber cuántas iteraciones se han realizado.
Indicamos que es una operación que debe ser interpretada utilizando
$((..))
Esto combinado con lo visto anteriormente, nos permite crear por ejemplo un programa que opere dos números.
Definimos cada variable que contendrá el valor de la operación que corresponda y luego mostramos su valor.
Variables asignadas con comandos
Puede sonar raro, pero Bash nos permite que una variable contenga como valor el resultado de un comando. Aunque pueda no parecerlo, esto nos permite hacer cosas realmente interesantes.
Para guardar el resultado de un comando en una variable lo hacemos con
$(comando)
Guardar en una variable un listado ficheros del directorio actual:
Podríamos por ejemplo con esta línea contar el número de ficheros que hay en el directorio actual.
find . | wc -l
Ahora lo metemos en una variable y podemos trabajar con ello:
Variables especiales
En Bash, hay algunas variables especiales que se refieren a información relacionada con el script. En este grupo de variables estarían también las vistas anteriormente en el apartado de parámetros.
$USER
el nombre del usuario que ha ejecutado el script
$HOSTNAME
se refiere al nombre de la máquina en la que se está ejecutando el script
$SECONDS
se refiere al tiempo (en segundos) transcurrido desde que se inició el script.
$RANDOM
devuelve un número aleatorio cada vez que se lee esta variable.
$LINENO
indica el número de líneas que tiene nuestro script.
Veamos un ejemplo:
Variables de entorno
Las variables de entorno son cadenas que contienen información acerca del entorno para el sistema y el usuario que ha iniciado sesión en ese momento.
Esto nos puede ser muy útil ya que nos da información dinámica, es decir, que va cambiando en función del sistema y el usuario.
Principales variables de entorno:
$LOGNAME
Contiene el nombre del usuario con el que estas logeado.
$SHELL
Contiene el tipo de terminal que utiliza el usuario.
$PWD
Su valor es la ruta en la que se encuentra el usuario.
$HOME
Su valor es el directorio personal del usuario.
$LANGUAGE
El idioma con el que va a trabajar el SO.
Un ejemplo:
Podemos consultar las variables de entorno escribiendo env
en la terminal
Operadores aritméticos
Ahora que ya sabemos trabajar con variables, estaría bien conocer los operadores aritméticos que Bash pone a nuestra disposición. Estos operadores serán muy útiles cuando estudiemos las estructuras de control.
Operador | Descripción | Ejemplo |
---|---|---|
+ | Suma dos números | |
- | Resta el segundo al primero | |
* | Multiplica dos números | |
/ | Devuelve el cociente de dividir el primero entre el segundo | |
% | Devuelve el resto de dividir el primero entre el segundo | |
+= | Incrementa el valor de la variable en 1 | |
-= | Decrementa el valor de la variable en 1 | |
*= | Multiplica el valor de la variable por el valor dado | |
/= | Divide el valor de la variable entre el valor dado | |
%= | Divide el valor de la variable entre el valor dado y devuelve el resto | |
** | Eleva un número a otro valor dado |
En algunos casos, cuando creamos ficheros no es necesario utilizar la expresión $((..))
para realizar las operaciones
Operadores lógicos
En Bash, una forma de validar si un comando ha sido exitoso o no, es usando operadores lógicos. Los principales son los siguientes:
Operador AND
Este operador devuelve un valor "true" si la operación anterior se ha completado, y si esto sucede, la siguiente operación se realizará.
En caso de que la operación anterior no se realice de forma correcta, la siguiente operación no se realizará.
Por ejemplo, podríamos crear una expresión para comprobar la conexión con los servidores DNS de Google 8.8.8.8 y en caso de tener conexión, que nos muestre un mensaje.
timeout 1 ping -c 1 8.8.8.8 && echo Correcto
El comando "timeout", lo utilizamos para prevenir que el comando "ping" se quede colgado por no tener conectividad.
Redirigimos todo al /dev/null con el carácter ">" para que no nos muestre por pantalla el resultado del primer comando.
El /dev/null es algo así como un agujero negro en el que redireccionar información que será descartada.
Otro ejemplo:
En este caso, utilizamos el operador AND para que, después de actualizar los repositorios exitosamente nos muestre un mensaje informativo por pantalla.
sudo apt update -y >/dev/null && echo Actualizado correctamente
En caso de no completarse exitosamente el comando, no mostraría nada por pantalla.
Operador OR
Este operador es prácticamente opuesto al anterior. Es decir, la segunda operación se realizará si la primera no es exitosa.
Podemos usar los ejemplos anteriores para ver las diferencias.
timeout 1 ping -c 1 8.8.8.8 || echo Comando anterior no completado
En este caso cuando el comando ping
no sea exitoso, nos mostrará el mensaje.
Estos operadores pueden ser realmente útiles para operaciones sencillas si los usamos en conjunto.
Ejemplos con AND y OR
1. Comprobar si un usuario existe e informar por pantalla
Podríamos comprobar fácilmente si un usuario existe y mostrar un mensaje informativo por pantalla.
grep usuario /etc/passwd >/dev/null && echo Existe || echo No existe
En este caso filtramos con el comando grep
por el usuario alex en el fichero
/etc/passwd que contiene un listado de los usuarios del sistema.
Como se puede ver, en función de si el usuario existe o no, veremos un mensaje por pantalla que nos lo indique.
2. Comprobar si un usuario existe e informar por pantalla
Otra cosa que podemos hacer usando estos operadores lógicos es comprobar si un fichero existe o no.
test -e fichero && echo El fichero existe || echo El fichero no existe
Comparaciones
Cuando vayas a hacer comparaciones te recomiendo que utilices siempre [[..]]
para evitar errores.
Podemos negar una expresión utilizando !
. Por ejemplo !=
significaría que no sea igual y ! -f
comprueba que no exista el fichero.
Comparando cadenas
A la hora de comparar cadenas debemos recordar utilizar comillas dobles ".."
para evitar errores. Puedes consultar ejemplos más abajo.
Expresión | Significado |
---|---|
< | Menor que |
> | Mayor que |
= | Igual a |
!= | Distinto de |
-z | Cadena vacía |
-n | Cadena no vacía |
Comparando enteros
Expresión | Significado |
---|---|
-lt | Menor que |
-gt | Mayor que |
-ge | Mayor o igual |
-le | Menor o igual |
-eq | Igual a |
-ne | Distinto de |
Comparando atributos de ficheros
Expresión | Significado |
---|---|
-e | El fichero o directorio existe |
-d | El directorio existe |
-f | El fichero existe |
-s | El archivo no está vacío |
-r | Tiene permiso de lectura |
-w | Tiene permiso de escritura |
-x | Tiene permiso de ejecución |
Condicionales con if / case
Una instrucción condicional es aquella que establece qué instrucciones deben de ejecutarse o no, en función del valor de una condición. Puede sonar complicado pero es realmente muy intuitivo, por ejemplo: Si llueve, no saldré a la calle.
Es decir, plantearemos una "prueba lógica" y en función de si se cumple o no, haremos una cosa u otra. Puedes ver ejemplos aquí
Utilizando if
Es la forma más habitual a la hora de comprobar si cierta condición se cumple. Ejecuta un bloque de código si se cumple la condición y otro bloque si no se cumple:
Esta es la forma más simple, ten en cuenta que el contenido ente []
es opcional.
if "condicion"
then
"comandos"
[else
"comandos"]
fi
Otra forma de hacerlo sería proponiendo ciertas condiciones que se deben cumplir, esto lo haremos con elif
, que nos permite comprobar de nuevo:
if "condicion"
then
"comandos"
elif "condicion"
then
"comandos"
[else
"comandos"]
fi
Utilizando case
Otra forma de comprobar si una condición se cumple, es utilizando case. La mayor diferencia que tiene frente a if
es que nos permite definir muchos casos de forma sencilla y con menos lineas de código.
Usando case, la expresión *)
actúa de forma parecida a else
, es decir si no se cumple ninguna de las condiciones anteriores, se ejecutará el código de *)
.
case expresión in
caso_1)
comandos;;
caso_2)
comandos;;
*)
comandos;;
esac
Ejemplos de uso
Veamos unos cuantos ejemplos de uso:
1. Ejemplo básico if / elif
Como se puede ver en las expresiones, haremos diferentes cosas en función del número introducido como primer parámetro.
2. Ejemplo con case
En este caso, para mostrar el funcionamiento de case
definimos un programa que creará un fichero o un directorio en función del primer parámetro introducido por el usuario.
En caso de no introducir un parámetro contemplado en el código, *)
le mostraremos un mensaje que se lo indique.
3. Ejemplo con if y case
Le he dado una vuelta de tuerca al ejemplo anterior para comprobar con if
que se introducen exactamente dos parámetros.
La primera comparación if [[ $# -ne 2 ]]
hace que el programa devuelva un mensaje de error en caso de no introducir dos parámetros.
Bucles con for / while
Los bucles son realmente útiles en programación ya que nos permiten ejecutar muchas veces la misma secuencia de código hasta que la condición dada deja de cumplirse.
Puedes ver ejemplos aquí
Utilizando for
Un bucle FOR nos permite repetir instrucciones un número fijo de veces que indicamos con una serie de condiciones sobre una variable iteradora (que contendrá el valor en cada ciclo).
La estructura básica de for es esta:
#!/bin/bash
for nombre [in lista]
do
comandos que pueden utilizar $nombre
done
Encontramos varias formas de usarlo:
En este caso, vamos a iterar con la variable p sobre cada elemento de la lista, y en cada ciclo vamos a mostrar con echo
el nombre de la página.
#!/bin/bash
for p in www.zonabit.es www.google.es
do # El output sería:
echo "Pagina: $i" # Primer ciclo -> Pagina: www.zonabit.es
done # Segundo ciclo -> Pagina: www.google.es
Otra forma de usarlo es dándole un rango de números (en este caso del 1 al 5). De esta forma logramos que el bucle se ejecute el número de veces que nosotros queremos.
#!/bin/bash
for i in {1..5}
do
echo "Numero: $i" # En cada ciclo, el script mostraría el numero por el que va
done
Por último, vemos una forma un poco más complicada pero también más versátil. En este caso tenemos tres elementos.
c=1
Inicializamos la variablec
con el valor1
.c<=5
Definimos que el bucle se ejecutará mientrasc
sea menor o igual a5
.c++
En cada iteración la variablec
aumentará su valor en1
.
#!/bin/bash
for (( c=1; c<=5; c++ ))
do
echo "Numero: $c" # La salida de este comando será exactamente la misma al ejemplo anterior
done
Utilizando while
Un bucle WHILE nos permite repetir instrucciones mientras una condición sea verdadera, es muy útil cuando no se sabe cuántas veces habrá que repetir una tarea hasta que consigamos lo que necesitamos.
while [ condición ]
do
// Código a ejecutar
done
Algo muy útil que podemos hacer es leer lineas de un fichero (ver ejemplo más adelante).
while read line;
do
// Código a ejecutar
done < fichero
Ejemplos de uso
1. Ejemplo con for
En este ejemplo podemos ver como utilizando for
iteramos sobre $@
que como ya sabemos es un array de todos los parámetros introducidos.
Además, para poder contar el parámetro en cuestión, utilizo la variable par
que se irá incrementando en 1 en cada iteración del bucle.
2. Ejemplo con while
En este caso vemos cómo utilizar un bucle while para leer un fichero línea a línea. La variable line
será la que almacena cada línea en el bucle.
Igual que antes, cuento con una variable num
que llevará la cuenta de la línea en la que estamos.
3. Ejemplo con for y while
Para este ejemplo utilizaré ambos bucles. El bucle for
servirá para iterar por cada parámetro introducido por el usuario (como en el primer ejemplo).
En cada iteración, el bucle while
leerá línea a línea cada fichero y mostrará su contenido por pantalla.
Funciones en Bash
Una función Bash es un bloque de código que agruparemos bajo un nombre, cada vez que queramos ejecutar ese bloque de código, simplemente tenemos que llamar a la función.
El propósito de una función es ayudarnos a hacer que nuestros scripts sean más legibles, y evitar escribir el mismo código una y otra vez.
Declarar una función
Para declarar una función, lo haremos de la siguiente manera:
nombre_funcion () {comandos_a_ejecutar}
Veamos un ejemplo:
Empezaremos escribiendo el nombre de la función hola_zonabit () {
tras esa línea, escribimos los comandos que queremos ejecutar y cerramos la llave }
Para llamar a la función simplemente escribimos el nombre de esta.
# Declaramos la función
hola_zonabit () {
echo 'Bienvenid@ a Zonabit' # Comando que se ejecutará cuando llamemos a la función
}
hola_mundo # Llamada a la función
Alcance de las Variables
Antes de definir variables en nuestras funciones debemos conocer los tipos de variables que encontramos en Bash.
Variables globales
Son variables a las que se puede acceder desde cualquier parte del script. En Bash, todas las variables por defecto se definen como globales, incluso si se declaran dentro de la función.
Variables locales
Las variables locales se pueden declarar dentro del cuerpo de la función con la palabra clave local
y se pueden usar solo dentro de esa función.
Tenemos el siguiente script:
Al ejecutarlo podemos ver como la variable local (definida en la función) no ha afectado a la variable global var1
definida anteriormente.
Esto sucede ya que las variables locales solo toman un valor mientras se ejecuta la función.
Probaremos ahora a sacar el valor de la variable var1
dentro de la función.
Veamos la salida:
Como vemos, mientras se ejecuta la función, la variable var1
toma el valor que hemos definido en la función, es decir "1".
Parámetros en funciones
Antes vimos como a un programa en Bash le podemos pasar datos utilizando parámetros, de la misma forma podemos hacerlo con funciones.
Como vemos, la función se ejecuta con los valores que le hemos pasado por argumento en cada caso.
Ejemplo de uso
Este pequeño programa hará operaciones matemáticas con los parámetros introducidos por el usuario a la hora de ejecutar el script.
En este caso, llamamos a la función con los parámetros $1
y $2
ya que queremos que nuestra función haga operaciones con los datos introducidos por el usuario a la hora de ejecutar el script.
Veamos el resultado:
Como podemos ver, las funciones nos permiten tener una estructura más sana en nuestro código, ya que todo estará organizado en bloques.