1. La forma de case
case compara un valor con una serie de patrones, en orden, y ejecuta los comandos del primero que coincide. Se cierra con esac, que es case al revés, como fi lo es de if.
case valor in
patrón1)
comandos
;;
patrón2)
comandos
;;
esac
Cada caso empieza con un patrón seguido de un paréntesis de cierre, y termina con ;;. Cuando los comandos son breves, el caso entero cabe en una línea, y es lo habitual:
read -rp "¿Continuar? [s/n] " respuesta
case "$respuesta" in
s) echo "Seguimos" ;;
n) echo "Cancelado"; exit 0 ;;
esac
Si el valor no coincide con ningún patrón, no se ejecuta nada y case termina con código 0. Si coincide con uno, su código de salida es el del último comando ejecutado en ese caso.
2. Patrones y alternativas
Los patrones de case son los mismos patrones de comodines que ya conoces de los nombres de archivo, del tema 6 y de [[ ]] en el tema 11: *, ? y […]. Como en [[ ]], deben coincidir con el valor completo. Además, el símbolo | separa alternativas: el caso se elige si coincide cualquiera de ellas.
| Patrón | Coincide con |
|---|---|
si | Exactamente si. |
s|si|sí | s, si o sí. |
*.txt | Cualquier valor que termine en .txt. |
[0-9] | Un solo dígito. |
[sS]* | Cualquier valor que empiece con s o S. |
-* | Cualquier valor que empiece con guion: parece una opción. |
? | Exactamente un carácter. |
"" | La cadena vacía. |
$ for f in a.tar.gz a.tgz a.zip a.gz a.txt; do
> case $f in
> *.tar.gz|*.tgz) echo "$f: tar comprimido" ;;
> *.gz) echo "$f: gzip" ;;
> *.zip) echo "$f: zip" ;;
> esac
> done
a.tar.gz: tar comprimido
a.tgz: tar comprimido
a.zip: zip
a.gz: gzip
a.txt no coincidió con ningún patrón y no produjo salida. El > al comienzo de las líneas es el indicador que muestra Bash mientras un comando de varias líneas sigue abierto; no se escribe.
3. El caso por defecto
Como * coincide con cualquier cosa, un último caso con ese patrón funciona como el else de un if: atrapa todo lo que no coincidió con los anteriores. Casi todo case debería tenerlo, aunque sea para informar un error, porque un valor inesperado que no hace nada es un fallo silencioso.
case "$formato" in
gzip) extension=gz ;;
xz) extension=xz ;;
zstd) extension=zst ;;
*)
echo "Formato no admitido: $formato (opciones: gzip, xz, zstd)" >&2
exit 2
;;
esac
El mensaje del caso por defecto debería decir qué valor llegó y cuáles se esperaban. Es lo primero que necesita saber quien se equivocó al escribirlo.
4. El orden importa
case prueba los patrones en el orden en que están escritos, y se detiene en el primero que coincide. Cuando un valor puede coincidir con varios, el resultado depende del orden, y los patrones más específicos deben ir antes que los más generales:
$ case a.tar.gz in *.gz) echo "gzip" ;; *.tar.gz) echo "tar comprimido" ;; esac
gzip
El caso *.tar.gz nunca se alcanza, porque todo lo que termina en .tar.gz termina también en .gz. En el ejemplo de la sección 2 estaban en el orden correcto. Por la misma razón, el caso * va siempre al final: puesto antes, atraparía todo.
5. Comillas en el valor y en los patrones
El valor que sigue a case no pasa por la división en palabras ni por la expansión de comodines, como el interior de [[ ]]. Por eso case $var in es seguro aunque var tenga espacios. Ponerle comillas igual no hace daño, y en este curso se las ponemos, por costumbre.
En los patrones, en cambio, las comillas tienen el mismo efecto que en el lado derecho de ==: desactivan los comodines. Un patrón entre comillas se compara literalmente:
$ v="hola"
$ case $v in "*") echo "literalmente un asterisco" ;; *) echo "cualquier cosa" ;; esac
cualquier cosa
Esto importa cuando el patrón viene de una variable. $patron sin comillas se usa como patrón, con sus comodines; "$patron" se compara como texto exacto. Elige según lo que quieras, igual que en el tema 6.
6. Normalizar antes de comparar
Las comparaciones de case distinguen mayúsculas y minúsculas. Para aceptar S, s, Si y SI sin listar todas las combinaciones, se normaliza el valor en la misma línea, con la expansión del tema 6:
$ for r in s SI no N quizás ""; do
> case "${r,,}" in
> s|si|sí) echo "«$r»: sí" ;;
> n|no) echo "«$r»: no" ;;
> *) echo "«$r»: no entiendo" ;;
> esac
> done
«s»: sí
«SI»: sí
«no»: no
«N»: no
«quizás»: no entiendo
«»: no entiendo
Lo mismo vale para las extensiones de archivo, que llegan como .JPG, .jpg o .Jpg: se compara "${archivo,,}" con patrones en minúsculas. El valor original no cambia, y puede seguir usándose dentro de cada caso.
7. Los terminadores ;;, ;& y ;;&
Lo normal es que cada caso termine con ;;, que sale del case. Bash tiene otros dos terminadores, de uso poco frecuente:
| Terminador | Después de ejecutar el caso… |
|---|---|
;; | Sale del case. |
;& | Ejecuta también los comandos del caso siguiente, sin comprobar su patrón. |
;;& | Sigue probando los patrones siguientes, y ejecuta los que coincidan. |
$ case abc in a*) echo "empieza con a" ;;& *c) echo "termina con c" ;;& *) echo "siempre" ;; esac
empieza con a
termina con c
siempre
;;& sirve cuando un valor puede tener varias propiedades independientes, cada una con su acción. ;& sirve para niveles acumulativos, donde cada uno incluye lo del siguiente. Son útiles, pero hacen que el flujo sea menos evidente: si los usas, conviene un comentario que lo señale.
8. Patrones extendidos
Los comodines básicos no pueden expresar «uno o más dígitos» ni «cualquier cosa excepto». Bash tiene una sintaxis extendida de patrones, llamada extglob, que agrega esas posibilidades:
| Patrón | Coincide con | Ejemplo |
|---|---|---|
@(a|b) | Exactamente una de las alternativas. | *.@(jpg|jpeg|png) |
+(patrón) | Una o más repeticiones. | +([0-9]): un número entero |
*(patrón) | Cero o más repeticiones. | v*([0-9.]) |
?(patrón) | Cero o una vez. | ?(-)+([0-9]): entero con signo opcional |
!(patrón) | Cualquier cosa excepto el patrón. | !(*.bak) |
Dentro de [[ ]] funcionan siempre, pero en case y en los nombres de archivo hay que activarlos con shopt -s extglob. Y hay una regla traicionera: la opción debe estar activa cuando Bash lee el código, no solo cuando lo ejecuta. Si se escribe shopt -s extglob en la misma línea que el case, o dentro de la misma función, Bash ya analizó el patrón sin la opción y da un error de sintaxis. Lo seguro es ponerla al comienzo del script, en una línea propia:
shopt -s extglob # al comienzo del script
case "$dias" in
+([0-9])) echo "Número válido: $dias" ;;
*) echo "No es un número: $dias" >&2; exit 2 ;;
esac
Para validaciones más complejas, las expresiones regulares del tema 28 son más expresivas y más conocidas. Pero para elegir entre alternativas, como varias extensiones, los patrones extendidos son concisos y suficientes.
9. Usos típicos
case aparece una y otra vez en scripts reales. Tres ejemplos frecuentes:
Actuar según el tipo de archivo
archivo="${1:?Uso: descomprimir ARCHIVO}"
case "${archivo,,}" in
*.tar.gz|*.tgz) tar -xzf "$archivo" ;;
*.tar.xz|*.txz) tar -xJf "$archivo" ;;
*.tar.bz2|*.tbz2) tar -xjf "$archivo" ;;
*.tar) tar -xf "$archivo" ;;
*.gz) gunzip -k "$archivo" ;;
*.zip) unzip -q "$archivo" ;;
*.7z) 7z x "$archivo" ;;
*)
echo "No sé descomprimir: $archivo" >&2
exit 2
;;
esac
Las extensiones dobles van antes que las simples, por la regla de la sección 4, y la comparación se hace en minúsculas sin alterar el nombre que se pasa al comando.
Adaptarse al sistema
source /etc/os-release # define ID, VERSION_ID y otras variables
case "$ID" in
ubuntu|debian) instalar=(sudo apt install -y) ;;
fedora|rhel|rocky) instalar=(sudo dnf install -y) ;;
arch) instalar=(sudo pacman -S --noconfirm) ;;
*) echo "Distribución no reconocida: $ID" >&2; exit 1 ;;
esac
"${instalar[@]}" rsync
El archivo /etc/os-release está escrito en formato CLAVE=valor, y cargarlo con source, como vimos en el tema 2, define esas variables en el script. El comando se guarda en un array, como en el tema 8, para ejecutarlo después con sus argumentos intactos.
Reconocer opciones
case "${1:-}" in
-h|--help) mostrar_ayuda; exit 0 ;;
-v|--version) echo "1.4.2"; exit 0 ;;
-*) echo "Opción desconocida: $1" >&2; exit 2 ;;
esac
Con un bucle que recorra todos los argumentos, este patrón se convierte en un analizador completo de opciones, como veremos en los temas 20 y 21.
10. case, elif o un array asociativo
Las tres estructuras pueden elegir entre varias alternativas. Cada una tiene su terreno:
| Situación | Conviene | Por qué |
|---|---|---|
| Un mismo valor comparado con textos o patrones | case | Se escribe el valor una vez; los patrones y las alternativas son concisos. |
| Condiciones distintas, sobre distintas variables, o numéricas | if/elif | case solo compara un valor con patrones. |
| Rangos numéricos | if/elif con (( )) | Los patrones comparan texto: [0-9] es un dígito, no un rango de números. |
| Traducir un valor a otro dato, sin acciones distintas | Un array asociativo | Es una tabla de datos, no de código, y se consulta en una línea (tema 8). |
| Ejecutar acciones distintas según una orden | case | Cada caso contiene su propio código. |
La penúltima fila merece un ejemplo. Si cada caso solo asigna un valor, como en la sección 3, la tabla es más clara como datos: declare -A extension=([gzip]=gz [xz]=xz [zstd]=zst), y luego ${extension[$formato]}. Cuando cada caso hace algo diferente, case es la herramienta.
11. Un script con subcomandos
Muchas herramientas se usan con la forma programa orden: git commit, systemctl restart, docker ps. Es una forma natural de reunir varias operaciones relacionadas en un solo script, y su núcleo es un case sobre el primer argumento.
Este script administra un servidor web de prueba, el que trae Python, que sirve los archivos de un directorio. Lo inicia en segundo plano, guarda su número de proceso en un archivo, y con ese número puede consultarlo o detenerlo después.
#!/usr/bin/env bash
#
# servidor-web — inicia, detiene y consulta un servidor web de prueba.
#
# Uso: servidor-web {iniciar|detener|reiniciar|estado|ayuda}
# Entorno: PUERTO (8000), DIRECTORIO (el actual)
# Salida: estado devuelve 0 si está en ejecución y 3 si no; 2 ante una orden desconocida.
#
set -euo pipefail
readonly PUERTO="${PUERTO:-8000}"
readonly DIRECTORIO="${DIRECTORIO:-.}"
readonly PIDFILE="${XDG_RUNTIME_DIR:-/tmp}/servidor-web-$PUERTO.pid"
uso() {
cat <<FIN
Uso: ${0##*/} ORDEN
Órdenes:
iniciar inicia el servidor en el puerto $PUERTO
detener lo detiene
reiniciar lo detiene y lo vuelve a iniciar
estado informa si está en ejecución
ayuda muestra este mensaje
FIN
}
en_ejecucion() {
[[ -f "$PIDFILE" ]] && kill -0 "$(< "$PIDFILE")" 2> /dev/null
}
iniciar() {
if en_ejecucion; then
echo "Ya está en ejecución (PID $(< "$PIDFILE"))"
return
fi
python3 -m http.server "$PUERTO" --directory "$DIRECTORIO" > /dev/null 2>&1 &
echo "$!" > "$PIDFILE"
echo "Iniciado en http://localhost:$PUERTO (PID $!)"
}
detener() {
if ! en_ejecucion; then
echo "No está en ejecución"
rm -f "$PIDFILE"
return
fi
kill "$(< "$PIDFILE")"
rm -f "$PIDFILE"
echo "Detenido"
}
case "${1:-}" in
iniciar|start) iniciar ;;
detener|stop) detener ;;
reiniciar|restart) detener; iniciar ;;
estado|status)
if en_ejecucion; then
echo "En ejecución (PID $(< "$PIDFILE"))"
else
echo "Detenido"
exit 3
fi
;;
ayuda|-h|--help) uso ;;
"") uso >&2; exit 2 ;;
*) echo "Orden desconocida: $1" >&2; uso >&2; exit 2 ;;
esac
$ servidor-web estado; echo "código $?"
Detenido
código 3
$ servidor-web iniciar
Iniciado en http://localhost:8000 (PID 48213)
$ servidor-web iniciar
Ya está en ejecución (PID 48213)
$ servidor-web reiniciar
Detenido
Iniciado en http://localhost:8000 (PID 48230)
$ servidor-web arrancar
Orden desconocida: arrancar
Uso: servidor-web ORDEN
…
$ servidor-web detener
Detenido
El script reúne buena parte del bloque, y adelanta piezas que veremos en detalle más adelante:
- El
casefinal es todo el «programa principal». Cada orden llama a una función, y cada función se ocupa de una sola cosa. Las funciones son el tema 18; aquí basta ver que son bloques con nombre, y quereturntermina la función, comoexittermina el script. - Cada orden acepta un nombre en español y otro en inglés, con alternativas:
iniciar|start. Sin argumento, o con una orden desconocida, se muestra la ayuda en la salida de errores y se termina con 2, el código de uso incorrecto del tema 10. - El código 3 de
estadosigue la convención de los scripts de servicios de Linux: 0 si el servicio está en ejecución, 3 si está detenido. Así, otro script puede escribirservidor-web estado > /dev/null || servidor-web iniciar. - El
&final lanza el servidor en segundo plano, y$!contiene el número del proceso recién lanzado, que se guarda en el archivo.$(< archivo)es una forma rápida de$(cat archivo), que Bash resuelve sin crear un proceso. kill -0no envía ninguna señal: solo comprueba si el proceso existe y se le pueden enviar señales. Es la forma de saber si el número guardado sigue correspondiendo a un proceso vivo, o quedó de una ejecución anterior.- Las operaciones son repetibles: iniciar algo que ya está en ejecución, o detener algo que ya está detenido, no es un error. Es la idempotencia que el tema 1 contó entre las cualidades de un buen script, y que retomaremos en el tema 27.
12. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
Un caso sin ;; | Error de sintaxis al llegar al patrón siguiente. | Terminar cada caso con ;;. |
*) antes que otros casos | Los casos siguientes nunca se alcanzan. | *) siempre al final. |
*.gz) antes que *.tar.gz) | El caso más específico nunca se ejecuta. | Lo específico primero. |
Sin caso *) | Un valor inesperado no hace nada, sin aviso. | Un caso por defecto con un mensaje de error. |
"*.txt") | Compara con el texto literal *.txt. | *.txt) sin comillas. |
SI|Si|si|sI) | Funciona, pero es largo y fácil de olvidar una variante. | case "${r,,}" in si) |
[1-31]) para un día del mes | Es una clase de un carácter: 1, 2, 3 o 1. | Un rango numérico con (( )). |
shopt -s extglob en la misma línea o función que el patrón | Error de sintaxis: el patrón se analizó antes. | shopt al comienzo del script. |
13. Resumen
case valor in patrón) comandos ;; esacejecuta los comandos del primer patrón que coincide con el valor.- Los patrones son de comodines (
*,?,[…]), deben coincidir con el valor completo, y|separa alternativas. - El caso
*), al final, atrapa todo lo demás. Casi todocasedebería tenerlo, con un mensaje que diga qué se esperaba. - El orden importa: los patrones más específicos van antes que los generales.
- El valor no se divide ni se expande; en los patrones, las comillas desactivan los comodines.
- Normalizar con
"${valor,,}"evita listar todas las combinaciones de mayúsculas. ;;sale delcase;;&continúa con el caso siguiente;;;&sigue probando patrones.- Los patrones extendidos, como
@(a|b)o+([0-9]), necesitanshopt -s extgloben una línea anterior al código que los usa. casepara comparar un valor con patrones;elifpara condiciones distintas o numéricas; un array asociativo para traducir valores.- Un
casesobre el primer argumento es el núcleo de un script con subcomandos, con un mensaje de uso y código 2 ante una orden desconocida.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué ocurre si el valor de un case no coincide con ningún patrón?
No se ejecuta ningún caso, y case termina con código 0. Por eso conviene un caso *) que informe el valor inesperado.
2. ¿Qué significa el patrón s|si|sí)?
Que el caso se elige si el valor es exactamente s, si o sí. El símbolo | separa alternativas.
3. ¿Coincide el patrón txt) con el valor informe.txt?
No. Los patrones deben coincidir con el valor completo. Para «termina en txt» se escribe *txt), o mejor *.txt).
4. ¿Por qué *.tar.gz) debe ir antes que *.gz)?
Porque case se detiene en el primer patrón que coincide, y todo lo que termina en .tar.gz termina también en .gz. Si *.gz) estuviera antes, el caso de .tar.gz nunca se ejecutaría.
5. ¿Hace falta poner comillas en case $archivo in si el nombre tiene espacios?
No: el valor de case no pasa por la división en palabras. Ponerlas no hace daño, y mantiene la costumbre.
6. ¿Qué diferencia hay entre los patrones *) y "*")?
*) coincide con cualquier valor. "*") solo coincide con un asterisco literal, porque las comillas desactivan el comodín.
7. ¿Cómo se acepta si escrito con cualquier combinación de mayúsculas y minúsculas?
Normalizando el valor a minúsculas antes de comparar: case "${respuesta,,}" in si) ….
8. ¿Qué hace el terminador ;;&?
Después de ejecutar el caso, sigue probando los patrones siguientes y ejecuta también los que coincidan, en lugar de salir del case.
9. ¿Por qué falla shopt -s extglob; case $x in +([0-9])) … escrito en una sola línea?
Porque Bash lee y analiza la línea completa antes de ejecutarla. Cuando analiza el patrón, extglob todavía no está activo, y +( es un error de sintaxis. La opción debe activarse en una línea anterior.
10. ¿Por qué el patrón [1-31] no sirve para validar un día del mes?
Porque […] es una clase de un solo carácter: [1-31] significa «un carácter entre 1 y 3, o un 1». Los rangos numéricos se comprueban con (( )).
11. ¿Cuándo es preferible un array asociativo a un case?
Cuando cada caso solo traduce un valor a otro dato, sin acciones distintas. Una tabla de datos es más clara y se consulta en una línea.
12. En servidor-web, ¿qué hace kill -0?
No envía ninguna señal: comprueba si el proceso con ese número existe y se le pueden enviar señales. Sirve para saber si el servidor sigue en ejecución.
13. ¿Por qué un script con subcomandos debería terminar con código 2 ante una orden desconocida?
Porque 2 es la convención para un uso incorrecto. Así, quien lo llama puede distinguir un error de uso de un fallo en la operación misma.
Para practicar
- Escribe un script que reciba un nombre de archivo y diga si es una imagen, un documento, un video o «otro tipo», según su extensión, sin distinguir mayúsculas.
- Reproduce el ejemplo de
*.gzy*.tar.gzen los dos órdenes y comprueba la diferencia. - Prueba el terminador
;;&con un valor que cumpla varias propiedades: empieza con mayúscula, contiene un dígito, termina en.txt. - Activa
extgloby escribe uncaseque distinga números enteros, números con signo y cualquier otra cosa. - Lee
/etc/os-releaseconsourcey escribe uncaseque muestre qué gestor de paquetes usa tu sistema. - Instala
servidor-weby pruébalo con todas sus órdenes. Agrega una ordenabrirque muestre la dirección del servidor solo si está en ejecución. - Convierte en un script con subcomandos dos o tres tareas relacionadas que hagas a menudo, por ejemplo
notas nueva,notas listarynotas buscar.