Tot el curs ha donat per suposada una cosa: que l'intèrpret és Bash 5 sobre Ubuntu 24.04. És una suposició raonable per a srv-veloz-01/02/03, i per això hem fet servir arrays associatius, mapfile, [[ ]], ${s^^} i set -o pipefail sense pensar-ho dues vegades. Però el dia que calgui escriure l'entrypoint.sh del contenidor Alpine de la veloz-api, o un script d'arrencada que corre abans que Bash estigui disponible, o una eina que un company executarà al seu macOS, aquesta suposició es trenca i l'script falla amb errors desconcertants. Aquesta lliçó explica què és POSIX, quines construccions nostres són bashismes, quins són els seus equivalents, per què les eines externes donen més problemes que el mateix shell, com comprovar la portabilitat i —el més important— quan val la pena i quan és un llast.

Contingut

  1. Què és POSIX i què és un bashisme
  2. L'escenari real: qui és /bin/sh a cada sistema
  3. Taula de bashismes i el seu equivalent POSIX
  4. Detalls que cal conèixer en baixar a POSIX
  5. Les eines externes fan més mal que el shell
  6. Com comprovar la portabilitat
  7. La decisió de disseny, amb criteri explícit per a Veloz Envíos
  8. Declarar la dependència: comprovar BASH_VERSINFO
  9. Què aporta cada versió de Bash i el problema de macOS
  10. Balanç del mòdul 8

  1. Què és POSIX i què és un bashisme

POSIX és un estàndard (IEEE 1003.1) que defineix, entre moltes altres coses, el llenguatge del shell: quina sintaxi ha d'entendre qualsevol shell que es declari conforme. És el mínim comú denominador, i és deliberadament auster: no té arrays, ni [[ ]], ni substitució de cadenes a les expansions.

Un bashisme és qualsevol construcció que Bash entén i l'estàndard no exigeix. No són errors —són les raons per les quals Bash és còmode—, però només funcionen on hi ha Bash. El problema sorgeix per una confusió concreta: escriure #!/bin/sh a la primera línia i fer servir sintaxi de Bash a dins. A Ubuntu, si a més el proves executant bash script.sh, funciona; al contenidor Alpine, on /bin/sh és un altre programa, falla.

  1. L'escenari real: qui és /bin/sh a cada sistema

Sistema /bin/sh és Conseqüència
Debian / Ubuntu dash Ràpid i estricte: cap bashisme no funciona
Alpine (contenidors) ash (BusyBox) Encara més reduït; a més, les eines són BusyBox
RHEL / Fedora bash en mode POSIX Molts bashismes «funcionen»: falsa sensació de seguretat
macOS bash 3.2 o zsh Bash antic: sense arrays associatius ni mapfile
FreeBSD / Solaris sh propi, ksh Diferències en els detalls

Les dues files perilloses són la de RHEL i la de macOS, per motius oposats. A RHEL el teu script amb bashismes i #!/bin/sh funciona, així que ningú no detecta el problema fins que arriba a Debian. A macOS l'intèrpret es diu bash però és la versió 3.2 del 2006 —Apple no l'actualitza per motius de llicència—, així que un script perfectament correcte per a Bash 5 falla amb missatges críptics.

L'error típic es veu així:

$ dash /tmp/informe.sh
/tmp/informe.sh: 12: [[: not found

[[: not found és la signatura inconfusible d'un bashisme a dash: no és un error de sintaxi, és que dash busca una ordre anomenada [[.

  1. Taula de bashismes i el seu equivalent POSIX

Bashisme Equivalent POSIX Nota
[[ $a == $b ]] [ "$a" = "$b" ] En POSIX les cometes són obligatòries
[[ $s == a* ]] case $s in a*) ;; esac POSIX no té comparació amb patró a [ ]
[[ $s =~ regex ]] expr "$s" : 'regex' o grep -q Sense BASH_REMATCH; el més dolorós
arr=(a b c), ${arr[@]} Paràmetres posicionals: set -- a b c, "$@" Un sol «array» per àmbit
declare -A No existeix Sense arrays associatius: fitxer temporal o awk
${s//a/b} printf '%s' "$s" | sed 's/a/b/g' Costa un procés
${s^^} / ${s,,} tr '[:lower:]' '[:upper:]' Ídem
${s:0:3} printf '%s' "$s" | cut -c1-3 ${s#...} i ${s%...} que són POSIX
$(( a + b )) $(( a + b )) Sí que és POSIX, fes-lo servir sense por
(( i++ )) i=$(( i + 1 )) L'ordre (( )) no és POSIX
local x (no és a l'estàndard) L'implementen dash, ash i ksh: segur a la pràctica
function f { }, source f f() { }, . f La forma amb parèntesis i el punt són les portables
echo -e "a\tb" printf 'a\tb\n' echo amb opcions no és portable
<<< "$cadena" <<EOFEOF o printf ... | El here-string és de Bash
<(cmd) Fitxer temporal amb mktemp Substitució de procés: sense equivalent
arr+=(x), s+=text s="${s}text" L'operador += no és POSIX
${BASH_SOURCE[0]}, read -a, mapfile $0; while read + set -- Sense arrays no hi ha equivalent directe
set -o pipefail, trap ... ERR No existeixen Vegeu l'apartat 4; només trap ... EXIT INT TERM
{1..10} seq 1 10 o while L'expansió de claus no és POSIX
&> fitxer > fitxer 2>&1 Molt fàcil d'oblidar

  1. Detalls que cal conèixer en baixar a POSIX

[ ] no és [[ ]]. És una ordre, no sintaxi, així que els seus arguments pateixen divisió en paraules. [ $a = $b ] amb $a buida es converteix en [ = valor ] i dóna un error de sintaxi. Amb cometes —[ "$a" = "$b" ]— funciona sempre. Fes servir =, no ==, i -a/-o estan obsolets: encadena amb && i || entre dos [ ].

Sense pipefail. És la pèrdua més seriosa, perquè cmd1 | cmd2 retorna el codi de cmd2 i una fallada del primer passa inadvertida (05-03). La sortida practicable és trencar la canonada: escriure la primera etapa en un temporal amb mktemp, comprovar-ne el codi de sortida i llegir el temporal a la segona. A la pràctica, un script POSIX es reestructura per no encadenar ordres la fallada de les quals importi.

Sense arrays. El substitut són els paràmetres posicionals:

set -- Valencia Sevilla Bilbao Madrid    # "array" de 4 elements
for ciutat in "$@"; do printf 'processant %s\n' "$ciutat"; done
printf 'total: %d\n' "$#"

És un únic conjunt per àmbit, així que si necessites dues llistes alhora, o en guardes una en una funció o passes a awk.

Sense local, en teoria. L'estàndard no l'inclou, però dash, ash, ksh i zsh l'implementen. La recomanació pràctica: fes-lo servir, i documenta a la capçalera que l'script requereix un shell amb local.

  1. Les eines externes fan més mal que el shell

Aquesta és la part que gairebé tothom subestima. Pots escriure un script POSIX impecable i que falli a macOS perquè sed -i funciona diferent. Linux fa servir GNU coreutils; macOS i els BSD fan servir les eines BSD; Alpine fa servir BusyBox, que és una tercera implementació reduïda.

Ús GNU (Linux) BSD / macOS Portable
Editar al lloc sed -i 's/a/b/' f sed -i '' 's/a/b/' f sed 's/a/b/' f > t && mv t f
Data relativa date -d '3 days ago' date -v-3d Calcular amb $(( )) sobre date +%s
Data des de l'època date -d "@$s" date -r "$s" Cap: detectar la variant
Camí absolut readlink -f No existeix (fes servir realpath o stat) cd "$(dirname "$f")" && pwd
Regex Perl grep -P '\d+' No suportat grep -E '[0-9]+'
Mida de fitxer stat -c %s f stat -f %z f wc -c < f
Sense entrada, no executar xargs -r Comportament per defecte xargs -r només a GNU/BusyBox
Format a find find . -printf '%s\n' No existeix find . -exec stat ... \;

Tres estratègies, en ordre de preferència:

  1. Cenyir-se a les opcions POSIX de cada eina. sed 's/a/b/' sense -i, grep -E, find -exec. És el més robust i gairebé sempre suficient.
  2. Detectar la variant en arrencar i adaptar-s'hi:
if date -d '@0' >/dev/null 2>&1
    then data_de_epoca() { date -d "@$1" +%F; }    # GNU
    else data_de_epoca() { date -r "$1" +%F; }     # BSD
fi
  1. Declarar la dependència: exigir coreutils i comprovar-ho amb veloz_requereix (06-03). A macOS s'instal·len amb brew install coreutils, encara que queden amb prefix g (gsed, gdate).

  1. Com comprovar la portabilitat

Tres eines complementàries, més una prova real:

$ checkbashisms entrypoint.sh          # paquet devscripts
possible bashism in entrypoint.sh line 14 (should be '.', not 'source'):
source /etc/veloz/env.sh
possible bashism in entrypoint.sh line 22 ([[ ... ]]):
if [[ -z "$VELOZ_API_URL" ]]; then

$ shellcheck -s sh entrypoint.sh       # 08-05, dialecte POSIX
In entrypoint.sh line 22:
   ^-- SC2039 (warning): In POSIX sh, [[ ]] is undefined.

$ dash -n entrypoint.sh                # nomes sintaxi, pero es l'interpret real
$ dash entrypoint.sh                   # execucio real: el mes fiable

checkbashisms és l'especialista i detecta coses que ShellCheck no marca; shellcheck -s sh s'integra amb la resta del flux i explica el perquè; dash -n valida la sintaxi amb l'intèrpret que realment es farà servir a Debian. Però la comprovació definitiva és executar-lo on ha de córrer:

$ docker run --rm -v "$PWD:/w" -w /w alpine:3.20 sh entrypoint.sh --dry-run

Allà es descobreixen alhora els bashismes i les diferències de BusyBox, que són la meitat del problema. Mereix una feina de CI (08-05) al costat de la de ShellCheck.

  1. La decisió de disseny, amb criteri explícit per a Veloz Envíos

POSIX pur no és «millor»: és una restricció que es paga en llegibilitat i en processos. Un ${s^^} es converteix en un tr amb el seu fork, i un array associatiu que resolia una agregació en deu línies es converteix en un fitxer temporal ordenat. Escriure en POSIX el que sempre correrà a Ubuntu és pagar un cost sense comprar res.

Val la pena POSIX És un llast
entrypoint.sh d'un contenidor mínim sense Bash Eines internes d'un parc homogeni
Scripts d'arrencada i initramfs Scripts amb lògica de dades i agregació
Instal·ladors que s'executen en sistemes desconeguts Automatització amb arrays i expressions regulars
Fragments que altres incrusten al seu /bin/sh Tot el que ja funciona en Bash 5

Criteri per al toolkit, escrit i aplicat:

  • lib/comu.sh i els cinc scripts de bin/ continuen sent Bash 5, amb #!/usr/bin/env bash, arrays associatius, [[ ]], mapfile i set -euo pipefail. Corren en tres servidors idèntics que administrem nosaltres; renunciar a aquestes eines empitjoraria el codi sense cap guany.
  • L'entrypoint.sh del contenidor de la veloz-api s'escriu en POSIX pur amb #!/bin/sh. La imatge base és Alpine, no porta Bash, i afegir-lo només per a l'arrencada significa augmentar la imatge i la seva superfície d'atac. L'script és curt —comprova variables d'entorn, espera la base de dades i fa exec del servei—, justament el tipus de lògica que POSIX cobreix sense esforç.
#!/bin/sh
# entrypoint.sh - arrencada de veloz-api. POSIX pur: la imatge Alpine no porta bash.
set -eu                                     # sense pipefail: no es POSIX
: "${VELOZ_API_PORT:=8080}"                 # valor per defecte, POSIX
: "${VELOZ_BD_HOST:?falta VELOZ_BD_HOST}"   # obligatoria o avortar (03-06)

espera=0
while ! nc -z "$VELOZ_BD_HOST" 5432; do
    espera=$(( espera + 1 ))
    [ "$espera" -ge 30 ] && { echo "la base de dades no respon" >&2; exit 1; }
    sleep 1
done
echo "veloz-api arrencant al port $VELOZ_API_PORT"
exec /usr/local/bin/veloz-api --port "$VELOZ_API_PORT" "$@"

Ni un bashisme: [ ] en lloc de [[ ]], $(( )) que sí que és POSIX, : amb ${var:=} i ${var:?}, echo sense opcions i exec final perquè el servei hereti el PID 1 i rebi els senyals (05-02). La decisió queda documentada a la capçalera, que és tan important com la decisió mateixa.

  1. Declarar la dependència: comprovar BASH_VERSINFO

Si un script requereix Bash 4 o superior, digues-ho i falla aviat amb un missatge clar, en lloc de petar cinquanta línies després amb «syntax error near unexpected token». La comprovació va al principi de lib/comu.sh, just després del shebang, i fa servir BASH_VERSINFO (01-02):

#!/usr/bin/env bash
# lib/comu.sh - llibreria comuna del toolkit. Requereix Bash 4.3+.

if [ -z "${BASH_VERSINFO:-}" ]; then
    echo "Error: aquest script requereix Bash, no sh/dash." >&2
    exit 1
fi
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then
    echo "Error: cal Bash 4.3 o superior; s'ha trobat ${BASH_VERSION}." >&2
    echo "A macOS: brew install bash i feu servir /opt/homebrew/bin/bash." >&2
    exit 1
fi

Dos detalls del codi. La primera comprovació s'escriu amb [ ] i echo expressament: si l'intèrpret resulta ser dash, aquestes dues línies han de funcionar per poder donar el missatge; un [[ ]] aquí produiria l'error críptic que precisament volem evitar. I el missatge diu què cal fer, no només què falta: un error que indica la solució estalvia mitja hora a qui se'l trobi.

  1. Què aporta cada versió de Bash i el problema de macOS

Versió Novetats rellevants Any
3.2 La de macOS. Sense arrays associatius, sense mapfile, sense ${s^^}, sense &>> 2006
4.0 Arrays associatius (declare -A), mapfile/readarray, ${s^^}/${s,,}, |& 2009
4.2 printf -v amb %(fmt)T (dates sense date), declare -g 2011
4.3 declare -n (referències de nom, 08-03), millores a [[ ]] 2014
4.4 ${var@Q} (posada entre cometes segura), mapfile -d, local - 2016
5.0+ EPOCHSECONDS i EPOCHREALTIME (temps sense date), SRANDOM 2018-22

La frontera pràctica és Bash 4.0: gairebé tot el que fa còmode el Bash modern va arribar allà. Ubuntu 24.04 porta la 5.2, així que a la flota no hi ha problema.

El problema és macOS, que continua amb la 3.2 del 2006 per la llicència GPLv3 de les versions posteriors. Si un company desenvolupa en Mac, els seus scripts fallaran en fer servir declare -A o mapfile encara que als servidors funcionin. Tres respostes possibles: instal·lar Bash modern amb Homebrew i fer servir #!/usr/bin/env bash amb el nou primer al PATH —l'opció normal—; cenyir-se a Bash 3.2, cosa que és renunciar a massa; o desenvolupar dins d'un contenidor amb la mateixa imatge que producció, que a més elimina la resta de diferències d'eines de l'apartat 5.

  1. Balanç del mòdul 8

El mòdul va començar amb tres preguntes sobre el toolkit: s'entén?, és ràpid?, és segur? Set lliçons després, ~/veloz-ops/ és una altra cosa:

Lliçó Què hi va aportar
08-01 Llegible: estil uniforme, estructura canònica, funcions curtes, comentaris que expliquen el perquè
08-02 Ràpid: mesurat abans i després; informe-diari.sh de 3 min 12 s a 3,9 s
08-03 Auditat: sense eval, entrada validada amb llista blanca, secrets fora del codi, temporals segurs, sudo acotat
08-04 Versionat: història amb motius, branques, etiquetes, .gitignore que protegeix la configuració, hook pre-commit
08-05 Analitzat: 47 avisos corregits, entre ells un rm -rf amb variable buida; format automàtic amb shfmt
08-06 Provat: tests/ amb dobles i dades fictícies, executat a cada commit i a CI
08-07 Amb la portabilitat decidida: Bash 5 documentat i comprovat, POSIX on de debò cal

I tot sense afegir ni una funció nova. Aquesta és la idea del mòdul: la diferència entre un script que funciona i programari del qual et pots refiar no és en el que fa, sinó en com està fet.

Errors Habituals i Consells

  • Posar #!/bin/sh i escriure Bash. L'error central. Si fas servir bashismes, el shebang és #!/usr/bin/env bash.
  • Provar amb bash script.sh un script que declara sh. El shebang s'ignora en invocar-lo així, i el problema queda amagat. Prova amb dash script.sh.
  • Creure que POSIX és «més segur» o «millor». És més restrictiu: triar-lo sense necessitat produeix codi més llarg, més lent i més difícil de llegir.
  • Oblidar les cometes a [ ]. A [[ ]] perdonava; aquí una variable buida produeix un error de sintaxi.
  • Suposar que les eines són GNU. sed -i, date -d i readlink -f no existeixen igual a macOS ni a BusyBox, i allà no hi ajuda cap shell.
  • Consell: un contenidor Alpine a CI costa segons i detecta alhora els bashismes i les diferències de BusyBox.
  • Consell: escriu la decisió a la capçalera del fitxer. «Requereix Bash 4.3+» o «POSIX pur: la imatge no porta bash» evita que el següent la desfaci sense voler.

Exercicis

Exercici 1. Converteix aquest fragment a POSIX pur mantenint el comportament.

#!/usr/bin/env bash
ciutats=(Valencia Sevilla Bilbao)
for c in "${ciutats[@]}"; do
    [[ $c == V* ]] && echo -e "prioritaria:\t${c^^}"
done

Exercici 2. Escriu una funció veloz_fa_dies que retorni la data de fa N dies en format AAAA-MM-DD i funcioni tant amb date de GNU com de BSD.

Exercici 3. Un company informa que informe-diari.sh falla al seu macOS amb declare: -A: invalid option. Explica'n la causa i proposa dues solucions amb les seves contrapartides.

Solucions

Solució 1.

#!/bin/sh
set -- Valencia Sevilla Bilbao              # parametres posicionals com a "array"
for c in "$@"; do
    case $c in
        V*) majus=$(printf '%s' "$c" | tr '[:lower:]' '[:upper:]')
            printf 'prioritaria:\t%s\n' "$majus" ;;
    esac
done

Quatre substitucions: l'array passa a set -- i "$@"; la comparació amb patró [[ $c == V* ]] passa a case, que és la forma POSIX de comparar amb patrons; ${c^^} passa a tr; i echo -e passa a printf, que interpreta \t de manera portable i a més evita el problema que echo -e imprimeixi literalment -e en alguns shells.

Solució 2.

# veloz_fa_dies <n>  ->  data AAAA-MM-DD de fa n dies
veloz_fa_dies() {
    if date -d '@0' +%F >/dev/null 2>&1; then
        date -d "$1 days ago" +%F                       # GNU
    else
        date -v "-$1"d +%F                              # BSD / macOS
    fi
}

La detecció es fa provant una opció exclusiva de GNU i descartant-ne la sortida i l'error. L'alternativa realment universal és aritmètica sobre l'època —date -d "@$(( $(date +%s) - 86400 * $1 ))"— però té el mateix problema en convertir de tornada, i a més ignora els canvis d'horari, així que la detecció de variant és preferible.

Solució 3. La causa és que macOS porta Bash 3.2, anterior als arrays associatius que van arribar a la 4.0, i #!/usr/bin/env bash resol al /bin/bash del sistema. Dues solucions:

  • Instal·lar Bash modern amb brew install bash i anteposar el seu directori al PATH, de manera que env bash trobi el 5.x. Contrapartida: cada persona ha de configurar la seva màquina, i un PATH mal ordenat reprodueix la fallada en silenci.
  • Desenvolupar en un contenidor amb la mateixa imatge que producció. Contrapartida: flux de treball una mica més pesat; avantatge: elimina també les diferències de sed, date i stat de l'apartat 5, que apareixerien igualment.

Reescriure el toolkit per a Bash 3.2 seria una tercera opció, i és la pitjor: penalitza tres servidors per acomodar un portàtil. En qualsevol cas, la comprovació de BASH_VERSINFO de l'apartat 8 hauria convertit aquell error críptic en un missatge que explica què cal fer.

Conclusió

La portabilitat és una decisió, no una virtut automàtica. POSIX defineix el mínim comú denominador del shell, i tot el que Bash hi afegeix per sobre —[[ ]], arrays, ${s//a/b}, ${s^^}, mapfile, <<<, <( ), +=, set -o pipefail, trap ERR— és un bashisme que desapareix tan bon punt /bin/sh és dash a Debian, ash a Alpine o ksh en un altre Unix; l'error [[: not found n'és la signatura. La taula d'equivalències cobreix l'essencial: [ "$a" = "$b" ] amb cometes obligatòries, case per comparar amb patrons, set -- i "$@" com a substitut dels arrays, tr i sed en lloc de les expansions de cadena, . en comptes de source, printf en comptes d'echo -e i reestructurar el codi allà on falta pipefail. Però el problema més gran no és al shell sinó a les eines externes: sed -i, date -d, readlink -f, grep -P i stat es comporten de manera diferent a GNU, BSD i BusyBox, i davant d'això només hi caben tres estratègies —cenyir-se a les opcions POSIX, detectar la variant en arrencar o declarar la dependència de coreutils—. La portabilitat es comprova, no se suposa: checkbashisms, shellcheck -s sh, dash -n i, sobretot, una execució real en un contenidor Alpine dins de CI. El criteri de Veloz Envíos queda escrit: lib/comu.sh i els cinc scripts de bin/ continuen sent Bash 5 perquè corren en tres servidors idèntics i renunciar a les seves eines no compraria res, mentre que l'entrypoint.sh del contenidor de la veloz-api s'escriu en POSIX pur perquè la imatge Alpine no porta Bash i la seva lògica és curta. Aquesta dependència es declara i es comprova amb BASH_VERSINFO en arrencar, amb [ ] i echo perquè el missatge arribi fins i tot si l'intèrpret és dash, i dient què cal fer i no només què falta. La frontera pràctica és Bash 4.0 —arrays associatius, mapfile, ${s^^}— amb declare -n a la 4.3, i l'obstacle recurrent és el Bash 3.2 de macOS, que es resol amb Homebrew o desenvolupant en contenidor.

Amb això es tanca el mòdul 8, i amb ell la transformació del toolkit: els mateixos cinc scripts i la mateixa llibreria que van començar el mòdul són ara llegibles, ràpids, auditats, versionats, analitzats, provats i amb la portabilitat decidida, sense haver afegit ni una sola funció nova. Ja saps escriure Bash i ja saps escriure'l bé. El que queda és ajuntar-ho tot. El Mòdul 9 són cinc projectes complets construïts pas a pas, aplicant alhora tot el que s'ha après: un recol·lector d'informació del sistema (09-01), un analitzador de registres que agrega, detecta patrons i genera informes (09-02), un sistema de còpies de seguretat automatitzat amb retenció, verificació i restauració provada (09-03), un monitor de xarxa amb llindars i alertes (09-04) i, com a tancament del curs, la integració final del toolkit (09-05), on les peces dels quatre projectes anteriors s'unifiquen en una eina única, instal·lable, versionada, provada i desplegada a la flota de Veloz Envíos.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats