Anexo A: Bash Scripting - Mini-Curso Práctico
Anexo A: Bash Scripting - Mini-Curso Práctico
Introducción
Bash (Bourne Again Shell) es el intérprete de comandos por defecto en Linux. Aunque puedes ejecutar comandos uno a uno en la terminal, crear scripts de Bash te permite automatizar tareas complejas. Este mini-curso es completamente práctico con ejemplos reales que usarás en Abacom.
Ver definiciones rapidas en: Glosario del curso
🗺️ En este anexo aprenderás:
- Variables y tipos de datos en Bash
- Condicionales (if/else, case)
- Loops (for, while)
- Funciones y modularización
- Manejo de argumentos
- Flags y opciones (getopts)
- Redirección y pipes
- Debugging y limpieza (trap)
- Scripts prácticos listos para usar
⏱️ Duración estimada: 2 horas de práctica
¿Por Qué Aprender Bash?
En administración de servidores Linux, el 80% de tu trabajo es automatizar tareas. Sin Bash, ejecutarías comandos manualmente:
BASH
# Manual (tedioso)
apt update
apt upgrade -y
apt install nginx
systemctl enable nginx
systemctl start nginx
# ... repetir en 20 servidoresCon Bash, lo automatizas:
BASH
1#!/bin/bash
# Instalar y configurar nginx en un script
apt update && apt upgrade -y
apt install -y nginx
systemctl enable nginx
systemctl start nginx- 1
- #!/bin/bash es el “shebang” - le dice al SO que execute este archivo con Bash
Beneficios:
- ⏱️ Una línea en vez de 4 pasos manuales
- 🔄 Reutilizable en múltiples máquinas
- 🐛 Menos errores humanos
- 📚 Documentación del proceso
💡 Scripting en Diferentes SOs
BASH
#!/bin/bash
# Script nativo en Linux y macOS
# Variables
SERVIDOR="abacom-prod"
PUERTO=8080
# Función
setup_server() {
echo "Configurando $SERVIDOR en puerto $PUERTO"
systemctl restart nginx
netstat -tulpn | grep $PUERTO
}
# Loop
for i in {1..5}; do
echo "Intento $i"
setup_server && break
done
# Ejecutar
chmod +x script.sh
./script.shDónde funciona:
- ✅ Linux (Bash nativo)
- ✅ macOS (Bash/Zsh - compatible)
- ❌ Windows (requiere WSL o Git Bash)
POWERSHELL
# Equivalente en Windows PowerShell
# Variables
$Servidor = "abacom-prod"
$Puerto = 8080
# Función
function Setup-Server {
param([string]$Server)
Write-Host "Configurando $Server en puerto $Puerto"
Get-NetTCPConnection -LocalPort $Puerto | Select-Object State
}
# Loop
for ($i = 1; $i -le 5; $i++) {
Write-Host "Intento $i"
Setup-Server -Server $Servidor
if ($?) { break }
}
# Ejecutar
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
.\script.ps1Dónde funciona:
- ✅ Windows (PowerShell nativo)
- ⚠️ Linux/macOS (PowerShell 7+ disponible)
PYTHON
#!/usr/bin/env python3
# Mejor alternativa para scripts portables
import subprocess
import sys
# Variables
SERVIDOR = "abacom-prod"
PUERTO = 8080
def setup_server(server):
"""Configurar servidor"""
print(f"Configurando {server} en puerto {PUERTO}")
try:
result = subprocess.run(
["systemctl", "restart", "nginx"],
capture_output=True
)
return result.returncode == 0
except Exception as e:
print(f"Error: {e}")
return False
# Loop
for i in range(1, 6):
print(f"Intento {i}")
if setup_server(SERVIDOR):
break
# Ejecutar
# chmod +x script.py
# ./script.pyDónde funciona:
- ✅ Linux (Python 3)
- ✅ macOS (Python 3)
- ✅ Windows (Python 3)
JAVASCRIPT
#!/usr/bin/env node
// Alternativa moderna en JavaScript
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const SERVIDOR = "abacom-prod";
const PUERTO = 8080;
async function setupServer(server) {
console.log(`Configurando ${server} en puerto ${PUERTO}`);
try {
const { stdout } = await execAsync('systemctl restart nginx');
return true;
} catch (error) {
console.error(`Error: ${error}`);
return false;
}
}
// Main
(async () => {
for (let i = 1; i <= 5; i++) {
console.log(`Intento ${i}`);
if (await setupServer(SERVIDOR)) break;
}
})();
// Ejecutar
// chmod +x script.js
// node script.jsDónde funciona:
- ✅ Linux (Node.js)
- ✅ macOS (Node.js)
- ✅ Windows (Node.js)
Recomendación para Abacom:
- Use Bash para scripts Linux-only (producción)
- Use Python para herramientas multi-SO
- Use PowerShell solo si necesita automatizar Windows
Concepto 1: Variables
Declarar Variables
BASH
1NOMBRE="Diego"
EDAD=35
SERVIDOR="web-prod-01"- 1
- NOMBRE=“Diego” asigna el valor “Diego” a la variable NOMBRE (sin espacios alrededor de =)
Reglas en Bash:
- Sin espacios alrededor del = (correcto: VAR=valor, incorrecto: VAR = valor)
- Nombres en MAYÚSCULAS por convención
- Pueden contener letras, números, guiones bajos
- Acceder con
$NOMBREo${NOMBRE}
Usar Variables
BASH
1echo "Hola, $NOMBRE"- 1
-
echo imprime texto.
$NOMBREreemplaza con valor almacenado
Salida esperada:
Hola, Diego
Variables de Entrada
BASH
#!/bin/bash
echo "¿Cuál es tu nombre?"
1read NOMBRE
echo "Bienvenido, $NOMBRE"- 1
- read NOMBRE espera entrada del usuario y la almacena en NOMBRE
Ejecución:
BASH
$ bash script.sh
¿Cuál es tu nombre?
Diego ← Usuario escribe aquí
Bienvenido, DiegoVariables Especiales
- 1
- $0 = nombre del script
- 2
- $1 = primer argumento pasado al script
- 3
-
$@= todos los argumentos - 4
- $? = código de salida del comando anterior (0=éxito, >0=error)
- 5
- $$ = PID (Process ID) del script actual
Ejemplo:
BASH
#!/bin/bash
echo "Script: $0"
echo "Primer arg: $1"
echo "Todos los args: $@"Ejecución:
BASH
$ bash script.sh diego 35 linux
Script: script.sh
Primer arg: diego
Todos los args: diego 35 linuxConcepto 2: Condicionales (if/else)
if Básico
BASH
#!/bin/bash
EDAD=35
1if [ $EDAD -gt 18 ]
then
echo "Eres adulto"
fi- 1
-
[ $EDAD -gt 18 ]verifica si EDAD es MAYOR QUE 18 (-gt = “greater than”)
Operadores de comparación numérica:
- -eq = igual (equal)
- -ne = no igual (not equal)
- -lt = menor que (less than)
- -le = menor o igual
- -gt = mayor que (greater than)
- -ge = mayor o igual
if/else
BASH
#!/bin/bash
SISTEMA="Linux"
1if [ "$SISTEMA" = "Linux" ]
then
echo "Sistema: Linux"
else
echo "No es Linux"
fi- 1
-
[ "$SISTEMA" = "Linux" ]compara strings (notar comillas alrededor de $SISTEMA)
Operadores de string:
- = = igual
- != = no igual
- -z = string vacío
- -n = string no vacío
if/elif/else
BASH
#!/bin/bash
1DISTRO=$1
if [ "$DISTRO" = "ubuntu" ]
then
echo "Package Manager: apt"
elif [ "$DISTRO" = "centos" ]
then
echo "Package Manager: yum/dnf"
elif [ "$DISTRO" = "arch" ]
then
echo "Package Manager: pacman"
else
echo "Distribución desconocida"
fi- 1
- $1 es el primer argumento pasado al script
Verificar Archivos
BASH
#!/bin/bash
CONFIG_FILE="/etc/nginx/nginx.conf"
1if [ -f "$CONFIG_FILE" ]
then
echo "Archivo existe"
else
echo "Archivo no encontrado"
fi- 1
- [ -f $FILE ] verifica si el archivo existe y es un archivo regular
Operadores de archivo:
- -f = es archivo regular
- -d = es directorio
- -e = existe (archivo o directorio)
- -r = legible (readable)
- -w = escribible (writable)
- -x = ejecutable
Concepto 2.5: case (múltiples opciones)
Cuando tienes muchas opciones (distro, entorno, modo de ejecución), case suele ser más claro que varios elif.
BASH
#!/bin/bash
1DISTRO="${1:-}"
2case "$DISTRO" in
ubuntu|debian)
echo "Package manager: apt"
;;
centos|rocky|rhel)
echo "Package manager: dnf"
;;
arch)
echo "Package manager: pacman"
;;
"")
echo "Uso: $0 <distro>" >&2
echo "Ejemplo: $0 ubuntu" >&2
exit 2
;;
*)
echo "Distribución no soportada: $DISTRO" >&2
exit 2
;;
esac- 1
- “${1:-}” evita errores si no pasan argumentos; deja DISTRO vacío si falta
- 2
- case “$DISTRO” in evalúa alternativas y ejecuta el bloque que coincida
Concepto 2.6: Flags y opciones con getopts
getopts permite parsear opciones tipo -u diego -g developers sin depender del orden.
BASH
#!/bin/bash
1set -euo pipefail
2USUARIO=""
3GRUPO=""
4SHELL="/bin/bash"
5while getopts ":u:g:s:h" opt; do
case "$opt" in
u) USUARIO="$OPTARG" ;;
g) GRUPO="$OPTARG" ;;
s) SHELL="$OPTARG" ;;
h)
echo "Uso: $0 -u <usuario> -g <grupo> [-s <shell>]" >&2
exit 0
;;
:)
echo "Falta valor para -$OPTARG" >&2
exit 2
;;
\?)
echo "Opción inválida: -$OPTARG" >&2
exit 2
;;
esac
done
6if [ -z "$USUARIO" ] || [ -z "$GRUPO" ]
then
echo "Uso: $0 -u <usuario> -g <grupo> [-s <shell>]" >&2
exit 2
fi
7echo "OK: usuario='$USUARIO' grupo='$GRUPO' shell='$SHELL'"- 1
- set -euo pipefail hace que el script falle temprano ante errores comunes
- 2
- USUARIO=““ inicializa variable para validar más adelante
- 3
- GRUPO=““ idem; lo exigimos como parámetro obligatorio
- 4
-
SHELL=“/bin/bash” define valor por defecto si no pasan
-s - 5
-
getopts “:u:g:s:h” define flags;
u,g,sesperan argumento yhmuestra ayuda - 6
- -z valida que los campos obligatorios no estén vacíos
- 7
-
echo aquí simula la acción; en producción reemplaza por
useradd,usermod, etc.
Concepto 3: Loops (for, while)
Loop for con Lista
BASH
#!/bin/bash
SERVIDORES="web-1 web-2 web-3"
1for SERVIDOR in $SERVIDORES
do
echo "Reinsticiando $SERVIDOR"
done- 1
- for SERVIDOR in itera sobre cada valor en SERVIDORES
Salida:
Reiniciando web-1
Reiniciando web-2
Reiniciando web-3
Loop for con Rango
BASH
#!/bin/bash
1for i in {1..5}
do
echo "Número: $i"
done- 1
- {1..5} genera rango del 1 al 5
Alternativa (sintaxis C):
BASH
1for ((i=1; i<=5; i++))
do
echo "Número: $i"
done- 1
- Sintaxis similar a C: inicialización, condición, incremento
Loop while
BASH
- 1
- while [ condición ] ejecuta mientras sea verdadera
- 2
- $((expresión)) realiza aritmética en Bash
Salida:
Iteración 1
Iteración 2
Iteración 3
Iterar sobre Archivos
BASH
#!/bin/bash
1for ARCHIVO in /var/log/*.log
do
echo "Procesando: $ARCHIVO"
done- 1
- *.log es wildcard que expande a todos los archivos .log
Concepto 4: Funciones
Función Básica
- 1
- mostrar_fecha() define una función sin parámetros
- 2
- $(date …) ejecuta comando y captura resultado
- 3
- mostrar_fecha llama la función
Salida:
Fecha actual: 2024-01-29
Función con Parámetros
BASH
- 1
- sumar() define función que recibirá 2 argumentos
- 2
- local VAR declara variable local a la función
- 3
- $(sumar 10 20) captura el resultado de la función
Salida:
10 + 20 = 30
Función con Retorno
BASH
- 1
- id “usuario” verifica si el usuario existe. &>/dev/null silencia la salida
- 2
- return 0 = éxito
- 3
- return 1 = error
Concepto 5: Redirección y Pipes
Redirección de Salida
- 1
- > redirecciona salida a archivo (sobrescribe si existe)
- 2
- >> redirecciona salida a archivo (agrega al final)
Redirección de Error
BASH
#!/bin/bash
1ls /directorio/inexistente 2> error.log- 1
- 2> redirecciona stderr (error standard) a archivo
Combinaciones útiles:
- > archivo = redirige stdout (salida normal)
- 2> archivo = redirige stderr (errores)
- &> archivo = redirige ambos
- 2>&1 = redirige stderr a stdout
Pipes (|)
BASH
#!/bin/bash
1ps aux | grep nginx- 1
- | (pipe) envía salida del primer comando como entrada al segundo
Ejemplo práctico:
BASH
1cat /var/log/syslog | grep "error" | wc -l- 1
- cat muestra archivo → grep filtra líneas con “error” → wc -l cuenta líneas
Concepto 5.5: Debugging y limpieza con trap
En producción, un buen script hace dos cosas bien: (1) falla de forma clara y (2) deja el sistema limpio (por ejemplo, borra archivos temporales).
trap (cleanup al salir)
BASH
- 1
- set -euo pipefail reduce fallas silenciosas y evita variables indefinidas
- 2
- mktemp crea un archivo temporal con nombre seguro
- 3
- cleanup() define la limpieza que quieres garantizar
- 4
- trap … EXIT ejecuta cleanup al terminar el script (exito o error)
Debug rapido (ver lo que ejecuta)
BASH
1$ bash -x script.sh
+ mktemp
+ echo "Archivo temporal: ..."- 1
- bash -x imprime cada comando antes de ejecutarlo (muy util para depurar)
💡 Ejemplos Prácticos Reales
Ejemplo 1: Script para Monitorear Disk Usage
BASH
- 1
- UMBRAL=80 define límite del 80%
- 2
- $(df | …) captura todas las particiones
- 3
- awk extrae el porcentaje de uso
- 4
-
if [ "$USO" -gt "$UMBRAL" ]alerta si supera umbral
Guardar como: check_disk.sh
Uso:
BASH
bash check_disk.sh
# Salida si hay alerta:
# ALERTA: / está al 85% llenoEjemplo 2: Script para Respaldar Directorio
BASH
#!/bin/bash
# Script para respaldar carpeta con timestamp
1CARPETA_ORIGEN=$1
CARPETA_BACKUP="/backup"
2FECHA=$(date '+%Y%m%d_%H%M%S')
3if [ -z "$CARPETA_ORIGEN" ]
then
echo "Uso: $0 /ruta/a/respaldar"
exit 1
fi
4NOMBRE_BACKUP="${CARPETA_BACKUP}/backup_$(basename $CARPETA_ORIGEN)_${FECHA}.tar.gz"
5tar -czf "$NOMBRE_BACKUP" "$CARPETA_ORIGEN"
echo "Respaldo completado: $NOMBRE_BACKUP"- 1
- $1 primer argumento es carpeta a respaldar
- 2
- FECHA captura fecha/hora para nombre único
- 3
- -z verifica si string está vacío
- 4
- basename extrae solo el nombre sin ruta
- 5
- tar -czf comprime carpeta
Uso:
BASH
bash respaldar.sh /home/diego
# Salida:
# Respaldo completado: /backup/backup_diego_20240129_143022.tar.gzEjemplo 3: Script para Actualizar Sistema (Debian/Ubuntu)
BASH
#!/bin/bash
# Script para actualizar sistema de forma segura
echo "=== Iniciando actualización del sistema ==="
# Actualizar lista de paquetes
1apt update
2if [ $? -ne 0 ]
then
echo "Error al actualizar repositorios"
exit 1
fi
# Actualizar paquetes
3apt upgrade -y
if [ $? -ne 0 ]
then
echo "Error durante apt upgrade"
exit 1
fi
# Eliminar paquetes sin usar
4apt autoremove -y
echo "=== Actualización completada ==="
# Información del sistema
echo "Versión del kernel: $(uname -r)"
echo "Última actualización: $(date)"- 1
- apt update actualiza lista de paquetes
- 2
- if [ $? -ne 0 ] verifica si comando anterior falló
- 3
- apt upgrade -y actualiza paquetes (confirmación automática)
- 4
- apt autoremove elimina paquetes huérfanos
Ejemplo 4: Script para Crear Usuarios en Lote
BASH
#!/bin/bash
# Script para crear múltiples usuarios
USUARIOS="diego carlos ana benjamin"
GRUPO="developers"
# Crear grupo si no existe
1if ! getent group "$GRUPO" > /dev/null 2>&1
then
groupadd "$GRUPO"
echo "Grupo $GRUPO creado"
fi
# Crear cada usuario
2for USUARIO in $USUARIOS
do
if id "$USUARIO" &>/dev/null
then
echo "Usuario $USUARIO ya existe"
else
3 useradd -m -s /bin/bash -G "$GRUPO" "$USUARIO"
echo "Usuario $USUARIO creado"
fi
done
echo "Usuarios en grupo $GRUPO:"
getent group "$GRUPO"- 1
- getent group verifica si el grupo existe
- 2
- for USUARIO in itera sobre cada usuario
- 3
- useradd -m crea usuario con directorio home (-s especifica shell)
🔧 Construcción de un Script Profesional
Template Completo
BASH
#!/bin/bash
#
# Nombre: backup_sistema.sh
# Descripción: Respalda directorios críticos
# Autor: Diego Saavedra
# Fecha: 2024-01-29
# Versión: 1.0
#
1set -euo pipefail
# ============ CONSTANTES ============
2readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/backup.log"
readonly BACKUP_DIR="/backup"
3readonly CARPETAS=("/home" "/etc" "/var/www")
# ============ FUNCIONES ============
4log() {
local NIVEL=$1
shift
local MENSAJE="$@"
local TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] [$NIVEL] $MENSAJE" | tee -a "$LOG_FILE"
}
5verificar_permisos() {
if [ "$EUID" -ne 0 ]
then
log "ERROR" "Este script debe ejecutarse como root"
exit 1
fi
}
crear_respaldo() {
local ORIGEN=$1
local NOMBRE=$(basename "$ORIGEN")
local FECHA=$(date '+%Y%m%d_%H%M%S')
local ARCHIVO="${BACKUP_DIR}/${NOMBRE}_${FECHA}.tar.gz"
log "INFO" "Creando respaldo de $ORIGEN"
tar -czf "$ARCHIVO" "$ORIGEN" 2>/dev/null
if [ -f "$ARCHIVO" ]
then
log "OK" "Respaldo creado: $ARCHIVO ($(du -h $ARCHIVO | cut -f1))"
else
log "ERROR" "Error creando respaldo de $ORIGEN"
fi
}
# ============ MAIN ============
main() {
log "INFO" "=== Iniciando respaldo del sistema ==="
verificar_permisos
# Crear directorio de backup si no existe
mkdir -p "$BACKUP_DIR"
# Respaldar cada carpeta
6 for CARPETA in "${CARPETAS[@]}"
do
if [ -d "$CARPETA" ]
then
crear_respaldo "$CARPETA"
else
log "WARN" "Carpeta no existe: $CARPETA"
fi
done
log "INFO" "=== Respaldo completado ==="
}
# Ejecutar main si el script se ejecuta directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
main "$@"
fi- 1
- set -euo pipefail = salir si error, no permitir variables indefinidas, fallar en pipes
- 2
- readonly = variable inmutable
- 3
-
(@)= array (múltiples valores) - 4
- log() función para mensajes con timestamp
- 5
- verificar_permisos() asegurar que es root
- 6
-
"${CARPETAS[@]}"itera sobre elementos del array
⚠️ Errores Comunes en Bash
Error 1: Sin comillas alrededor de variables
BASH
# MALO
if [ $NOMBRE = "Diego" ] # Falla si $NOMBRE está vacío
# BUENO
if [ "$NOMBRE" = "Diego" ] # Funciona incluso si vacíoError 2: Espacios alrededor de =
BASH
# MALO
VAR = valor # Error de sintaxis
# BUENO
VAR=valorError 3: = vs -eq para números
BASH
# MALO (compara como strings)
if [ "10" = "2" ] # Retorna verdadero
# BUENO (compara como números)
if [ 10 -gt 2 ] # Retorna verdaderoError 4: Olvidar fi, done, etc
BASH
# MALO
if [ $AGE -gt 18 ]
then
echo "Mayor"
# Falta fi
# BUENO
if [ $AGE -gt 18 ]
then
echo "Mayor"
fi📊 Tabla de Referencia Rápida
| Operador | Uso | Ejemplo |
|---|---|---|
| -eq | Igual (números) | [ $A -eq $B ] |
| -ne | No igual | [ $A -ne $B ] |
| -lt | Menor que | [ $A -lt $B ] |
| -gt | Mayor que | [ $A -gt $B ] |
| = | Igual (strings) | [ "$S" = "hola" ] |
| != | No igual (strings) | [ "$S" != "hola" ] |
| -f | Es archivo | [ -f /ruta/archivo ] |
| -d | Es directorio | [ -d /ruta/dir ] |
| -z | String vacío | [ -z "$VAR" ] |
| > | Redir salida | echo "hola" > file.txt |
| >> | Agregar a archivo | echo "hola" >> file.txt |
| | | Pipe (tubería) | cat file \| grep "error" |
🎓 Quiz: Verificar Comprensión
¿Cuál es el error en este código?
BASH
NOMBRE = Diego
echo $NOMBREa) Falta el signo $ en la asignación
b) Los espacios alrededor de = causan error de sintaxis (Correcto ✓)
c) echo necesita comillas
d) No hay error
Explicación: Bash es estricto con espacios. NOMBRE = Diego intenta ejecutar comando NOMBRE, no asignar variable.
¿Qué operador compara STRINGS en Bash?
a) -eq
b) = (Correcto ✓)
c) -lt
d) -gt
Explicación: = compara strings. -eq compara números. Usar incorrecto causa comparación inesperada.
¿Cuál es la diferencia entre for y while?
a) No hay diferencia, son iguales
b) for itera sobre una lista, while ejecuta mientras condición sea verdadera (Correcto ✓)
c) for es más rápido
d) while solo funciona con números
Explicación: for itera conociendo fin. while evalúa condición cada iteración.
👨💻 Práctica: Crear tu Primer Script
Ejercicio 1: Script de Saludo
BASH
#!/bin/bash
# Crea un script que:
# 1. Pida nombre al usuario (read)
# 2. Verifique que no está vacío
# 3. Imprima saludo personalizado
# SOLUCIÓN:
#!/bin/bash
echo "¿Cuál es tu nombre?"
read NOMBRE
if [ -z "$NOMBRE" ]
then
echo "Error: Nombre no puede estar vacío"
exit 1
fi
echo "¡Hola, $NOMBRE! Bienvenido a Bash"Ejercicio 2: Script de Números Pares
BASH
#!/bin/bash
# Crea un script que imprima números pares del 1 al 20
# SOLUCIÓN:
#!/bin/bash
for i in {1..20}
do
if [ $((i % 2)) -eq 0 ]
then
echo "$i"
fi
doneEjercicio 3: Script para Contar Líneas
BASH
#!/bin/bash
# Crea un script que:
# 1. Reciba ruta a archivo como argumento
# 2. Verifique que existe
# 3. Cuente líneas
# SOLUCIÓN:
#!/bin/bash
if [ -z "$1" ]
then
echo "Uso: $0 /ruta/archivo"
exit 1
fi
if [ ! -f "$1" ]
then
echo "Error: Archivo no existe"
exit 1
fi
LINEAS=$(wc -l < "$1")
echo "El archivo tiene $LINEAS líneas"📚 Recursos Adicionales
- Bash Manual Oficial: https://www.gnu.org/software/bash/manual/
- ShellCheck (Validador): https://www.shellcheck.net/
- Google Shell Style Guide: https://google.github.io/styleguide/shellguide.html
- Bash Pitfalls: https://mywiki.wooledge.org/BashPitfalls
Conclusión
Ahora sabes: ✓ Variables, tipos y conversiones ✓ Condicionales (if/else/elif) ✓ Loops (for/while) ✓ Funciones y modularización ✓ Redirección y pipes ✓ Scripts profesionales listos para usar
Próximo paso: Usa estos conocimientos en los scripts que encontrarás en configuraciones de sistemas.