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 bincompila a la carpetabin.java -cp bin Mainejecuta la clase conbinen el *classpath*.echoescribe 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 —
grepbusca,wccuenta,sortordena—; 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.
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.