El mòdul 6 va acabar amb una promesa incòmoda: saber què és un sistema operatiu no serveix de res a les tres de la matinada si no saps quina ordre has d'escriure primer. I totes aquestes ordres s'escriuen al mateix lloc: la shell. Abans d'aprendre a diagnosticar cal entendre bé la interfície per la qual passa el diagnòstic, perquè la majoria dels errors d'un administrador novell no són errors d'anàlisi, sinó de shell: un nom de fitxer amb espais que es va partir en dos arguments, una canonada que va retornar èxit encara que la primera ordre fallés, un PATH mal posat que va executar el binari equivocat.

Aquesta lliçó desmunta la shell des de dins. Veuràs que no forma part del sistema operatiu: és un programa d'usuari corrent que fa exactament el que vas aprendre a 02-01fork, execve, wait— i que tota la seva màgia consisteix a traduir text en crides al sistema. Entendràs per què cd no pot ser un programa, per què 2>&1 > fitxer no fa el que sembla i per què set -euo pipefail hauria d'encapçalar tots els teus guions. Acabarem amb una cadena que extreu de meteo-api.log les estacions més actives i amb un guió real de comprovació de les dades de Meteora.

Els serveis i l'arrencada són la lliçó següent, i les eines de rendiment la de després; aquí ens quedem a la interfície.

Contingut

  1. Què és realment una shell
  2. Shells disponibles i quina fer servir
  3. Interactiva, no interactiva, d'inici de sessió: quin fitxer es llegeix
  4. Anatomia d'una ordre i ordres internes davant d'externes
  5. L'ordre d'expansió, pas a pas
  6. Cometes i escapament: el 80 % de les errades
  7. Redirecció i descriptors de fitxer
  8. Canonades, estat de sortida i pipefail
  9. Variables de shell, variables d'entorn i PATH
  10. Control de tasques i senyals
  11. Les eines de text imprescindibles
  12. Una cadena completa sobre meteo-api.log
  13. Scripting: de l'ordre solta al programa
  14. Guió complet: verificació de les dades de Meteora

Què és realment una shell

Molta gent creu que la shell «és Linux» o que forma part del nucli. No ho és. /bin/bash és un executable ELF corrent, com ls o com meteo-api, que corre en mode usuari (01-06) i que només pot fer el mateix que qualsevol altre programa: crides al sistema. El seu bucle principal, despullat de tot l'accessori, és aquest: escriure el prompt i llegir una línia; analitzar el text (dividir-lo en paraules, expandir-lo, detectar redireccions i canonades); si l'ordre és interna, executar-la en el mateix procés; si és externa, fork() per crear un fill, aplicar-hi les redireccions, execve() el programa i waitpid() al pare; desar l'estat a $? i tornar a començar.

Ho pots veure amb l'eina presentada a 01-06:

strace -f -e trace=clone,execve,wait4 -o /tmp/shell.log bash -c 'ls /etc/meteora'
grep -E 'clone|execve|wait4' /tmp/shell.log
# execve("/bin/bash", ["bash", "-c", "ls /etc/meteora"], 0x7ffd...) = 0
# clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|SIGCHLD, ...) = 4711
# [pid  4711] execve("/bin/ls", ["ls", "/etc/meteora"], 0x55f...) = 0
# wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 4711

Què demostra. El mecanisme complet, sense misteri: la shell arrenca (execve de bash), es duplica (clone, que és el que implementa fork a Linux), el fill es transforma en ls (segon execve) i el pare espera (wait4). -f segueix els fills i -e trace= limita el soroll. Tot el que escrius en un terminal acaba sent aquesta seqüència.

D'aquesta naturalesa en surten tres conseqüències pràctiques. La shell no pot canviar res del seu procés pare: si un guió fa export PATH=..., aquesta variable mor amb el guió, i per això els fitxers de configuració es carreguen amb source (o .), que els executa a la shell actual. Cada ordre externa costa un procés: un bucle que llança grep un milió de vegades fa un milió de fork+execve, i amb les desenes de microsegons per parell que vam mesurar a 02-01 això són minuts de pur fork; d'aquí que les eines de text estiguin dissenyades per processar fluxos sencers d'un sol cop. I la shell és substituïble: canviar bash per zsh no canvia gens ni mica el sistema operatiu.

Shells disponibles i quina fer servir

Shell Camí habitual Paper Notes
sh /bin/shdash a Debian Shell POSIX mínima Ràpida, sense extensions; executa els guions del sistema
bash /bin/bash Estàndard de facto a Linux Arrays, [[ ]], PIPESTATUS, substitució de processos
zsh /bin/zsh Interactiva avançada Autocompleció superior, globbing recursiu natiu
fish /usr/bin/fish Interactiva amigable Sintaxi no POSIX: no la facis servir en guions
nologin /usr/sbin/nologin Cap Rebutja l'inici de sessió; és la «shell» de meteora

A Debian, /bin/sh apunta a dash, no pas a bash. Conseqüència real: un guió que comença per #!/bin/sh però fa servir [[ ]] o arrays fallarà a Debian i funcionarà allà on /bin/sh sigui bash. És el clàssic «a la meva màquina funciona».

I recorda del mòdul 5 que getent passwd meteora retorna meteora:x:990:990:Meteora service:/var/lib/meteora:/usr/sbin/nologin. L'últim camp és la shell d'inici de sessió, i nologin és un programa que imprimeix un missatge i surt amb codi 1. Com que el procés d'inici de sessió fa execve d'aquesta shell, ningú no pot obtenir un intèrpret interactiu com a meteora encara que n'aconsegueixi la contrasenya: el mínim privilegi de 05-01 aplicat amb una sola paraula.

Interactiva, no interactiva, d'inici de sessió: quin fitxer es llegeix

Aquí hi viu un dels errors més frustrants del principiant. Bash té dos eixos independents: interactiva o no (llegeix ordres d'un terminal amb un humà al davant, o executa un guió?) i d'inici de sessió o no (és la primera shell de la sessió, o una d'arrencada dins d'una sessió existent?). Cada combinació llegeix fitxers diferents:

Situació Inici de sessió? Interactiva? Fitxers que llegeix
ssh joan@meteo-01 /etc/profile i el primer de ~/.bash_profile, ~/.bash_login, ~/.profile
Obrir una pestanya del terminal No /etc/bash.bashrc, ~/.bashrc
bash guio.sh No No Cap (llevat de $BASH_ENV)
ssh meteo-01 'ordre' No No Cap
su - meteora Els d'inici de sessió
Servei de systemd No No Cap

L'error clàssic, en la seva forma exacta: poses export METEORA_HOME=/var/lib/meteora a ~/.bashrc, obres un terminal i funciona; després ssh meteo-01 'echo $METEORA_HOME' surt buit i un servei falla perquè la variable no existeix. No és una errada de la variable: la shell no interactiva no llegeix ~/.bashrc.

La regla pràctica és senzilla. El que defineix entorn (variables, PATH) va a ~/.profile, que es llegeix en iniciar sessió i s'hereta per tot el que arrenquis des d'allà. El que defineix comoditat interactiva (àlies, prompt, colors, historial) va a ~/.bashrc, perquè en un guió no té sentit. I el que necessita un servei no va a cap fitxer de shell: va a la seva unitat de systemd amb Environment= o EnvironmentFile= (07-02).

Per això el ~/.bashrc per defecte de Debian comença amb case $- in *i*) ;; *) return;; esac: $- conté els flags actius i la lletra i només hi apareix si la shell és interactiva. Si no ho és, return avorta la lectura. Evita una errada real i difícil de depurar: si ~/.bashrc escriu alguna cosa per la sortida estàndard —un missatge de benvinguda, per exemple—, trenca scp i rsync, que esperen un flux net per aquest canal.

Anatomia d'una ordre i ordres internes davant d'externes

Una línia es divideix en paraules separades per espais o tabuladors: la primera és l'ordre, la resta arguments, i entremig sol haver-hi opcions.

Forma Exemple Detall
Opció curta -n Una lletra; agrupable: -in = -i -n
Curta amb valor -o fitxer o -ofitxer Depèn del programa
Opció llarga --color=auto Llegible; fes-la servir en guions perquè s'entengui d'aquí a un any
Fi d'opcions -- Tot el que ve després és operand, encara que comenci per -

El separador -- no és cosmètic. Si crees ./-informe.txt, l'ordre rm -informe.txt respon rm: opció no vàlida -- 'i', perquè rm interpreta el nom com una tirallonga d'opcions; rm -- -informe.txt funciona. És una defensa obligatòria en guions que manegen noms d'origen extern: qui pugui crear fitxers en un directori que tu recorres pot injectar opcions a les teves ordres.

Interna davant d'externa. Una ordre interna (builtin) està compilada dins de la shell i no crea cap procés; una d'externa és un fitxer al disc que s'executa amb fork+execve.

type cd echo grep ll
# cd is a shell builtin
# echo is a shell builtin
# grep is /usr/bin/grep
# ll is aliased to `ls -alF'
which cd          # (res: which només busca fitxers al PATH)
type -a echo      # echo is a shell builtin / echo is /usr/bin/echo

Què demostra. type coneix el món sencer de la shell —àlies, funcions, builtins i externs—, mentre que which només recorre el PATH i per això menteix sobre els builtins: fes servir type. Fixa't en echo: n'hi ha dos, l'intern i /usr/bin/echo, i es comporten de manera diferent davant d'opcions com -e. És un dels motius pels quals en guions seriosos es prefereix printf.

I per què cd no pot ser un programa extern? Perquè el directori de treball és un atribut del procés, desat a la seva task_struct i visible a /proc/<pid>/cwd (04-02). Si cd fos un executable, la shell faria fork, el fill cridaria chdir(), canviaria el seu directori i moriria; el pare continuaria exactament on era. El canvi ha de passar dins del procés mateix de la shell. La mateixa lògica explica export, umask, ulimit, exec i source.

L'ordre d'expansió, pas a pas

Abans d'executar res, bash transforma la línia seguint un ordre fix i no negociable, que explica gairebé tots els comportaments «estranys»:

  1. Claus: {a,b}, {1..5} — 2. Titlla: ~, ~meteora — 3. Paràmetres i variables: $VAR, ${VAR:-def} — 4. Substitució d'ordres: $(ordre) — 5. Aritmètica: $(( 2 + 2 )) — 6. Divisió en paraules segons IFS — 7. Globbing: *, ?, [...] — 8. Eliminació de cometes.
mkdir -p /var/lib/meteora/lectures/{2026,2025}   # claus: genera text ABANS que existeixi res
echo ~meteora                                    # -> /var/lib/meteora  (consulta /etc/passwd)
DATA=2026-08-31; echo /var/lib/meteora/lectures/$DATA.dat
                                                 # -> /var/lib/meteora/lectures/2026-08-31.dat
echo "Fitxers: $(ls /var/lib/meteora/lectures | wc -l)"    # substitució d'ordres
echo "Bytes per lectura: $(( 17280000 / 720000 ))"         # -> 24, la nostra constant del curs

Per què l'ordre importa, amb tres demostracions que fallarien si fos un altre:

# (a) El globbing (7) va DESPRÉS de l'expansió de variables (3)
PATRO='*.dat'
echo $PATRO       # -> 2026-08-30.dat 2026-08-31.dat   (s'ha expandit!)
echo "$PATRO"     # -> *.dat                            (literal)

# (b) L'expansió de claus (1) va ABANS que la de variables (3)
N=3
echo {1..$N}      # -> {1..3}   (no és un rang vàlid i es deixa tal qual)
seq 1 "$N"        # -> 1 2 3    (la forma correcta)

# (c) La divisió en paraules (6) va DESPRÉS de la substitució d'ordres (4)
DIR=$(echo '/tmp/informe agost')
ls $DIR           # ls: no es pot accedir a '/tmp/informe' ... ni a 'agost'
ls "$DIR"         # correcte

Explicació. A (a), en substituir $PATRO obtenim el text *.dat i, com que el globbing és posterior, aquest text se sotmet un altre cop a l'expansió de noms de fitxer; amb cometes no es divideix ni es globalitza. A (b), quan bash processa les claus encara no ha substituït $N, així que veu un rang no vàlid: és un límit estructural de l'ordre, no una errada. A (c), el resultat de $(...) conté un espai i la divisió en paraules el parteix en dos arguments; les cometes dobles suprimeixen els passos 6 i 7. Mesurat en incidències reals, (c) és la causa número u de guions trencats.

Cometes i escapament: el 80 % de les errades

Forma Què protegeix Què deixa passar
'text' Tot, sense excepció Res; ni tan sols s'hi pot escapar una cometa simple a dins
"text" La divisió en paraules i el globbing $var, $(cmd), `cmd`, \ i ! (en interactiva)
\c Aquest únic caràcter
Sense cometes Res Tot s'expandeix i es divideix

La regla d'or, sense matisos: posa entre cometes totes les expansions de variable llevat que tinguis un motiu explícit per no fer-ho, i aquest motiu és rar. El cas dels noms amb espais es mereix l'exemple complet, perquè és on es fa més mal:

# MALAMENT: es trenca amb espais i amb noms que comencen per guionet
for f in $(ls /var/lib/meteora/informes); do rm $f; done

# BÉ: glob directe, cometes i -- com a tallafoc
for f in /var/lib/meteora/informes/*; do
    [ -e "$f" ] || continue      # protegeix el cas «no hi ha fitxers»
    rm -- "$f"
done

Què canvia. La versió dolenta acumula tres defectes: analitza la sortida d'ls (que formata per a humans, no per a màquines), perd el camí complet i, sense cometes, parteix cada nom pels seus espais, de manera que informe agost.pdf es converteix en dos esborrats fallits. La versió bona fa servir el globbing de la shell —que retorna camins complets i no divideix pels espais—, comprova que el patró s'ha expandit realment (si el directori és buit, bash deixa el patró literal) i fa servir --.

Redirecció i descriptors de fitxer

Reprenem 04-04: tot procés arrenca amb tres descriptors oberts —0 stdin, 1 stdout, 2 stderr— i la shell simplement els manipula entre el fork i l'execve, amb open() i dup2(). Aquest detall temporal és el que fa que el programa executat no se n'assabenti de res: quan arrenca, ja té el fd 1 apuntant on tu has dit.

Sintaxi Efecte Sintaxi Efecte
> f stdout a f, truncant-lo < f stdin des de f
>> f stdout a f, afegint-hi <<< 'txt' here-string com a stdin
2> f stderr a f <<EOF here-doc com a stdin
2>&1 stderr cap on apunta ara stdout &> f Tots dos a f (bash)

L'ordre contraintuïtiu. ordre > sortida.log 2>&1 envia tot al fitxer; ordre 2>&1 > sortida.log envia stderr a la pantalla i només stdout al fitxer. La raó és que 2>&1 significa literalment «fes que el fd 2 sigui una còpia d'on apunta el fd 1 en aquest instant»: és una fotografia, no un enllaç permanent. En el primer cas el fd 1 ja s'ha mogut al fitxer quan es processa 2>&1; en el segon encara apunta al terminal. És el dup2() de 04-04, sense cap màgia afegida.

# Separar fluxos, que és el que és professional en un guió automatitzat
/usr/local/bin/agregador > /var/log/meteora/agregador.out 2> /var/log/meteora/agregador.err

find / -name 'meteora.conf' 2>/dev/null          # descartar els «Permís denegat»

cat > /etc/meteora/limits.conf <<'EOF'           # here-doc SENSE expansió de variables
max_lectures_hora=720000
cami_dades=/var/lib/meteora/lectures
EOF

Què fan. El primer deixa traces separades i permet alertar només sobre el fitxer d'errors. El segon aprofita que find escriu els avisos per stderr i els resultats per stdout, així que descartant el 2 queda una llista neta. Al tercer, fixa't en les cometes del delimitador: <<'EOF' no expandeix variables dins del bloc, mentre que <<EOF sense cometes sí que ho fa; confondre'ls produeix fitxers de configuració amb valors buits.

Canonades, estat de sortida i pipefail

Una canonada connecta el stdout d'un procés amb el stdin del següent mitjançant el pipe() de 03-03: una memòria intermèdia de 64 KB al nucli, amb bloqueig automàtic quan s'omple o es buida. Els processos es llancen tots alhora, no en seqüència, així que en una màquina amb diverses CPU es reparteixen de debò; i quan head acaba i tanca el seu extrem, el procés anterior rep SIGPIPE i mor, per això head sobre una canonada enorme no espera que acabi tot.

El problema de l'estat de sortida. Per defecte, l'estat d'una canonada és el de l'última ordre, cosa que amaga les errades:

cat /var/lib/meteora/lectures/inexistent.dat | wc -l
echo $?
# cat: ...inexistent.dat: No existeix el fitxer o el directori
# 0        <-- èxit! perquè wc ha acabat bé

echo "${PIPESTATUS[@]}"   # -> 1 0   (estat de CADA ordre)
set -o pipefail           # a partir d'aquí, la canonada retorna 1

Per què és greu. En un guió amb set -e, aquesta canonada no avorta l'execució: el guió continua creient que tot va bé i processa zero línies com si fossin dades vàlides. És la mena d'errada silenciosa que acaba en «l'informe d'ahir va sortir buit i ningú no se'n va adonar». PIPESTATUS és un array de bash amb l'estat de tots els elements; pipefail canvia la regla perquè la canonada retorni l'estat de l'última ordre que ha fallat. És el que vols en tot guió no trivial.

tee duplica un flux: agregador 2>&1 | tee -a /var/log/meteora/agregador.log | grep -i error desa tot al registre (amb -a per afegir-hi, no truncar) i alhora deixa passar una còpia cap a grep, que mostra per pantalla només els errors. Sense tee hauries de triar entre veure o desar.

Variables de shell, variables d'entorn i PATH

Tipus Com es crea L'hereten els fills? On viu
De shell VAR=valor No Només al procés de la shell
D'entorn export VAR=valor Al bloc d'entorn, copiat per execve
LOCAL=nomes_aqui; export GLOBAL=heretada
bash -c 'echo "[$LOCAL] [$GLOBAL]"'              # -> [] [heretada]
tr '\0' '\n' < /proc/$$/environ | grep GLOBAL    # l'entorn REAL del procés

Què demostra. El fill rep una còpia del bloc d'entorn, que és el tercer argument d'execve(); les variables no exportades no hi entren mai. I /proc/<pid>/environ —una altra vegada 02-01— mostra l'entorn de qualsevol procés, amb els valors separats per bytes nuls, d'aquí el tr. Nota de seguretat: no passis mai secrets per l'entorn, perquè és llegible pel propietari del procés i apareix als bolcats; els secrets de Meteora viuen a /etc/meteora/meteora.conf amb mode 600 justament per això.

PATH és la llista de directoris separats per : on es busquen les ordres externes, d'esquerra a dreta, aturant-se a la primera coincidència. Reprenent 05-02: incloure . al PATH, i sobretot posar-lo al davant, és una errada clàssica; si un atacant deixa un fitxer anomenat ls a /tmp i un administrador amb sudo fa cd /tmp i escriu ls, executa el programa de l'atacant amb privilegis de root. Regles: no incloguis mai . ni directoris on puguin escriure altres; en guions automatitzats fes servir camins absoluts o fixa el PATH explícitament; i comprova amb type -a què s'executarà realment abans de donar res per fet.

Control de tasques i senyals

Una tasca (job) és una canonada completa llançada des de la shell interactiva. La shell li assigna un número i la pot moure entre el primer pla (rep el teclat) i el segon pla.

Acció Efecte Senyal
ordre & Arrenca en segon pla
Ctrl-C Interromp la tasca en primer pla SIGINT (2)
Ctrl-Z La suspèn SIGTSTP (20)
Ctrl-\ Acaba amb bolcat de memòria SIGQUIT (3)
Ctrl-D Cap senyal: tanca stdin (fi de fitxer)
jobs / fg %1 / bg %1 Llistar, portar a primer pla, reprendre en segon SIGCONT a fg/bg
kill %1 Acabar la tasca SIGTERM (15)
gzip /var/lib/meteora/lectures/2026-07-*.dat
^Z                    # [1]+  Aturat          gzip ...
bg %1                 # [1]+ gzip ... &
jobs -l               # [1]+ 12934 En execució  gzip ... &

Què ha passat. Ctrl-Z ha enviat SIGTSTP al grup de processos en primer pla i el procés ha passat a l'estat T (aturat) que vas veure a 02-01; bg li ha enviat SIGCONT i l'ha deixat corrent sense el terminal. Ho pots confirmar amb ps -o pid,stat,cmd -p 12934.

SIGHUP i nohup. En tancar una sessió SSH el terminal desapareix i el nucli envia SIGHUP als processos associats, que per defecte moren. nohup cmd > log 2>&1 & fa que el procés ignori aquest senyal; setsid cmd </dev/null >log 2>&1 va més lluny i crea una sessió nova sense terminal de control, de manera que el senyal ni tan sols es genera; i tmux és l'opció preferible a la pràctica perquè, a més, permet reconnectar i veure la sortida. Per a tasques periòdiques de debò, cap de les tres: es fa servir un temporitzador de systemd, tema de 07-02.

Les eines de text imprescindibles

Eina Per a què Opcions més usades
grep Filtrar línies -i, -v, -c, -n, -E, -o, -F, -r
sed Substituir i editar per línies s/a/b/g, -n '5,10p', -i
awk Processar per camps i calcular '{print $5}', -F:, bloc END
cut Extreure columnes -d' ' -f2, -c1-10
sort Ordenar -n, -r, -k2, -u, -t:
uniq Col·lapsar repetits (ordenat abans!) -c, -d
wc Comptar -l, -c, -w
head/tail Extrems -n 20, tail -f, tail -F
find Cercar per criteris -name, -mtime, -size, -exec, -print0
xargs Convertir l'entrada en arguments -0, -n, -P, -I{}

Expressions regulars bàsiques: ^ i $ són principi i final de línia; . és qualsevol caràcter; [0-9] un dígit; *, + i ? signifiquen zero o més, un o més i opcional (els dos últims requereixen -E); {3} amb -E són exactament tres repeticions; i | amb -E és l'alternativa.

grep -E ' 5[0-9]{2} ' /var/log/meteora/meteo-api.log | wc -l           # respostes 5xx
grep -oE '^[0-9]{1,3}(\.[0-9]{1,3}){3}' /var/log/meteora/meteo-api.log | sort -u
tail -F /var/log/meteora/meteo-api.log | grep --line-buffered 'ERROR'  # seguiment en viu

Detalls que importen. -E activa les expressions esteses i evita escapar {}, | i +; -o imprimeix només la part coincident. tail -F (majúscula) reobre el fitxer si es rota —imprescindible amb el logrotate de 05-04—, mentre que tail -f es queda mirant un inode que ja no escriu ningú. I --line-buffered obliga grep a escriure línia a línia: sense aquesta opció fa servir una memòria intermèdia de 4 KB en detectar que la seva sortida és una canonada, i veuries els errors amb minuts de retard; és exactament el buffering de 01-06.

find + xargs amb seguretat:

# MALAMENT: es trenca amb espais o salts de línia als noms
find /var/lib/meteora/lectures -name '*.dat' -mtime +90 | xargs rm
# BÉ: separador nul als dos extrems
find /var/lib/meteora/lectures -name '*.dat' -mtime +90 -print0 | xargs -0 --no-run-if-empty rm --
# Alternativa sense xargs, agrupant en poques invocacions
find /var/lib/meteora/lectures -name '*.dat' -mtime +90 -exec rm -- {} +

Per què. El byte nul és l'únic caràcter que no pot aparèixer en un nom de fitxer a Linux, així que -print0 amb xargs -0 és l'únic aparellament segur; --no-run-if-empty evita que rm s'executi sense arguments. A la tercera forma, -exec ... + agrupa molts fitxers per invocació, mentre que -exec ... \; llança un procés per fitxer: amb 10.000 fitxers la diferència és d'uns pocs processos davant de 10.000 parells fork+execve, o sigui segons davant de minuts.

Una cadena completa sobre meteo-api.log

Format de línia a /var/log/meteora/meteo-api.log (estil de registre combinat, amb la durada al final):

10.20.3.41 - - [31/Aug/2026:03:12:07 +0200] "GET /v1/lectures?estacio=EST-0142 HTTP/1.1" 200 8213 0.043

Objectiu: les 10 estacions que generen més peticions.

grep -F 'GET /v1/lectures' /var/log/meteora/meteo-api.log \
  | grep -oE 'estacio=EST-[0-9]{4}' \
  | cut -d= -f2 | sort | uniq -c | sort -rn | head -n 10
#   48213 EST-0142
#   31904 EST-0007
#   28755 EST-0311
Baula Què fa Per què així
grep -F Filtra l'endpoint -F busca text fix: més ràpid i sense que / o ? s'interpretin
grep -oE Extreu només el paràmetre -o descarta la resta; exigir 4 dígits elimina valors mal formats
cut -d= -f2 Es queda amb EST-0142 Delimitador =, camp 2
sort Agrupa els iguals Obligatori: uniq només col·lapsa línies adjacents
uniq -c Compta cada grup Retorna recompte valor
sort -rn Ordena per recompte -n numèric (si no, «9» aniria darrere de «48213»); -r descendent
head -n 10 Talla el top 10 A més tanca la canonada i avorta la resta via SIGPIPE

La versió amb awk, que fa el mateix en un sol procés:

awk -F'estacio=' '/GET \/v1\/lectures/ && NF>1 { split($2, p, /[" &]/); comptador[p[1]]++ }
     END { for (e in comptador) printf "%8d %s\n", comptador[e], e }' \
     /var/log/meteora/meteo-api.log | sort -rn | head -n 10

Per què és millor. -F'estacio=' parteix cada línia per aquesta cadena, de manera que $2 comença per l'identificador, i NF>1 descarta les línies sense el paràmetre; split talla al primer caràcter que no forma part del valor i p[1] queda amb EST-0142; l'array associatiu acumula i el bloc END bolca els totals. Sobre un registre de 5 milions de línies, la cadena de sis processos amb dos sort complets triga uns 40 segons i escriu temporals al disc; la versió awk recorre el fitxer una sola vegada i només ordena unes centenes de línies finals: entre 6 i 8 segons. La lliçó general és que sort sobre milions de línies sol ser el coll d'ampolla d'una canonada, i que agregar abans d'ordenar canvia l'ordre de magnitud.

Scripting: de l'ordre solta al programa

La primera línia d'un guió, el shebang, diu al nucli quin intèrpret ha de fer servir a l'execve. El nucli llegeix els dos bytes #!, pren la resta com a camí absolut i executa intèrpret camí_del_guio. Escriure #!/bin/bash exigeix que bash sigui exactament allà; #!/usr/bin/env bash el busca al PATH, cosa que funciona també allà on viu a /usr/local/bin. La contrapartida és aquesta dependència del PATH, així que en guions amb sudo o de servei convé fixar-lo.

La capçalera obligatòria, opció a opció:

set -euo pipefail
IFS=$'\n\t'
Opció Efecte Per què
-e Avorta si una ordre retorna un estat ≠ 0 Evita continuar treballant sobre una errada
-u Error en fer servir una variable no definida Converteix rm -rf "$DIR/" amb DIR buit en un error, no en un desastre
-o pipefail La canonada falla si falla qualsevol baula Sense això, -e no veu les errades de les canonades
IFS=$'\n\t' Divideix només per salt de línia i tabulador Impedeix que un espai parteixi una paraula

set -e té paranys coneguts: no salta dins d'una funció cridada en una condició, ni a l'última ordre d'una canonada sense pipefail, ni amb local var=$(cmd) —l'estat que compta és el de local, que sempre és 0—. Per això el correcte és declarar primer (local sortida) i assignar després (sortida=$(ordre)).

if [[ -f "$FITXER" && -s "$FITXER" ]]; then ... ; fi   # existeix i no és buit
[[ "$n" -gt 100 ]]           # comparació numèrica
[[ "$s" == EST-* ]]          # patró (sense cometes a la dreta)
[[ "$s" =~ ^EST-[0-9]+$ ]]   # expressió regular
while IFS= read -r linia; do ...; done < fitxer
comprovar_mida() {
    local cami="$1" esperada="$2" real
    real=$(stat -c '%s' "$cami")
    [[ "$real" -eq "$esperada" ]]   # l'estat de l'última ordre és el retorn
}

Detalls clau. [[ ]] és una construcció de bash que no divideix paraules ni fa globbing a dins, així que és més segura que [ ], que segueix les regles POSIX i falla amb valors buits. while IFS= read -r linia és la forma canònica de llegir línies: IFS= evita retallar espais als extrems i -r impedeix que \ actuï com a escapament. Una funció retorna l'estat de la seva última ordre, o el que li donis amb return N.

Els arguments posicionals són $1, $2…, amb $# per al nombre, "$@" per a tots cadascun com una paraula (fes servir sempre aquesta forma i no "$*", que els uneix en una sola cadena), $? per a l'últim estat i $$ per al PID. La convenció de codis de sortida és la del sistema: 0 èxit, 1 error genèric, 2 error d'ús, i >2 errors específics que tu defineixis.

trap per netejar, imprescindible en qualsevol guió que creï temporals o prengui un bloqueig:

TREBALL=$(mktemp -d)
netejar() { local codi=$?; rm -rf -- "$TREBALL"; exit "$codi"; }
trap netejar EXIT INT TERM

Què fa. trap registra un gestor per a senyals —els mateixos de 03-03— i per al pseudoesdeveniment EXIT, que es dispara sempre que el guió acaba, amb èxit o amb error, de manera que el temporal s'esborra encara que mori a mitges. Desar $? al començament de la funció i fer-lo servir a l'exit preserva el codi original, que altrament trepitjaria l'rm.

Guió complet: verificació de les dades de Meteora

Aquest és l'exercici d'integració de la lliçó: comprovar que els fitxers de /var/lib/meteora/lectures/ són complets i estan al dia, i avisar si falta el del dia en curs.

#!/usr/bin/env bash
# verificar-lectures.sh — Integritat i antiguitat de les dades de Meteora.
# Sortides: 0 = correcte | 1 = advertiments | 2 = error d'ús | 3 = errada crítica
set -euo pipefail
IFS=$'\n\t'

readonly DIR_DADES="/var/lib/meteora/lectures"
readonly MIDA_ESPERADA=17280000       # 720.000 lectures x 24 B
readonly TOLERANCIA_PCT=2
readonly PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

advertiments=0; critics=0
log() { local n="$1"; shift; printf '%s [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$n" "$*" >&2; }
avis()   { log WARN  "$@"; advertiments=$((advertiments + 1)); }
critic() { log ERROR "$@"; critics=$((critics + 1)); }

TREBALL=$(mktemp -d -t meteora-verif-XXXXXX)
netejar() { local codi=$?; rm -rf -- "$TREBALL"; exit "$codi"; }
trap netejar EXIT INT TERM

verbos=0; directori="$DIR_DADES"
while getopts ':d:v' o; do
    case "$o" in
        d) directori="$OPTARG" ;;
        v) verbos=1 ;;
        *) log ERROR "Ús: ${0##*/} [-d DIRECTORI] [-v]"; exit 2 ;;
    esac
done

[[ -d "$directori" && -r "$directori" ]] || { critic "No es pot llegir $directori"; exit 3; }

# --- 1. Hi és el fitxer d'avui i s'hi està escrivint? ---
avui=$(date '+%Y-%m-%d'); fitxer_avui="$directori/$avui.dat"
if [[ ! -f "$fitxer_avui" ]]; then
    critic "FALTA el fitxer del dia: $fitxer_avui"
else
    edat_min=$(( ( $(date +%s) - $(stat -c '%Y' "$fitxer_avui") ) / 60 ))
    (( edat_min > 10 )) && avis "Sense escriptures des de fa $edat_min min: ingestor aturat?"
fi

# --- 2. Integritat de mida dels fitxers ja tancats ---
minim=$(( MIDA_ESPERADA * (100 - TOLERANCIA_PCT) / 100 ))
maxim=$(( MIDA_ESPERADA * (100 + TOLERANCIA_PCT) / 100 ))
find "$directori" -maxdepth 1 -type f -name '*.dat' ! -name "$avui.dat" -print0 \
    | sort -z > "$TREBALL/fitxers.lst"

while IFS= read -r -d '' fitxer; do
    mida=$(stat -c '%s' "$fitxer"); base=$(basename -- "$fitxer")
    if (( mida % 24 != 0 )); then
        critic "$base: $mida bytes no és múltiple de 24 (fitxer truncat)"
    elif (( mida < minim || mida > maxim )); then
        avis "$base: $mida bytes, fora del rang [$minim, $maxim]"
    elif (( verbos )); then
        log INFO "$base: OK ($((mida / 24)) lectures)"
    fi
done < "$TREBALL/fitxers.lst"

# --- 3. Buits en la sèrie dels últims 30 dies ---
for desplacament in $(seq 1 30); do
    dia=$(date -d "$desplacament days ago" '+%Y-%m-%d')
    [[ -f "$directori/$dia.dat" ]] || avis "Falta el fitxer del dia $dia"
done

# --- 4. Espai lliure ---
us_pct=$(df --output=pcent "$directori" | tail -n1 | tr -dc '0-9')
if   (( us_pct >= 90 )); then critic "El sistema de fitxers és al $us_pct %"
elif (( us_pct >= 80 )); then avis   "El sistema de fitxers és al $us_pct %"
fi

log INFO "Verificació acabada: $critics crítics, $advertiments advertiments"
(( critics > 0 ))      && exit 3
(( advertiments > 0 )) && exit 1
exit 0

Decisions de disseny comentades:

  • readonly i PATH fix. El guió s'executarà des d'un temporitzador de systemd, on no hi ha PATH heretat ni directori de treball predictible; fixar-lo elimina el segrest d'ordres de 05-02.
  • log escriu a stderr. Deixa lliure la sortida estàndard per si algun dia el guió produeix dades, i journald captura tots dos fluxos igualment.
  • mktemp -d + trap. No facis servir mai un camí fix com /tmp/treball: és una condició de cursa i un vector d'atac per enllaços simbòlics. mktemp crea un directori amb nom impredictible i permisos 700.
  • find -print0 | sort -z amb read -r -d ''. El trio complet de seguretat davant de noms estranys: el separador és el byte nul de principi a fi.
  • mida % 24 != 0. És la verificació d'integritat més barata i informativa per a aquest format: com que cada Lectura ocupa exactament 24 bytes, una mida que no sigui múltiple de 24 significa escriptura truncada, segurament per un tall durant un write() sense fsync posterior (04-05).
  • Tolerància del 2 %. Un dia real rarament té 720.000 lectures exactes: una estació pot perdre cobertura uns minuts. Alertar amb tolerància zero produiria soroll diari i l'alerta acabaria ignorada.
  • Codis de sortida diferenciats. Permeten que systemd o el sistema de monitoratge distingeixin «cal mirar-ho demà» (1) de «cal actuar ara» (3).

Abans de posar en producció qualsevol guió, passa-li l'analitzador estàtic: shellcheck verificar-lectures.sh detecta exactament les errades d'aquesta lliçó —variables sense cometes, $(ls), [ ] mal fets servir, cd sense comprovar, read sense -r— i és l'eina amb més relació benefici/esforç de l'ecosistema de shell.

Errors Habituals i Consells

Error Per què passa Solució
for f in $(ls) ls formata per a humans, no per a màquines for f in ./*
Variables sense cometes La divisió en paraules és el pas 6 "$var" sempre
2>&1 > f en lloc de > f 2>&1 2>&1 copia el destí actual del fd 1 Redirigeix stdout primer
Canonada que «funciona» amb dades buides L'estat és el de l'última ordre set -o pipefail
Variable de ~/.bashrc invisible per SSH no interactiu Aquest fitxer no es llegeix ~/.profile o la unitat de systemd
rm $DIR/* amb $DIR buit Es converteix en rm /* set -u i comprovar [[ -n "$DIR" ]]
[ $a == $b ] falla amb valors buits [ ] rep menys arguments dels previstos [[ $a == $b ]]
find ... -exec cmd \; lentíssim Un procés per fitxer -exec cmd {} + o xargs -0
uniq no agrupa res Només col·lapsa línies adjacents sort abans, sempre
#!/bin/sh amb sintaxi de bash A Debian sh és dash #!/usr/bin/env bash
Funciona a mà i falla en automàtic PATH, cwd i entorn diferents Camins absoluts i PATH fix
tail -f deixa de mostrar línies El fitxer s'ha rotat tail -F

Consells que estalvien hores: abans d'un rm o d'un mv massiu, substitueix l'acció per echo i revisa la sortida completa; fes servir Ctrl-R per buscar a l'historial en lloc de reescriure ordres llargues; depura amb set -x o bash -x guio.sh, que imprimeix cada ordre ja expandida abans d'executar-la; comenta el perquè, no el què; i procura que tot guió automatitzat sigui idempotent, perquè tard o d'hora algú el reintentarà.

Exercicis

Exercici 1: expansió i cometes

Sense executar res, prediu la sortida exacta i digues quin pas de l'expansió la determina. Després comprova-ho.

cd /tmp && mkdir -p demo && cd demo
touch 'informe agost.dat' 'informe-juliol.dat' '-estrany.dat'
A='*.dat'; N=2
echo 1: $A
echo 2: "$A"
echo 3: {1..$N}
for f in *.dat; do echo "4: [$f]"; done

Exercici 2: anàlisi del registre de l'API

Amb el format de meteo-api.log d'aquesta lliçó, escriu una sola canonada (o un awk) per a: (1) comptar les peticions amb codi 5xx; (2) treure les 5 IP amb més peticions i el seu recompte; (3) calcular la latència mitjana de /v1/lectures en mil·lisegons; (4) llistar les hores del dia amb més de 100.000 peticions.

Exercici 3: guió de rotació de dades antigues

Escriu arxivar-lectures.sh que comprimeixi amb gzip els .dat de /var/lib/meteora/lectures/ amb més de 90 dies, salti els que ja estan comprimits, no toqui el del dia en curs, faci servir un bloqueig per no encavalcar-se amb ell mateix, registri el que ha fet, admeti un mode -n de simulació i retorni un codi de sortida correcte. Ha de ser segur davant de noms amb espais i ha de passar shellcheck.

Solucions

Solució 1

1: informe agost.dat informe-juliol.dat -estrany.dat
2: *.dat
3: {1..2}
4: [-estrany.dat]
4: [informe agost.dat]
4: [informe-juliol.dat]
  • Línia 1. $A se substitueix al pas 3 donant el text *.dat i, com que el globbing és el pas 7, aquest text s'expandeix un altre cop contra el directori. És el parany de desar patrons en variables.
  • Línia 2. Les cometes dobles suprimeixen els passos 6 i 7, així que s'imprimeix el valor literal.
  • Línia 3. L'expansió de claus és el pas 1, anterior a la de variables: bash veu {1..$N}, que no és un rang vàlid, i el deixa tal qual. La forma correcta és seq 1 "$N".
  • Línia 4. El bucle sobre el glob és correcte: cada nom arriba sencer, amb espais inclosos, i l'ordre és el de la configuració regional (per això -estrany.dat va primer). Si a dins hi fessis servir rm $f sense cometes, -estrany.dat s'interpretaria com a opcions: d'aquí rm -- "$f".

Solució 2

# 1. Peticions 5xx
awk '$9 >= 500 && $9 < 600 {n++} END {print n+0}' /var/log/meteora/meteo-api.log

# 2. Top 5 d'IP
awk '{c[$1]++} END {for (ip in c) printf "%8d %s\n", c[ip], ip}' \
    /var/log/meteora/meteo-api.log | sort -rn | head -n 5

# 3. Latència mitjana en ms de /v1/lectures
awk '/GET \/v1\/lectures/ {suma += $NF; n++}
     END {if (n) printf "mitjana=%.1f ms sobre %d peticions\n", suma*1000/n, n; else print "sense dades"}' \
     /var/log/meteora/meteo-api.log

# 4. Hores amb més de 100.000 peticions
awk '{split($4, t, ":"); c[t[2]]++}
     END {for (h in c) if (c[h] > 100000) printf "%s:00 -> %d\n", h, c[h]}' \
     /var/log/meteora/meteo-api.log | sort

Comentaris. A (1), $9 és el codi d'estat del format combinat i n+0 obliga a imprimir 0 en lloc d'una cadena buida quan no hi ha coincidències, detall que importa si la sortida alimenta un sistema d'alertes. A (2) es recorre el fitxer una sola vegada i només s'ordenen les IP diferents, no els milions de línies. A (3), $NF és l'últim camp (durada en segons) i la guarda if (n) evita la divisió per zero, que en awk és fatal; recorda que la mitjana amaga la cua, i a 07-03 veuràs per què el p99 és la mètrica que importa. A (4), el camp 4 és [31/Aug/2026:03:12:07, així que en partir-lo per : l'element t[2] és l'hora.

Solució 3

#!/usr/bin/env bash
# arxivar-lectures.sh — Comprimeix lectures amb més de 90 dies.
# Sortides: 0 = correcte | 1 = amb incidències | 2 = ús | 3 = crític
set -euo pipefail
IFS=$'\n\t'

readonly DIR="/var/lib/meteora/lectures"
readonly DIES=90
readonly BLOQUEIG="/run/meteora/arxivar.lock"
readonly PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

simulacio=0; errades=0; comprimits=0
log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$1" "${*:2}" >&2; }

while getopts ':n' o; do
    case "$o" in
        n) simulacio=1 ;;
        *) log ERROR "Ús: ${0##*/} [-n]"; exit 2 ;;
    esac
done

mkdir -p -- "$(dirname -- "$BLOQUEIG")"
exec 9>"$BLOQUEIG"
flock -n 9 || { log INFO "Ja hi ha una altra execució en curs; s'omet aquesta."; exit 0; }
trap 'flock -u 9' EXIT

[[ -d "$DIR" ]] || { log ERROR "No existeix $DIR"; exit 3; }
avui=$(date '+%Y-%m-%d')

while IFS= read -r -d '' f; do
    base=$(basename -- "$f")
    [[ "$base" == "$avui.dat" ]] && continue     # mai el del dia en curs
    [[ -e "$f.gz" ]] && { log WARN "Ja existeix $base.gz, s'omet"; continue; }
    (( simulacio )) && { log INFO "[simulació] gzip $base"; continue; }
    if gzip -9 -- "$f"; then
        comprimits=$((comprimits + 1)); log INFO "Comprimit $base"
    else
        errades=$((errades + 1)); log ERROR "Errada en comprimir $base"
    fi
done < <(find "$DIR" -maxdepth 1 -type f -name '*.dat' -mtime "+$DIES" -print0)

log INFO "Acabat: $comprimits comprimits, $errades errades (simulacio=$simulacio)"
(( errades > 0 )) && exit 1
exit 0

Decisions comentades:

  • exec 9>"$BLOQUEIG" + flock -n 9. Obre el fitxer de bloqueig al descriptor 9 del guió mateix i pren un bloqueig exclusiu no bloquejant: el mateix mecanisme de 04-04 que fa servir l'agregador. Si una altra instància el té, sortim amb codi 0, perquè «ja s'està executant» no és una errada del sistema i no ha de disparar cap alerta.
  • done < <(find ...) en lloc de find ... | while. És crucial: amb una canonada, el while correria en una subshell i els comptadors es perdrien en acabar, i quedarien sempre a zero.
  • -mtime "+$DIES" i -print0 deleguen el filtratge per data a find, que consulta l'inode directament, i garanteixen que cap nom amb espais o salts de línia trenqui el bucle.
  • Comprovar -e "$f.gz" evita perdre dades si una execució anterior va quedar a mitges; gzip -9 és raonable perquè els històrics s'escriuen un cop i es llegeixen molt de tant en tant, i sobre registres binaris de 24 bytes molt regulars redueix entre un 60 i un 70 %.
  • Mode simulació abans de qualsevol operació destructiva, i registre per stderr perquè journald el reculli tal qual.

Conclusió

La shell ha deixat de ser una caixa negra. Ara saps que és un programa d'usuari més el bucle del qual és llegir, expandir, fork, execve, wait, i que aquesta naturalesa explica tota la resta: per què cd ha de ser intern, per què les variables no exportades no arriben als fills, per què un guió no pot canviar el directori del seu pare i per què cada ordre externa costa un procés.

Has vist els quatre mecanismes que cal dominar sí o sí. L'ordre d'expansió, amb els seus vuit passos, que explica per què {1..$N} no funciona i per què un patró desat en una variable s'expandeix dues vegades. Les cometes, que no són decoració: suprimeixen la divisió en paraules i el globbing, i la seva absència és la primera causa de guions trencats. La redirecció, que és dup2() amb sucre sintàctic, d'on surt l'asimetria de > f 2>&1 davant de 2>&1 > f. I les canonades, que són el pipe() del mòdul 3 amb la seva memòria intermèdia de 64 KB, el seu SIGPIPE i el seu parany de l'estat de sortida que pipefail corregeix.

Sobre aquesta base has muntat eines reals: una cadena que treu del meteo-api.log les estacions més actives —i la seva versió en awk, cinc vegades més ràpida perquè agrega abans d'ordenar— i un guió de verificació que aplica totes les defenses de cop: set -euo pipefail, mktemp amb trap, -print0 amb read -d '', camins absoluts, getopts, codis de sortida diferenciats i comprovacions d'integritat basades en el format real de 24 bytes per lectura.

Però un guió solt no és un sistema. L'agregador s'ha d'executar cada hora encara que la màquina hagi estat apagada, meteo-api ha d'arrencar només després del muntatge de /var/lib/meteora i reiniciar-se si cau, i tot això ha de sobreviure a un reinici del servidor. Això ja no ho resol la shell: ho resol el gestor de serveis, la peça que arrenca amb PID 1 i decideix què corre, quan i sota quins límits.

És el tema següent: Serveis, Arrencada i systemd, on seguirem meteo-01 des que es prem el botó d'engegada fins que meteo-api accepta la primera petició al port 443.

Fonaments de Sistemes Operatius

Mòdul 1: Introducció als Sistemes Operatius

Mòdul 2: Gestió de Recursos

Mòdul 3: Concurrència

Mòdul 4: Estructures de Fitxers

Mòdul 5: Protecció i Seguretat del Sistema

Mòdul 6: Virtualització i Contenidors

Mòdul 7: Administració i Diagnòstic a la Pràctica

© Copyright 2026. Tots els drets reservats