Al cerrar el Módulo 3 quedó un muro señalado con el dedo: la línea de crontab llena de escapes, la tubería del informe que ya no cabía en la pantalla, y esa dependencia insoportable de que tú te acordaras de cada paso a las tres de la madrugada. Este módulo derriba ese muro con la idea más simple del mundo: un script no es más que un fichero de texto con los comandos que ya sabes escribir.

No hay lenguaje nuevo que aprender: grep, awk, find, curl y df siguen siendo los mismos. Lo que cambia es que dejan de vivir en tu memoria para vivir en un fichero con nombre, permisos, autor y fecha; un fichero que puede ejecutarse igual mil veces y correr solo cuando tú duermes. Esta lección monta los cimientos y termina con revision_salud.sh, el primer script del hilo conductor, que las seis lecciones siguientes irán mejorando.

Contenido

  1. ¿Merece esto ser un script?
  2. Qué es exactamente un script
  3. El shebang: quién interpreta tu fichero
  4. Las tres formas de ejecutar un script
  5. Permisos de ejecución
  6. Dónde viven los scripts y el PATH
  7. Anatomía de un script decente
  8. Comentarios útiles y comentarios inútiles
  9. Códigos de salida
  10. Primer script real: revision_salud.sh
  11. Estilo y herramientas: shellcheck y shfmt

  1. ¿Merece esto ser un script?

Escribir un script tiene un coste: hay que mantenerlo, versionarlo, documentarlo y recordar que existe. Antes de abrir el editor, sitúa lo que quieres hacer en esta escala.

Forma Cuándo Ventaja Límite
One-liner / alias Lo haces una vez, o es un comando fijo diario Coste cero, o una línea en .bashrc Se pierde al cerrar sesión; el alias no acepta argumentos en medio
Función de shell Lógica corta con argumentos, solo para ti Vive en tu entorno, sin subproceso No la puede ejecutar cron ni otro usuario
Script Procedimiento repetible, con pasos y decisiones Versionable, compartible, programable Bash se vuelve incómodo por encima de ~300 líneas
Ansible Configurar máquinas, y más de una Idempotencia e inventario de serie Requiere infraestructura y aprendizaje
Lenguaje de programación Estructuras de datos, JSON, concurrencia, API Tipos, tests, librerías Dependencias que instalar y mantener

La regla práctica: si lo has hecho tres veces a mano, o si va a ejecutarlo alguien que no eres tú, o si va a ejecutarlo cron, es un script. Si además empieza a manejar datos anidados o a hablar con APIs JSON, la alarma está sonando y toca mirar hacia Python; volveremos a ello en 04-07. La revisión de salud que Marta pide cada mañana cumple los tres criterios a la vez: es un script.

  1. Qué es exactamente un script

Un fichero de texto plano con comandos que el intérprete lee y ejecuta en orden. No se compila, no se enlaza, no genera binarios.

operador@srv-tramontana:~$ printf 'echo "Hola"\ndate +%%F\n' > /tmp/primero.sh
operador@srv-tramontana:~$ bash /tmp/primero.sh
Hola
2026-08-18

bash ha abierto el fichero, ha leído la primera línea, la ha ejecutado como si tú la hubieras tecleado, y ha pasado a la segunda. Esa equivalencia total entre «lo que tecleas» y «lo que va en un script» es la razón de que este módulo te resulte natural: ya sabes escribir el contenido, solo faltaba el recipiente.

  1. El shebang: quién interpreta tu fichero

Si el fichero se ejecuta por sí mismo, el kernel necesita saber qué programa debe interpretarlo: lo dicen los dos primeros caracteres, #!, seguidos de la ruta del intérprete. Al ejecutar ./script.sh, el kernel lee esos bytes mágicos y arranca en realidad /bin/bash ./script.sh. Sin shebang el resultado depende de dónde estés, así que nunca lo dejes al azar.

Shebang Qué hace Cuándo usarlo
#!/bin/bash Ruta fija a Bash Servidores Linux, donde Bash siempre está ahí
#!/usr/bin/env bash Busca bash en el PATH Portabilidad: macOS, BSD, Bash en /usr/local
#!/bin/sh El shell POSIX del sistema Scripts que deben correr en cualquier Unix, sin extensiones

En este curso usaremos #!/usr/bin/env bash: cuesta lo mismo y te ahorra el día en que ejecutes el script en un macOS donde el Bash moderno está en /opt/homebrew/bin/bash.

Por qué /bin/sh no es Bash en Ubuntu

En Ubuntu y Debian, ls -l /bin/sh revela un enlace a dash, un shell minúsculo que implementa POSIX y nada más. Se eligió así porque los scripts de arranque son miles y dash los ejecuta mucho más rápido. La consecuencia es que todo lo específico de Bash falla:

operador@srv-tramontana:~$ cat > /tmp/prueba_sh.sh <<'EOF'
#!/bin/sh
if [[ "reservas" == reserv* ]]; then
    echo "coincide"
fi
EOF
operador@srv-tramontana:~$ bash /tmp/prueba_sh.sh
coincide
operador@srv-tramontana:~$ sh /tmp/prueba_sh.sh
/tmp/prueba_sh.sh: 2: [[: not found

El mismo fichero, dos intérpretes, dos resultados. [[ ]] es de Bash, igual que los arrays, ${VAR^^}, local -n o <<<. Y observa el detalle cruel: el script no falló al escribirlo, falló al ejecutarse con el intérprete equivocado, posiblemente semanas después y dentro de cron. Si escribes #!/bin/sh, no uses nada de Bash.

  1. Las tres formas de ejecutar un script

Hay tres —bash script.sh arranca un Bash nuevo que lee el fichero, ./script.sh deja que el kernel lea el shebang, y source script.sh (o . script.sh, la forma POSIX) lo ejecuta el shell actual— y la diferencia no es cosmética: cambia en qué proceso se ejecutan tus comandos.

flowchart LR
    A["Tu shell interactivo<br/>PID 3120, cwd=/home/operador"] -->|"bash script.sh<br/>./script.sh"| B["Subproceso Bash<br/>PID 3455, copia del entorno"]
    B -->|"al terminar, muere"| C["El shell original<br/>no ha cambiado"]
    A -->|"source script.sh"| D["Mismo proceso PID 3120<br/>cd, variables y funciones persisten"]

La demostración clásica, un script que cambia de directorio:

operador@srv-tramontana:~$ cat > /tmp/ir_releases.sh <<'EOF'
#!/usr/bin/env bash
cd /opt/tramontana/releases
echo "Dentro del script estoy en: $PWD"
EOF
operador@srv-tramontana:~$ chmod +x /tmp/ir_releases.sh
operador@srv-tramontana:~$ /tmp/ir_releases.sh
Dentro del script estoy en: /opt/tramontana/releases
operador@srv-tramontana:~$ pwd
/home/operador
operador@srv-tramontana:~$ source /tmp/ir_releases.sh
Dentro del script estoy en: /opt/tramontana/releases
operador@srv-tramontana:~$ pwd
/opt/tramontana/releases

El script sí cambió de directorio... en su propio proceso, que ya no existe. Un hijo hereda una copia del entorno de su padre, pero nada de lo que el hijo cambie vuelve al padre: ni el directorio, ni las variables, ni las funciones. Es la herencia de una sola dirección de 03-01; con source no hay proceso nuevo y por eso el cd persiste.

Forma ¿Proceso nuevo? ¿Hace falta chmod +x? ¿Respeta el shebang? Efectos que persisten
bash script.sh Sí No No (manda bash) Ninguno
./script.sh Sí Sí Sí Ninguno
source script.sh No No No cd, variables, funciones

Dos reglas prácticas. Los scripts se ejecutan con ./ o por su nombre, la única forma que respeta el shebang y la que usará cron. Y source se reserva para configuración y librerías de funciones, que es para lo que lo usaremos en 04-05. Un aviso: como source no crea proceso, un exit 1 dentro de un fichero que haces source en tu terminal te cierra la sesión. Le ha pasado a todo el mundo una vez.

  1. Permisos de ejecución

Un fichero de texto no es ejecutable hasta que se lo dices al sistema. Retomando 02-07:

operador@srv-tramontana:~/scripts$ ./revision_salud.sh
-bash: ./revision_salud.sh: Permiso denegado
operador@srv-tramontana:~/scripts$ chmod +x revision_salud.sh && ls -l revision*
-rwxr-x--- 1 operador operador 512 ago 18 09:14 revision_salud.sh

chmod +x respeta el umask 027 del Módulo 2: añade x a usuario y grupo, no a otros; chmod 750 dice lo mismo sin depender del umask. Dos matices: el bit x no significa «esto es código máquina» sino «permíteme intentar ejecutarlo» —quien decide cómo interpretarlo es el shebang—, y bash script.sh funciona sin bit x porque ahí el fichero solo se lee, que es la forma cómoda de probar algo a medio escribir.

  1. Dónde viven los scripts y el PATH

Teclear /home/operador/scripts/revision_salud.sh cada mañana garantiza que nadie lo use. La solución es el PATH, que ya diseccionaste en 03-01.

Ubicación Para quién Notas
~/scripts Tú, con ruta explícita Donde vive el código fuente en este curso
~/bin o ~/.local/bin Solo tu usuario Ubuntu lo añade al PATH si existe al iniciar sesión
/usr/local/bin Todos los usuarios El sitio correcto para scripts locales del sistema (/usr/bin es del gestor de paquetes: nunca pongas nada tuyo ahí)
operador@srv-tramontana:~$ mkdir -p ~/bin ~/scripts
operador@srv-tramontana:~$ ln -s ~/scripts/revision_salud.sh ~/bin/revision-salud
operador@srv-tramontana:~$ source ~/.profile && which revision-salud
/home/operador/bin/revision-salud

Usamos un enlace simbólico en vez de mover el fichero: el código sigue en ~/scripts (versionado en git, ya en 04-07) y ~/bin solo publica un nombre cómodo, sin extensión y con guiones, como los comandos de Unix. Sigue vigente la regla del Módulo 3: . nunca en el PATH, porque bastaría con que alguien dejara un fichero llamado ls en un directorio compartido para que ejecutaras su código sin saberlo.

  1. Anatomía de un script decente

Esta es la plantilla con la que vas a empezar todos tus scripts desde hoy. Cada línea evita un problema concreto que verás durante el módulo.

#!/usr/bin/env bash
#
# nombre.sh - Una línea diciendo qué hace.
#
# Autor : Nombre Apellido <correo>
# Fecha : 2026-08-18
# Uso   : nombre.sh [opciones] <argumento>
#
# Descripción algo más larga si hace falta: qué comprueba, qué modifica,
# qué deja escrito y dónde.

set -euo pipefail

# --- Constantes ----------------------------------------------------------
DIRECTORIO_LOG="/var/log/tramontana"
UMBRAL_DISCO=80

# --- Cuerpo --------------------------------------------------------------
# ... aquí el trabajo real ...
exit 0
  • Cabecera. Nombre, propósito, autor, fecha y modo de uso: cuando dentro de ocho meses encuentres este fichero a las cuatro de la mañana, esas cinco líneas valdrán más que todo lo demás.
  • set -euo pipefail. El modo estricto: -e aborta si un comando falla, -u aborta si usas una variable no definida, y pipefail hace que una tubería falle si falla cualquiera de sus tramos, no solo el último (lo anunciamos en 03-04). Ponlo desde hoy en todo lo que escribas; el porqué detallado y —sobre todo— dónde no funciona es el corazón de 04-06.
  • Constantes en MAYÚSCULAS, minúsculas para las variables de trabajo, y todas agrupadas arriba: cambiar un umbral no debe obligar a leer el script entero.
  • exit 0 explícito. Sin él, el script devuelve el código del último comando, que puede ser cualquier cosa. exit 0 dice «he llegado hasta aquí y todo fue bien», y eso es lo que leen cron y la monitorización.

  1. Comentarios útiles y comentarios inútiles

Todo lo que sigue a # se ignora hasta el fin de línea, salvo el shebang. Pero comentar mucho no es comentar bien.

# INÚTIL: repite lo que el código ya dice
uso=$(df --output=pcent / | tail -1)   # asigna el uso de disco a uso

# ÚTIL: documenta una decisión que parece un error
# No usamos 'grep -c' a secas porque devuelve 1 si no hay coincidencias
# y con 'set -e' eso abortaría el script un día tranquilo sin errores.
errores=$(grep -c "^$hoy" "$LOG_ERRORES" || true)

La regla: el código dice qué hace; el comentario dice por qué. Si necesitas un comentario para entender qué hace una línea, la solución suele ser reescribirla o extraerla a una función con buen nombre (04-05).

  1. Códigos de salida

Todo proceso termina devolviendo un entero de 0 a 255. Ya lo conoces como $?; ahora tú eres quien lo produce, y eso cambia la responsabilidad: el código de salida es la única parte de tu script que otro programa va a leer. La convención es al revés de lo intuitivo —0 es éxito— y tiene sentido: hay una sola forma de acertar y muchas de fallar, cada una con su número.

Código Significado
0 Éxito
1 Error genérico
2 Uso incorrecto: faltan argumentos, opción desconocida
126 / 127 Existe pero no se pudo ejecutar (falta el bit x) / comando no encontrado (típicamente, el PATH de cron)
128+N Terminado por la señal N: 130 = Ctrl+C, 143 = SIGTERM, 137 = SIGKILL

(Y exit -1 acaba en 255, fuera de rango.) exit sin argumento devuelve el código del último comando: sé explícito. Y evita inventarte códigos por encima de 125, que chocan con los reservados; tienes de sobra con el rango 3-125 para tus propios significados, documentados en la cabecera. En 04-03 formalizaremos una tabla para nuestros scripts.

  1. Primer script real: revision_salud.sh

Marta pide lo mismo cada mañana: «¿está la web arriba, queda disco, y hubo errores esta noche?». Lo escribimos tal cual, como una tirada de comandos, aplicando la plantilla del apartado anterior.

#!/usr/bin/env bash
#
# revision_salud.sh - Revisión rápida de salud de Tramontana Reservas.
#
# Autor : Operador de sistemas <operador@srv-tramontana>
# Fecha : 2026-08-18
# Uso   : revision_salud.sh
#
# Comprueba tres cosas: que la aplicación responde en el puerto 8080,
# cuánto disco queda en la raíz y cuántos errores se han registrado hoy.
# No modifica nada: es de solo lectura y seguro de ejecutar en producción.

set -euo pipefail

# --- Constantes ----------------------------------------------------------
URL_SALUD="http://10.0.2.15:8080/salud"
LOG_ERRORES="/var/log/tramontana/errores.log"
UMBRAL_DISCO=80
ESPERA_MAX=5

# --- Cuerpo --------------------------------------------------------------
echo "=== Revisión de salud de Tramontana Reservas ==="
echo "Servidor : $(hostname)"
echo "Fecha    : $(date '+%F %T')"
echo

# -s silencia la barra de progreso, -o /dev/null tira el cuerpo de la
# respuesta, -w imprime solo el código, --max-time evita colgarse.
codigo_http=$(curl -s -o /dev/null -w '%{http_code}' \
    --max-time "$ESPERA_MAX" "$URL_SALUD" || true)
echo "HTTP 8080  : $codigo_http"

# --output=pcent da solo la columna del porcentaje; tail -1 salta la
# cabecera "Use%"; tr -dc '0-9' deja el número limpio para comparar.
uso_disco=$(df --output=pcent / | tail -1 | tr -dc '0-9')
echo "Disco /    : ${uso_disco}% usado (umbral ${UMBRAL_DISCO}%)"

# grep -c devuelve 1 cuando no encuentra nada y con set -e eso abortaría
# el script justo el día que todo va bien. De ahí el || true.
hoy=$(date '+%Y-%m-%d')
errores_hoy=$(grep -c "^$hoy" "$LOG_ERRORES" || true)
echo "Errores hoy: $errores_hoy"
exit 0
operador@srv-tramontana:~$ chmod 750 ~/scripts/revision_salud.sh
operador@srv-tramontana:~$ ~/scripts/revision_salud.sh; echo "código: $?"
=== Revisión de salud de Tramontana Reservas ===
Servidor : srv-tramontana
Fecha    : 2026-08-18 09:22:41

HTTP 8080  : 200
Disco /    : 30% usado (umbral 80%)
Errores hoy: 6
código: 0

Ya es infinitamente mejor que tres comandos en el historial. Pero es honestamente malo, y conviene que veas por qué desde el principio, porque esa lista es el índice del resto del módulo:

  • Las constantes están dentro del script y no acepta opciones: ni -h, ni un umbral distinto, ni modo silencioso (04-02 y 04-03).
  • Imprime el umbral pero no lo compara con nada. No hay ninguna decisión: si el disco estuviera al 95 % diría «95%» tan tranquilo (04-04).
  • Tres bloques casi idénticos que piden a gritos ser funciones (04-05), y si errores.log no existe el script muere a medias con un mensaje feo (04-06).
  • Siempre devuelve 0, así que ninguna monitorización puede usarlo (04-07).

Guárdalo tal cual: al final del módulo lo compararás con su versión final.

  1. Estilo y herramientas: shellcheck y shfmt

Bash tiene una sintaxis llena de trampas y un intérprete que casi nunca se queja: escribe $fichero sin comillas y funcionará durante meses, hasta el día en que aparezca un nombre con un espacio. El análisis estático no es opcional en shell.

operador@srv-tramontana:~$ sudo apt install -y shellcheck shfmt
operador@srv-tramontana:~$ printf '#!/usr/bin/env bash\ndir=/opt/tramontana/releases\ncd $dir\nrm -f $1\n' > /tmp/malo.sh
operador@srv-tramontana:~$ shellcheck /tmp/malo.sh
In /tmp/malo.sh line 3:
cd $dir
   ^--^ SC2086: Double quote to prevent globbing and word splitting.
^-----^ SC2164: Use 'cd ... || exit' in case cd fails.
In /tmp/malo.sh line 4:
rm -f $1
      ^-- SC2086: Double quote to prevent globbing and word splitting.

Tres avisos en cuatro líneas y los tres son bombas de relojería: sin comillas, un directorio con espacios se parte en dos argumentos; y si el cd falla, el rm -f se ejecuta en el directorio en el que estuvieras. Cada código SCxxxx tiene su página en el wiki de ShellCheck; en 04-06 desmenuzaremos los cuatro más frecuentes. La regla del módulo desde hoy: ningún script se da por terminado hasta que shellcheck no dice nada.

operador@srv-tramontana:~$ shellcheck ~/scripts/revision_salud.sh && echo "Limpio"
Limpio
operador@srv-tramontana:~$ shfmt -i 4 -d ~/scripts/revision_salud.sh   # -d: solo el diff
operador@srv-tramontana:~$ shfmt -i 4 -w ~/scripts/revision_salud.sh   # -w: reescribe

shfmt formatea en lugar de criticar: -i 4 fija cuatro espacios, -d enseña qué cambiaría sin tocar nada (la costumbre de --dry-run del curso) y -w escribe. Sobre estilo, tres decisiones que no discutiremos más: cuatro espacios de indentación (nunca tabuladores, que se rompen al copiar), líneas de menos de 100 caracteres partiendo con \, y una instrucción por línea.

Errores Comunes y Consejos

  • Escribir el script en Windows y subirlo. Los finales CRLF hacen que el shebang sea #!/usr/bin/env bash\r y el error, bad interpreter: No such file or directory, no dice nada. Diagnostícalo con file script.sh y arréglalo con dos2unix.
  • Dejar líneas antes del shebang. Debe ser la primera línea, empezando en la columna 1. Y no llames test.sh a tu script: test es un builtin y el conflicto de nombres da quebraderos de cabeza.
  • Ejecutar con sh script.sh un script con shebang de Bash. El shebang se ignora cuando invocas el intérprete a mano: es la causa número uno de «funciona en mi terminal y falla en el servidor».
  • Esperar que un script cambie tu directorio o tus variables. No puede: es otro proceso. Si de verdad lo necesitas, es una función o un fichero para source. Y no olvides el exit 0 final: si la última línea es un grep sin coincidencias, tu script correcto devuelve 1 y cron te avisa de un fallo inexistente.
  • Consejo: durante el desarrollo, bash -n script.sh comprueba la sintaxis sin ejecutar nada; cuesta un segundo y evita descubrir un fi que falta a mitad de un despliegue. Integra además shellcheck en tu editor y empieza cada script copiando la plantilla del apartado 7.

Ejercicios

Ejercicio 1. Demuestra empíricamente, con un solo fichero y sin editarlo entre pruebas, la diferencia entre bash script.sh, ./script.sh y source script.sh. El script debe definir una variable, cambiar de directorio e imprimir su propio PID. Explica qué observas en cada caso y por qué.

Ejercicio 2. Escribe ~/scripts/info_release.sh: qué versión de Tramontana está activa (resolviendo /opt/tramontana/app), cuánto ocupa ese release y cuándo se desplegó. Aplica la plantilla completa, déjalo limpio de shellcheck y publícalo en ~/bin como info-release.

Ejercicio 3. Luis ha dejado este script en el servidor. Encuentra cuatro problemas distintos sin ejecutarlo, justifica cada uno y reescríbelo.

#! /bin/sh
# limpia temporales
cd /srv/tramontana/backups/temporales
rm -rf *
if [[ $? == 0 ]]; then echo ok; fi

Soluciones

Solución 1. $$ contiene el PID del shell que ejecuta el código: es el testigo que delata si ha habido proceso nuevo.

operador@srv-tramontana:~$ cat > /tmp/demo_ambito.sh <<'EOF'
#!/usr/bin/env bash
echo "PID del script    : $$"
MI_VARIABLE="definida dentro"
cd /opt/tramontana/releases
echo "Directorio dentro : $PWD"
EOF
operador@srv-tramontana:~$ chmod +x /tmp/demo_ambito.sh; echo "Mi shell: $$"
Mi shell: 3120
operador@srv-tramontana:~$ /tmp/demo_ambito.sh
PID del script    : 3456
Directorio dentro : /opt/tramontana/releases
operador@srv-tramontana:~$ echo "$PWD - ${MI_VARIABLE:-(no existe)}"
/home/operador - (no existe)
operador@srv-tramontana:~$ source /tmp/demo_ambito.sh
PID del script    : 3120
Directorio dentro : /opt/tramontana/releases
operador@srv-tramontana:~$ echo "$PWD - ${MI_VARIABLE:-(no existe)}"
/opt/tramontana/releases - definida dentro

bash /tmp/demo_ambito.sh da el mismo resultado que ./: PID distinto del de tu shell, luego proceso nuevo, y el cd y la variable mueren con él. Solo cambia el mecanismo: bash fichero arranca Bash explícitamente e ignora el shebang, mientras que ./fichero hace que el kernel lea el shebang y decida —y por eso ese caso, y solo ese, exige el bit x—. El tercero imprime el mismo PID 3120: no hay proceso nuevo, lo ha ejecutado tu propio Bash, y por eso todo persiste. El ${MI_VARIABLE:-(no existe)} evita que echo imprima un vacío ambiguo; lo verás en detalle en 04-02.

Solución 2.

#!/usr/bin/env bash
#
# info_release.sh - Informa del release activo de Tramontana Reservas.
#
# Autor : Operador de sistemas <operador@srv-tramontana>
# Fecha : 2026-08-18
# Uso   : info_release.sh  (solo lectura)

set -euo pipefail

ENLACE_APP="/opt/tramontana/app"

# readlink -f resuelve el enlace hasta su destino final en ruta absoluta,
# necesario porque 'app' es un enlace *relativo* al release activo.
ruta_release=$(readlink -f "$ENLACE_APP")
version=$(basename "$ruta_release")
tamano=$(du -sh "$ruta_release" | cut -f1)

# stat sobre el ENLACE (no sobre su destino) da cuándo se cambió, que es
# exactamente cuándo se desplegó. cut quita la fracción de segundo.
desplegado=$(stat -c '%y' "$ENLACE_APP" | cut -d'.' -f1)

printf 'Release activo : %s\nRuta           : %s\n' "$version" "$ruta_release"
printf 'Tamaño         : %s\nDesplegado     : %s\n' "$tamano" "$desplegado"

exit 0
operador@srv-tramontana:~$ chmod 750 ~/scripts/info_release.sh
operador@srv-tramontana:~$ shellcheck ~/scripts/info_release.sh && echo "Limpio"
Limpio
operador@srv-tramontana:~$ ln -s ~/scripts/info_release.sh ~/bin/info-release
operador@srv-tramontana:~$ info-release
Release activo : 3.2.1
Ruta           : /opt/tramontana/releases/3.2.1
Tamaño         : 98M
Desplegado     : 2026-08-11 17:42:03

Solución 3. Los cuatro problemas, de más grave a menos:

  1. cd sin comprobar seguido de rm -rf *. El defecto letal: si el directorio no existe o no tienes permiso, el cd falla, el script continúa donde estuvieras y borra todo lo que haya allí. Es SC2164, y se arregla con cd ... || exit o con modo estricto.
  2. #! /bin/sh con [[ ]]. Dos errores: el espacio tras #! lo admite Linux pero no todos los Unix, y sobre todo /bin/sh es dash, que no conoce [[ ]]. El script muere en la última línea con [[: not found.
  3. if [[ $? == 0 ]]. Redundante y frágil: en Bash se ramifica directamente sobre el comando (if rm -rf ...; then), como veremos en 04-04. Además == compara cadenas donde debería usarse un test numérico.
  4. Falta todo lo demás: sin cabecera, sin modo estricto, sin exit explícito, y el comentario «limpia temporales» no dice qué considera temporal ni desde cuándo. Un rm -rf * merece más explicación que ninguna otra línea del fichero.

La versión reescrita hace además lo que el nombre promete —borrar lo antiguo, no todo—:

#!/usr/bin/env bash
#
# limpiar_temporales.sh - Borra los temporales de copia con más de 7 días.
# Autor : Operador de sistemas <operador@srv-tramontana>  Fecha: 2026-08-18

set -euo pipefail
DIR_TEMPORALES="/srv/tramontana/backups/temporales"
DIAS_RETENCION=7

# -mindepth 1 no toca el propio directorio; -xdev no cruza montajes;
# -delete evita depender de un glob que podría quedar sin expandir e
# intentar borrar un fichero llamado literalmente *.
find "$DIR_TEMPORALES" -mindepth 1 -xdev -type f \
    -mtime +"$DIAS_RETENCION" -delete
echo "Temporales de más de ${DIAS_RETENCION} días eliminados."
exit 0

Ya no hay cd, así que el peligro desaparece de raíz, y el modo estricto aborta si find falla.

Conclusión

Tienes los cimientos, y son más de los que parece.

  • Sabes cuándo escribir un script y cuándo basta un alias, una función o Ansible, con la regla de las tres repeticiones como criterio.
  • Entiendes que un script es texto plano con comandos, dominas el shebang y por qué #!/usr/bin/env bash es la elección por defecto, y conoces la trampa de que en Ubuntu /bin/sh es dash y [[ ]] no existe allí.
  • Distingues las tres formas de ejecutar un script y por qué solo source afecta a tu shell; colocas los scripts en ~/scripts, los publicas con un enlace en ~/bin y sigues sin poner . en el PATH.
  • Arrancas todo script con la misma plantilla —shebang, cabecera con uso, set -euo pipefail, constantes en mayúsculas, cuerpo y exit 0—, comentas el porqué, devuelves códigos de salida con significado y pasas shellcheck antes de dar nada por terminado.

revision_salud.sh ya existe y ya funciona, pero tiene las constantes clavadas en el código, no compara nada y no sabe decir si el resultado es bueno o malo. En la próxima lección, Variables y Tipos de Datos, atacamos la primera carencia: verás que en Bash todo es una cadena y qué implica eso, dominarás la expansión de parámetros —esa colección de ${VAR#...}, ${VAR:-...} y ${VAR//.../...} que convierte diez líneas en una—, aprenderás por qué Bash no sabe sumar decimales y qué hacer con el importe medio de reservas.csv, y contarás reservas por casa con arrays asociativos sin salir del shell. Al terminarla, revision_salud.sh tendrá su configuración fuera del cuerpo del script.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados