Bash (Bourne Again SHell) es un interprete de comandos y lenguaje de scripting que actua como interfaz entre el usuario y el sistema operativo Unix o Linux. Desarrollada como parte del proyecto GNU, es la evolución moderna de la shell Bourne original (sh) e incorpora caracteristicas de otras shells como Korn shell (ksh) y C shell (csh).
Como shell predeterminada en la mayoria de distribuciones Linux y en Windows a través de WSL (Windows Subsystem for Linux), Bash es una herramienta esencial para administradores de sistemas, desarrolladores backend y perfiles DevOps. En macOS el shell por defecto paso a ser zsh, pero Bash sigue disponible y, como veremos, la mayor parte del scripting portable escrito en Bash funciona también en zsh con cambios mínimos.
Este curso se centra en la versión estable en producción, Bash 5.x, que aporta arrays asociativos robustos, mejoras en la gestión de jobs, variables EPOCHSECONDS y EPOCHREALTIME, y compatibilidad con herramientas modernas de ecosistema como ShellCheck para análisis estático y bats-core para testing unitario.
El pipeline en Bash: stdin, stdout y stderr
El modelo de flujos estándar es la piedra angular del trabajo en línea de comandos. Cada proceso recibe tres descriptores: 0 (stdin), 1 (stdout) y 2 (stderr). El operador | conecta la salida estándar de un comando a la entrada estándar del siguiente, permitiendo componer herramientas pequenas en procesos de datos complejos.
flowchart LR
stdin[[stdin 0]] --> cmd[Comando]
cmd -->|descriptor 1| stdout[[stdout 1]]
cmd -->|descriptor 2| stderr[[stderr 2]]
stdout -->|`|` tuberia| next[Siguiente comando]
stderr -->|`2>` redirección| logfile[(archivo de log)]
Procesos: foreground, background y control de jobs
Bash gestiona procesos de forma explícita. Un comando normal se ejecuta en primer plano y bloquea la terminal. Anadiendo & al final, pasa a segundo plano; con Ctrl+Z se suspende y con bg o fg se retoma. El builtin jobs lista los trabajos asociados a la sesión.
stateDiagram-v2
[*] --> Foreground: comando
Foreground --> Suspended: Ctrl+Z
Suspended --> Background: bg
Foreground --> Background: `&` al final
Background --> Foreground: fg
Foreground --> [*]: exit
Background --> [*]: exit
Ciclo fork, exec y wait
Cuando escribes un comando externo, la shell bifurca un proceso hijo con fork(), sustituye la imagen del hijo por el binario del comando con exec() y se queda esperando su finalización con wait(). Entender este ciclo explica por que las variables exportadas viajan a los hijos pero las no exportadas no, y por que un builtin como cd no puede cambiar el directorio si se ejecutase en un subproceso.
sequenceDiagram
participant S as Bash (padre)
participant H as Proceso hijo
S->>H: fork() crea proceso identico
H->>H: exec("/bin/ls") sustituye la imagen
H-->>S: exit code
S->>S: wait() recoge el estado y actualiza $?
Variables de entorno y shells anidadas
Las variables exportadas viajan a los procesos hijos a través del entorno, mientras que las variables locales quedan confinadas a la shell actual. Al invocar un script como ./script.sh se lanza una subshell que hereda el entorno pero no las variables no exportadas ni las funciones no exportadas con export -f.
flowchart TB
subgraph Padre[Bash interactivo]
PATH_P["PATH (exportada)"]
TEMP_P["TEMP (no exportada)"]
FUNC_P["función saludar"]
end
subgraph Hijo[subshell `./script.sh`]
PATH_H["PATH heredada"]
TEMP_H["TEMP ausente"]
end
PATH_P --> PATH_H
TEMP_P -.no viaja.-> TEMP_H
FUNC_P -.solo con `export -f`.-> Hijo
Heredocs: bloques de texto como entrada
Los here-documents permiten pasar un bloque multilinea como stdin sin crear un fichero intermedio. Son útiles para SQL, generación de ficheros de configuración y scripts SSH remotos. Con <<EOF se expanden variables dentro del bloque; con <<'EOF' (entre comillas) el contenido es literal y no se expanden variables ni comandos.
flowchart LR
script["Script Bash"] --> here[<<EOF ... EOF]
here -->|stdin multilinea| cat["cat / mysql / ssh host"]
here --> variante{Entre comillas?}
variante -->|<<EOF| exp["Expande `$variable` y `$(comando)`"]
variante -->|<<'EOF'| lit["Literal, sin expansión"]
Control de flujo: bucles y cortes
Los bucles for, while y until se combinan con break y continue para detener o saltar iteraciones. La sentencia case permite comparar una variable con varios patrones de forma legible. Este diagrama resume el flujo típico de un bucle con corte y continuación.
flowchart TD
start([inicio]) --> check{condición?}
check -->|falsa| end1([fin])
check -->|verdadera| body[ejecutar cuerpo]
body --> break_q{break?}
break_q -->|si| end1
break_q -->|no| cont_q{continue?}
cont_q -->|si| check
cont_q -->|no| work[seguir con el resto del cuerpo]
work --> check
Sintaxis básica
La estructura general de un comando es:
comando [opciones] [argumentos]
Por ejemplo:
ls -la /home/usuario
Encadenamientos útiles:
echo "Primero"; echo "Segundo" # secuencial
mkdir directorio && cd directorio # AND logico, el 2 solo si el 1 tuvo exito
grep "patron" f.txt || echo "no existe" # OR logico, el 2 solo si el 1 fallo
Variables
nombre="Ana"
edad=25
echo "Hola, $nombre. Tienes $edad anos."
echo "${nombre:-anonimo}" # valor por defecto si esta vacia
echo "${nombre^^}" # mayusculas (Bash 4+)
Variables especiales frecuentes: $0 nombre del script, $1..$N argumentos, $# número de argumentos, $? código de salida, $$ PID, $! PID del último background.
Arrays
Bash 5.x soporta indexados y asociativos:
frutas=("manzana" "naranja" "platano")
echo "${frutas[0]}"
frutas+=("uva")
declare -A colores
colores[rojo]="#FF0000"
colores[verde]="#00FF00"
echo "${colores[rojo]}"
echo "${!colores[@]}" # todas las claves
Operadores y aritmetica
resultado=$(( (a + b) * 2 ))
((contador++))
if [[ "$a" == "$b" ]]; then echo "iguales"; fi
if (( a < b )); then echo "a menor"; fi
if [[ -f "$fichero" ]]; then echo "es fichero"; fi
Control de flujo
if [[ -z "$var" ]]; then
echo "vacia"
elif [[ "$var" == "admin" ]]; then
echo "privilegiado"
else
echo "normal"
fi
case "$accion" in
start|restart) echo "arrancar" ;;
stop) echo "parar" ;;
*) echo "desconocido" ;;
esac
for archivo in *.log; do
echo "procesando $archivo"
done
while IFS= read -r linea; do
echo "leido: $linea"
done < entrada.txt
Funciones
saludar() {
local nombre="${1:?falta nombre}"
echo "Hola, $nombre"
}
saludar "Ana"
Redirección y tuberias
comando > salida.log # sobrescribe stdout
comando >> salida.log # anade
comando 2> errores.log # solo stderr
comando > out.log 2>&1 # stdout y stderr al mismo destino
comando &> out.log # forma moderna equivalente
comando | tee -a log.txt # ver y guardar a la vez
ls *.txt | wc -l
Scripting robusto con Bash 5.x
Patrón recomendado en la cabecera de cualquier script de producción:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly LOG_FILE="/var/log/app/$(basename "$0").log"
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >> "$LOG_FILE"; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; exit 1; }
trap 'error "fallo en linea $LINENO"' ERR
trap 'rm -f /tmp/app.$$' EXIT
Principales flags de set:
-eaborta al primer error con código distinto de cero-utrata como error el uso de variables no definidas-o pipefailpropaga fallos en tuberias
Ecosistema moderno
- ShellCheck: análisis estático obligatorio antes de dar por terminado un script. En CI se ejecuta con
shellcheck --severity=warning deploy.shy suele integrarse con pre-commit. - bats-core: framework de testing unitario para Bash, con ficheros
.batsque describen casos con@test. Permite ejecutar suites desdebats tests/y combinarse conshellchecken un pipeline de integración continua. - shfmt: formateador consistente (indentación, espacios, comillas) útil como equivalente a
gofmtoprettieren el mundo de los scripts. - POSIX sh frente a Bash: los scripts pensados para ser portables (contenedores Alpine con dash, busybox) deben evitar
[[ ]], arrays,{1..10}y otras extensiones de Bash. Para estos casos se escribe#!/bin/shy se valida conshellcheck --shell=sh. - Interoperabilidad con zsh: la mayor parte del scripting portable en Bash 5.x funciona en zsh cambiando el shebang, pero el división de palabras y algunos comportamientos de expansión difieren; conviene probar con el interprete concreto de destino antes de publicar.
Buenas practicas
- Entrecomillar siempre las expansiones de variables:
"$var","${array[@]}". - Preferir
[[ ... ]]frente a[ ... ]en scripts Bash. - Usar
localdentro de funciones para evitar contaminar el ámbito global. - Validar dependencias con
command -v herramienta >/dev/null || exit 1. - Devolver siempre un código de salida explícito (
exit 0en exito;exit 1..125en error según convención). - Integrar
shellcheckybatsen CI; tratar los warnings como errores en scripts de producción. - Evitar
evalsobre entrada del usuario y nunca imprimir secretos en logs.
Este curso combina teoria ilustrada con diagramas, tutoriales paso a paso, ejercicios de test, puzzles de rellenar huecos, retos de código y un proyecto final integrador que reune todas las piezas en un script profesional de backup y deploy.