Ja saps escriure Bash i ja saps escriure'l bé; el que queda és ajuntar-ho tot. Aquest mòdul no ensenya matèria nova: construeix. Cada lliçó agafa un problema real de Veloz Envíos i el porta des de l'enunciat fins a una eina acabada, explicant a cada pas per què es decideix el que es decideix i de quina lliçó ve la tècnica. Comencem pel projecte més contingut dels cinc, info-sistema.sh, que no per això és trivial: és on s'aprèn la separació entre recollir i formatar, la decisió de disseny que reapareixerà en els quatre projectes següents.

Contingut

  1. El problema: tres necessitats, un sol script
  2. Requisits escrits com una llista comprovable
  3. Disseny: recollir i formatar són dues coses diferents
  4. Versió 1: la mínima que funciona
  5. Les fonts de dades fiables
  6. Versió 2: una funció per àrea que retorna parells clau=valor
  7. Els tres formatadors
  8. Llindars i colors
  9. La interfície: opcions i ajuda
  10. Robustesa: què fer quan la dada no hi és
  11. Posada en ús: inventariar la flota

  1. El problema: tres necessitats, un sol script

A Veloz Envíos, la informació bàsica d'un servidor fa falta en tres situacions ben diferents:

Situació Qui ho llegeix Quin format necessita
Diagnòstic ràpid: «srv-veloz-02 va lent» Una persona, en un terminal Text llegible, alineat, amb el que és anòmal destacat
Inventari de la flota: versions, RAM, discos Un full de càlcul o un awk Una línia per servidor, camps separats
Context adjunt a una alerta L'API interna i el webhook JSON

La temptació és escriure tres scripts, o un amb un if gegant i tres còpies de cada consulta. Totes dues coses envelleixen malament: el dia que hi afegeixes «càrrega mitjana» s'ha de tocar en tres llocs i te'n descuidaràs un. La solució és la que ja vas fer servir sense anomenar-la a 04-02 quan retornaves valors per stdout: la recollida no sap res del format de sortida.

  1. Requisits escrits com una llista comprovable

Abans d'escriure codi, els requisits, en forma de llista que es pugui marcar:

  • [ ] Recull identificació (host, distribució, kernel, temps encès), maquinari (CPU, RAM), disc, memòria i càrrega, xarxa i estat de serveis.
  • [ ] Tres formats de sortida: text, tsv, json; i es pot demanar una sola secció (--seccio disc).
  • [ ] No necessita root per a l'essencial i no falla si alguna cosa no està disponible.
  • [ ] Marca a la sortida de text els valors fora de llindar, amb color només si la sortida és un terminal i NO_COLOR no està definida.
  • [ ] Passa shellcheck -x, acaba en menys d'un segon i s'executa per SSH als tres servidors sense canvis.

Aquesta llista és el contracte. Quan dubtis de si una funció ha d'existir, la resposta és aquí; i quan l'script estigui acabat, es recorre marcant caselles.

  1. Disseny: recollir i formatar són dues coses diferents

L'script té dues meitats que es comuniquen mitjançant un format intern molt simple: línies clau=valor.

recull_identificacio / _maquinari / _disc   ──┐                 ──► formata_text
recull_memoria_carrega / _xarxa / _serveis  ──┴─► clau=valor ───┼─► formata_tsv
                                                               ──► formata_json

Les funcions de recollida escriuen a stdout (04-02) i no imprimeixen ni una capçalera. Els formatadors llegeixen aquest flux i decideixen com es veu. Afegir una dada nova és afegir una línia dins d'una funció; afegir un format nou és afegir una funció que no toca cap consulta al sistema.

  1. Versió 1: la mínima que funciona

Comencem pel més petit que resol el cas 1 de la taula. Res d'opcions, res de formats.

#!/usr/bin/env bash
# info-sistema.sh - v1: identificacio basica en text.
set -euo pipefail
printf 'host=%s\n'   "$(hostname -f)"
printf 'kernel=%s\n' "$(uname -r)"
printf 'uptime=%s\n' "$(uptime -p)"

Vint segons de feina i ja és útil. Aquesta versió importa per dues raons. Primera: fixa el format intern (clau=valor) des de la primera línia, i tota la resta creixerà a sobre. Segona: és executable i verificable ara, no quan estigui «acabat». Construir incrementalment vol dir que en cap moment hi ha un script trencat a mig escriure.

  1. Les fonts de dades fiables

A 06-03 vam veure la regla: llegeix fitxers, no sortides pensades per a humans. Aquí s'aplica a cada dada.

Dada Font triada Per què no l'alternativa
Distribució /etc/os-release És un fitxer amb format CLAU="valor", apte per a source; lsb_release pot no estar instal·lat
Kernel uname -r Sortida d'un sol camp, estable des de fa dècades
CPU nproc Comptar línies de /proc/cpuinfo dóna un altre número amb hyperthreading i contenidors
Memòria /proc/meminfo free canvia de columnes entre versions; MemAvailable és el número que de debò importa
Càrrega /proc/loadavg Un sol read en comptes de retallar uptime amb sed
Disc df -P -P força el format POSIX: una línia per sistema de fitxers, sense partir-la quan el dispositiu és llarg
IP i serveis ip -4 addr, systemctl is-active ifconfig està obsolet a Ubuntu 24.04; is-active retorna una paraula i un codi de sortida

LC_ALL=C al principi de l'script (06-03) garanteix que els decimals portin punt i que els missatges d'error no canviïn amb l'idioma del servidor.

  1. Versió 2: una funció per àrea que retorna parells clau=valor

Cada funció és curta, no imprimeix capçaleres i mai avorta l'script si li falta la font.

recull_identificacio() {
  [[ -r /etc/os-release ]] && . /etc/os-release   # shellcheck disable=SC1091
  printf 'host=%s\n'   "$(hostname -f 2>/dev/null || hostname)"
  printf 'so=%s\n'     "${PRETTY_NAME:-desconegut}"
  printf 'kernel=%s\n' "$(uname -r)"
  printf 'uptime=%s\n' "$(uptime -p 2>/dev/null || echo n/d)"
}

recull_memoria_carrega() {
  local total dispo carrega
  read -r total dispo < <(awk '/^MemTotal:|^MemAvailable:/ {printf "%s ", $2}' /proc/meminfo)
  read -r carrega _ < /proc/loadavg
  printf 'mem_total_mb=%s\n' "$((total / 1024))"
  printf 'mem_usada_pct=%s\n' "$(veloz_percentatge "$((total - dispo))" "$total")"
  printf 'carrega_1m=%s\n' "$carrega"
  printf 'carrega_per_cpu=%s\n' "$(echo "$carrega / $(nproc)" | bc -l | cut -c1-4)"
}

recull_disc() {
  df -P -x tmpfs -x devtmpfs 2>/dev/null |
    awk 'NR>1 {gsub(/%/,"",$5); printf "disc_%s_pct=%s\ndisc_%s_lliure=%s\n", $6, $5, $6, $4}'
}

recull_serveis() {
  local s
  for s in "${SERVEIS[@]}"; do   # veloz-api, nginx, ssh...
    printf 'servei_%s=%s\n' "$s" "$(systemctl is-active "$s" 2>/dev/null || echo desconegut)"
  done
}

Quatre detalls que vénen de lliçons anteriors. El . /etc/os-release és la tècnica de 05-06 aplicada a un fitxer de dades: en lloc de retallar-lo amb cut, es carrega i les seves variables queden disponibles. veloz_percentatge surt de lib/comu.sh i evita repetir aritmètica entera (04-06). L'awk de recull_disc fa en una sola passada el que un bucle while read faria en vint línies (06-01), i el gsub treu el % perquè el valor sigui numèric i comparable. I el || echo desconegut del systemctl és la resposta al requisit de no fallar en un contenidor sense systemd: val més un valor honest que una mort per set -e.

  1. Els tres formatadors

Reben el flux clau=valor per l'entrada estàndard. Cap d'ells consulta res al sistema.

formata_text() {
  local clau valor
  while IFS='=' read -r clau valor; do
    printf '  %-22s %s%s%s\n' "$clau" "$(color_de "$clau" "$valor")" "$valor" "$RESET"
  done
}

formata_tsv() {
  local clau valor fila=()
  while IFS='=' read -r clau valor; do fila+=("$valor"); done
  local IFS=$'\t'; printf '%s\n' "${fila[*]}"
}

formata_json() {
  local args=() clau valor
  while IFS='=' read -r clau valor; do args+=(--arg "$clau" "$valor"); done
  jq -n "${args[@]}" '$ARGS.named'
}

formata_text fa servir printf amb amplada fixa %-22s (04-04) perquè les claus quedin alineades sense column. formata_tsv acumula en un array i aprofita que "${fila[*]}" uneix els elements amb el primer caràcter d'IFS (03-06): assignar IFS=$'\t' com a local fa que el separador sigui un tabulador només dins de la funció. I formata_json construeix els arguments de jq -n --arg en un array (06-05) perquè l'escapament de cometes, accents i barres el faci jq i no nosaltres; $ARGS.named és l'objecte amb tots els --arg rebuts. Escriure aquest JSON a mà amb printf seria la manera més ràpida de generar JSON invàlid el dia que un hostname porti un guionet estrany.

  1. Llindars i colors

Els llindars viuen en una sola taula, no repartits pel codi:

declare -A LLINDAR=([mem_usada_pct]=85 [carrega_per_cpu]=1.5)

color_de() {  # $1=clau $2=valor -> sequencia de color o res
  [[ $COLOR == no ]] && return 0
  local limit=${LLINDAR[$1]:-}
  [[ -z $limit ]] && { [[ $1 == disc_*_pct ]] && limit=90 || return 0; }
  awk -v v="$2" -v l="$limit" 'BEGIN {exit !(v+0 > l)}' && printf '%s' "$VERMELL"
}

La comparació es delega a awk perquè els valors poden ser decimals i [[ ]] només compara enters (04-06). El +0 força a número, de manera que un valor no numèric com ara desconegut s'avaluï com a 0 i no es pinti mai de vermell. La decisió sobre el color, en canvi, es pren una sola vegada, en arrencar (03-05):

COLOR=no; VERMELL=''; RESET=''
[[ -t 1 && -z ${NO_COLOR:-} && $FORMAT == text ]] &&
  { COLOR=si; VERMELL=$'\033[31m'; RESET=$'\033[0m'; }

Tres condicions i les tres importen. -t 1 detecta que stdout és un terminal: si l'script es redirigeix a un fitxer o s'encadena amb grep, els codis d'escapament embrutarien el resultat. NO_COLOR és una convenció respectada per moltes eines i costa una línia. I el color es desactiva sempre en TSV i JSON, perquè allà la sortida la llegeix una màquina.

  1. La interfície: opcions i ajuda

us() {
  cat <<'EOF'
Us: info-sistema.sh [opcions]
  -f, --format text|tsv|json   Format de sortida (per defecte: text)
  -s, --seccio NOM             Nomes una seccio; repetible
  -h, --ajuda                  Aquesta ajuda
EOF
}

FORMAT=text; SECCIONS=()
while [[ $# -gt 0 ]]; do
  case $1 in
    -f|--format) FORMAT=${2:?falta el valor de --format}; shift 2 ;;
    -s|--seccio) SECCIONS+=("${2:?falta el valor}"); shift 2 ;;
    -h|--ajuda)  us; exit 0 ;;
    --)          shift; break ;;
    -*)          veloz_morir 2 "opcio desconeguda: $1" ;;
    *)           break ;;
  esac
done
[[ $FORMAT =~ ^(text|tsv|json)$ ]] || veloz_morir 2 "format invalid: $FORMAT"

Es tria el bucle while+case per damunt de getopts (03-05) per una raó concreta: volem opcions llargues llegibles al runbook, i getopts només gestiona les curtes. El ${2:?...} talla d'arrel l'error clàssic de --format sense valor. El -- explícit permet que un dia hi hagi arguments posicionals. I validar $FORMAT amb una expressió regular (05-04) en comptes d'amb un case al despatxador vol dir que l'error es detecta en arrencar, no a mitja execució.

El despatxador principal recorre les seccions demanades i canalitza tot cap a un únic formatador:

main() {
  veloz_requereix jq awk df
  local sec
  [[ ${#SECCIONS[@]} -eq 0 ]] && SECCIONS=("${TOTES[@]}")
  {
    for sec in "${SECCIONS[@]}"; do
      declare -F "recull_$sec" >/dev/null || veloz_morir 2 "seccio desconeguda: $sec"
      "recull_$sec"
    done
  } | "formata_$FORMAT"
}
main "$@"

La clau que agrupa el bucle (05-05) permet que tota la sortida del bucle entri per una sola canonada al formatador. I declare -F comprova que la funció existeix abans d'invocar-la: la secció es converteix en nom de funció, però només si aquesta funció està definida, que és la llista blanca de 08-03 aplicada sense escriure la llista dues vegades.

  1. Robustesa: què fer quan la dada no hi és

set -euo pipefail mata l'script davant de qualsevol fallada, i això és correcte per a errors de programació però desastrós per a una dada absent. La regla del projecte: una dada que falta és n/d, no una mort.

Situació Efecte sense protecció Què fem
Contenidor sense systemctl, màquina sense ip command not found i sortida 127 command -v previ o 2>/dev/null || echo n/d
jq no instal·lat JSON invàlid a mig escriure veloz_requereix jq en arrencar
df sobre un muntatge NFS penjat L'script es queda bloquejat timeout 5 df -P (05-02)

L'últim cas és el més traïdor: una dada que no arriba mai és pitjor que una dada que falla, perquè un script penjat no genera alerta. timeout converteix el bloqueig en un error visible.

  1. Posada en ús: inventariar la flota

Amb el format TSV, l'inventari dels tres servidors és una línia reutilitzant flota.sh (07-06):

~/veloz-ops/bin/flota.sh 'bash -s' < ~/veloz-ops/bin/info-sistema.sh --format tsv | column -t -s$'\t'

L'script s'envia per l'entrada estàndard en lloc d'instal·lar-lo a cada servidor: així l'inventari reflecteix sempre l'última versió i no cal sincronitzar res. I per adjuntar context a una alerta, info-sistema.sh --format json --seccio memoria_carrega produeix exactament l'objecte que espera el webhook de 07-04.

Errors Habituals i Consells

  • Barrejar recollida i format. Si una funció de recollida imprimeix printf 'Memoria: %s%%', el JSON queda inservible. La prova de foc: cap funció recull_* no pot contenir espais d'alineació, ni dos punts, ni colors.
  • Construir JSON a mà. printf '{"host":"%s"}' funciona fins que un valor porti una cometa o una barra. jq -n --arg sempre.
  • Comparar decimals amb [[ ]]. [[ 1.5 -gt 1 ]] no dóna error de sintaxi, dóna un error d'execució i amb set -e mata l'script. Els decimals, amb awk o bc.
  • Oblidar -P a df o LC_ALL=C als decimals. Sense -P, un dispositiu de nom llarg parteix la línia en dues i l'awk llegeix la columna equivocada.
  • Acolorir sense comprovar -t 1. El dia que algú faci info-sistema.sh > estat.txt, el fitxer s'omplirà de \033[31m.
  • Consell: cronometra l'script amb time (08-02). Si triga més d'un segon, gairebé segur que és un for que llança un procés per dada; agrupa-ho en un sol awk.

Exercicis

  1. Secció de paquets. Afegeix recull_paquets, que informi de paquets_installats i paquets_actualitzables (fes servir apt list --upgradable a Ubuntu), i de requereix_reinici segons si existeix /var/run/reboot-required. Ha de degradar a n/d en un sistema sense apt.
  2. Comparar dues execucions. Escriu el subcomandament --comparar FITXER: desa una execució en clau=valor i, en invocar-lo, mostra només les claus el valor de les quals hagi canviat respecte al fitxer, amb el format clau: abans -> ara.
  3. Secció de temperatura. Afegeix recull_temperatura llegint /sys/class/thermal/thermal_zone*/temp (mil·lèsimes de grau) amb comodins (02-05) i $(<fitxer) en comptes de cat (08-02), i marca en vermell per sobre de 75 °C afegint la seva clau a la taula LLINDAR, sense tocar color_de.

Solucions

1. La clau és aïllar cada consulta amb la seva pròpia salvaguarda:

recull_paquets() {
  local inst=n/d act=n/d
  if command -v dpkg-query >/dev/null; then
    inst=$(dpkg-query -f '.\n' -W 2>/dev/null | wc -l)
    act=$(apt list --upgradable 2>/dev/null | grep -c upgradable || true)
  fi
  printf 'paquets_installats=%s\npaquets_actualitzables=%s\n' "$inst" "$act"
  printf 'requereix_reinici=%s\n' "$([[ -f /var/run/reboot-required ]] && echo si || echo no)"
}

El || true després de grep -c és imprescindible: grep retorna 1 quan no troba res, i amb set -e això seria el final de l'script encara que «zero actualitzables» sigui la millor notícia possible.

2. S'aprofita que el format intern ja és clau=valor, així que n'hi ha prou amb un array associatiu (04-03):

formata_comparar() {  # $REFERENCIA = fitxer d'una execucio anterior
  declare -A abans; local k v
  while IFS='=' read -r k v; do abans[$k]=$v; done < "$REFERENCIA"
  while IFS='=' read -r k v; do
    [[ ${abans[$k]:-<nou>} != "$v" ]] && printf '%s: %s -> %s\n' "$k" "${abans[$k]:-<nou>}" "$v"
  done
  return 0
}

Que sigui un formatador més és l'avantatge d'haver separat les dues meitats: comparar no ha requerit tocar ni una consulta al sistema.

3. El bucle recorre /sys/class/thermal/thermal_zone*/temp amb [[ -r $z ]] || continue per saltar-se les zones illegibles, i emet printf 'temp_%s=%s\n' "$n" "$(( $(<"$z") / 1000 ))". Afegir [temp_0]=75 a LLINDAR n'hi ha prou perquè l'acoloriment funcioni sol.

Conclusió

info-sistema.sh està acabat i compleix les vuit caselles del contracte de l'apartat 2. Més important que l'script és la decisió de disseny que el vertebra: separar la recollida del formatatge, amb un format intern mínim (clau=valor) com a frontera entre totes dues. Gràcies a ella, tres formats de sortida conviuen sense una sola consulta duplicada, afegir una dada és una línia, afegir un format és una funció, i comparar dues execucions —l'exercici 2— es va resoldre sense tocar la recollida. La resta del projecte ha estat aplicar el que ja havies après: fonts fiables de /proc i fitxers en comptes de sortides per a humans (06-03), printf amb amplades (04-04), jq -n --arg (06-05), llista blanca via declare -F (08-03), timeout contra els bloqueigs (05-02) i una interfície amb opcions llargues i validació primerenca (03-05). I una regla d'operació que val per a tot el mòdul: una dada que falta és n/d, no una excepció.

A 09-02 el material d'entrada deixa de ser un grapat de fitxers de /proc amb una línia cadascun i passa a ser un milió de línies d'acces.log i app.log. Construirem analitza-logs.sh: una sola passada amb awk, lectura transparent de rotats i comprimits, filtres per data i nivell, agregacions, histograma dibuixat amb printf, detecció d'anomalies i informe en text i JSON —el projecte més exigent del curs en processament de text.

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