El toolkit funciona sol, però la pregunta que tancava el Mòdul 7 continua oberta: és mantenible? D'aquí a sis mesos, a les tres de la matinada, amb el vigilant avisant que la còpia de seguretat ha fallat, algú —probablement tu— obrirà copia.sh i haurà d'entendre en dos minuts què fa la línia 47. Si aquesta línia diu d=${f##*/}; e=${d%%.*}, no se'n sortirà. En Bash la llegibilitat no és estètica: és la diferència entre arreglar una incidència i provocar-ne una altra. Aquesta lliçó converteix aquesta intuïció en una guia d'estil concreta i en una refactorització real del codi que ja tens escrit.
Contingut
- Per què en Bash la llegibilitat és supervivència
- Què vol dir «llegible» de manera mesurable
- Noms i espais de noms
- Format: indentació, longitud de línia i continuació amb
\ - Claus, claudàtors dobles i uniformitat
- L'estructura canònica d'un script
- Comentaris: el perquè, no el què
- Funcions curtes i clàusules de guarda
- Taula d'«això no / això sí» i missatges de sortida
- La guia d'estil de l'equip i una refactorització real
- Per què en Bash la llegibilitat és supervivència
Tots els llenguatges premien el codi clar, però Bash té tres agreujants que ho converteixen en obligació:
- La sintaxi és densa per disseny.
${enviament##*/},2>&1,"${arr[@]}",<( ). Has après totes aquestes construccions en aquest curs, però cap no s'explica sola. - No hi ha compilador ni tipus que et protegeixin. Un error de nom en Python peta en executar-se; en Bash,
$carpeta_destimal escrit s'expandeix a la cadena buida irm -rf "$carpeta_desti"/*esborra/*. L'únic mecanisme de defensa és que un humà llegeixi el codi i vegi la fallada. - Els scripts d'operacions es llegeixen sota pressió. Ningú no obre
vigilant.shun dimarts tranquil per curiositat: s'obre quan alguna cosa està trencada, amb pressa i sense marge d'error.
A això s'hi suma una realitat incòmoda: els scripts viuen molt més del que estava previst. informe-diari.sh va néixer com a deu línies per sortir del pas i avui publica l'informe que llegeix la direcció cada matí. El codi temporal és el que dura més.
- Què vol dir «llegible» de manera mesurable
«Llegible» sona subjectiu, però aterra en criteris comprovables en trenta segons:
| Criteri | Com es mesura | Llindar pràctic |
|---|---|---|
| Longitud de funció | Línies del bloc | Cap en una pantalla: ≤ 40 |
| Profunditat d'imbricació | Nivells d'if/for |
≤ 3 |
| Longitud de línia | Columnes | ≤ 100, tallant amb \ |
| Noms d'una lletra | Cerca visual | Només i en bucles curts |
| Comentaris que expliquen el què | Lectura | Cap: sobren |
| Capçalera de documentació | Presència | Obligatòria en scripts i funcions públiques |
La prova definitiva no és una mètrica sinó una pregunta: pot una altra persona de l'equip modificar aquesta funció sense preguntar-te res? Si la resposta és no, el codi no és llegible per molt que a tu t'ho sembli.
- Noms i espais de noms
El nom és el comentari que mai no queda obsolet:
ciutat_desti="Valencia" # variables: minuscules, descriptives
total_enviaments_lliurats=0
readonly VELOZ_DIR_DADES="/srv/veloz/dades" # constants: MAJUSCULES, readonly (03-02)
veloz_calcula_percentatge() { :; } # funcions: verb + prefix (05-06)Tres regles. Les variables van en minúscules, perquè les majúscules estan reservades per convenció a l'entorn i a les constants; si dius PATH a la teva variable de camins, trenques l'script sencer. Les funcions comencen per verb (veloz_calcula_…, veloz_llegeix_…, veloz_requereix), perquè una funció fa alguna cosa; si no trobes el verb, probablement en fa dues. I el prefix veloz_ de 05-06 no és decoració: en fer source lib/comu.sh totes aquestes funcions entren a l'espai global del shell, i sense prefix una log teva xocaria amb qualsevol altra log. Sobre la longitud: i està bé a for i in {1..3}; f no està bé si s'utilitza trenta línies més avall. El nom ha de créixer amb la distància entre la seva definició i el seu darrer ús.
- Format: indentació, longitud de línia i continuació amb
\
\Indenta amb 4 espais (el que utilitza aquest curs) o amb 2, però tria'n un i no els barregis mai. Els tabuladors donen problemes dins dels here-documents i es veuen diferents a cada editor. Això no es discuteix a cada revisió: es fixa un cop i ho aplica una eina (shfmt, a 08-05).
tar --create --gzip --file "$fitxer_desti" \
--exclude='*.tmp' --directory "$VELOZ_DIR_DADES" .
grep -F 'ERROR' /var/log/veloz/app.log \
| awk '{print $4}' \
| sort | uniq -c | sort -rnDos detalls importen. El primer és un parany clàssic: la barra invertida ha de ser l'últim caràcter de la línia. Si hi deixes un espai al darrere, Bash escapa aquest espai en comptes del salt de línia, l'ordre es parteix en dues i apareix un error incomprensible del tipus --file: no s'ha trobat l'ordre. És invisible a l'ull; ShellCheck ho detecta (SC1101). El segon: en una canonada pots prescindir de la barra si talles després de la |, perquè una línia que acaba en | ja continua sola. Posar la | al principi de cada línia, com a l'exemple, és més llegible perquè alinea verticalment les etapes, però exigeix la barra invertida.
- Claus, claudàtors dobles i uniformitat
echo "$ciutat" # correcte i suficient
echo "${ciutat}_informe" # AQUI les claus son obligatories
echo "${enviaments[2]}" "${cami##*/}" # i en arrays i expansionsHi ha dues escoles: claus sempre (uniformitat absoluta, més soroll visual) o només quan aclareixen o són obligatòries. Aquest curs segueix la segona; el que no és opcional és ser uniforme dins d'un mateix fitxer. Amb les condicions la decisió ve de 03-03: fes servir [[ ]] sempre. No només perquè evita la divisió en paraules i admet =~ i &&, sinó per uniformitat: si el 90 % del toolkit fa servir [[ ]], el [ ] restant fa que el lector s'aturi a preguntar-se per què és diferent. Sorprendre el lector és un defecte.
- L'estructura canònica d'un script
Tots els scripts del toolkit segueixen el mateix ordre. Quan obres flota.sh saps que la configuració és a dalt i main a baix, i trobes el que busques sense llegir el fitxer sencer.
#!/usr/bin/env bash
#
# estat-servei.sh - Comprova la salut de veloz-api i avisa si esta caiguda
#
# Us: estat-servei.sh [-v] [-t SEGONS] [HOST...]
#
# Opcions: -v Sortida detallada
# -t SEGONS Espera maxima per host (per defecte 5)
#
# Codis de sortida: 0 tots responen | 1 error d'us | 69 servei no disponible
#
# Autor: Equip d'Operacions - Veloz Envios Des de: 2026-03-11
set -euo pipefail
# --- Constants --------------------------------------------------------------
readonly VERSION="1.4.0"
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly VELOZ_ESPERA_PER_DEFECTE=5
# --- Llibreries -------------------------------------------------------------
source "${SCRIPT_DIR}/../lib/comu.sh"
# --- Funcions ---------------------------------------------------------------
us() { sed -n '3,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
comprova_host() { local host="$1"; ...; }
main() { local espera="$VELOZ_ESPERA_PER_DEFECTE"; ...; }
main "$@" # --- Punt d'entrada: SEMPRE la ultima linia del fitxerDe dalt a baix: el shebang portable (03-01); la capçalera de documentació amb ús, opcions, codis de sortida i autoria —el primer que llegeix qui obre el fitxer, i que us() reaprofita amb sed (06-02) per no duplicar el text—; set -euo pipefail amb les cauteles de 05-03; les constants agrupades i readonly; el source de les llibreries fent servir SCRIPT_DIR per no dependre del directori de treball (05-06); les funcions; i main "$@" com a última línia del fitxer, que garanteix que res no s'executi fins que l'script estigui completament llegit (04-02).
- Comentaris: el perquè, no el què
# MALAMENT: repeteix el que el codi ja diu
# Incrementa el comptador en 1
(( comptador++ ))
# CORRECTE: explica una decisio que el codi no pot explicar
# Ho reintentem 3 cops: l'API triga fins a 8 s a arrencar despres del desplegament
# nocturn i el primer curl sempre falla (incidencia INC-2481).
veloz_espera_servei 3El codi diu què fa; només un comentari pot dir per què. Els valuosos documenten una decisió no òbvia, una limitació d'una eina externa, una referència a un incident o una advertència («no canviïs l'ordre: flock s'ha d'adquirir abans de crear el temporal»). I hi ha una cosa pitjor que no comentar: el comentari que menteix. Un que diu «avisa si el disc passa del 80 %» al costat d'un codi que compara amb 90 és un parany actiu, perquè el lector s'hi refia i no ho comprova. Quan canviïs el codi, canvia el comentari en la mateixa edició o esborra'l. Per a les funcions públiques de lib/comu.sh, capçalera amb format fix:
# veloz_percentatge <part> <total>
# Calcula el percentatge de <part> sobre <total> amb un decimal.
# Escriu el resultat a la sortida estandard, sense el simbol %.
# Codis: 0 correcte | 1 arguments no numerics | 2 total igual a zeroSignatura, què fa, què escriu a stdout, quins codis retorna. Quatre línies que estalvien obrir la implementació.
- Funcions curtes i clàusules de guarda
Una funció ha de fer una cosa i el seu nom ha de dir quina. El símptoma que en fa dues és que a la seva descripció hi apareix una «i»: «llegeix el CSV i genera l'informe». El límit pràctic: si no cap en una pantalla, extreu. Extreure és mecànic: (1) localitza el bloc coherent, les seves entrades —seran arguments— i la seva sortida —anirà a stdout—; (2) mou-lo a una funció amb nom de verb, declarant local tot el que és intern (04-02); (3) executa l'script i confirma que continua donant el mateix. El tercer pas se'l salta tothom i és l'únic que garanteix que la refactorització no ha canviat res; a 08-06 l'automatitzarem amb Bats.
L'altre enemic és la imbricació: a partir del tercer nivell has de pujar amb la mirada per saber en quina branca ets. Reprenent les clàusules de guarda de 03-04, aquest bloc de quatre nivells amb la feina útil al fons…
if [[ -f "$fitxer" ]]; then
if [[ -r "$fitxer" ]]; then
if [[ -s "$fitxer" ]]; then
awk -F, -v c="$objectiu" '$3 == c { print $1 }' "$fitxer"
else veloz_log_error "fitxer buit"; return 1; fi
else veloz_log_error "sense permis"; return 1; fi
else veloz_log_error "no existeix"; return 1; fi…es converteix en això, amb un sol nivell d'indentació i errors que diuen quin és el fitxer:
[[ -f "$fitxer" ]] || { veloz_log_error "no existeix: $fitxer"; return 1; }
[[ -r "$fitxer" ]] || { veloz_log_error "sense permis: $fitxer"; return 1; }
[[ -s "$fitxer" ]] || { veloz_log_error "fitxer buit: $fitxer"; return 1; }
awk -F, -v c="$objectiu" '$3 == c { print $1 }' "$fitxer"El patró és: primer tot el que desqualifica l'entrada, després la feina.
- Taula d'«això no / això sí» i missatges de sortida
Reescriptures que apareixen un cop i un altre a les revisions de codi de Bash:
| Això no | Això sí | Per què |
|---|---|---|
ordre; if [ $? -eq 0 ] |
if ordre; then |
$? es perd amb qualsevol ordre intermèdia (03-04) |
cat fitxer | grep patro |
grep patro fitxer |
Un procés menys i menys soroll (02-04) |
for f in $(ls *.log) |
for f in *.log |
ls es trenca amb espais; el glob mai (02-05) |
echo -e "a\tb" |
printf 'a\tb\n' |
echo -e no és portable (03-06) |
grep x f | wc -l |
grep -c x f |
grep ja sap comptar |
if [ x$v = xabc ] |
if [[ $v == abc ]] |
El truc de la x sobra amb [[ ]] |
rm -rf $dir/* |
rm -rf "${dir:?}"/* |
Si dir és buida, esborres l'arrel (03-06) |
T=$(date +%s) |
inici_execucio=$(date +%s) |
Noms, apartat 3 |
Un script d'operacions té dos públics i cal servir-los tots dos: l'humà vol llegir i la màquina —el vigilant, un grep, el journal— vol analitzar. La solució és la de 07-04: una línia amb estructura fixa i camps separats, que continua sent llegible (2026-08-03T09:15:22+02:00 [INFO] informe-diari: enviaments processats: 1284). Tres regles convé formalitzar-les: els diagnòstics van a stderr i les dades a stdout (02-04), perquè dades=$(script) no capturi avisos; els errors diuen què ha fallat, amb quin valor i què cal fer («no es pot llegir /srv/veloz/dades/enviaments.csv: comprova els permisos», no «error»); i el codi de sortida acompanya sempre el missatge (05-03), perquè qui crida l'script no llegeix text.
- La guia d'estil de l'equip i una refactorització real
Tot l'anterior cap en un ESTIL.md dins del repositori, al costat del codi —el posarem sota Git a 08-04—. Una pàgina amb les decisions preses i, sobretot, amb les excepcions justificades que l'equip hagi acordat. És un document viu: quan una discussió es repeteix per segona vegada en una revisió, la decisió s'escriu allà i deixa de discutir-se. Com a referència externa, la Google Shell Style Guide és el document més utilitzat del sector i una bona base, encara que convé adoptar-la amb criteri: fixa coses útils (local, main al final, longitud màxima) i d'altres discutibles (2 espais, 80 columnes). L'important no és quina guia tries, sinó que n'hi hagi una i que l'apliqui una eina en lloc d'una persona: d'això se n'encarrega shfmt, a 08-05.
Aquesta funció d'informe-diari.sh fa des del Mòdul 4 que no es toca:
# ABANS
gen() {
T=0; E=0
while IFS=, read -r a b c d e f; do
if [ "$a" != "id_enviament" ]; then
T=`expr $T + 1`
if [ $e = "lliurat" ]; then E=`expr $E + 1`; fi
fi
done < $1
echo "Total: $T Lliurats: $E `echo "scale=1;$E*100/$T" | bc`%"
} # 11 linies, 6 defectes d'estil i 2 processos per cada enviament del fitxerI així queda després d'aplicar la guia:
# DESPRES
# resumeix_lliuraments <fitxer_csv>
# Resumeix totals d'un CSV d'enviaments (amb capcalera).
# Escriu a stdout: "total lliurats percentatge", separats per espais.
# Codis: 0 correcte | 1 el fitxer no es pot llegir | 2 no hi ha files de dades
resumeix_lliuraments() {
local fitxer_csv="${1:?falta el fitxer CSV}"
[[ -r "$fitxer_csv" ]] || { veloz_log_error "no es pot llegir: $fitxer_csv"; return 1; }
local total lliurats
read -r total lliurats < <(
awk -F, 'NR > 1 { total++; if ($5 == "lliurat") lliurats++ }
END { print total + 0, lliurats + 0 }' "$fitxer_csv"
)
(( total > 0 )) || { veloz_log_error "sense files de dades: $fitxer_csv"; return 2; }
printf '%d %d %s\n' "$total" "$lliurats" "$(veloz_percentatge "$lliurats" "$total")"
}Decisió per decisió: el nom passa de gen a resumeix_lliuraments, verb primer i sense ambigüitat. Les globals T/E passen a local amb noms complets, cosa que a més evita trepitjar variables de l'script. La validació puja al principi com a clàusula de guarda, amb ${1:?} (03-06) i un missatge que inclou el camí. El bucle while read de sis camps —cinc sense fer servir— se substitueix per un sol awk (06-01): més curt, més ràpid (ho mesurarem a 08-02) i sense trencar-se amb camps buits. L'obsolet `expr` desapareix i les cometes invertides deixen pas a $( ), que s'imbrica i es llegeix. El percentatge es delega en veloz_percentatge, que ja existeix i està provada, en comptes de reimplementar-lo amb bc. La sortida deixa de ser una frase decorada i passa a tres camps que qui crida formata com vulgui i read trosseja sense esforç. I els codis de sortida distingeixen «no puc llegir» de «no hi ha dades», que operativament són incidències molt diferents.
Errors Habituals i Consells
- Espai darrere de la barra invertida. L'error més frustrant de la lliçó, perquè és invisible. Configura l'editor perquè marqui els espais finals i deixa que ShellCheck (SC1101) els caci.
- Refactoritzar sense poder comprovar. Reescriure a ull i desplegar és tan arriscat com no refactoritzar. Desa la sortida abans (
./informe-diari.sh > /tmp/abans.txt), refactoritza i compara ambdiff. - Confondre «curt» amb «llegible».
[[ -f $f ]]&&. $f||exit 1és curtíssim i illegible. L'objectiu és la claredat, no la brevetat: els caràcters són gratis. - Consell: llegeix el teu script en veu alta. Si en arribar a una línia t'has d'aturar a desxifrar-la, allà hi falta un nom millor o un comentari que expliqui el perquè.
- Consell: el millor moment per refactoritzar és quan vas a tocar el codi. No obris una tasca de «netejar el toolkit»; neteja la funció que ja estaves modificant.
Exercicis
Exercici 1. Reescriu aquesta funció aplicant la guia: nom, local, clàusules de guarda, [[ ]], cometes, printf i capçalera de documentació.
chk() {
if [ -d $1 ]; then
if [ -w $1 ]; then echo -e "OK\t$1"
else echo "no escrivible"; return 1; fi
else echo "no existeix"; return 1; fi
}Exercici 2. Localitza els cinc defectes d'estil d'aquest fragment i corregeix-los, explicant-los un per un.
for f in `ls /var/log/veloz/*.log`; do
cat $f | grep ERROR | wc -l > /tmp/c
if [ $? -eq 0 ]; then echo "$f: `cat /tmp/c`"; fi
doneSolucions
Solució 1.
# comprova_directori_escrivible <cami>
# Verifica que <cami> es un directori en que l'usuari actual pot escriure.
# Escriu a stdout una linia "OK <cami>" si tot es correcte.
# Codis: 0 correcte | 1 no existeix o no es directori | 2 existeix pero no es escrivible
comprova_directori_escrivible() {
local cami="${1:?falta el cami}"
[[ -d "$cami" ]] || { veloz_log_error "no es un directori: $cami"; return 1; }
[[ -w "$cami" ]] || { veloz_log_error "sense permis d'escriptura: $cami"; return 2; }
printf 'OK\t%s\n' "$cami"
}Nom descriptiu amb verb; local amb ${1:?} per fallar aviat si falta l'argument; guardes en lloc d'imbricació; [[ ]] amb la variable entre cometes, perquè sense cometes un camí amb espais trencaria el test; printf en comptes d'echo -e, que no és portable; els errors a stderr via veloz_log_error per no contaminar la sortida útil; dos codis diferents perquè «no existeix» i «no puc escriure» s'arreglen de manera diferent; i capçalera fixa.
Solució 2.
for fitxer in /var/log/veloz/*.log; do
[[ -f "$fitxer" ]] || continue
errors=$(grep -c 'ERROR' "$fitxer" || true)
printf '%s: %d\n' "$fitxer" "$errors"
doneEls cinc defectes: (1) for f in $(ls ...) recorre la sortida d'ls, que es parteix pels espais; el glob directe és correcte i, a més, no crea processos. (2) cat | grep | wc -l són tres processos on n'hi ha prou amb grep -c. (3) El temporal /tmp/c és innecessari, no és únic —dues execucions simultànies es trepitgen— i és insegur (ho veurem a 08-03); una variable ja fa el fet. (4) if [ $? -eq 0 ] comprova el codi de l'última ordre de la canonada, que era wc, no grep: sempre dóna 0 i la condició no comprova res. (5) Les variables sense cometes. El || true és necessari perquè grep -c retorna 1 quan no troba res i amb set -e això avortaria el bucle (05-03); la guarda [[ -f ]] cobreix el cas que el glob no encaixi amb cap fitxer.
Conclusió
En Bash la llegibilitat és defensa activa: sense compilador ni tipus, l'únic filtre entre una $carpeta mal escrita i un rm -rf / és que algú llegeixi el codi i l'entengui. «Llegible» es mesura: funcions que caben en una pantalla, tres nivells d'imbricació com a màxim, línies de menys de cent columnes i cap comentari que repeteixi el que el codi ja diu. Les convencions del toolkit són minúscules per a les variables, MAJÚSCULES readonly per a les constants, verb al principi de cada funció i prefix veloz_ per no xocar en fer source. El format es fixa un cop —4 espais, [[ ]] sempre, claus quan aclareixen, continuació amb \ sense espai al darrere— i l'aplica una eina, no una discussió. Tots els scripts comparteixen la mateixa estructura: shebang, capçalera de documentació amb ús i codis de sortida, set -euo pipefail, constants, source de llibreries, funcions i main "$@" a l'última línia. Els comentaris expliquen el perquè —la decisió, la limitació, l'incident— i s'actualitzen amb el codi, perquè un comentari que menteix és pitjor que cap. Les funcions fan una sola cosa, i quan apareix una «i» a la seva descripció s'extreuen; la imbricació profunda es dissol en clàusules de guarda que treuen els errors al principi. La taula de reescriptures resumeix la resta: if ordre en comptes de $?, glob en comptes d'ls, printf en comptes d'echo -e, grep -c en comptes de grep | wc -l. I la refactorització de resumeix_lliuraments demostra que llegibilitat i qualitat arriben juntes: la versió clara és també més curta, més ràpida i amb millors codis d'error.
Just aquí apareix un dubte raonable: hem substituït un bucle while read per un awk afirmant que és «més ràpid». Quant més? Importa amb 1.200 enviaments? I amb 500.000? Això no s'endevina, es mesura. La lliçó 08-02 ensenya a mesurar de debò amb time, SECONDS i date +%s%N, a identificar el cost que domina en Bash —crear processos— i a optimitzar només allò que de veritat costa, començant per perfilar informe-diari.sh sobre mig milió de línies.
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
