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
- ¿Merece esto ser un script?
- Qué es exactamente un script
- El shebang: quién interpreta tu fichero
- Las tres formas de ejecutar un script
- Permisos de ejecución
- Dónde viven los scripts y el PATH
- Anatomía de un script decente
- Comentarios útiles y comentarios inútiles
- Códigos de salida
- Primer script real:
revision_salud.sh - Estilo y herramientas:
shellcheckyshfmt
- ¿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.
- 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-18bash 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.
- 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 foundEl 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.
- 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/releasesEl 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.
- 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.shchmod +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.
- 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-saludUsamos 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.
- 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:-eaborta si un comando falla,-uaborta si usas una variable no definida, ypipefailhace 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 0explícito. Sin él, el script devuelve el código del último comando, que puede ser cualquier cosa.exit 0dice «he llegado hasta aquí y todo fue bien», y eso es lo que leen cron y la monitorización.
- 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).
- 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.
- Primer script real:
revision_salud.sh
revision_salud.shMarta 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 0operador@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: 0Ya 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.logno 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.
- Estilo y herramientas:
shellcheck y shfmt
shellcheck y shfmtBash 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: reescribeshfmt 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\ry el error,bad interpreter: No such file or directory, no dice nada. Diagnostícalo confile script.shy arréglalo condos2unix. - Dejar líneas antes del shebang. Debe ser la primera línea, empezando en la columna 1. Y no llames
test.sha tu script:testes un builtin y el conflicto de nombres da quebraderos de cabeza. - Ejecutar con
sh script.shun 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 elexit 0final: si la última línea es ungrepsin coincidencias, tu script correcto devuelve 1 y cron te avisa de un fallo inexistente. - Consejo: durante el desarrollo,
bash -n script.shcomprueba la sintaxis sin ejecutar nada; cuesta un segundo y evita descubrir unfique falta a mitad de un despliegue. Integra ademásshellchecken 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; fiSoluciones
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 dentrobash /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 0operador@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:03Solución 3. Los cuatro problemas, de más grave a menos:
cdsin comprobar seguido derm -rf *. El defecto letal: si el directorio no existe o no tienes permiso, elcdfalla, el script continúa donde estuvieras y borra todo lo que haya allí. Es SC2164, y se arregla concd ... || exito con modo estricto.#! /bin/shcon[[ ]]. Dos errores: el espacio tras#!lo admite Linux pero no todos los Unix, y sobre todo/bin/shes dash, que no conoce[[ ]]. El script muere en la última línea con[[: not found.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.- Falta todo lo demás: sin cabecera, sin modo estricto, sin
exitexplícito, y el comentario «limpia temporales» no dice qué considera temporal ni desde cuándo. Unrm -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 0Ya 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 bashes la elección por defecto, y conoces la trampa de que en Ubuntu/bin/shes dash y[[ ]]no existe allí. - Distingues las tres formas de ejecutar un script y por qué solo
sourceafecta a tu shell; colocas los scripts en~/scripts, los publicas con un enlace en~/biny 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 yexit 0—, comentas el porqué, devuelves códigos de salida con significado y pasasshellcheckantes 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
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
