Tenim nou scripts que funcionen, cadascun amb les seves opcions, la seva configuració i la seva manera d'invocar-se. Això no és un producte: és una carpeta. Un company que entra demà no sap quin ha d'executar, l'ajuda està repartida en nou --help diferents, la instal·lació és «copia això i recorda't dels permisos», i ningú no sap quina versió corre a srv-veloz-02. Aquest últim projecte converteix la col·lecció en veloz-ops: una única ordre amb subordres, instal·lable, configurable, versionada, provada, desplegada a la flota i documentada. És el tancament del curs, i el pas que separa «sé programar en Bash» de «he lliurat alguna cosa».

Contingut

  1. El toolkit vist sencer
  2. El patró d'ordre única amb subordres
  3. Reorganització final del repositori
  4. Configuració unificada i veloz-ops config
  5. Autocompleció de Bash
  6. Instal·lació idempotent
  7. Versionatge semàntic i veloz-ops version
  8. Qualitat abans de desplegar
  9. Desplegament a la flota i tornada enrere
  10. Documentació: README i runbook

  1. El toolkit vist sencer

flowchart TD
  U["veloz-ops (ordre unica)"]
  U --> S2[estat] --> L2[libexec/info-sistema.sh]
  U --> S3[logs] --> L3[libexec/analitza-logs.sh]
  U --> S4[copia] --> L4[libexec/copia.sh]
  U --> S5[xarxa] --> L5[libexec/monitor-xarxa.sh]
  U --> S6[flota] --> L6[libexec/flota.sh]
  L2 & L3 & L4 & L5 & L6 --> LIB["lib/comu.sh<br/>veloz_log · veloz_requereix · veloz_api_get"]
  LIB --> CFG["etc/veloz-ops.conf + etc/*.d/"]
  L3 --> D1[("/var/log/veloz/*.log")]
  L4 --> D2[("/srv/veloz/dades + /copies")]
  L5 --> D3(["veloz-api :8080 · flota SSH"])
  TMR["systemd timers"] --> U

El diagrama mostra la propietat que busquem: una porta d'entrada, una llibreria comuna, una configuració. Els scripts passen a libexec/ perquè deixen de ser interfície pública; ningú no els invoca directament, els invoca veloz-ops.

  1. El patró d'ordre única amb subordres

És el patró de git, systemctl o docker, i en Bash s'implementa amb case i funcions (04-05):

#!/usr/bin/env bash
# veloz-ops - punt d'entrada unic del toolkit.
set -euo pipefail
export LC_ALL=C
readonly ARREL=${VELOZ_ARREL:-/opt/veloz-ops}
readonly LIBEXEC=$ARREL/libexec
# shellcheck source=lib/comu.sh
. "$ARREL/lib/comu.sh"

ajuda() {
  cat <<'EOF'
Us: veloz-ops <subordre> [opcions]
  estat    Informacio del sistema (09-01)
  logs     Analisi de registres (09-02)
  copia    Copies de seguretat, verificacio i restauracio (09-03)
  xarxa    Monitoratge de xarxa i servei (09-04)
  flota    Executa una ordre als tres servidors
  config   Configuracio efectiva i el seu origen; version; ajuda <sub>
EOF
}

main() {
  local sub=${1:-ajuda}; shift || true
  case $sub in
    estat)   exec "$LIBEXEC/info-sistema.sh"   "$@" ;;
    logs)    exec "$LIBEXEC/analitza-logs.sh"  "$@" ;;
    copia)   exec "$LIBEXEC/copia.sh"          "$@" ;;
    xarxa)   exec "$LIBEXEC/monitor-xarxa.sh"  "$@" ;;
    flota)   exec "$LIBEXEC/flota.sh"          "$@" ;;
    config)  mostra_config "$@" ;;
    version) printf 'veloz-ops %s\n' "$VERSION" ;;
    ajuda|-h|--help) [[ $# -gt 0 ]] && ajuda_de "$1" || ajuda ;;
    *) printf 'Subordre desconeguda: %s\n\n' "$sub" >&2; ajuda >&2; exit 2 ;;
  esac
}
main "$@"

Tres decisions. exec reemplaça el procés en lloc de crear-ne un de fill: estalvia un procés i, més important, el codi de sortida i els senyals arriben a l'script real sense intermediari, així que Ctrl-C sobre veloz-ops copia interromp la còpia de debò. El case és una llista blanca explícita (08-03): construir el camí com a "$LIBEXEC/$sub.sh" seria més curt i permetria a qualsevol executar veloz-ops ../../bin/elquesigui. I la subordre desconeguda surt amb codi 2 i escriu a stderr, seguint el conveni de 05-03.

  1. Reorganització final del repositori

veloz-ops/
├── bin/veloz-ops              # unic executable al PATH
├── libexec/                   # scripts interns, no al PATH
│   ├── info-sistema.sh  analitza-logs.sh  copia.sh  monitor-xarxa.sh  flota.sh
│   └── acces.awk              # programes awk separats (09-02)
├── lib/comu.sh                # funcions veloz_*, es carrega amb source
├── etc/veloz-ops.conf         # 600; mes copia.d/ i monitor-xarxa.d/
├── systemd/*.service *.timer   ├── tests/*.bats   ├── docs/runbook.md
└── completion/  install.sh  verificar.sh  README.md  CHANGELOG.md

El criteri val la pena enunciar-lo: bin/ és el que l'usuari invoca, libexec/ el que invoca el programa, lib/ el que es carrega amb source i mai no s'executa, etc/ el que s'edita sense desplegar. Aquesta última frase decideix els dubtes: si canviar-ho exigeix revisió de codi, és codi; si és un camí, un llindar o una llista d'objectius, és configuració.

  1. Configuració unificada i veloz-ops config

La precedència de 05-06 —defecte < fitxer < entorn < opcions— s'implementa en un únic punt de lib/comu.sh, i es fa auditable desant l'origen de cada valor:

declare -A CFG ORIGEN
veloz_config_carrega() {
  local f=$1 k v env_var
  while IFS='=' read -r k v; do            # fitxer: trepitja els defectes
    [[ $k == \#* || -z $k ]] && continue
    k=${k// /}; v=${v%\"}; v=${v#\"}; CFG[$k]=$v; ORIGEN[$k]="fitxer:$f"
  done < "$f"
  for k in "${!CFG[@]}"; do                # entorn: trepitja el fitxer
    env_var=VELOZ_$k
    [[ -n ${!env_var:-} ]] && { CFG[$k]=${!env_var}; ORIGEN[$k]=entorn; }
  done
}

${!env_var} és l'expansió indirecta (03-06): construeix el nom VELOZ_LLINDAR_DISC i llegeix aquesta variable. Desar l'origen al costat del valor costa un array associatiu més i resol la pregunta que consumeix hores en producció: «el llindar és a 95, però el fitxer diu 85, d'on surt?». veloz-ops config ho imprimeix en tres columnes —clau, valor, origen— i per això és una subordre de primera classe i no un echo de depuració.

  1. Autocompleció de Bash

# completion/veloz-ops.bash  ->  /etc/bash_completion.d/veloz-ops
_veloz_ops() {
  local act=${COMP_WORDS[COMP_CWORD]} opcions='estat logs copia xarxa flota config version ajuda'
  (( COMP_CWORD > 1 )) && case ${COMP_WORDS[1]} in
    copia) opcions='executa verifica restaura informe --simula' ;;
    estat) opcions='--format --seccio --ajuda' ;;
    xarxa) opcions='comprova resum directe --objectiu' ;;
  esac
  mapfile -t COMPREPLY < <(compgen -W "$opcions" -- "$act")
}
complete -F _veloz_ops veloz-ops

COMP_WORDS i COMP_CWORD són les variables que Bash omple en prémer Tab, i compgen -W filtra una llista pel prefix teclejat. No és cosmètica: l'autocompleció és la documentació que la gent llegeix de debò, i evita la meitat dels errors de teclejat en una subordre delicada com restaura.

  1. Instal·lació idempotent

install.sh ha de poder executar-se cent vegades amb el mateix resultat (07-02):

installa() {
  install -d -m 755 "$PREFIX"/{bin,libexec,lib,etc,docs}
  install -m 755 bin/veloz-ops "$PREFIX/bin/veloz-ops"
  install -m 755 libexec/*.sh  "$PREFIX/libexec/"
  install -m 644 libexec/*.awk lib/comu.sh -t "$PREFIX/lib/"
  [[ -e $PREFIX/etc/veloz-ops.conf ]] ||
    install -m 600 etc/veloz-ops.conf.exemple "$PREFIX/etc/veloz-ops.conf"
  install -m 644 completion/veloz-ops.bash /etc/bash_completion.d/veloz-ops
  ln -sfn "$PREFIX/bin/veloz-ops" /usr/local/bin/veloz-ops
  install -m 644 systemd/*.service systemd/*.timer /etc/systemd/system/
  systemctl daemon-reload
  systemctl enable --now veloz-copia.timer veloz-xarxa.timer
  "$PREFIX/bin/veloz-ops" version
}

install en lloc de cp + chmod perquè crea el destí i fixa els permisos en una sola operació, i és idempotent per naturalesa; ln -sfn sobreescriu l'enllaç sense queixar-se; systemctl enable --now activa i arrenca, i repetir-ho no fa cap mal. La línia clau és la condicional: la configuració existent no es trepitja mai. Un instal·lador que sobreescriu veloz-ops.conf esborra els llindars que algú va ajustar a les tres de la matinada; per això el repositori versiona veloz-ops.conf.exemple i no el fitxer real (08-04). La desinstal·lació és simètrica: --desinstalla atura i deshabilita els timers, esborra les unitats, fa daemon-reload, elimina l'enllaç i l'arbre, i conserva etc/ i les còpies de seguretat, avisant d'on queden.

  1. Versionatge semàntic i veloz-ops version

Canvi Incrementa Exemple
S'elimina o es reanomena una subordre o opció; canvia el format d'un fitxer d'estat MAJOR --seccio passa a --area
Subordre o opció noves, compatibles MENOR S'afegeix veloz-ops xarxa resum
Correcció sense canvi d'interfície PEDAÇ S'arregla el càlcul de la mediana

La versió surt de Git, no d'una constant que algú s'oblidarà d'actualitzar (08-04):

VERSION=$(git -C "$ARREL" describe --tags --dirty 2>/dev/null || cat "$ARREL/VERSION" 2>/dev/null || echo desconeguda)

git describe --tags --dirty dóna v1.4.0 en una etiqueta exacta, v1.4.0-7-gab12cd3 si hi ha set commits a sobre —cosa que revela a l'instant que en aquell servidor hi ha alguna cosa sense publicar— i afegeix -dirty si hi ha canvis sense confirmar, que en producció és una alerta en si mateixa. L'install.sh escriu a més un fitxer VERSION per al desplegament per rsync sense repositori.

  1. Qualitat abans de desplegar

Un sol script recull totes les comprovacions del mòdul 8:

#!/usr/bin/env bash
# verificar.sh - tot el que ha de passar abans de desplegar.
set -uo pipefail
fallades=0
mapfile -t SCRIPTS < <(find bin libexec lib -type f)
pas() { printf '\n== %s ==\n' "$1"; shift; "$@" || ((fallades++)); }

pas sintaxi    bash -n "${SCRIPTS[@]}"
pas shellcheck shellcheck -x -S style "${SCRIPTS[@]}"
pas format     shfmt -d -i 2 -ci "${SCRIPTS[@]}"
pas proves     bats tests/
pas ajuda      bin/veloz-ops ajuda
exit $(( fallades > 0 ))

L'ordre va del que és ràpid al que és lent: bash -n triga mil·lisegons i descarta un error de sintaxi abans de gastar trenta segons en Bats. shellcheck -x segueix els source per analitzar també lib/comu.sh (08-05), i -S style puja el llistó per damunt dels errors. shfmt -d no reformata, només mostra la diferència i falla: en CI cal fallar, no arreglar per sorpresa. La bateria de Bats s'amplia a les subordres (08-06):

@test "subordre desconeguda surt amb 2 i escriu a stderr" {
  run bin/veloz-ops noexisteix
  [ "$status" -eq 2 ]
  [[ $output == *"Subordre desconeguda"* ]]
}

@test "copia --simula no crea cap directori" {
  local abans; abans=$(find "$BATS_TMPDIR/copies" -type d | wc -l)
  run bin/veloz-ops copia executa --simula
  [ "$status" -eq 0 ]
  [ "$(find "$BATS_TMPDIR/copies" -type d | wc -l)" -eq "$abans" ]
}

La segona prova és la més valuosa del toolkit: verifica que el mode simulació no té efectes, que és la promesa en què es recolza qualsevol abans de llançar una còpia amb --delete. El hook pre-commit (08-04) executa les tres primeres etapes i CI la bateria completa a cada branca.

  1. Desplegament a la flota i tornada enrere

desplega() {  # $1 = etiqueta, p. ex. v1.4.0
  local etiqueta=$1 host
  git tag -l "$etiqueta" | grep -q . || veloz_morir 2 "etiqueta inexistent: $etiqueta"
  ./verificar.sh || veloz_morir 3 "no es desplega codi que no verifica"
  for host in srv-veloz-01 srv-veloz-02 srv-veloz-03; do
    printf '\n--- %s ---\n' "$host"
    ssh -n "$host" "cd /opt/veloz-ops && git fetch --tags -q &&
                    git checkout -q $etiqueta && sudo ./install.sh" || return 1
    ssh -n "$host" 'veloz-ops version && veloz-ops estat --seccio serveis' || return 1
  done
}

El desplegament és seqüencial i amb verificació posterior a cada amfitrió: si srv-veloz-01 falla, no es toca el segon, i en el pitjor dels casos queda un servidor malament i dos d'intactes. ssh -n (07-06) impedeix que el bucle es mengi l'entrada estàndard, l'error clàssic que fa que només es desplegui el primer amfitrió. Es desplega una etiqueta, mai una branca: git checkout v1.4.0 dóna exactament el codi que es va provar, i la tornada enrere és la mateixa ordre amb l'etiqueta anterior (desplega v1.3.2). Que revertir sigui idèntic a desplegar és el que fa que algú s'atreveixi a executar-ho a les quatre de la matinada. Amb una excepció per al runbook: revertir el codi és trivial, revertir un canvi de format dels fitxers d'estat o de l'històric no ho és, i per això aquest tipus de canvi incrementa la versió MAJOR.

  1. Documentació: README i runbook

Són dos documents amb dos lectors diferents i formen part del lliurable. El README.md el llegeix qui arriba nou, en fred: què és, què requereix, com s'instal·la en cinc ordres copiables, les subordres amb un exemple real cadascuna i on és la configuració. Si algú no pot instal·lar-lo i executar veloz-ops estat seguint només el README, el README està malament. El docs/runbook.md el llegeix algú mig adormit amb una alerta al mòbil: s'organitza per alerta, no per script, sense prosa i amb ordres copiables.

## ALERTA: copia amb fallades a srv-veloz-01
1. Confirmar:    veloz-ops copia informe
2. Veure el log: journalctl -u veloz-copia.service -n 50
3. Si "sense espai" (codi 5): df -h /copies; revisar RETENCIO_* del perfil
4. Si "verificacio fallida" (codi 4): NO esborris res; restaura de srv-veloz-03
5. Escalar a Operacions si el perfil dades falla dues nits seguides.

Un script sense documentació és un script que només pot executar qui el va escriure; i qui el va escriure acabarà de vacances justament la nit que falli.

Errors Habituals i Consells

  • Construir el camí de la subordre amb l'argument de l'usuari. "$LIBEXEC/$1.sh" obre la porta a ../. Llista blanca amb case, sempre.
  • Instal·ladors que trepitgen la configuració. Versiona .conf.exemple; el .conf real només es crea si no existeix.
  • Desplegar una branca en lloc d'una etiqueta. main significa una cosa diferent cada hora; una etiqueta significa sempre el mateix.
  • Bucle for host amb ssh sense -n. El primer ssh consumeix la llista i la resta de la flota es queda sense desplegar, en silenci.
  • Documentar per script en comptes de per alerta. A les quatre de la matinada ningú no busca «analitza-logs»; busca el text que li ha arribat al mòbil.
  • Consell: executa veloz-ops config als tres servidors i compara. Les diferències no explicades entre nodes són l'origen de la meitat dels incidents de «en aquest servidor sí que funciona».

Exercicis

  1. Subordre doctor. Verifica la salut de la mateixa instal·lació: dependències (jq, rsync, curl, awk), permisos d'etc/ (600), timers actius, antiguitat de l'última còpia i de l'últim cicle del monitor. Un símbol per comprovació i codi 0/1/2 global.
  2. Panell de l'estat de la flota. Un veloz-ops flota estat que reculli el JSON d'estat dels tres servidors en paral·lel i marqui les diferències de versió, kernel i configuració efectiva.
  3. Reptes oberts d'ampliació. Empaqueta el toolkit com a .deb amb postinst/prerm i la configuració marcada com a conffiles; afegeix un veloz-ops api que exposi els informes per HTTP decidint què no exposar; unifica un --json a totes les subordres amb una prova Bats que validi cada sortida; i porta el toolkit a Debian o Alpine anotant cada bashisme que calgui resoldre (08-07).

Solucions

1. doctor no comprova res pel seu compte: reutilitza el que ja està construït.

doctor() {
  local fallades=0
  chk() { local n=$1; shift
          if "$@" >/dev/null 2>&1; then printf '  [OK]      %s\n' "$n"
          else printf '  [FALLADA] %s\n' "$n"; ((fallades++)); fi; }
  chk "dependencies"    veloz_requereix jq rsync curl awk
  chk "permisos d'etc"  test "$(stat -c %a "$ARREL/etc/veloz-ops.conf")" = 600
  chk "timer de copia"  systemctl is-active --quiet veloz-copia.timer
  chk "copia recent"    test "$(dies_ultima_copia)" -le 1
  (( fallades == 0 )) && return 0 || return 2
}

La funció imbricada chk, que rep una ordre i l'executa, evita repetir el mateix if deu vegades; i retornar 0/2 permet que el mateix doctor es vigili des de vigilant.sh amb el conveni de 07-04.

2. flota.sh ja executa en paral·lel, així que n'hi ha prou amb consolidar els JSON amb jq -s:

veloz-ops flota 'veloz-ops estat --format json' | jq -s '
  map({host, version, kernel})
  | {nodes: ., kernels_diferents: (map(.kernel) | unique | length)}'

Detectar la divergència —no llistar els valors— és el que converteix el panell en una eina útil: si kernels_diferents és més gran que 1, hi ha un servidor sense actualitzar.

Conclusió

Vam començar el curs escrivint echo "Hola" en un terminal i l'acabem amb un producte instal·lat en tres servidors: una ordre única amb subordres i ajuda, configuració amb precedència i auditable, autocompleció, instal·lador idempotent i desinstal·lador simètric, versionatge semàntic lligat a les etiquetes de Git, una bateria de proves que corre a cada commit, desplegament per etiqueta amb tornada enrere i documentació escrita per a qui la llegirà de matinada. Cap d'aquestes peces no és Bash avançat: són case, funcions, arrays, printf, source i codis de sortida —això del mòdul 4— aplicats amb el criteri del mòdul 8. Aquest és el missatge del curs sencer: la diferència entre un script i una eina no és al llenguatge, és a la disciplina.

Convé acabar amb honestedat sobre els límits. Bash és insuperable enganxant programes, movent fitxers i automatitzant el sistema; és exactament l'eina correcta per als cinc projectes d'aquest mòdul. Deixa de ser-ho quan apareixen estructures de dades imbricades (més de dos nivells de JSON i jq ja no basta), quan calen proves unitàries de lògica de negoci, concurrència real, aritmètica de coma flotant sostinguda, o quan l'script passa de les mil línies i cada canvi fa por. Si et trobes escrivint un analitzador d'expressions o un client HTTP amb estat en Bash, el senyal és clar: toca Python, Go o el llenguatge que faci servir el teu equip. Saber quan parar forma part de dominar l'eina, i reescriure en un altre llenguatge un script de Bash ben estructurat és senzill precisament perquè estava ben estructurat.

I per on continuar. El manual de Bash (man bash, i sobretot la seva secció d'expansions, que es llegeix sencera un cop a la vida) és l'única font definitiva. La guia d'estil de shell de Google, per discutir decisions amb arguments en comptes de per gustos. ShellCheck com a mestre permanent: cada avís que no entenguis, busca'l pel seu codi i aprendràs alguna cosa que no sabies. I sobretot, el que de debò consolida: automatitza el teu. Agafa aquella tasca que fas a mà cada dilluns, escriu-la com un script amb set -euo pipefail, dóna-li opcions, posa-li un --simula, prova-la amb Bats, puja-la a Git i dispara-la amb un timer. En això consisteix l'ofici, i ja ho saps fer sencer.

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