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
- El problema: tres necessitats, un sol script
- Requisits escrits com una llista comprovable
- Disseny: recollir i formatar són dues coses diferents
- Versió 1: la mínima que funciona
- Les fonts de dades fiables
- Versió 2: una funció per àrea que retorna parells clau=valor
- Els tres formatadors
- Llindars i colors
- La interfície: opcions i ajuda
- Robustesa: què fer quan la dada no hi és
- Posada en ús: inventariar la flota
- 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.
- 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
rootper 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_COLORno 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.
- 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_jsonLes 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 --argsempre. - Comparar decimals amb
[[ ]].[[ 1.5 -gt 1 ]]no dóna error de sintaxi, dóna un error d'execució i ambset -emata l'script. Els decimals, ambawkobc. - Oblidar
-PadfoLC_ALL=Cals decimals. Sense-P, un dispositiu de nom llarg parteix la línia en dues i l'awkllegeix la columna equivocada. - Acolorir sense comprovar
-t 1. El dia que algú faciinfo-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 unforque llança un procés per dada; agrupa-ho en un solawk.
Exercicis
- Secció de paquets. Afegeix
recull_paquets, que informi depaquets_installatsipaquets_actualitzables(fes servirapt list --upgradablea Ubuntu), i derequereix_reinicisegons si existeix/var/run/reboot-required. Ha de degradar an/den un sistema senseapt. - Comparar dues execucions. Escriu el subcomandament
--comparar FITXER: desa una execució enclau=valori, en invocar-lo, mostra només les claus el valor de les quals hagi canviat respecte al fitxer, amb el formatclau: abans -> ara. - Secció de temperatura. Afegeix
recull_temperaturallegint/sys/class/thermal/thermal_zone*/temp(mil·lèsimes de grau) amb comodins (02-05) i$(<fitxer)en comptes decat(08-02), i marca en vermell per sobre de 75 °C afegint la seva clau a la taulaLLINDAR, sense tocarcolor_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
- 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
