En las tres lecciones anteriores has escrito if, case y [[ ]] guiándote por el parecido con otros lenguajes. Funcionaba, pero no sabías por qué. Toca arreglarlo empezando por la idea que en Bash lo gobierna todo y que casi ningún tutorial enuncia con claridad: aquí no se ramifica según un valor booleano, se ramifica según el código de salida de un comando. if no espera una expresión: espera un comando, lo ejecuta y mira si devolvió 0; todo lo demás —[ ], [[ ]], (( ))— son comandos que existen para producir ese 0 o 1. Con esa pieza en su sitio, el resto encaja solo. Al final, revision_salud.sh sabrá decir OK, AVISO o CRÍTICO, y nacerá purgar_releases.sh.
Contenido
- La condición es un código de salida
if,elif,else- Los tres tipos de test y cuál usar
- Tabla completa de operadores
- Expresiones regulares con
=~yBASH_REMATCH casey sus tres terminadores- Bucles:
for,while,until break,continueyselect- Cortocircuitos
&&y||, y su trampa - Bucles y rendimiento: cuándo quieres
awk - Aplicación: niveles de alerta y
purgar_releases.sh
- La condición es un código de salida
operador@srv-tramontana:~$ if grep -q 'db_timeout' /var/log/tramontana/errores.log
> then echo "hay timeouts de base de datos"; fi
hay timeouts de base de datosNo hay ningún == true por ninguna parte. if ejecutó grep -q, grep devolvió 0 porque encontró coincidencias, y 0 significa éxito, así que se entró en el then. Si no hubiera encontrado nada habría devuelto 1 y se habría saltado el bloque.
Tres consecuencias que conviene tener presentes desde ya:
- Cualquier comando puede ser una condición:
if ping -c1 -W2 10.0.2.15,if [[ -f fichero ]],if mi_funcion. Todos son lo mismo. Y el[clásico es un comando de verdad, con su ejecutable en/usr/bin/[; por eso[ $a = $b ]necesita espacios: son argumentos separados. - La lógica está invertida respecto a los lenguajes con booleanos: 0 es verdadero. Si escribes una función que comprueba algo, debe devolver 0 cuando la respuesta es «sí».
if, elif, else
if, elif, elseuso_disco=$(df --output=pcent / | tail -1 | tr -dc '0-9')
if (( uso_disco >= 90 )); then
nivel="CRITICO"
elif (( uso_disco >= 80 )); then
nivel="AVISO"
else
nivel="OK"
fiLa sintaxis exige then y fi; el ; antes de then permite ponerlo en la misma línea. Los elif se evalúan en orden y el primero que acierta gana, así que las condiciones van de la más restrictiva a la más laxa: si intercambiaras los dos primeros bloques, un disco al 95 % diría AVISO. Para negar, ! delante: if ! systemctl is-active --quiet tramontana; then .... Y ojo con if [ $? -eq 0 ], casi siempre redundante: pon el comando directamente en el if.
- Los tres tipos de test y cuál usar
| Forma | Qué es | Cuándo usarla |
|---|---|---|
[ ... ] o test ... |
Comando POSIX | Solo si el script debe correr en dash |
[[ ... ]] |
Palabra reservada de Bash | Siempre, por defecto |
(( ... )) |
Evaluación aritmética | Comparaciones y cuentas numéricas |
La diferencia entre [ ] y [[ ]] no es cosmética. [[ ]] es sintaxis del propio shell, no un comando, así que Bash no divide en palabras ni expande globs dentro de él:
operador@srv-tramontana:~$ f="informe agosto.txt"
operador@srv-tramontana:~$ [ -f $f ] && echo existe
-bash: [: informe: se esperaban operadores binarios
operador@srv-tramontana:~$ [[ -f $f ]] && echo existe || echo "no existe, pero sin error"
no existe, pero sin errorCon [ ] la variable sin comillas se partió en dos argumentos y el comando ni siquiera pudo evaluarse. Con [[ ]] no hace falta entrecomillar, y además admite &&, ||, patrones y =~, que [ ] no tiene. Para números, (( )) es mucho más legible: compara if (( errores > 20 && conexiones >= 200 )) con [ "$errores" -gt 20 -a "$conexiones" -ge 200 ]. Recuerda de 04-02 que dentro de (( )) las variables van sin $.
- Tabla completa de operadores
| Grupo | Operador | Cierto si… |
|---|---|---|
| Ficheros | -e / -f / -d / -L |
Existe / es fichero regular / es directorio / es enlace |
| Ficheros | -s / -r / -w / -x |
No está vacío / permiso de lectura, escritura, ejecución |
| Ficheros | a -nt b / a -ot b |
a es más nuevo / más viejo que b |
| Cadenas | -z "$s" / -n "$s" |
Está vacía / no está vacía |
| Cadenas | $s == patrón |
Coincide con el patrón de globbing (sin comillas a la derecha) |
| Cadenas | == / != / < / > |
Iguales / distintas / orden lexicográfico según la locale |
| Cadenas | $s =~ regex |
Coincide con la expresión regular (apartado 5) |
| Números | -eq -ne -lt -le -gt -ge |
Igual, distinto, menor, menor o igual, mayor, mayor o igual |
| Números | Dentro de (( )) |
== != < <= > >=, y && || ! |
La confusión clásica merece un aviso en negrita: -eq es para números y == para cadenas. [[ "01" -eq "1" ]] es cierto (numéricamente iguales) pero [[ "01" == "1" ]] es falso (cadenas distintas). Usar el operador equivocado da resultados correctos durante meses y luego falla con un cero a la izquierda o con una versión como 3.10.
Otro detalle que sorprende: dentro de [[ ]], el lado derecho de == no se entrecomilla si quieres que sea un patrón:
operador@srv-tramontana:~$ v="3.2.1"; [[ $v == 3.2.* ]] && echo "rama 3.2"
rama 3.2
operador@srv-tramontana:~$ [[ $v == "3.2.*" ]] && echo "rama 3.2" || echo "sin coincidencia"
sin coincidenciaLas comillas convierten el patrón en texto literal: es la fuente número uno de «mi comparación no funciona y no sé por qué».
- Expresiones regulares con
=~ y BASH_REMATCH
=~ y BASH_REMATCH=~ compara con una expresión regular extendida (la ERE de 03-02) y, además de decir sí o no, deja las capturas en el array BASH_REMATCH: la posición 0 es la coincidencia completa y las siguientes, los grupos entre paréntesis.
validar_fecha() {
local fecha="$1"
if [[ $fecha =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
printf 'válida: año %s, mes %s, día %s\n' \
"${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
return 0
fi
printf 'formato inválido: %s\n' "$fecha" >&2
return 1
}
operador@srv-tramontana:~$ validar_fecha "2026-08-18"
válida: año 2026, mes 08, día 18
operador@srv-tramontana:~$ validar_fecha "18/08/2026"; echo "código: $?"
formato inválido: 18/08/2026
código: 1Con esto validas la columna fecha de reservas.csv antes de fiarte de ella. Dos reglas de oro: la regex no va entre comillas —si la entrecomillas se vuelve texto literal, igual que con ==— y si la guardas en una variable, úsala también sin comillas: [[ $f =~ $PATRON ]].
case y sus tres terminadores
case y sus tres terminadorescase compara una cadena contra patrones de globbing —no expresiones regulares— y ejecuta la primera rama que case.
clasificar_codigo() {
case "$1" in
2*) echo "éxito" ;; 3*) echo "redirección" ;;
404) echo "no encontrado" ;; 4*) echo "error del cliente" ;;
500|503) echo "error del servidor" ;; *) echo "desconocido" ;;
esac
}
operador@srv-tramontana:~$ for c in 200 404 418 503 999; do
> printf '%s->%s ' "$c" "$(clasificar_codigo "$c")"; done; echo
200->éxito 404->no encontrado 418->error del cliente 503->error del servidor
999->desconocidoEl orden importa: 404 va antes que 4* porque gana el primero que case. | separa alternativas y *) es el caso por defecto, que va el último y conviene poner siempre. Los tres terminadores:
| Terminador | Efecto |
|---|---|
;; |
Termina el case. El que usarás el 99 % de las veces |
;& |
Cae en la rama siguiente sin comprobar su patrón (el fallthrough de C) |
;;& |
Sigue comprobando los patrones restantes y ejecuta los que casen |
;;& es raro pero útil para etiquetas acumulativas; ;& es peligroso porque un lector distraído no lo ve. Úsalos solo con un comentario que lo justifique.
- Bucles:
for, while, until
for, while, untilfor sobre listas y globs
for release in 3.1.0 3.2.0 3.2.1; do printf 'release %s\n' "$release"; done
for fichero in /var/log/tramontana/*.log; do
[[ -e $fichero ]] || continue # protege del glob sin coincidencias
printf '%-40s %s líneas\n' "$fichero" "$(wc -l < "$fichero")"
doneLa segunda forma —iterar sobre un glob— es la correcta para recorrer ficheros, y trae dos avisos. El primero: si el glob no encuentra nada, Bash deja el patrón literal y el bucle se ejecuta una vez con /var/log/tramontana/*.log como valor; de ahí el [[ -e ... ]] || continue, o bien shopt -s nullglob de 03-02. El segundo, más importante:
Nunca escribas
for f in $(ls).
Lo verás en internet constantemente y está mal por tres motivos acumulativos: ls devuelve texto que el shell parte por espacios, así que «informe agosto.txt» se convierte en dos elementos; los caracteres especiales del nombre se expanden como globs; y ls puede colorear o alinear según opciones y alias. El glob no tiene ninguno de esos problemas porque Bash trabaja con nombres de fichero reales, no con texto. Y existe la forma aritmética, for (( i = 0; i < ${#versiones[@]}; i++ )), para cuando el índice importa.
while y until
while repite mientras la condición devuelva 0; until repite hasta que devuelva 0. Son la misma estructura con la condición negada, y until gana cuando la frase natural es «espera hasta que»:
intentos=0
until curl -sf -o /dev/null --max-time 3 "http://10.0.2.15:8080/salud"; do
(( ++intentos ))
(( intentos >= 10 )) && { echo "la aplicación no arranca" >&2; exit 69; }
sleep 2
done
printf 'respondió tras %d intentos\n' "$intentos"Ese bucle es el que necesita un despliegue para esperar a que el servicio esté listo, y lo reutilizaremos en 04-07. Ya conoces además el while read de 04-03, que es el tercer uso habitual.
break, continue y select
break, continue y selectbreak sale del bucle y continue salta a la siguiente iteración. Ambos aceptan un número que indica cuántos niveles afectan: break 2 sale de dos bucles anidados de golpe.
for dir in /opt/tramontana/releases/*/; do
for fichero in "$dir"*.conf; do
[[ -e $fichero ]] || continue
grep -q 'db_password' "$fichero" || continue
printf 'credencial en %s\n' "$fichero" >&2
break 2 # deja de buscar en todos los releases
done
done
PS3="Elige un release para desplegar: " # select: menú numerado gratis
select v in 3.2.1 3.3.0 cancelar; do
[[ -n $v ]] || { echo "opción no válida" >&2; continue; }
[[ $v == cancelar ]] && break
printf 'desplegando %s\n' "$v"; break
doneSi te encuentras con tres niveles de anidamiento, eso no es un problema de bucles: es una función que aún no has escrito, y lo veremos en 04-05. En cuanto a select, es un bucle y sin break no termina nunca; PS3 es el texto del aviso y $REPLY guarda lo que tecleó el usuario.
- Cortocircuitos
&& y ||, y su trampa
&& y ||, y su trampaYa los usas desde 02-01: A && B ejecuta B solo si A tuvo éxito; A || B ejecuta B solo si A falló. Como condicional de una línea son insuperables: [[ -d $DIR ]] || mkdir -p "$DIR", o command -v shellcheck >/dev/null || { echo "falta shellcheck" >&2; exit 69; }. Pero mucha gente escribe condicion && accion || otra_cosa creyendo que es un if/else, y no lo es:
La clave: || no mira la condición, mira el resultado de la acción anterior. Si la acción falla por cualquier motivo —un mkdir sin permiso, un grep sin coincidencias— la rama del || se ejecuta además de la del &&. La regla: encadena && y || solo cuando la acción no pueda fallar; en cuanto haya un else de verdad, usa if.
- Bucles y rendimiento: cuándo quieres
awk
awkEste apartado te ahorrará horas de espera. Contemos las 14 respuestas 500 de acceso.log de dos formas:
operador@srv-tramontana:~$ time (n=0; while read -r l; do
> grep -q ' 500 ' <<<"$l" && (( ++n )); done < /var/log/tramontana/acceso.log
> echo "$n")
14
real 0m1,873s
operador@srv-tramontana:~$ time awk '$5 == 500 { n++ } END { print n+0 }' \
> /var/log/tramontana/acceso.log
14
real 0m0,006sTrescientas veces más rápido, y solo con 412 líneas. La causa no es que Bash interprete despacio: es que el bucle lanza un proceso grep por cada línea, y crear un proceso cuesta unos milisegundos que se multiplican. Con las 10.000 líneas que tendrá acceso.log en un mes, el bucle tardaría casi un minuto y awk seguiría en centésimas. La regla:
- Si el bucle procesa texto línea a línea, probablemente quieres
awk(ogrep,sed,sort), que hacen una sola pasada dentro de un único proceso. - Si el bucle debe llamar a un comando externo en cada vuelta —
curl,systemctl,tar— entonces sí es trabajo de Bash, porque no hay alternativa. Y si necesitas Bash sobre texto, al menos no lances procesos dentro:readcon varios nombres,casey expansión de parámetros hacen mucho sin salir del shell.
- Aplicación: niveles de alerta y
purgar_releases.sh
purgar_releases.shrevision_salud.sh por fin decide. Sustituimos el bloque de impresión por comprobaciones con nivel:
# --- Evaluación ---------------------------------------------------------
nivel_global="OK"
peor() { # eleva el nivel global si el nuevo es más grave
case "$1" in
CRITICO) nivel_global="CRITICO" ;;
AVISO) [[ $nivel_global == OK ]] && nivel_global="AVISO" ;;
esac
}
case "$codigo_http" in
200) estado_http="OK" ;;
000) estado_http="CRITICO" ;; # curl no llegó ni a conectar
*) estado_http="AVISO" ;;
esac
if (( uso_disco >= 90 )); then estado_disco="CRITICO"
elif (( uso_disco >= UMBRAL_DISCO )); then estado_disco="AVISO"
else estado_disco="OK"; fi
if (( errores_hoy >= 50 )); then estado_err="CRITICO"
elif (( errores_hoy >= 10 )); then estado_err="AVISO"
else estado_err="OK"; fi
peor "$estado_http"; peor "$estado_disco"; peor "$estado_err"
LC_ALL=C printf '%-10s %-8s %s\n' \
"HTTP" "$estado_http" "código $codigo_http" \
"Disco" "$estado_disco" "${uso_disco}% usado (umbral ${UMBRAL_DISCO}%)" \
"Errores" "$estado_err" "$errores_hoy hoy"
printf '\nEstado global: %s\n' "$nivel_global"
case "$nivel_global" in OK) exit 0 ;; AVISO) exit 1 ;; CRITICO) exit 2 ;; esac
operador@srv-tramontana:~$ ~/scripts/revision_salud.sh; echo "código: $?"
HTTP OK código 200
Disco OK 30% usado (umbral 80%)
Errores OK 6 hoy
Estado global: OK
código: 0
operador@srv-tramontana:~$ ~/scripts/revision_salud.sh -u 25 | tail -1
Estado global: AVISOCon esos códigos —0 correcto, 1 aviso, 2 crítico— el script ya sirve para monitorización: Nagios, Zabbix y compañía usan exactamente esa convención. Y el script nuevo:
#!/usr/bin/env bash
#
# purgar_releases.sh - Elimina releases antiguos conservando el activo,
# el anterior y los más nuevos.
# Autor : Operador de sistemas <operador@srv-tramontana> Fecha: 2026-08-18
# Uso : purgar_releases.sh [-n] [-v] Salida: 0 ok | 2 uso | 66 sin releases
set -euo pipefail
readonly BASE="/opt/tramontana"
readonly DIR_RELEASES="$BASE/releases"
DRY_RUN=0; VERBOSE=0
# log(), error() y ejecutar() son las de 04-03; getopts con ":hnv" fija -n y -v.
activo=$(basename "$(readlink -f "$BASE/app")") # a donde apunta el enlace
log "release activo: $activo"
# Iteramos sobre el glob (nunca sobre ls) y ordenamos con sort -V, que sabe
# que 3.10.0 va después de 3.9.0, cosa que sort a secas ignora. mapfile
# vuelca cada línea en un elemento del array, sin partir por espacios.
lista=()
for d in "$DIR_RELEASES"/*/; do [[ -d $d ]] && lista+=("$(basename "$d")"); done
(( ${#lista[@]} )) || { error "no hay releases en $DIR_RELEASES"; exit 66; }
mapfile -t versiones < <(printf '%s\n' "${lista[@]}" | sort -V)
indice_activo=-1 # posición del activo, para saber cuál es "el anterior"
for (( i = 0; i < ${#versiones[@]}; i++ )); do
[[ ${versiones[i]} == "$activo" ]] && indice_activo=$i
done
(( indice_activo >= 0 )) || { error "el activo $activo no está en la lista"; exit 66; }
for (( i = 0; i < ${#versiones[@]}; i++ )); do
v="${versiones[i]}"
(( i >= indice_activo - 1 )) && { log "conservo $v"; continue; }
printf 'purgando %s (%s)\n' "$v" "$(du -sh "$DIR_RELEASES/$v" | cut -f1)"
ejecutar rm -rf -- "${DIR_RELEASES:?}/$v"
done
exit 0operador@srv-tramontana:~$ ~/scripts/purgar_releases.sh -n -v
[2026-08-18 11:04:12] release activo: 3.2.1
purgando 3.1.0 (97M)
[simulado] rm -rf -- /opt/tramontana/releases/3.1.0
[2026-08-18 11:04:12] conservo 3.2.0
[2026-08-18 11:04:12] conservo 3.2.1
[2026-08-18 11:04:12] conservo 3.3.0Justo lo que queríamos: solo 3.1.0 es purgable. Fíjate en dos detalles defensivos: rm -rf -- "${DIR_RELEASES:?}/$v" usa ${VAR:?} de 04-02 para que, si la variable quedara vacía por un error, el comando aborte en vez de convertirse en rm -rf /; y -- evita que un nombre de directorio con guion inicial se lea como opción.
Errores Comunes y Consejos
- Olvidar los espacios dentro de
[ ]o[[ ]].[[$a == $b]]daorden no encontrada: los corchetes son comandos y palabras reservadas, no sintaxis pegada. - Mezclar
-eqcon cadenas o==con números.[[ "3.10" > "3.9" ]]es falso lexicográficamente: para versiones,sort -V; para números,(( )). Y no entrecomilles el lado derecho de==o=~, o el patrón se vuelve texto literal. for f in $(ls). Se rompe con espacios y con caracteres especiales; usa siempre el glob. Y protege el glob sin coincidencias con[[ -e $f ]] || continueoshopt -s nullglob, o el bucle correrá una vez con el patrón literal.- Confiar en
cond && accion || otracomoif/else. Siaccionfalla, se ejecutan las dos ramas. Y anidar tres bucles es la señal inequívoca de que falta una función. - Consejo: compara números siempre con
(( )): es más legible y no confundirás el operador. Pon siempre la rama*)en uncase, aunque solo sea para registrar «valor inesperado». Y antes de escribir un bucle que procese texto, pregúntate siawklo haría de una pasada: casi siempre la respuesta es sí.
Ejercicios
Ejercicio 1. Escribe es_version_valida(), que devuelva 0 si su argumento tiene el formato N.N.N (uno o más dígitos por tramo) y 1 si no, usando =~, e imprima los tres números por separado. Pruébala con 3.2.1, 3.10.0, 3.2 y v3.2.1.
Ejercicio 2. Escribe ~/scripts/resumen_franjas.sh, que sobre acceso.log clasifique cada petición en madrugada (00-05), mañana (06-13), tarde (14-21) o noche (22-23) con case, y muestre peticiones y errores por franja. Hazlo sin lanzar procesos dentro del bucle y compara el tiempo con awk.
Ejercicio 3. Luis ha escrito este fragmento para decidir si hay que purgar. Encuentra los cuatro defectos y reescríbelo.
libre=`df / | tail -1 | awk '{print $5}'`
if [ $libre > 80 ]; then
echo "purgando" && rm -rf /opt/tramontana/releases/* || echo "error"
fiSoluciones
Solución 1.
es_version_valida() {
local v="$1"
# ^ y $ anclan la coincidencia a TODA la cadena: sin ellos, "v3.2.1x"
# también casaría. Los paréntesis crean los grupos de BASH_REMATCH.
[[ $v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || return 1
printf 'mayor=%s menor=%s parche=%s\n' \
"${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
}
for v in 3.2.1 3.10.0 3.2 v3.2.1; do
if es_version_valida "$v"; then printf ' %-8s válida\n' "$v"
else printf ' %-8s NO válida\n' "$v"; fi
done
operador@srv-tramontana:~$ bash /tmp/versiones.sh
mayor=3 menor=2 parche=1
3.2.1 válida
mayor=3 menor=10 parche=0
3.10.0 válida
3.2 NO válida # falta un tramo
v3.2.1 NO válida # el ancla ^ rechaza la 'v'Los puntos van escapados (\.) porque en una regex . significa «cualquier carácter»; sin escapar, 3x2y1 también sería válida. Y fíjate en cómo la función se usa directamente en el if: es el apartado 1 en acción. (Sin return 0 explícito, la función devuelve el código del printf, que es 0.)
Solución 2.
#!/usr/bin/env bash
#
# resumen_franjas.sh - Peticiones y errores por franja horaria en acceso.log.
# Autor : Operador de sistemas <operador@srv-tramontana> Fecha: 2026-08-18
set -euo pipefail
readonly LOG="${1:-/var/log/tramontana/acceso.log}"
declare -A peticiones errores
# read trocea la línea sin lanzar procesos: la hora es el segundo campo y
# el código el quinto. ${hora%%:*} deja solo HH, y el 10# fuerza base
# decimal porque "08" y "09" serían octal inválido (04-02).
while read -r _fecha hora _metodo _ruta codigo _resto; do
case "$(( 10#${hora%%:*} ))" in
[0-5]) franja="madrugada" ;;
[6-9]|1[0-3]) franja="mañana" ;;
1[4-9]|2[01]) franja="tarde" ;;
*) franja="noche" ;;
esac
peticiones["$franja"]=$(( ${peticiones["$franja"]:-0} + 1 ))
[[ $codigo == [45]* ]] && errores["$franja"]=$(( ${errores["$franja"]:-0} + 1 ))
done < "$LOG"
LC_ALL=C printf '%-12s %12s %8s\n' FRANJA PETICIONES ERRORES
for f in madrugada mañana tarde noche; do
LC_ALL=C printf '%-12s %12d %8d\n' "$f" "${peticiones[$f]:-0}" "${errores[$f]:-0}"
done
operador@srv-tramontana:~$ time ~/scripts/resumen_franjas.sh
FRANJA PETICIONES ERRORES
madrugada 88 15
mañana 147 3
tarde 141 4
noche 36 1
real 0m0,048sLos 15 errores de madrugada son los de la franja de las 03:00 que ya conoces, y los 23 totales cuadran. La versión en awk —una línea con substr($2,1,2)+0 y el ternario para la franja— tarda 0,007 s: siete veces menos aun sin procesos dentro del bucle, solo por el coste del intérprete. Pero fíjate en la escala: centésimas contra centésimas. La diferencia catastrófica aparece cuando hay $(...) dentro del bucle, no por el bucle en sí. Y nota el patrón [45]* del case: globbing, no regex.
Solución 3. Los cuatro defectos:
` `en vez de$( ). Las backticks no se anidan, tienen reglas de escape distintas y son ilegibles. Además$5dedfincluye el%, así que la comparación numérica recibe30%.[ $libre > 80 ]. Dentro de[ ],>no compara: es una redirección, así que ese comando crea un fichero llamado80en el directorio actual y el test siempre es cierto. Hay que usar-gto, mejor,(( )).rm -rf /opt/tramontana/releases/*borra todo, incluido el release activo al que apunta el enlaceapp: la aplicación se cae en el acto.echo ... && rm ... || echo "error". Si elrmfalla parcialmente se imprimen ambos mensajes y el script no devuelve ningún código útil. Además no hay--dry-runni registro de qué se borró.
uso_disco=$(df --output=pcent / | tail -1 | tr -dc '0-9')
if (( uso_disco > 80 )); then
printf 'disco al %d%%, purgando releases antiguos\n' "$uso_disco" >&2
if ~/scripts/purgar_releases.sh -v; then printf 'purga completada\n' >&2
else printf 'la purga falló (código %d)\n' "$?" >&2; exit 1; fi
fiLa lógica destructiva se delega en purgar_releases.sh, que ya sabe qué conservar, tiene --dry-run y registra lo que hace. Reutilizar en vez de reescribir es también una estructura de control.
Conclusión
Ya no escribes condicionales por imitación: sabes qué ocurre por debajo.
- Tienes claro que la condición es un código de salida, que 0 es éxito, y que cualquier comando —incluida una función tuya— puede ir en un
if. - Usas
if/elif/elseordenando de la condición más restrictiva a la más laxa, y eliges entre los tres tipos de test:[[ ]]por defecto porque no divide palabras y admite patrones,(( ))para lo numérico, y[ ]solo si necesitas POSIX. - Manejas la tabla completa de operadores de fichero, cadena y número; no confundes
-eqcon==ni entrecomillas el lado derecho de un patrón; validas con=~yBASH_REMATCH, con la regex sin comillas y anclada con^y$; y escribescasecon patrones de globbing, el*)obligatorio y los terminadores;;,;&y;;&. - Recorres con
forsobre globs, nunca sobre$(ls), con la forma aritmética cuando el índice importa, y conwhile/untilpara esperas y lecturas; controlas el flujo conbreak N,continue Nyselect. Conoces la trampa decond && accion || otray la regla de rendimiento: si el bucle procesa texto, probablemente quieresawk, medido en un caso real. - Y tus scripts deciden:
revision_salud.shdevuelve OK, AVISO o CRÍTICO con códigos aptos para monitorización, ypurgar_releases.shrecorre los releases conservando el activo, el anterior y los más nuevos.
Mira ahora los scripts juntos: log() está copiada tres veces, error() también, el envoltorio ejecutar() va por su segunda copia y el bloque de getopts es casi idéntico en todos. Cada vez que mejores uno tendrás que acordarte de mejorar los demás, y algún día no te acordarás. En la siguiente lección, Funciones y Librerías, resolvemos eso: cuándo extraer una función y por qué el nombre es documentación, el ámbito dinámico de Bash y por qué toda variable debe ser local, las tres formas reales de devolver datos cuando return solo admite un número de 0 a 255, y cómo montar lib/comunes.sh con carga robusta, guardas de inclusión y hasta un pequeño test que compruebe que la librería funciona. Al terminarla, esas funciones duplicadas vivirán en un solo sitio.
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
