Al final de la lliçó anterior vas escriure una funció que recorria quatre ciutats amb un bucle només per comprovar si una cadena hi era. Funciona, però és l'eina equivocada: comparar un valor contra una llista tancada d'alternatives és la feina de case. Ja la vas fer servir de passada a 03-05, dins del while [[ $# -gt 0 ]] que analitzava opcions llargues, amb la promesa d'explicar-la del tot més endavant. Aquest moment és ara. A més, case és la meitat d'una parella: l'altra és select, la construcció que genera menús numerats en tres línies i amb la qual construirem veloz-menu.sh, la porta d'entrada interactiva del toolkit.
Contingut
- Sintaxi de
case - Els patrons són globs, no regex
- El comodí
*)i l'ordre d'avaluació - Terminadors:
;;,;&i;;& casedavant d'if/elifshopt -s nocasematch- Usos idiomàtics de
case - L'anàlisi d'opcions llargues, explicada del tot
- Menús amb
select - Bones pràctiques de menús
veloz-menu.shiinforme-diari.shamb subordres
- Sintaxi de
case
casecase "$estat" in
lliurat) echo "Enviament completat" ;;
en_repartiment) echo "En ruta" ;;
incidencia) echo "Requereix atenció" ;;
*) echo "Estat desconegut: $estat" >&2; return 1 ;;
esacLes peces, una a una: case "$valor" in introdueix la paraula que es compararà (posa-la entre cometes per costum, encara que case és un dels pocs contextos que no fa divisió en paraules); patro) obre una branca, amb el parèntesi de tancament obligatori i el d'obertura opcional; el cos admet qualsevol nombre d'ordres, en una línia o en diverses; ;; tanca la branca, i sense ell hi ha error de sintaxi; i esac —case a l'inrevés— tanca el bloc.
Diverses alternatives a la mateixa branca se separen amb |, que es llegeix com a «o»: s|S|si|SI|y|yes) echo "Confirmat" ;;. Si cap patró no coincideix i no hi ha *), case no fa res i retorna 0: silenci absolut, i per això el *) és gairebé obligatori.
- Els patrons són globs, no regex
Aquesta és la confusió número u. Els patrons de case fan servir les mateixes regles que l'expansió de comodins de fitxers (02-05), tot i que aquí s'apliquen a una cadena qualsevol.
| Patró | Coincideix amb | No coincideix amb |
|---|---|---|
Valencia |
exactament Valencia |
valencia |
*.csv |
enviaments.csv, a.b.csv |
csv, enviaments.CSV |
app.log.? |
app.log.1 |
app.log.12 |
[0-9][0-9] / [!0-9]* |
08, 31 / a12 |
8, 123 / 2026 |
Val*|Sev* |
Valencia, Sevilla |
Bilbao |
El que no funciona perquè és sintaxi d'expressions regulars: +, {2,3}, \d, ^, $ i els grups (...). Un patró [0-9]+ en case significa literalment «un dígit seguit d'un signe més». Les classes POSIX sí que estan disponibles ([[:digit:]], [[:alpha:]]), i per validar un identificador n'hi ha prou amb case "$id" in E[0-9][0-9][0-9][0-9]) ....
Si necessites de debò una expressió regular —«entre dos i quatre dígits», «una adreça de correu»— l'eina és [[ $s =~ regex ]], tema de 05-04; amb case cobreixes el 90% dels casos reals i amb molta menys complexitat. Un detall important: el valor comparat no se sotmet a globbing, però el patró sí que s'expandeix. Això permet patrons dinàmics: si p="Val*", la branca $p) coincideix amb Valencia. I si vols que un patró sigui literal, posa'l entre cometes: "*") coincideix només amb un asterisc.
- El comodí
*) i l'ordre d'avaluació
*) i l'ordre d'avaluaciócase avalua les branques de dalt a baix i s'atura a la primera que coincideix. L'ordre, per tant, és semàntic: el específic primer, el general després.
case "$fitxer" in
app.log) echo "Registre actiu" ;;
app.log.*.gz) echo "Rotat i comprimit" ;;
app.log.*) echo "Rotat" ;;
*.log) echo "Un altre registre" ;; # el general, DESPRÉS dels específics
*) echo "No és un registre" ;;
esacSi *.log) fos a dalt de tot, es menjaria app.log) i les branques específiques no s'executarien mai. El *) final és la xarxa de seguretat: cobreix-lo sempre, encara que només sigui per registrar un avís. Un case sense *) que rep un valor inesperat no falla, simplement no fa res, i aquest silenci és el pitjor comportament possible en un script d'operacions.
- Terminadors:
;;, ;& i ;;&
;;, ;& i ;;&Bash 4 va afegir dos terminadors més al clàssic ;;.
| Terminador | Què fa després d'executar la branca |
|---|---|
;; |
Surt del case. L'habitual. |
;& |
Executa el cos de la branca següent sense comprovar-ne el patró (caiguda, com el fallthrough de C) |
;;& |
Continua comprovant els patrons restants i executa els que coincideixin |
nivell="ERROR"
case "$nivell" in
ERROR) echo "→ enviar alerta" ;;& # ;;& → continua comprovant patrons
ERROR|WARN) echo "→ registrar" ;;&
*) echo "→ comptabilitzar" ;;
esacSortida: les tres línies, → enviar alerta, → registrar i → comptabilitzar. Amb ;; a totes les branques només s'hauria imprès la primera línia. ;;& és útil per a accions acumulatives per categoria; ;& és rar i confon qui el llegeix. Recomanació: fes servir ;; per defecte i ;;& només quan l'acumulació sigui l'objectiu explícit, amb un comentari que ho digui.
case davant d'if/elif
case davant d'if/elif| Criteri | case |
if/elif |
|---|---|---|
| Què compara | Una sola variable contra patrons | Qualsevol condició, diferent a cada branca |
| Tipus de prova | Coincidència de patró glob | Codis de sortida, números, fitxers |
| Alternatives per branca | Sí, amb | |
Requereix || encadenats |
| Llegibilitat amb 5+ branques | Excel·lent | Es degrada de pressa |
Rangs numèrics i combinar i/o |
Maldestre o impossible | Natural amb (( )) |
Regla: si totes les branques pregunten pel mateix valor i amb igualtat o patró, fes servir case; si cada branca pregunta per una cosa diferent o hi ha comparacions numèriques, fes servir if/elif. Un if [[ "$sub" == "resum" ]]; then ... elif [[ "$sub" == "detall" ]]; then ... repeteix la variable a cada branca; el case equivalent l'anomena una sola vegada i alinea els patrons en columna, cosa que fa evident d'un cop d'ull quins valors es contemplen.
shopt -s nocasematch
shopt -s nocasematchPer defecte la comparació distingeix majúscules de minúscules. L'opció nocasematch la desactiva, i afecta també [[ == ]]:
shopt -s nocasematch seguit de case "$1" in valencia) ... fa que hi coincideixin VALENCIA, Valencia i vAlEnCiA, i shopt -u nocasematch la desactiva. És una opció global del shell: mentre és activa, qualsevol comparació posterior deixa de distingir la caixa, incloses les de funcions que no esperaven aquell comportament. Per això és preferible l'alternativa explícita de 04-04, case "${1,,}" in valencia) ...: més curta, sense efectes globals i evident en llegir-la.
- Usos idiomàtics de
case
casea) Validar contra una llista tancada. La funció de l'exercici de 04-04, ara en una línia: es_ciutat_valida() { case "${1,,}" in valencia|sevilla|bilbao|madrid) return 0 ;; *) return 1 ;; esac; }.
b) Confirmació sí/no d'un read (que ja coneixes de 03-05):
read -r -p "Voleu purgar els registres de més de 30 dies? [s/N] " resp
case "${resp,,}" in
s|si|y|yes) purgar_logs ;;
*) echo "Operació cancel·lada" ;; # l'opció segura sempre és la de per defecte
esacc) Ramificar per extensió de fitxer, aprofitant que els patrons són globs:
processar() {
case "$1" in
*.tar.gz) tar -tzf "$1" ;; # ha d'anar ABANS que *.gz!
*.gz) zcat "$1" | tail -n 20 ;;
*.csv) column -t -s, "$1" ;;
*.log) tail -n 20 "$1" ;;
*) echo "Tipus no admès: $1" >&2; return 1 ;;
esac
} # *.tar.gz és més específic que *.gz; col·locat després, no s'assoliria maid) Despatx de subordres. L'ús més potent, amb el qual un sol script ofereix diverses operacions (igual que git commit, git push):
sub="${1:-resum}"; shift || true # el shift consumeix la subordre...
case "$sub" in
resum) cmd_resum "$@" ;; # ...perquè "$@" porti només ELS SEUS arguments
detall) cmd_detall "$@" ;;
ciutats) cmd_ciutats "$@" ;;
ajuda|-h|--help) us; exit 0 ;;
*) echo "Subordre desconeguda: $sub" >&2; us; exit 2 ;;
esacEl || true evita fallar si no hi havia cap argument per consumir. El desenvolupes del tot a l'apartat 11.
- L'anàlisi d'opcions llargues, explicada del tot
Reprenem el bloc de 03-05 i ara s'entén cada línia:
while [[ $# -gt 0 ]]; do
case "$1" in
--data) data="${2:?--data requereix un valor}"; shift 2 ;;
--data=*) data="${1#*=}"; shift ;;
--ciutat) ciutat="${2:?--ciutat requereix un valor}"; shift 2 ;;
--ciutat=*) ciutat="${1#*=}"; shift ;;
-v|--verbose) detallat=1; shift ;;
-h|--help) us; exit 0 ;;
--) shift; break ;;
-*) echo "Opció desconeguda: $1" >&2; exit 2 ;;
*) posicionals+=("$1"); shift ;;
esac
done
set -- "${posicionals[@]}"Ara cada peça té nom: les branques --data) i --data=*) accepten les dues maneres d'escriure una opció, i la segona fa servir ${1#*=} de 04-04 per quedar-se amb el que hi ha després de l'=; shift 2 descarta opció i valor mentre que shift descarta només l'opció; -- és el marcador convencional d'«aquí s'acaben les opcions», de manera que el que vingui després són arguments encara que comenci per guionet; -*) captura qualsevol opció no reconeguda i falla sorollosament en comptes de tractar-la com a argument; i *) acumula els posicionals en un array (04-03) que set -- "${posicionals[@]}" retorna a $1, $2… ja nets d'opcions. És el patró complet d'anàlisi manual, i ara el pots escriure de memòria.
- Menús amb
select
selectselect és un for especialitzat: imprimeix una llista numerada, mostra un indicador, llegeix un número, executa el cos amb la variable posada a l'element triat i ho repeteix en bucle.
PS3="Tria una operació (número): "
select opcio in "Informe d'avui" "Veure errors" "Sortir"; do
case "$opcio" in
"Informe d'avui") informe-diari.sh ;;
"Veure errors") tail -n 20 /var/log/veloz/app.log ;;
"Sortir") echo "Fins aviat"; break ;;
*) echo "Opció no vàlida: $REPLY" >&2 ;;
esac
doneA pantalla surt la llista numerada (1) Informe d'avui…) i després l'indicador. El que cal saber:
PS3és la variable que conté l'indicador. Si no la fixes, surt el poc amistós#?.opciorep el text de l'element triat;REPLYconté el que l'usuari ha teclejat literalment. Si escriu una cosa que no és un número vàlid,opcioqueda buida peròREPLYconserva l'entrada: per això el*)delcaseinforma fent servir$REPLY.- El bucle és implícit i infinit. Només se'n surt amb
break, ambCtrl+Do ambexit. Sense una branca «Sortir» ambbreak, el menú no s'acaba mai. Prémer Enter en blanc repeteix la llista. - La llista pot venir d'un array:
select c in "${CIUTATS[@]}" Sortir; do ... done.
- Bones pràctiques de menús
Valida sempre amb un *) que capturi entrades fora de rang; no donis mai per fet que l'usuari escriu un número correcte. Inclou una opció de sortida explícita, perquè confiar en Ctrl+C maltracta l'usuari i deixa processos a mitges. I sobretot: els menús són la interfície secundària, mai la principal, ja que un script que només funciona interactivament no es pot ficar al cron (07-01) ni encadenar amb d'altres. La regla és que el comportament per defecte sigui no interactiu, amb opcions i subordres, i que el menú s'ofereixi només quan no hi ha arguments i l'entrada és un terminal. L'idioma és if [[ $# -eq 0 && -t 0 ]]; then menu_principal; else despatxar "$@"; fi. La comprovació [[ -t 0 ]] (03-05) pregunta si l'entrada estàndard és un terminal; des del cron no ho és, així que l'script no es quedarà mai esperant una resposta que no arribarà. Aquest patró és la base del que automatitzaràs a 07-02.
veloz-menu.sh i informe-diari.sh amb subordres
veloz-menu.sh i informe-diari.sh amb subordresPrimer, el menú del toolkit, a ~/veloz-ops/bin/veloz-menu.sh:
#!/usr/bin/env bash
# veloz-menu.sh — Menú interactiu d'operacions de Veloz Envíos.
set -u
readonly LOG_APP="/var/log/veloz/app.log"
menu_principal() {
local PS3="veloz-ops> " opcio
select opcio in "Informe diari" "Últims errors" "Estat de l'API" Sortir; do
case "$opcio" in
"Informe diari") informe-diari.sh resum ;;
"Últims errors") tail -n 20 "$LOG_APP" | grep --color=auto ERROR ;;
"Estat de l'API") systemctl is-active veloz-api ;;
Sortir) break ;;
*) printf 'Opció no vàlida: %s\n' "$REPLY" >&2 ;;
esac
echo
done
}
if [[ $# -eq 0 && -t 0 ]]; then menu_principal; else echo "Ús: $0" >&2; exit 2; fiI el despatxador d'informe-diari.sh, que substitueix el main lineal de 04-02:
us() { cat <<'FI'
Ús: informe-diari.sh <subordre> [opcions]
resum Taula de totals per ciutat (per defecte)
detall Una línia per enviament amb incidència
ciutats Només la llista de ciutats amb activitat
FI
}
main() {
local sub="${1:-resum}"; [[ $# -gt 0 ]] && shift
validar_entorn || exit $?
case "$sub" in
resum) acumular_dia "$(date +%F)" && taula_ciutats ;;
detall) llistar_incidencies "$@" ;;
ciutats) printf '%s\n' "${CIUTATS[@]}" ;;
ajuda|-h|--help) us; exit 0 ;;
*) printf 'Subordre desconeguda: %s\n' "$sub" >&2; us >&2; exit 2 ;;
esac
}
main "$@"Un sol script cobreix així tres operacions diferents, cadascuna amb la seva anàlisi d'opcions, i el menú l'embolcalla per a qui prefereixi prémer números. El bloc cat <<'FI' ... FI és un here-document, forma còmoda d'escriure text de diverses línies que veuràs a 05-05.
Errors Habituals i Consells
- Descuidar el
;;al final d'una branca: és error de sintaxi, i el missatge de Bash apunta a la línia equivocada. Recorda a més que el bloc es tanca ambesac,casea l'inrevés, igual queif/fi. - Posar el patró general abans de l'específic.
*.gzabans que*.tar.gzdeixa morta la segona branca. - Fer servir sintaxi de regex als patrons.
+,{n}i\dno signifiquen res en un glob. - Ometre el
*). Un valor inesperat farà que elcaseno faci absolutament res, en silenci. - Menú sense
break.selectés un bucle infinit; sense una opció de sortida l'usuari queda atrapat. - Deixar
nocasematchactivat. És global; desactiva'l o, millor, fes servir${var,,}. - Consell: quan el cos d'una branca passi de tres línies, extreu-lo a una funció (
cmd_resum). Uncasede despatx s'ha de llegir com un índex, no com el programa sencer.
Exercicis
Exercici 1. Escriu classifica_estat() que rebi un estat d'enviament i imprimeixi OK, PENDENT o REVISAR segons si és lliurat, en_repartiment o incidencia, acceptant qualsevol combinació de majúscules i retornant codi 1 davant d'un estat desconegut.
Exercici 2. Escriu un case que, donat un nom de fitxer, imprimeixi com caldria llegir-lo: zcat per a .gz, tar -tzf per a .tar.gz, column -t -s, per a .csv i less per a la resta. Comprova l'ordre de les branques.
Exercici 3. Construeix un menú amb select que ofereixi les quatre ciutats més una opció «Totes» i una «Sortir»; en triar una ciutat ha d'imprimir quants enviaments té, i «Totes» les ha de recórrer.
Solucions
Solució 1.
classifica_estat() {
case "${1,,}" in
lliurat) echo "OK" ;;
en_repartiment) echo "PENDENT" ;;
incidencia) echo "REVISAR" ;;
*) printf 'Estat desconegut: %s\n' "$1" >&2; return 1 ;;
esac
}${1,,} normalitza sense tocar nocasematch, així que la funció no altera el comportament global del shell.
Solució 2.
case "$fitxer" in
*.tar.gz|*.tgz) echo "tar -tzf $fitxer" ;; # primer, el més específic
*.gz) echo "zcat $fitxer" ;;
*.csv) echo "column -t -s, $fitxer" ;;
*) echo "less $fitxer" ;;
esacSi *.gz) anés abans, enviaments.tar.gz hi cauria i no es detectaria mai el tar.
Solució 3.
readonly CIUTATS=(Valencia Sevilla Bilbao Madrid) CSV="/srv/veloz/dades/enviaments.csv"
comptar_ciutat() { printf '%-10s %4d enviaments\n' "$1" "$(grep -c ",$1," "$CSV")"; }
PS3="Ciutat (número): "
select c in "${CIUTATS[@]}" Totes Sortir; do
case "$c" in
Sortir) break ;;
Totes) for x in "${CIUTATS[@]}"; do comptar_ciutat "$x"; done ;;
"") echo "Entrada no vàlida: $REPLY" >&2 ;; # número fora de rang o text
*) comptar_ciutat "$c" ;;
esac
doneLa branca "") és imprescindible: quan l'entrada no correspon a cap element, select deixa la variable buida i REPLY amb el que s'ha escrit.
Conclusió
case és la manera correcta de comparar un valor contra un conjunt tancat d'alternatives: sintaxi case valor in patro) cos ;; esac, patrons que són globs (amb | per a alternatives i *) com a xarxa de seguretat), avaluació de dalt a baix amb aturada a la primera coincidència, i els terminadors ;;, ;& i ;;& per als casos en què vulguis continuar. El faràs servir sobretot per a tres coses: validar entrades contra llistes tancades, analitzar opcions —el bloc de 03-05 ja no té secrets— i despatxar subordres, que és el que ha convertit informe-diari.sh en una eina amb resum, detall i ciutats. select completa el quadre amb menús numerats en tres línies, sempre com a interfície secundària i mai com a única porta d'entrada.
Queda una sola peça del mòdul, i és la que anem ajornant des del principi: els números. El nostre informe imprimeix 128 enviaments, 11 incidències però és incapaç de dir que això és un 8,59%, perquè hem anat truncant cèntims amb ${import%.*} i sumant només enters. A 04-06 veuràs per què Bash només fa aritmètica entera, com funcionen (( )) i $(( )), per què un 08 pot rebentar un script de dates, i com obtenir decimals de debò amb bc per tancar l'informe amb percentatges, mitjanes i variació respecte al dia anterior.
Curs de Programació en Bash
Mòdul 1: Introducció a Bash
- Què és Bash?
- Configurar el teu Entorn
- Navegació Bàsica per la Línia d'Ordres
- Entendre el Shell
- Trobar Ajuda: man, help i --help
Mòdul 2: Ordres Bàsiques de Bash
- Operacions amb Fitxers i Directoris
- Ordres de Processament de Text
- Permisos i Propietat dels Fitxers
- Redirecció i Canonades
- Comodins i Expansió de Rutes
- Historial i Dreceres de Teclat
Mòdul 3: Fonaments de Scripting
- Crear i Executar un Script
- Variables i Constants
- Operadors Bàsics
- Sentències Condicionals
- Arguments i Entrada de l'Usuari
- Cometes, Expansió i Substitució
Mòdul 4: Scripting Intermedi
- Bucles en Bash
- Funcions en Bash
- Arrays i Arrays Associatius
- Manipulació de Cadenes
- La Sentència case i els Menús Interactius
- Aritmètica i Càlculs Numèrics
Mòdul 5: Tècniques Avançades de Scripting
- Operacions Avançades amb Fitxers
- Gestió de Processos
- Gestió d'Errors i Depuració
- Expressions Regulars
- Entrada/Sortida Avançada: Descriptors i Here-Documents
- Scripts Modulars i Llibreries Reutilitzables
Mòdul 6: Treballar amb Eines Externes
Mòdul 7: Automatització i Programació
- Tasques Cron
- Automatitzar Tasques
- Scripts de Còpia i Restauració
- Monitoratge i Registre
- Serveis i Temporitzadors amb systemd
- Automatització Remota amb SSH
Mòdul 8: Bones Pràctiques i Optimització
- Escriure Codi Llegible
- Optimitzar Scripts en Bash
- Consideracions de Seguretat
- Control de Versions amb Git
- Anàlisi Estàtica amb ShellCheck i shfmt
- Proves Automatitzades amb Bats
- Portabilitat: POSIX sh enfront de Bashismes
