El hook pre-commit de 08-04 invocava shellcheck -x sense explicar què era. Aquesta lliçó ho explica. ShellCheck és un programa que llegeix els teus scripts sense executar-los i et diu on són els errors: la variable sense cometes de 03-06, el cd sense || de 05-01, el $? mal utilitzat de 03-04, el local x=$(cmd) que s'empassa el codi de sortida de 04-02. És a dir, gairebé tot el que aquest curs t'ha ensenyat a evitar, més un centenar de casos que no hem vist. I al seu costat, shfmt, el formatador que tanca el que 08-01 va deixar pendent: que l'estil deixi de ser una discussió d'equip i passi a ser una ordre.

Contingut

  1. Què és l'anàlisi estàtica i per què en Bash és imprescindible
  2. Instal·lar i executar ShellCheck
  3. Opcions que canvien el resultat
  4. Anatomia d'un avís
  5. Els avisos que més veuràs
  6. Taula resum de codis
  7. Silenciar amb criteri
  8. shfmt: el format deixa d'opinar-se
  9. Integració: editor, hook i CI
  10. Altres eines
  11. Aplicació: passar ShellCheck a tot el toolkit

  1. Què és l'anàlisi estàtica i per què en Bash és imprescindible

Anàlisi estàtica és examinar el codi sense executar-lo, buscant patrons que gairebé sempre indiquen una fallada. Existeix per a tots els llenguatges, però en Bash és especialment valuosa per una raó concreta: l'intèrpret no avisa de gairebé res fins que és massa tard.

directori="/srv/veloz/dades de juliol"
rm -rf $directori/*.tmp

Bash executa això sense una sola queixa. No hi ha compilador, ni comprovació de tipus, ni avís: simplement esborra en dos camins diferents, cap dels quals és el que volies. I el pitjor és que funciona perfectament durant mesos mentre cap directori no tingui espais. Els bugs de Bash no fallen aviat, esperen.

Mètode Quan detecta la fallada Cost
Executar i veure què passa Quan es donin les condicions (potser en producció, de matinada) Alt
Revisió per part d'un company Si s'hi fixa; les fallades de cometes es passen per alt molt fàcilment Mitjà
Proves amb Bats (08-06) Si el cas està cobert per una prova Mitjà
ShellCheck Abans de desar el fitxer Gairebé zero

  1. Instal·lar i executar ShellCheck

$ sudo apt install shellcheck          # Debian/Ubuntu; brew install a macOS
$ shellcheck bin/*.sh lib/comu.sh

També hi ha imatge de contenidor (koalaman/shellcheck) i binari estàtic, útils quan no pots instal·lar paquets al servidor. El que el fa automatitzable és el seu codi de sortida: 0 si no hi ha avisos, 1 si n'hi ha. Per això el pre-commit de 08-04 es podia limitar a cridar-lo i comprovar el resultat.

  1. Opcions que canvien el resultat

Opció Què fa Quan utilitzar-la
-s bash Força el dialecte Fitxers sense shebang o llibreries que es fan source
-s sh Analitza com a POSIX sh Comprovar portabilitat (08-07)
-S warning Només aquesta severitat o superior Començar en un script heretat amb 200 avisos
-x Segueix els source d'altres fitxers Gairebé sempre, amb el toolkit
-P dir Camí on buscar els fitxers de source Quan el source fa servir una variable
-f gcc / -f json Sortida per línies / estructurada Editors, CI, grep, informes
-e SC2086 Exclou aquest codi en tota l'execució Convencions de l'equip

L'opció -x mereix explicació, perquè sense ella el toolkit dóna avisos falsos. bin/informe-diari.sh fa source "$DIR_BASE/lib/comu.sh" (05-06); sense -x, ShellCheck no llegeix aquesta llibreria, no sap que veloz_log_info existeix ni que VELOZ_DIR_DADES està definida, i emet SC1091 i SC2154. Amb -x segueix el source i analitza el conjunt:

$ shellcheck -x -P lib bin/informe-diari.sh       # format tty: colors i context

In bin/informe-diari.sh line 42:
    if [ $us -gt $LLINDAR ]; then
         ^-^ SC2086 (info): Double quote to prevent globbing and word splitting.

$ shellcheck -f gcc bin/informe-diari.sh          # una linia per avis
bin/informe-diari.sh:42:10: note: Double quote to prevent globbing... [SC2086]

El format tty és per llegir; gcc és el que consumeixen editors, grep i els sistemes de CI.

  1. Anatomia d'un avís

In bin/copia.sh line 87:
    cd $DESTI
    ^-------^ SC2164 (warning): Use 'cd ... || exit' in case cd fails.
       ^----^ SC2086 (info): Double quote to prevent globbing and word splitting.

Quatre elements: fitxer i línia amb la porció exacta subratllada —una mateixa línia pot acumular diversos avisos—; el codi SCxxxx, identificador estable que és el que busques i el que silencies; la severitat (error, warning, info, style); i el missatge, curt i accionable.

I una cosa que convé interioritzar: cada codi té la seva pàgina de wiki a shellcheck.net/wiki/SC2086, amb el problema, exemples correctes i incorrectes i les excepcions legítimes. Quan no entenguis un avís, aquesta pàgina és la resposta; llegir-la és com s'aprèn Bash de debò després d'un curs.

  1. Els avisos que més veuràs

SC2086 — Double quote to prevent globbing and word splitting. El més freqüent amb diferència, i el problema de 03-06: rm $fitxer es trenca amb espais i expandeix comodins. Correcció: rm "$fitxer".

SC2046 — Quote this to prevent word splitting. El mateix problema sobre una substitució d'ordres: chmod 600 $(find . -name '*.conf'). La correcció no és posar cometes —això passaria tota la llista com un únic argument— sinó find ... -print0 | xargs -0 chmod 600 (05-01).

SC2006 — Use $(...) instead of legacy backticks. Les cometes invertides no s'imbriquen bé i escapen de manera estranya; la correcció és mecànica.

SC2164 — Use cd ... || exit. Si el cd falla, l'script continua al directori anterior i les ordres destructives següents actuen on no toca. Correcció: cd "$dir" || veloz_morir 1 "no puc entrar a $dir".

SC2181 — Check exit code directly with if cmd;, not indirectly with $?. És 03-04: if grep -q ERROR "$log"; then en lloc de comprovar $? a la línia següent. A més de ser més clar, evita l'error clàssic d'intercalar un echo de depuració que canvia $?.

SC2148 — add a shebang. Falta el #!/usr/bin/env bash de 03-01. En llibreries que només es fan source no escau un shebang, i allà la solució és -s bash o la directiva # shellcheck shell=bash.

SC2034 — variable appears unused. Gairebé sempre és una variable morta d'una refactorització; de vegades, un fals positiu quan la consumeix un altre fitxer mitjançant source o un local -n.

SC2155 — Declare and assign separately to avoid masking return values. Subtil i molt important (04-02):

local data=$(date -d "$entrada" +%F)     # el codi de sortida es el de "local": 0 SEMPRE
local data                               # CORRECTE: dos passos
data=$(date -d "$entrada" +%F) || return 1

Amb set -e això pesa molt: la primera versió no avorta encara que date falli.

SC2016 — Expressions don't expand in single quotes. Sol ser un fals positiu a awk '{print $3}', on les cometes simples són exactament el que vols; se silencia amb una directiva. SC2154 (variable referenciada sense assignar) i SC1090/SC1091 (source no seguible) són típics de llibreries i es resolen amb -x -P lib o amb # shellcheck source=lib/comu.sh just a sobre del source, cosa preferible perquè queda documentada al mateix fitxer.

  1. Taula resum de codis

Codi Problema Correcció Lliçó
SC2086 Expansió sense cometes "$var" 03-06
SC2046 $(...) sense cometes en arguments -print0 + xargs -0 05-01
SC2006 Cometes invertides $(...) 03-06
SC2164 cd sense control d'error cd "$d" || veloz_morir ... 05-03
SC2181 $? en un if posterior if cmd; then 03-04
SC2148 Falta shebang #!/usr/bin/env bash 03-01
SC2034 Variable sense fer servir Esborrar-la o justificar-la 03-02
SC2155 local x=$(cmd) emmascara el codi Declarar i assignar per separat 04-02
SC2016 $ dins de cometes simples Sol ser correcte: silenciar 03-06
SC2154 Variable no assignada al fitxer -x per seguir el source 05-06
SC1090/91 source no seguible -x -P o # shellcheck source= 05-06
SC2115 rm -rf "$d/" amb $d possiblement buida ${d:?} 03-06
SC2207 arr=( $(cmd) ) mapfile -t arr < <(cmd) 04-03

  1. Silenciar amb criteri

De vegades ShellCheck s'equivoca, o l'avís és correcte però tu vols aquest comportament. La directiva disable té tres abasts:

#!/usr/bin/env bash
# shellcheck disable=SC2034            <- abast de fitxer, abans de tot el codi

# El desglossament d'opcions S'HA de dividir en paraules: es una llista d'arguments.
# shellcheck disable=SC2086            <- abast de l'ordre seguent
curl $OPCIONS_CURL "$url"

# shellcheck source=lib/comu.sh        <- indica el cami del source
source "$DIR_BASE/lib/comu.sh"

I un .shellcheckrc a l'arrel per a les decisions d'equip, amb source-path=lib i external-sources=true. La norma que s'ha d'imposar: tota directiva disable va acompanyada d'un comentari que la justifiqui, a la línia immediatament anterior. Un disable sense justificar és pitjor que l'avís original: l'avís com a mínim era visible i algú el podia investigar, mentre que el disable mut l'enterra i fa creure que el codi està revisat. Si silencies alguna cosa perquè no l'entens, has amagat un bug, no l'has corregit; llegeix la wiki primer, perquè en la immensa majoria dels casos l'avís té raó.

  1. shfmt: el format deixa d'opinar-se

A 08-01 vam acordar un estil —quatre espais, then a la mateixa línia— i va quedar pendent automatitzar-lo. shfmt és el formatador de Bash, instal·lable des del gestor de paquets o amb go install mvdan.cc/sh/v3/cmd/shfmt@latest.

Opció Efecte
-i 4 Indentar amb 4 espais (-i 0 per a tabuladors)
-ci Indentar els case amb un nivell extra
-bn Operadors binaris (&&, ||) al principi de la línia següent
-d Mostra el diff i surt amb 1 si el fitxer no està formatat
-w / -l Reescriu el fitxer / llista els mal formatats
$ shfmt -i 4 -ci -d bin/vigilant.sh      # veure que canviaria
$ shfmt -i 4 -ci -w bin/ lib/            # aplicar-ho

Amb un .editorconfig a l'arrel, shfmt en pren la configuració i no cal repetir les opcions. El valor en un equip és doble: elimina la discussió sobre estil —ho decideix el programa— i fa que els diffs de Git continguin només canvis reals, sense soroll de reindentacions. Compte amb una cosa: formata tot el projecte un sol cop, en un commit propi titulat «Formatar el toolkit amb shfmt»; barrejar format i lògica al mateix commit fa la revisió impossible.

  1. Integració: editor, hook i CI

Tres nivells, del més immediat al més definitiu. A l'editor és on més temps s'estalvia, perquè l'avís apareix mentre escrius: VS Code té timonwong.shellcheck i mkhl.shfmt; Vim i Neovim l'integren amb ALE o el LSP bash-language-server. Al hook pre-commit (08-04) s'impedeix que entri codi amb avisos encara que el company no tingui l'extensió. I en integració contínua, l'única barrera que ningú no es pot saltar amb --no-verify:

# .github/workflows/shell.yml
name: shell
on: [push, pull_request]
jobs:
  analisi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: ShellCheck
        run: |
          sudo apt-get update && sudo apt-get install -y shellcheck
          shellcheck -x -P lib -S warning bin/*.sh lib/*.sh
      - uses: mfinelli/setup-shfmt@v3
      - run: shfmt -i 4 -ci -d bin/ lib/

Tots dos passos aprofiten el codi de sortida: ShellCheck retorna 1 si hi ha avisos i shfmt -d retorna 1 si alguna cosa no està formatada, així que la feina falla sola. -S warning és una decisió conscient: exigim warning i error, i deixem info i style com a recomanació per no bloquejar ningú per un detall de forma.

  1. Altres eines

  • checkbashisms (paquet devscripts): detecta construccions de Bash en scripts que declaren #!/bin/sh. És l'eina central de la portabilitat i reapareix a 08-07.
  • shellharden: reescriu el codi afegint-hi les cometes que falten; potent i perillós a parts iguals. bashate: estil, més opinat sobre format que sobre correcció.
  • bash -n: no és anàlisi estàtica, només comprovació de sintaxi (05-03), però és gratis i universal. Al hook va abans de ShellCheck, perquè un fitxer amb un fi de més ni tan sols es pot analitzar.

  1. Aplicació: passar ShellCheck a tot el toolkit

$ shellcheck -x -P lib -f gcc bin/*.sh lib/comu.sh | tee /tmp/avisos.txt | wc -l
47
$ grep -oE 'SC[0-9]+' /tmp/avisos.txt | sort | uniq -c | sort -rn
     19 SC2086
      7 SC2046
      5 SC2155
      4 SC2181
      3 SC2164
      3 SC2034
      2 SC2006
      2 SC2115
      1 SC2207
      1 SC2016

Quaranta-set avisos en codi que ja havíem revisat dues vegades. Aquesta xifra és la lliçó sencera: la revisió humana no veu les cometes que falten.

Grup Avisos Acció
Corregir ja (risc real) SC2115 (2), SC2164 (3), SC2155 (5) ${d:?}, cd || morir, separar declaració i assignació
Corregir en bloc (mecànic) SC2086 (19), SC2046 (7), SC2006 (2) Cometes, -print0/xargs -0, $(...)
Netejar SC2034 (3), SC2181 (4), SC2207 (1) Esborrar variables mortes, if cmd;, mapfile
Silenciar justificat SC2016 (1) $3 dins del programa d'awk: directiva amb comentari

Els dos SC2115 eren la troballa greu: un rm -rf "$VELOZ_DIR_COPIA/$data" a copia.sh on, si $data quedava buida per un date fallit, l'ordre es convertia en rm -rf "/srv/veloz/copies/". La correcció és de tres caràcters i evita un desastre:

rm -rf -- "${VELOZ_DIR_COPIA:?}/${data:?}"

Amb ${var:?} (03-06) l'script avorta amb un missatge clar si la variable està buida, en lloc d'esborrar l'arrel de la còpia de seguretat. Després d'aplicar-ho tot i formatar amb shfmt -i 4 -ci -w, shellcheck -x -P lib bin/*.sh lib/comu.sh passa sense avisos.

Errors Habituals i Consells

  • Executar ShellCheck sense -x en un projecte amb llibreries. Genera desenes de SC1091 i SC2154 falsos que sepulten els avisos reals.
  • Silenciar en massa per «deixar-ho net». Un .shellcheckrc amb deu disable no és codi revisat, és codi amb els avisos apagats.
  • Creure que sense avisos l'script és correcte. ShellCheck no sap si la teva lògica de negoci està bé; per a això hi ha les proves (08-06).
  • Ignorar els info per sistema. SC2086 és de severitat info i causa la meitat dels bugs de Bash en producció.
  • Consell: comença per -S error, després warning, després tot. En un script heretat amb 200 avisos, anar per severitats fa la feina abastable.
  • Consell: shellcheck.net permet enganxar un fragment i veure els avisos sense instal·lar res; mai amb codi que contingui dades internes.

Exercicis

Exercici 1. Executa mentalment ShellCheck sobre aquest fragment: enumera els avisos amb el seu codi i reescriu-lo corregit.

cd $DIR_COPIA
fitxers=`find . -name "*.tar.gz" -mtime +30`
for f in $fitxers; do
    rm -f $f
done
grep -q ERROR $LOG
if [ $? -eq 0 ]; then
    resum=$(wc -l < $LOG)
    echo "hi ha errors: $resum"
fi

Exercici 2. ShellCheck avisa de SC2086 a veloz_log_info $missatge dins de lib/comu.sh. Explica per què és una fallada real i no un fals positiu, amb un exemple concret de Veloz Envíos.

Exercici 3. Escriu el pas de CI que exigeixi alhora ShellCheck sense avisos de severitat warning o superior i format shfmt correcte, fent fallar la feina si qualsevol de les dues coses no es compleix.

Solucions

Solució 1. Avisos: SC2164 (cd sense control), SC2086 (a $DIR_COPIA, $f i $LOG dues vegades), SC2006 (cometes invertides), SC2044/SC2207 (iterar sobre la sortida de find) i SC2181 ($? a l'if).

cd "$DIR_COPIA" || veloz_morir 1 "no puc entrar a $DIR_COPIA"

find . -name '*.tar.gz' -mtime +30 -print0 | xargs -0 -r rm -f --

if grep -q ERROR "$LOG"; then
    resum=$(wc -l < "$LOG")
    printf 'hi ha errors: %s\n' "$resum"
fi

El bucle desapareix: find -print0 amb xargs -0 (05-01) resol alhora la divisió en paraules i els noms amb espais, i -r evita executar rm si no hi ha fitxers.

Solució 2. És una fallada real perquè el missatge es construeix amb dades externes. Amb missatge="incidencia a Palma de Mallorca", la funció rep cinc arguments en lloc d'un, i si internament fa servir $1 el registre només anotarà «incidencia». Pitjor: si el missatge prové d'una línia d'acces.log (08-03) i conté un *, el globbing el substitueix per la llista de fitxers del directori actual i el registre queda inservible. La correcció és veloz_log_info "$missatge", i dins de la funció fer servir "$*" o "$@" de manera conscient (03-05).

Solució 3.

      - name: Analisi estatica i format
        run: |
          set -euo pipefail
          sudo apt-get update -qq
          sudo apt-get install -y shellcheck
          shellcheck -x -P lib -S warning bin/*.sh lib/*.sh
          shfmt -i 4 -ci -d bin/ lib/

set -euo pipefail (05-03) atura el pas a la primera ordre que falli; ShellCheck retorna 1 si troba avisos de warning o superior i shfmt -d retorna 1 si algun fitxer no està formatat, així que totes dues condicions es tradueixen directament en el resultat de la feina sense comprovar res a mà.

Conclusió

ShellCheck és l'eina amb millor relació entre esforç i benefici de tot el mòdul: analitza l'script sense executar-lo, triga menys d'un segon i troba exactament les fallades que Bash no assenyalarà mai, perquè l'intèrpret accepta sense protestar un rm -rf $dir/* que funcionarà bé fins al dia en què aparegui un espai. S'executa sobre els fitxers i retorna 0 o 1, cosa que el fa automatitzable; -x amb -P lib és imprescindible al toolkit perquè segueixi els source de la llibreria i no ompli la sortida de SC1091 i SC2154 falsos, -S warning acota la severitat en projectes heretats i -f gcc produeix la sortida que consumeixen editors i CI. Cada avís porta fitxer, línia, fragment subratllat, severitat i un codi SCxxxx amb la seva pàgina de wiki, que és on de debò s'aprèn. Els que més veuràs ja els coneixes per la seva lliçó: SC2086 i SC2046 són les cometes de 03-06, SC2164 és el cd sense ||, SC2181 és el $? que havia de ser if cmd;, SC2155 és el local x=$(cmd) que emmascara el codi de sortida de 04-02, SC2148 el shebang de 03-01 i SC1090/SC1091 el source de 05-06. Silenciar és legítim quan l'avís és un fals positiu, però tota directiva disable porta un comentari que la justifica, perquè un disable mut enterra un bug i aparenta codi revisat. shfmt -i 4 -ci -w tanca el que 08-01 va prometre convertint l'estil en una ordre en lloc d'una discussió, amb l'avantatge que els diffs de Git deixen de tenir soroll —sempre que el formatatge inicial vagi al seu propi commit—. La integració es fa en tres nivells: l'editor, on l'avís arriba abans de desar; el hook pre-commit de 08-04; i CI, l'única barrera que no es pot saltar. L'aplicació al toolkit ho demostra: 47 avisos en codi ja revisat dues vegades per humans, entre ells un rm -rf que hauria esborrat l'arrel del directori de còpies si un date fallava.

Ara el toolkit està net d'errors de forma. Però ShellCheck no sap si veloz_percentatge 0 0 retorna alguna cosa sensata, ni si copia.sh conserva de debò els trenta dies de retenció, ni si la refactorització de rendiment de 08-02 va canviar el contingut de l'informe. Això només ho pot dir una prova que executi el codi i compari el resultat amb l'esperat. La lliçó 08-06 introdueix Bats: què val la pena provar en un script d'operacions, com aïllar la prova de l'entorn amb directoris temporals i dades fictícies, com substituir una ordre externa per un doble per provar veloz_api_get sense API i copia.sh sense tocar el disc, i com escriure tests/comu.bats amb proves reals de les funcions de la llibreria.

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