====== 01 - Scripts con bash ======
La terminal repite trabajo: teclear de nuevo la misma secuencia de comandos es lento y propenso a erratas. Un **script** guarda esa secuencia en un fichero y la ejecuta en orden con un solo comando: el mismo resultado cada vez, y el fichero documenta cómo se consigue.
**Bash** es la **shell** habitual de los sistemas Unix: el intérprete que lee y ejecuta comandos línea a línea. Un script de bash es eso mismo —una secuencia de comandos— guardada en un fichero.
===== 1. De comandos a script =====
La primera línea de un script es el **shebang**: indica con qué intérprete se ejecuta.
#!/usr/bin/env bash
Se guarda con extensión ''.sh'' y necesita permiso de ejecución:
$ chmod +x saludo.sh # solo la primera vez
$ ./saludo.sh
Los comentarios (''#'') no se ejecutan: explican el qué y el porqué. Los scripts se leen tanto como se escriben.
Cualquier secuencia de comandos cabe en un script. Un ejemplo cotidiano: automatizar compilar y ejecutar un programa:
#!/usr/bin/env bash
echo "Compilando..."
javac -d bin src/Main.java
echo "Ejecutando..."
java -cp bin Main
* ''javac -d bin'' compila a la carpeta ''bin''.
* ''java -cp bin Main'' ejecuta la clase con ''bin'' en el *classpath*.
* ''echo'' escribe en la salida estándar: los mensajes cuentan lo que el script hace.
===== 2. Variables =====
Un script suele tener valores que cambian: se guardan en **variables** y se asignan una vez, al principio.
FUENTE="datos"
* La asignación no lleva espacios alrededor del ''='': no es una comparación, es una asignación.
* Para usarla, ''$FUENTE'', y siempre entre **comillas dobles**: así un valor con espacios no se rompe.
* Por convención, en **MAYÚSCULAS**: distingue la configuración de las demás líneas.
La **sustitución de comandos**, ''$( )'', mete en una variable la salida de un comando: la shell lo ejecuta cada vez que corre el script.
HOY=$(date +%F)
DESTINO="copias/copia-$HOY.tgz"
echo "La copia de hoy irá a $DESTINO"
===== 3. Tuberías y redirecciones =====
La shell **conecta comandos**: la salida de uno puede alimentar al siguiente o terminar en un fichero.
^ Sintaxis ^ Efecto ^
| ''comando > fichero'' | escribe la salida en el fichero; si existe, lo sobreescribe |
| ''comando >> fichero'' | la añade al final del fichero |
| ''comando 2> fichero'' | escribe ahí la salida de **error** |
| ''comando < fichero'' | lee la entrada del fichero |
| ''comando1 \| comando2'' | **tubería**: la salida de uno es la entrada del otro |
$ ls -l > listado.txt
$ find . -name "*.bak" 2> errores.txt # los avisos de find, en un fichero
$ history | grep docker
$ ps aux | grep java
* Los errores van por otra vía, el **error estándar**: por eso el ''2>''. Un fallo silencioso en la salida normal pasaría desapercibido.
* Cada herramienta hace una cosa y la hace bien —''grep'' busca, ''wc'' cuenta, ''sort'' ordena—; la tubería las compone.
===== 4. Errores y códigos de salida =====
Cada comando termina con un **código de salida**: ''0'' si tuvo éxito, distinto de ''0'' si falló. En la terminal no se ve, pero es el contrato con el que comandos y scripts se comunican.
$ cat noexiste.txt
cat: noexiste.txt: No such file or directory
$ echo $? # el código del último comando
1
* ''$?'': el código de salida del último comando.
* ''exit n'': termina el script con ese código.
''set -e'' al principio del script detiene la ejecución en cuanto un comando falla. Fallar pronto: nada de continuar sobre un estado a medias.
#!/usr/bin/env bash
set -e
===== 5. Condicionales y bucles =====
''if'' ejecuta un bloque si la condición se cumple. Los corchetes son un comando de test: necesitan espacios alrededor y las variables entre comillas.
if [ -f "$FUENTE" ]; then
tar -czf copia.tgz "$FUENTE"
else
echo "No existe $FUENTE"
exit 1
fi
^ Test ^ Comprueba ^
| ''-f'' | que existe y es fichero |
| ''-d'' | que existe y es directorio |
| ''-z'' | que la cadena está vacía |
| ''='' | que dos cadenas son iguales |
| ''-eq'' | que dos números son iguales |
''for'' recorre una lista; con la sustitución de comandos, la lista puede ser la salida de ''find'':
for f in $(find . -name "*.txt"); do
echo "Texto: $f"
done
''while'' repite mientras la condición tenga éxito; con ''read'', recorre líneas de un fichero:
while read -r linea; do
echo "$linea"
done < ficheros.txt
===== 6. Funciones =====
Una **función** agrupa una tarea bajo un nombre: se define una vez y el script la llama cuando la necesita.
copia() {
echo "▶ Copiando..."
tar -czf "$DESTINO" "$FUENTE"
}
limpia() {
echo "▶ Limpiando..."
rm -f "$FUENTE"/*.tmp
}
* Una tarea, una función: el cuerpo del script se lee como la lista de tareas que hace.
* El nombre elegido dice la tarea —''copia'', ''limpia''—, no el comando que la resuelve.
===== 7. Subcomandos con case =====
El script admite un argumento que elige la tarea: ''$1'' es el primero (''$0'', el propio nombre del script). El ''case'' compara el argumento con cada caso y ejecuta el bloque correspondiente:
case "$1" in
copia)
copia
;;
limpia)
limpia
;;
*)
echo "Uso: $0 {copia|limpia}"
exit 1
;;
esac
* '';;'' cierra cada caso.
* ''*)'' es el resto: sin argumento o con uno erróneo, el script enseña su uso y termina con error.
* Es el patrón de cualquier herramienta de línea de comandos: ''git commit'', ''docker compose up''... Un comando, una tarea.
@startuml
start
:./tareas.sh "$1";
switch ($1)
case (copia)
:copia;
case (limpia)
:limpia;
case (*)
:uso;
:exit 1;
endswitch
stop
@enduml
===== 8. Componerlo todo =====
Todo junto, el esqueleto de un script con varias tareas:
#!/usr/bin/env bash
set -e
# Configuración
FUENTE="datos"
HOY=$(date +%F)
DESTINO="copias/copia-$HOY.tgz"
# Tareas
copia() {
echo "▶ Copiando..."
tar -czf "$DESTINO" "$FUENTE"
}
limpia() {
echo "▶ Limpiando..."
rm -f "$FUENTE"/*.tmp
}
# Despacho
case "$1" in
copia)
copia
;;
limpia)
limpia
;;
*)
echo "Uso: $0 {copia|limpia}"
exit 1
;;
esac
El esqueleto es siempre el mismo: configuración, tareas, despacho. Cambiar de tarea solo toca las funciones y el ''case''; la forma del script no cambia.