Versiones de Bash y parches: de dónde viene Bash, qué trajo cada versión de la 2.0 a la 5.3 (con un script real por cada salto, y lo que pasa con Bash 3.2), cómo saber cuál tienes, en qué se diferencia de sh y POSIX, por qué macOS sigue con Bash 3.2 y la historia de Shellshock, el fallo que llevaba 25 años escondido.
Bash es la shell de casi todos los servidores Linux, pero no es la única, ni siempre la misma: la versión que trae tu distribución, la de macOS y la de un contenedor pueden ser muy distintas, y un script que funciona en una puede fallar en otra. Esta página repasa de dónde viene Bash, qué trajo cada versión (con un script real por cada salto importante), cómo saber cuál tienes, en qué se diferencia de sh y qué pasó con Shellshock, el fallo que en 2014 obligó a parchear medio internet.
De sh a Bash
La primera shell de Unix, la de Ken Thompson, era poco más que un lanzador de órdenes. En 1979, Stephen Bourne, de los laboratorios Bell, publicó con la versión 7 de Unix la shell que lleva su nombre, el Bourne shell, con variables, bucles y condiciones: la base de la sintaxis que hoy define el estándar POSIX para sh. Le siguieron otras, como la C shell o la Korn shell, que añadieron historial y edición de la línea.
El Bourne shell era propietario, y el proyecto GNU necesitaba una alternativa libre. Brian Fox la escribió y publicó la primera beta, la 0.99, el 8 de junio de 1989. La llamó Bash, «Bourne Again SHell», un juego de palabras con «born again» (renacido). Desde principios de los noventa la mantiene Chet Ramey, de la Universidad Case Western Reserve, que sigue publicando cada versión y cada parche.
Qué versión tienes
Hay tres formas de saberlo: la opción --version, la variable BASH_VERSION, que es un texto, y el array BASH_VERSINFO, con cada número por separado, que es el que conviene usar dentro de un script. Esta es la imagen de Ubuntu 26.04 con la que se probó todo el curso:
terminal$ bash --version | head -n 1
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)
$ echo "$BASH_VERSION"
5.3.9(1)-release
$ echo "Bash ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}, parche ${BASH_VERSINFO[2]}"
Bash 5.3, parche 9
$ readlink -f /bin/sh
/usr/bin/dash
$ type -a bash
bash is /usr/bin/bash
bash is /bin/bashFíjate en la línea de /bin/sh: en Debian y Ubuntu no es Bash, sino dash, una shell mínima y más rápida que solo entiende lo que exige POSIX. Lo vemos más abajo, porque explica muchos «en mi máquina funciona».
Si un script necesita algo moderno, lo correcto es comprobar la versión al principio y salir con un mensaje claro, en lugar de fallar a mitad de camino con un error críptico:
#!/usr/bin/env bash
# Comprueba la versión antes de usar nada moderno.
if (( BASH_VERSINFO[0] < 4 )); then
echo "Este script necesita Bash 4 o posterior; tienes $BASH_VERSION" >&2
exit 1
fi
declare -A precios=([cafe]=35 [te]=28)
echo "Un café cuesta ${precios[cafe]} pesos"salida con Bash 5.3Un café cuesta 35 pesos
salida con Bash 3.2, la de macOSEste script necesita Bash 4 o posterior; tienes 3.2.57(2)-release código de salida: 1
Lo que trajo cada versión
Bash ha crecido sin romper casi nada: un script de Bash 2 sigue funcionando en Bash 5.3. Lo que no funciona es lo contrario. Estas son las versiones principales, con la fecha en que se publicaron en el servidor de GNU y lo que más se usa de cada una:
- VersiónFechaLo más útil que trajo
- 2.0diciembre de 1996Arrays indexados
- 3.0agosto de 2004
[[ =~ ]]con expresiones regulares, secuencias{1..10}yset -o pipefail - 3.1diciembre de 2005El operador
+=para añadir a variables y arrays - 3.2octubre de 2006Correcciones; es la versión que sigue trayendo macOS
- 4.0febrero de 2009Arrays asociativos,
mapfile,**recursivo,coproc,${var^^}y;&encase. Pasa a la licencia GPLv3 - 4.2febrero de 2011
lastpipeyprintf %(...)Tpara fechas - 4.3febrero de 2014Variables nameref (
declare -n) e índices negativos - 4.4septiembre de 2016Transformaciones
${var@Q},@E,@A… - 5.0enero de 2019
EPOCHSECONDS,EPOCHREALTIMEyBASH_ARGV0 - 5.1diciembre de 2020
SRANDOMy las transformaciones@U,@uy@L - 5.2septiembre de 2022
&en los reemplazos (patsub_replacement) - 5.3julio de 2025
${ orden; }sin proceso hijo,GLOBSORT,source -p
Bash 3: expresiones regulares, secuencias y pipefail
#!/usr/bin/env bash
# Novedades de Bash 3.0 y 3.1
fecha="2026-10-30"
if [[ $fecha =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then # =~ y BASH_REMATCH (3.0)
echo "Año ${BASH_REMATCH[1]}, mes ${BASH_REMATCH[2]}, día ${BASH_REMATCH[3]}"
fi
echo {1..5} # secuencias {x..y} (3.0)
lista="uno"
lista+=" dos" # += añade (3.1)
echo "$lista"
set -o pipefail # pipefail (3.0)
false | true
echo "Código de la tubería con pipefail: $?"salidaAño 2026, mes 10, día 30 1 2 3 4 5 uno dos Código de la tubería con pipefail: 1
De todo esto, lo que más importa en la práctica es pipefail: sin él, el código de salida de una tubería es el de la última orden, y un fallo en medio pasa desapercibido. Lo usamos en todos los scripts del curso a partir de la lección 7.
Bash 4.0: arrays asociativos, mapfile y **
La 4.0 fue el gran salto. Este script usa sus novedades más prácticas; la carpeta tiene notas.txt y dos subcarpetas con más archivos:
#!/usr/bin/env bash
# Novedades de Bash 4.0
declare -A capitales=([México]="Ciudad de México" [Perú]="Lima") # arrays asociativos
echo "La capital de Perú es ${capitales[Perú]}"
mapfile -t lineas <<'FIN'
primera
segunda
FIN
echo "mapfile leyó ${#lineas[@]} líneas; la última es «${lineas[1]}»"
nombre="decodigo"
echo "${nombre^^} ${nombre^}" # mayúsculas sin tr
shopt -s globstar
echo "Todos los .txt, en cualquier subcarpeta:" **/*.txt # ** recursivo
nivel=2
case $nivel in
2) echo "nivel 2" ;& # ;& sigue con el siguiente caso
1) echo "y también lo del nivel 1" ;;
esacsalidaLa capital de Perú es Lima mapfile leyó 2 líneas; la última es «segunda» DECODIGO Decodigo Todos los .txt, en cualquier subcarpeta: notas.txt viejo/2025/resumen.txt viejo/otra.txt nivel 2 y también lo del nivel 1
Y esto es lo que pasa con el mismo script en Bash 3.2, la versión de macOS. No hay un único error al principio: cada línea moderna falla por su lado y el script sigue adelante haciendo cosas raras. Sin declare -A, Bash 3.2 cree que [México] es el índice numérico de un array normal e intenta calcularlo; mapfile no existe y el array queda vacío; ** se comporta como un * y solo baja un nivel; y el ;& del case es directamente un error de sintaxis que detiene el script:
salida con Bash 3.2./bash4.sh: line 3: México: syntax error: invalid arithmetic operator (error token is "éxico")
./bash4.sh: line 4: Perú: syntax error: invalid arithmetic operator (error token is "ú")
./bash4.sh: line 6: mapfile: command not found
mapfile leyó 0 líneas; la última es «»
./bash4.sh: line 13: ${nombre^^} ${nombre^}: bad substitution
./bash4.sh: line 15: shopt: globstar: invalid shell option name
Todos los .txt, en cualquier subcarpeta: viejo/otra.txt
./bash4.sh: line 20: syntax error near unexpected token `&'
./bash4.sh: line 20: ` 2) echo "nivel 2" ;& # ;& sigue con el siguiente caso'
código de salida: 2Bash 4.2 a 4.4: fechas con printf, índices negativos y nameref
#!/usr/bin/env bash
# Novedades de Bash 4.2, 4.3 y 4.4
printf 'La época Unix empezó el %(%d/%m/%Y)T\n' 0 # printf %(...)T (4.2)
numeros=(10 20 30)
echo "El último: ${numeros[-1]}" # índices negativos (4.3)
declare -n alias_de=numeros # nameref (4.3)
alias_de+=(40)
echo "numeros ahora tiene ${#numeros[@]} elementos"
frase="Hola, 'mundo'"
echo "${frase@Q}" # transformaciones @ (4.4)
shopt -s lastpipe # lastpipe (4.2)
total=0
printf '%s\n' 3 4 5 | while read -r n; do (( total += n )); done
echo "Suma dentro del while de una tubería: $total"salidaLa época Unix empezó el 01/01/1970 El último: 30 numeros ahora tiene 4 elementos 'Hola, '\''mundo'\''' Suma dentro del while de una tubería: 12
La última línea es la más interesante: normalmente, el while de una tubería se ejecuta en una subshell y la suma se pierde al salir (está en las rarezas). Con shopt -s lastpipe, la última orden de la tubería se ejecuta en la shell principal y la variable sobrevive. Solo funciona en scripts, no en una terminal interactiva, donde el control de trabajos lo impide.
Bash 5.0 a 5.2: la hora sin date, SRANDOM y & en los reemplazos
#!/usr/bin/env bash
# Novedades de Bash 5.0, 5.1 y 5.2
(( EPOCHSECONDS > 1700000000 )) && echo "EPOCHSECONDS da la hora Unix sin llamar a date" # 5.0
BASH_ARGV0="mi-script" # 5.0: cambia $0
echo "Ahora \$0 vale $0"
n=$SRANDOM # 5.1: 32 bits de /dev/urandom
(( n >= 0 && n < 4294967296 )) && echo "SRANDOM dio un número de 32 bits"
ciudad="oaxaca"
echo "${ciudad@u} ${ciudad@U}" # 5.1: @u y @U
animal="gato"
echo "${animal/gat/[&]}" # 5.2: & es el texto encontradosalidaEPOCHSECONDS da la hora Unix sin llamar a date Ahora $0 vale mi-script SRANDOM dio un número de 32 bits Oaxaca OAXACA [gat]o
Dos detalles: SRANDOM sale de la fuente de aleatoriedad del sistema, así que, al contrario que RANDOM, no se puede repetir fijando una semilla (y sirve mejor para generar algo difícil de adivinar). Y desde la 5.2, un & en el reemplazo de ${var/patrón/reemplazo} significa «lo que se encontró», como en sed; si quieres un & literal, escríbelo como \&.
Bash 5.3: sustitución de órdenes sin proceso hijo y GLOBSORT
Bash 5.3 se publicó en julio de 2025 (Chet Ramey la anunció el 5 de julio). Su novedad más llamativa es una nueva forma de sustitución de órdenes, ${ orden; }, que captura la salida igual que $( ) pero sin crear un proceso hijo: es más rápida y, sobre todo, lo que la orden cambie se queda en la shell. La variante ${| orden; } devuelve lo que la orden deje en la variable REPLY, sin imprimir nada. Y la variable GLOBSORT decide cómo se ordenan los resultados de un comodín: por nombre (lo de siempre), tamaño, fecha de modificación o de acceso, o sin ordenar. En la carpeta hay tres .log de tamaños distintos:
#!/usr/bin/env bash
# Novedades de Bash 5.3
contador=1
texto=$( contador=2; echo "hola" ) # $( ) crea un proceso hijo: lo que cambie dentro se pierde
echo "Con \$( ): «$texto», contador = $contador"
texto=${ contador=3; echo "hola"; } # ${ ...; } se ejecuta en la propia shell
echo "Con \${ }: «$texto», contador = $contador"
doble() { REPLY=$(( $1 * 2 )); }
echo "Con \${| }: ${| doble 21; }" # ${| ...; } devuelve lo que quede en REPLY
echo "Por nombre:" *.log
GLOBSORT=size # ordenar los resultados de un glob por tamaño
echo "Por tamaño:" *.log
GLOBSORT=-size # con un guion delante, al revés
echo "Del más grande al más chico:" *.logsalidaCon $( ): «hola», contador = 1
Con ${ }: «hola», contador = 3
Con ${| }: 42
Por nombre: a-mediano.log b-grande.log c-chico.log
Por tamaño: c-chico.log a-mediano.log b-grande.log
Del más grande al más chico: b-grande.log a-mediano.log c-chico.logLa diferencia entre las dos primeras líneas es justo la que ShellCheck vigila: en la línea del $( ) avisa de que el cambio de contador «es local a la subshell» y se perderá (avisos SC2030 y SC2031; aquí los desactivamos para enseñar el efecto). Con Bash 3.2 (o con cualquiera anterior a la 5.3), la nueva sintaxis ni siquiera se entiende:
salida con Bash 3.2Con $( ): «hola», contador = 1
./bash53.sh: line 6: ${ contador=3; echo "hola"; }: bad substitution
Con ${ }: «hola», contador = 1
./bash53.sh: line 10: Con \${| }: ${| doble 21; }: bad substitution
Por nombre: a-mediano.log b-grande.log c-chico.log
Por tamaño: a-mediano.log b-grande.log c-chico.log
Del más grande al más chico: a-mediano.log b-grande.log c-chico.log
código de salida: 0source -p RUTA para buscar el archivo en otra lista de carpetas, las variables BASH_MONOSECONDS (el reloj monotónico, para medir tiempos) y BASH_TRAPSIG (la señal que disparó un trap), y un cambio que puede romper scripts viejos: test interpreta de otra forma las expresiones con paréntesis y más de cuatro argumentos, para comportarse como el de coreutils. Su código fuente se actualizó a C23, así que ya no compila con compiladores de C antiguos.Bash no es sh: POSIX y dash
El estándar POSIX define cómo debe comportarse una shell llamada sh, y Bash lo cumple… pero añade muchísimo más: [[ ]], arrays, {1..5}, ${var^^}, <( ). Todo eso son bashismos, y no existen en dash, que es el /bin/sh de Debian y Ubuntu, ni en el de muchos contenedores ligeros. Este script empieza con #!/bin/sh pero está escrito en Bash. ShellCheck lo detecta sin ejecutarlo:
#!/bin/sh
# Un script que dice ser POSIX (sh) pero usa cosas de Bash
nombre="Ana"
if [[ $nombre == A* ]]; then
echo "Empieza por A"
fi
frutas=(mango papaya)
echo "${frutas[1]}"shellcheckIn posix.sh line 4:
if [[ $nombre == A* ]]; then
^-----------------^ SC3010 (warning): In POSIX sh, [[ ]] is undefined.
^-- SC3014 (warning): In POSIX sh, == in place of = is undefined.
In posix.sh line 7:
frutas=(mango papaya)
^------------^ SC3030 (warning): In POSIX sh, arrays are undefined.
In posix.sh line 8:
echo "${frutas[1]}"
^----------^ SC3054 (warning): In POSIX sh, array references are undefined.
For more information:
https://www.shellcheck.net/wiki/SC3010 -- In POSIX sh, [[ ]] is undefined.
https://www.shellcheck.net/wiki/SC3014 -- In POSIX sh, == in place of = is ...
https://www.shellcheck.net/wiki/SC3030 -- In POSIX sh, arrays are undefined.Y esto es lo que ocurre al ejecutarlo con sh, es decir, con dash: el [[ no existe como orden, y la línea del array es directamente un error de sintaxis que detiene el script.
salida con sh (dash)./posix_sh.sh: 4: [[: not found
./posix_sh.sh: 7: Syntax error: "(" unexpectedLa regla práctica: si tu script usa algo de Bash, que empiece por #!/usr/bin/env bash y se ejecute con bash script.sh o ./script.sh, nunca con sh script.sh, que ignora la primera línea y usa dash. Si de verdad necesitas que funcione en cualquier sh (un script de instalación, una imagen Alpine), escríbelo en POSIX puro y revísalo con ShellCheck, que con #!/bin/sh avisa de cada bashismo.
Hasta algo tan básico como echo cambia de una shell a otra. El mismo script, primero con Bash y después con dash:
#!/bin/sh # echo con secuencias de escape: cada shell hace una cosa echo "uno\tdos" echo -e "uno\tdos" printf 'uno\tdos\n'
salida con bashuno\tdos uno dos uno dos
salida con sh (dash)uno dos -e uno dos uno dos
Bash solo interpreta \t con -e; dash lo interpreta siempre, y ni siquiera reconoce -e como opción, así que la escribe. printf es la única que se comporta igual en todas: por eso en scripts portables se usa printf y no echo.
Por qué macOS sigue con Bash 3.2
Abre una terminal en cualquier Mac y escribe bash --version: verás la 3.2.57, de 2014, una corrección de la rama 3.2 de 2006. Apple nunca ha explicado por qué no la actualiza, pero la razón que se da siempre es la licencia: hasta la 3.2, Bash se distribuía con la GPLv2; desde la 4.0, con la GPLv3, que impone condiciones que Apple evita en todo lo que incluye en su sistema. Así que Apple se quedó en la última versión con la licencia anterior.
Desde macOS Catalina (10.15), de 2019, la shell por omisión de las cuentas nuevas ya no es Bash, sino zsh. En palabras de Apple, zsh es «muy compatible con el Bourne shell y en su mayor parte compatible con bash, con algunas diferencias». Bash 3.2 sigue ahí para los scripts que lo piden, y si lo abres verás un aviso de que zsh es ahora la shell por omisión.
brew install bash) y empieza tus scripts con #!/usr/bin/env bash, que usa el primer bash del PATH en lugar de /bin/bash. Las herramientas también cambian: macOS trae las versiones BSD de sed, date o grep, con opciones distintas de las de GNU que usamos aquí; con Homebrew puedes instalar las de GNU (gsed, gdate…).Shellshock: el fallo que vivió 25 años
El 24 de septiembre de 2014 se publicó CVE-2014-6271, el fallo que acabaría llamándose Shellshock. Lo había encontrado Stéphane Chazelas, que se lo comunicó a Chet Ramey el 12 de septiembre, y la gravedad era máxima: según la base de datos de vulnerabilidades del NIST, todas las versiones de Bash hasta la 4.3 ejecutaban órdenes que alguien hubiera colado al final de una definición de función guardada en una variable de entorno. El código defectuoso estaba en Bash desde agosto de 1989.
¿Por qué era tan grave? Porque muchos programas pasan datos ajenos a Bash a través de variables de entorno sin pensarlo: los servidores web con scripts CGI copian cabeceras de la petición (como el navegador del visitante) en variables, los clientes de DHCP copian datos que manda la red y OpenSSH permite variables al conectarse. Si en algún punto se arrancaba Bash, un atacante remoto podía ejecutar órdenes en el servidor. Los primeros ataques llegaron en menos de una hora, y en pocos días había redes de equipos infectados escaneando internet.
El primer parche resultó incompleto, y en las dos semanas siguientes aparecieron cinco fallos más relacionados (CVE-2014-7169, CVE-2014-7186, CVE-2014-7187, CVE-2014-6277 y CVE-2014-6278) hasta que se cerró el problema de raíz: Bash dejó de buscar funciones en cualquier variable de entorno y pasó a exportarlas con un nombre especial, que ninguna variable normal puede tener. Así es como viajan hoy:
#!/usr/bin/env bash
# Funciones exportadas: así viajan en el entorno desde el parche de Shellshock
saludar() { echo "hola desde una función exportada"; }
export -f saludar
env | grep -A 1 '^BASH_FUNC_'
bash -c 'saludar'salidaBASH_FUNC_saludar%%=() { echo "hola desde una función exportada"
}
hola desde una función exportadaEl prefijo BASH_FUNC_ y el sufijo %% hacen imposible que una variable corriente, como la que copia un servidor web a partir de una cabecera, se interprete como una función. Cualquier Bash actualizado desde octubre de 2014 está protegido, y hoy Shellshock solo afecta a equipos que llevan una década sin actualizarse: aparatos de red, cámaras o sistemas embebidos abandonados. La lección que dejó sigue vigente: no pases datos que no controlas a una shell, ni por argumentos ni por el entorno. Está en Seguridad en Bash.
Cómo se parchea Bash
Bash publica una versión cada pocos años y, entre una y otra, parches numerados (bash53-001, bash53-002…), que suben el tercer número de BASH_VERSINFO. Casi nunca tendrás que aplicarlos a mano: llegan con las actualizaciones de tu sistema. Lo que sí conviene:
- Mantener el sistema actualizado:
sudo apt update && sudo apt upgradeen Debian y Ubuntu,sudo dnf upgradeen Fedora y Red Hat,brew upgrade bashsi lo instalaste con Homebrew. - Revisar las imágenes de contenedores: una imagen vieja trae un Bash viejo. Reconstrúyelas con la base actualizada.
- Comprobar la versión en los scripts que usen algo moderno, como en
exigir.sh. - No depender de que
/bin/shsea Bash: en Debian, Ubuntu y Alpine no lo es.
Los scripts de esta página se ejecutaron con Bash 5.3 (5.3.9) en Ubuntu 26.04 y con Bash 3.2.57 compilado desde las fuentes oficiales de GNU, en contenedores limpios sin red, y se revisaron con ShellCheck 0.11. Las fechas de cada versión son las de sus archivos en ftp.gnu.org; las novedades, las del archivo NEWS de Bash; los datos de Shellshock, los de la base de datos de vulnerabilidades del NIST.
