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.

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.

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"

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.

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

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

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.

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.

./tareas.sh "$1"$1copialimpiausoexit 1copia*limpia

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.

  • clase/daw/si/1eval/bash.txt
  • Última modificación: 2026/10/17 10:10
  • por cesguiro