Arribem a la fita del mòdul, i a una promesa que arrosseguem des de 04-02. informe-diari.sh és avui un programa robust: valida el seu entorn, es protegeix amb flock, avorta amb diagnòstic, valida text amb regex i dirigeix la seva sortida amb precisió. Però és també un únic fitxer de diversos centenars de línies on conviuen log_info, morir, validar_entorn i percentatge —funcions que no tenen res a veure amb informes i que qualsevol altre script del toolkit necessitaria—. La temptació, quan escriguis arxivar-historic.sh la setmana vinent, serà copiar i enganxar. I aquí comença el deteriorament: dues còpies que divergeixen, una fallada corregida en una i no en l'altra, tres convenis de registre diferents. En aquesta lliçó converteixes aquest fitxer en un projecte amb arquitectura.

Contingut

  1. source enfront d'executar
  2. Què és una llibreria en Bash
  3. Localitzar la llibreria sense que les rutes et traeixin
  4. Guardes d'inclusió
  5. Noms: què és públic i què és privat
  6. Fitxers de configuració
  7. Ordre de precedència
  8. L'estructura final del toolkit
  9. lib/comu.sh
  10. informe-diari.sh com a orquestrador
  11. Provar la llibreria a mà

  1. source enfront d'executar

A 03-01 vas veure les formes d'executar un script. La que ara importa és la que no crea un procés: enfront de ./script.sh o bash script.sh, que llancen un procés nou del qual no torna res, source script.sh (o la seva forma POSIX . script.sh) executa el fitxer en aquest shell.

Executar source
Procés Un de nou (fork + exec) L'actual
Variables i funcions que defineix Moren amb el fill Queden disponibles
cd que faci No t'afecta Et canvia de directori
Necessita permís +x i shebang No
exit a dins Acaba l'script Acaba el teu shell

Les dues últimes files expliquen per què una llibreria no és un script normal. Com que es carrega amb source, no necessita ni shebang executable ni permís d'execució. I sobretot: un exit en una llibreria mata qui la carrega, inclosa la teva sessió interactiva si l'estaves provant; dins d'una llibreria es fa servir return. Això no és nou: és el mateix mecanisme pel qual ~/.bashrc defineix funcions que el teu shell coneix (01-02). Una llibreria és un .bashrc per als teus scripts.

  1. Què és una llibreria en Bash

Una llibreria és un fitxer que conté només definicions: funcions, constants i, com a molt, valors per defecte. Res que actuï per si mateix.

# lib/comu.sh — CORRECTE: només defineix
log_info() { printf '%s [INFO] %s\n' "$(date '+%F %T')" "$*" >&2; }

# lib/dolent.sh — INCORRECTE: tot això ACTUA en carregar-se
log_info "carregant llibreria"        # embruta la sortida de qui la faci servir
set -euo pipefail                     # IMPOSA la seva política a qui la carrega
cd /srv/veloz                         # canvia el directori de qui la carrega

La regla és que carregar la llibreria ha de ser silenciós i sense efectes. El set -euo pipefail dins d'una llibreria és especialment traïdor: en fer source s'aplica al shell que la carrega, així que pot canviar el comportament d'errors d'un script que no s'ho esperava. La política estricta la decideix l'executable, no la llibreria. Convencions: extensió .sh (o .bash), sense shebang executable —opcionalment un #!/usr/bin/env bash com a pista per a editors i per a shellcheck, però sense permís +x—, i una capçalera de comentari que digui què ofereix.

  1. Localitzar la llibreria sense que les rutes et traeixin

Aquí hi ha el problema tècnic de debò. Com troba bin/informe-diari.sh la lib/comu.sh?

source lib/comu.sh               # MALAMENT: relatiu al directori ACTUAL, no a l'script
source ~/veloz-ops/lib/comu.sh   # Funciona, però només per a tu i al teu $HOME

Les rutes relatives es resolen des del directori de treball actual, que és on sigui qui llança l'script, no on viu l'script. Com que ~/veloz-ops/bin és al PATH (el vam posar allà a 01-02), el normal és invocar-lo des de qualsevol lloc, i el source falla. L'idioma canònic és aquest:

BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; readonly BASE_DIR
source "$BASE_DIR/lib/comu.sh"

Desmuntat de dins cap a fora:

  • ${BASH_SOURCE[0]} és la ruta del fitxer en execució. Es fa servir en comptes de $0 per dos motius: $0 val bash quan l'script s'invoca com a bash script.sh, i dins d'un fitxer carregat amb source, $0 segueix sent el de l'script principal mentre que BASH_SOURCE[0] és la llibreria. Per a una llibreria que necessita saber on és, $0 dona sempre la resposta equivocada.
  • dirname dona el directori que la conté (04-04) i /.. puja a l'arrel del projecte; cd ... && pwd converteix aquesta ruta, que pot ser relativa (./bin/..), en absoluta i normalitzada; i tot va entre cometes, per si la ruta conté un espai.

Amb això, l'script funciona invocat com a ./bin/informe-diari.sh, com a ~/veloz-ops/bin/informe-diari.sh, des del PATH o des de cron amb qualsevol directori de treball. Si a més vols seguir enllaços simbòlics fins al fitxer real, readlink -f (05-01) sobre ${BASH_SOURCE[0]} abans del dirname ho resol.

  1. Guardes d'inclusió

Si informe-diari.sh carrega comu.sh i també informe.sh, que al seu torn carrega comu.sh, el fitxer es processa dues vegades. Redefinir funcions és inofensiu, però un readonly duplicat provoca un error fatal sota mode estricte (VELOZ_VERSION: variable de només lectura). La solució és la mateixa idea que els include guards de C:

[[ -n "${_COMU_SH:-}" ]] && return 0         # ja carregada: sortir sense fer res
readonly _COMU_SH=1

El return 0 a nivell superior d'un fitxer carregat amb source és legal i significa «acaba de llegir aquest fitxer». El :- és obligatori perquè l'script que la carrega té set -u (05-03) i la variable encara no existeix la primera vegada. El guionet baix inicial marca la variable com a interna.

  1. Noms: què és públic i què és privat

Bash no té espais de noms: tot viu en un únic àmbit global. Si la teva llibreria defineix log() i la del company també, l'última carregada guanya silenciosament. El remei és un prefix:

veloz::log_info()   { ... }     # estil amb dos punts dobles
veloz_log_info()    { ... }     # estil amb guionet baix, més portable
_veloz_normalitza() { ... }     # guionet baix inicial: ús INTERN, no la cridis

Bash permet :: en noms de funció i queda molt llegible, però no és POSIX i algunes eines antigues s'hi ennueguen; veloz_ és l'opció segura. Tria'n un i sigues consistent. La distinció públic/privat és per conveni, no la imposa l'intèrpret: el guionet baix inicial és una promesa de «això pot canviar sense avís, no en depenguis». El que sí que pots controlar de debò és l'àmbit de les variables: tota variable dins d'una funció va amb local (04-02), i així no contamina l'script que la faci servir.

  1. Fitxers de configuració

~/veloz-ops/etc/veloz-ops.conf existeix des del principi del curs amb permisos 600. La seva forma més simple és un fitxer d'assignacions (VELOZ_CSV=/srv/veloz/dades/enviaments.csv, VELOZ_LOG_NIVELL=info, VELOZ_CIUTATS="Valencia Sevilla Bilbao Madrid") que es carrega amb [[ -r "$BASE_DIR/etc/veloz-ops.conf" ]] && source "$BASE_DIR/etc/veloz-ops.conf". És còmode —admet comentaris, cometes, fins i tot $(...)— i té un risc que has de conèixer: source executa codi arbitrari. Una línia rm -rf ~ en aquest fitxer s'executa amb els teus permisos. Per això els 600 no són decoratius: si algú pot escriure al teu fitxer de configuració, pot executar el que vulgui com tu. Les implicacions completes es tracten a 08-03.

L'alternativa segura és analitzar clau=valor sense executar res, fent servir el que has après a 05-04:

# _veloz_carrega_conf — Carrega clau=valor sense executar codi. Ús: ... <fitxer>
_veloz_carrega_conf() {
    local fitxer="${1:?}" clau valor linia
    [[ -r "$fitxer" ]] || return 0
    while IFS= read -r linia; do
        [[ "$linia" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue
        clau="${BASH_REMATCH[1]}"; [[ "$clau" == VELOZ_* ]] || continue  # només el nostre
        valor="${BASH_REMATCH[2]}"; valor="${valor%\"}"; valor="${valor#\"}"
        printf -v "$clau" '%s' "$valor"                           # assignació indirecta
    done < "$fitxer"
}

Aquí es troben tres mòduls: la regex amb BASH_REMATCH de 05-04 (que a més descarta comentaris i línies buides en no encaixar), les expansions ${v%\"} de 04-04 i el bucle while read de 04-01. El filtre VELOZ_* impedeix que el fitxer redefineixi PATH o HOME, i printf -v assigna a la variable el nom de la qual és a $clau: la manera segura de fer assignació indirecta sense eval.

  1. Ordre de precedència

Tanquem aquí el disseny d'opcions començat a 03-05. Quan el mateix valor pot venir de quatre llocs, cal un ordre explícit i documentat, de menor a major prioritat:

Nivell Origen Exemple Guanya sobre
1 Valor per defecte al codi VELOZ_CSV=/srv/veloz/dades/enviaments.csv Res
2 Fitxer de configuració etc/veloz-ops.conf El defecte
3 Variable d'entorn VELOZ_CSV=/tmp/x.csv informe-diari.sh Els anteriors
4 Opció de línia d'ordres --csv /tmp/x.csv Tots

El principi: com més a prop de l'usuari i del moment de l'execució, més prioritat. Implementar-ho és senzill si respectes l'ordre de càrrega, i : "${VAR:=valor}" és l'idioma per a «assigna només si és buida o no definida» —el := assigna, a diferència del :- que només substitueix, i el : inicial és l'ordre nul·la—, de manera que una variable d'entorn ja definida sobreviu:

: "${VELOZ_CSV:=/srv/veloz/dades/enviaments.csv}"  # 1. defecte, només si no existeix
_veloz_carrega_conf "$BASE_DIR/etc/veloz-ops.conf" # 2. config (respecta el ja definit)
# 3. l'entorn ja estava posat abans d'arrencar l'script
while [[ $# -gt 0 ]]; do case "$1" in              # 4. opcions (03-05)
    --csv) VELOZ_CSV="$2"; shift 2 ;; *) break ;;
esac; done

  1. L'estructura final del toolkit

~/veloz-ops/
├── bin/                    # executables (+x, amb shebang). Són al PATH
│   ├── informe-diari.sh
│   └── arxivar-historic.sh
├── lib/                    # llibreries (sense +x, sense shebang, només definicions)
│   ├── comu.sh             # registre, errors, validacions, utilitats
│   └── informe.sh          # lògica específica d'informes
├── etc/veloz-ops.conf      # configuració (permisos 600)
└── logs/                   # sortida: informes, traces, fitxers de bloqueig

La regla de repartiment en una frase: a bin/ va el que s'invoca, a lib/ el que es reutilitza, a etc/ el que canvia entre màquines i a logs/ el que es genera. És la mateixa lògica de l'FHS que vas veure a 01-03, aplicada a escala de projecte, i per això resulta familiar a qualsevol que obri el repositori. Un criteri pràctic per decidir on va una funció: si la necessitaria un script que no tingués res a veure amb informes, va a comu.sh; si només té sentit parlant d'enviaments i ciutats, va a informe.sh.

  1. lib/comu.sh

#!/usr/bin/env bash
# comu.sh — Utilitats compartides del toolkit de Veloz Envíos.
# Carregar amb: source "$BASE_DIR/lib/comu.sh"
[[ -n "${_COMU_SH:-}" ]] && return 0
readonly _COMU_SH=1 VELOZ_VERSION='1.0'
readonly _VELOZ_RE_DATA='^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'

# veloz_log_info — Missatge informatiu a stderr. Ús: veloz_log_info <text...>
veloz_log_info() {
    [[ "${VELOZ_LOG_NIVELL:-info}" == silencios ]] && return 0
    printf '%s [INFO] %s\n' "$(date '+%F %T')" "$*" >&2
}
# veloz_log_error — Missatge d'error a stderr. Ús: veloz_log_error <text...>
veloz_log_error() { printf '%s [ERROR] %s\n' "$(date '+%F %T')" "$*" >&2; }
# veloz_morir — Error fatal i sortida. Ús: veloz_morir <codi> <text...>
veloz_morir() { local c="${1:?}"; shift; veloz_log_error "$*"; exit "$c"; }

# veloz_validar_data — És AAAA-MM-DD i existeix? Ús: veloz_validar_data <data>
veloz_validar_data() { [[ "${1:-}" =~ $_VELOZ_RE_DATA ]] && date -d "$1" &>/dev/null; }

# veloz_percentatge — Calcula a/b*100 amb 2 decimals. Ús: veloz_percentatge <a> <b>
veloz_percentatge() {
    (( ${2:?} == 0 )) && { printf '0.00\n'; return 0; }
    bc -l <<< "scale=2; ${1:?} * 100 / $2"
}
# veloz_validar_entorn — Comprova dependències i rutes. Ús: veloz_validar_entorn
veloz_validar_entorn() {
    local cmd
    for cmd in bc date find flock; do
        command -v "$cmd" > /dev/null || veloz_morir 69 "Falta la utilitat: $cmd"
    done
    [[ -r "${VELOZ_CSV:?sense definir}" ]] || veloz_morir 66 "No puc llegir $VELOZ_CSV"
    mkdir -p "${VELOZ_LOG_DIR:?}" || veloz_morir 73 "No puc crear $VELOZ_LOG_DIR"
}

Fixa't en el que no hi ha: ni set -euo pipefail, ni shebang executable, ni una sola línia que actuï en carregar-se més enllà de la guarda i les constants. veloz_morir és l'única que crida exit, i és correcte perquè està pensada per fer-se servir des d'un executable, no des d'una sessió interactiva.

  1. informe-diari.sh com a orquestrador

#!/usr/bin/env bash
# informe-diari.sh — Informe diari de repartiment de Veloz Envíos.
set -Eeuo pipefail
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; readonly BASE_DIR
source "$BASE_DIR/lib/comu.sh"
source "$BASE_DIR/lib/informe.sh"
: "${VELOZ_CSV:=/srv/veloz/dades/enviaments.csv}"
: "${VELOZ_LOG_DIR:=$BASE_DIR/logs}"
_veloz_carrega_conf "$BASE_DIR/etc/veloz-ops.conf"

main() {
    local data subordre; data=$(date +%F)
    while [[ $# -gt 0 ]]; do case "$1" in       # el bucle d'opcions de 03-05
        --data) veloz_validar_data "${2:-}" || veloz_morir 64 "Data invàlida: ${2:-}"
                data="$2"; shift 2 ;;
        --debug) activar_debug; shift ;;
        -h|--help) us; exit 0 ;;
        *) break ;;
    esac; done
    subordre="${1:-resum}"

    exec 9> "$VELOZ_LOG_DIR/.informe.lock"
    flock -n 9 || veloz_morir 75 "Ja hi ha un informe en curs"
    TREBALL=$(mktemp -d); readonly TREBALL
    trap 'rm -rf "$TREBALL"' EXIT INT TERM
    veloz_validar_entorn
    acumular_dia "$data"
    case "$subordre" in
        resum)   generar_informe "$data" "$VELOZ_LOG_DIR/informe-$data.txt" ;;
        detall)  taula_detall "$data" ;;
        ciutats) taula_ciutats ;;
        *) veloz_morir 64 "Subordre desconeguda: $subordre" ;;
    esac
}
main "$@"

Aquest és el resultat de cinc mòduls. L'executable ja no implementa gairebé res: localitza la seva base, carrega les seves llibreries, resol la configuració, processa opcions, es protegeix amb flock i un trap, i despatxa. Tota la lògica reutilitzable viu a lib/. Quan escriguis arxivar-historic.sh, les tres primeres línies seran idèntiques i tindràs de franc el registre, els errors i les validacions.

  1. Provar la llibreria a mà

Una llibreria ben feta es pot carregar en una sessió interactiva, i això és el que la fa verificable:

$ source ~/veloz-ops/lib/comu.sh
$ veloz_percentatge 11 128                                      # 8.59
$ veloz_validar_data 2026-02-31 && echo ok || echo malament     # malament
$ declare -F | grep veloz          # totes les funcions definides
$ type veloz_percentatge           # veure el codi d'una funció

declare -F llista els noms de les funcions definides i type mostra el cos (01-04). Si en fer source apareix qualsevol sortida, hi ha codi de nivell superior que hi sobra. Per a proves automàtiques de debò —afirmacions, casos límit, informe de resultats— l'eina és bats, a 08-06. I shellcheck -x segueix els source per analitzar també les llibreries, a 08-05.

Errors Habituals i Consells

  • source lib/comu.sh amb ruta relativa. Funciona al teu directori i falla des de cron o el PATH.
  • Fer servir $0 per localitzar l'script. Dona bash amb bash script.sh i el fitxer equivocat dins d'una llibreria. Fes servir ${BASH_SOURCE[0]}.
  • exit dins d'una llibreria carregada al teu shell. Tanca el teu terminal. Fes servir return.
  • Codi de nivell superior a la llibreria, i en especial un set -euo pipefail: embruta la sortida i imposa la seva política a l'script que la carrega, potser sense que s'ho esperi.
  • readonly sense guarda d'inclusió, que en carregar-se dues vegades dona error fatal, i funcions sense local, que esclafen variables de l'script que les crida setmanes després.
  • Consell: documenta cada funció amb una línia # nom — què fa. Ús: nom <args> just a sobre. És el que llegiràs d'aquí a sis mesos, i el que permet generar l'ajuda del toolkit amb un simple grep '^# [a-z]' lib/*.sh.

Exercicis

Exercici 1. Escriu la capçalera completa d'arxivar-historic.sh (a bin/) que localitzi BASE_DIR de manera robusta, carregui lib/comu.sh, apliqui mode estricte i falli amb un missatge clar si la llibreria no és on hauria de ser.

Exercici 2. Afegeix a comu.sh una funció veloz_config que retorni el valor d'una clau de configuració respectant l'ordre de precedència (defecte < fitxer < entorn), amb guarda d'inclusió i sense fer servir eval.

Exercici 3. Divideix el toolkit: decideix per a cadascuna d'aquestes funcions si va a comu.sh, a informe.sh o al mateix executable, i justifica-ho: veloz_log_info, taula_ciutats, veloz_percentatge, acumular_dia, us, veloz_validar_data.

Solucions

Solució 1.

#!/usr/bin/env bash
# arxivar-historic.sh — Arxiva l'històric mensual de Veloz Envíos.
set -Eeuo pipefail
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 78
readonly BASE_DIR LIB="$BASE_DIR/lib/comu.sh"
[[ -r "$LIB" ]] || { printf 'Falta la llibreria %s\n' "$LIB" >&2; exit 78; }
source "$LIB"

La comprovació prèvia al source importa: sense ella, el missatge seria source: lib/comu.sh: No such file or directory, que no diu on s'ha buscat. El codi 78 (EX_CONFIG, de la taula de 05-03) és l'adequat: la instal·lació està malament, no les dades. I el missatge fa servir printf directe perquè veloz_log_error encara no existeix.

Solució 2.

# veloz_config — Retorna el valor d'una clau. Ús: veloz_config <CLAU> [defecte]
veloz_config() {
    local clau="${1:?}" defecte="${2:-}" valor l
    valor="${!clau:-}"                        # 1) és a l'entorn o ja carregada?
    if [[ -z "$valor" && -r "${VELOZ_CONF:-}" ]]; then
        while IFS= read -r l; do              # 2) buscar al fitxer
            [[ "$l" =~ ^[[:space:]]*"$clau"=\"?([^\"]*)\"?$ ]] || continue
            valor="${BASH_REMATCH[1]}"; break
        done < "$VELOZ_CONF"
    fi
    printf '%s\n' "${valor:-$defecte}"        # 3) defecte com a últim recurs
}

${!clau} és l'expansió indirecta: obté el valor de la variable el nom de la qual és a $clau. És la lectura equivalent al printf -v de la secció 6, i totes dues eviten eval, que executaria el contingut del fitxer de configuració —justament el risc del qual fugim—. L'ordre de les tres branques és la taula de precedència.

Solució 3.

Funció Destí Motiu
veloz_log_info, veloz_percentatge, veloz_validar_data lib/comu.sh Registre, aritmètica i validació genèrics: no saben res d'enviaments
acumular_dia, taula_ciutats lib/informe.sh Coneixen el format del CSV, els estats d'enviament i la seva presentació
us L'executable Cada script té les seves pròpies opcions; no és reutilitzable

El criteri és una sola pregunta: ho necessitaria un script que no parli d'enviaments? Si la resposta és sí, va a comu.sh. us és el cas interessant: encara que tots els scripts en tinguin una, el seu contingut és diferent a cadascun, així que el que es pot compartir no és la funció sinó el conveni que existeixi.

Conclusió

source executa un fitxer al shell actual, i d'aquí en surt tota la resta: les funcions i variables que defineix queden disponibles, no cal shebang ni permís d'execució, i un exit mataria qui la carrega —dins d'una llibreria es fa servir return—. Una llibreria conté només definicions, sense codi que actuï, sense set -euo pipefail que imposi polítiques alienes i sense missatges en carregar-se. Es localitza amb BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)", mai amb $0 ni amb rutes relatives, perquè un script s'invoca des de qualsevol directori i des del PATH. Una guarda [[ -n ${_COMU_SH:-} ]] && return 0 evita la doble càrrega i l'error dels readonly duplicats, i un prefix veloz_ substitueix els espais de noms que Bash no té, amb el guionet baix inicial marcant el que és privat. La configuració viu a etc/veloz-ops.conf amb permisos 600 —perquè carregar-lo amb source executa el que contingui— o s'analitza com a clau=valor amb BASH_REMATCH i printf -v, i resol els seus conflictes amb una precedència explícita: defecte < fitxer < entorn < línia d'ordres. El resultat és ~/veloz-ops/ amb bin/, lib/, etc/ i logs/, i un informe-diari.sh que ja no implementa: orquestra.

Amb això es tanca el Mòdul 5, i amb ell la part del curs dedicada al que Bash sap fer per si mateix. El toolkit de Veloz Envíos és avui robust, segur amb els seus fitxers i processos, capaç de diagnosticar les seves pròpies fallades, precís en validar text, amo de la seva entrada i sortida, i modular. Però continua sent maldestre en un terreny concret: cada vegada que cal fer comptes per columnes sobre el CSV, encadena cut, sort, uniq i bucles que llegeixen el fitxer diverses vegades; cada vegada que cal reescriure text, falten eines; i quan la veloz-api deixi de ser un procés que es comprova amb pgrep i passi a ser una API que cal consultar, Bash sol no hi arriba.

Al Mòdul 6 entren les eines externes que multipliquen el que Bash sap fer: awk, que processa columnes i agrega en una sola passada el que avui et costa vint línies (06-01); sed, per transformar text en flux amb les regex que ja domines (06-02); les ordres per interrogar el sistema —disc, memòria, usuaris, maquinari— (06-03); les eines de xarxa i el /dev/tcp que vam deixar apuntat (06-04); i curl amb jq per parlar amb la veloz-api i manejar JSON de debò, que és on el teu toolkit deixarà de llegir fitxers per començar a integrar-se amb la resta del sistema (06-05).

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