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-01 —fork, 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
- Què és realment una shell
- Shells disponibles i quina fer servir
- Interactiva, no interactiva, d'inici de sessió: quin fitxer es llegeix
- Anatomia d'una ordre i ordres internes davant d'externes
- L'ordre d'expansió, pas a pas
- Cometes i escapament: el 80 % de les errades
- Redirecció i descriptors de fitxer
- Canonades, estat de sortida i
pipefail - Variables de shell, variables d'entorn i
PATH - Control de tasques i senyals
- Les eines de text imprescindibles
- Una cadena completa sobre
meteo-api.log - Scripting: de l'ordre solta al programa
- 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) = 4711Què 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/sh → dash 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 |
Sí | Sí | /etc/profile i el primer de ~/.bash_profile, ~/.bash_login, ~/.profile |
| Obrir una pestanya del terminal | No | Sí | /etc/bash.bashrc, ~/.bashrc |
bash guio.sh |
No | No | Cap (llevat de $BASH_ENV) |
ssh meteo-01 'ordre' |
No | No | Cap |
su - meteora |
Sí | Sí | 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/echoQuè 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»:
- 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 segonsIFS— 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 cursPer 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" # correcteExplicació. 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"
doneQuè 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
EOFQuè 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 1Per 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 |
Sí | 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ésQuè 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 viuDetalls 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 10Per 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ó:
| 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 TERMQuè 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 0Decisions de disseny comentades:
readonlyiPATHfix. El guió s'executarà des d'un temporitzador de systemd, on no hi haPATHheretat ni directori de treball predictible; fixar-lo elimina el segrest d'ordres de 05-02.logescriu astderr. 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.mktempcrea un directori amb nom impredictible i permisos 700.find -print0 | sort -zambread -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 cadaLecturaocupa exactament 24 bytes, una mida que no sigui múltiple de 24 significa escriptura truncada, segurament per un tall durant unwrite()sensefsyncposterior (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]"; doneExercici 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.
$Ase substitueix al pas 3 donant el text*.dati, 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 ésseq 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.datva primer). Si a dins hi fessis servirrm $fsense cometes,-estrany.dats'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 | sortComentaris. 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 0Decisions 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 defind ... | while. És crucial: amb una canonada, elwhilecorreria en una subshell i els comptadors es perdrien en acabar, i quedarien sempre a zero.-mtime "+$DIES"i-print0deleguen el filtratge per data afind, 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
stderrperquè 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
- Conceptes Bàsics de Sistemes Operatius
- Història i Evolució dels Sistemes Operatius
- Tipus de Sistemes Operatius
- Funcions Principals d'un Sistema Operatiu
- Arquitectura del Nucli: Monolític, Microkernel i Híbrid
- Mode Usuari, Mode Nucli i Crides al Sistema
Mòdul 2: Gestió de Recursos
- Gestió de Processos
- Planificació de la CPU
- Gestió de Memòria
- Memòria Virtual i Paginació
- Gestió d'Emmagatzematge
- Gestió de Dispositius
- Controladors, Interrupcions i Operacions d'E/S
Mòdul 3: Concurrència
- Conceptes de Concurrència
- Fils i Processos
- Comunicació entre Processos (IPC)
- Sincronització i Exclusió Mútua
- Problemes Clàssics de Concurrència
- Interbloquejos: Prevenció, Detecció i Recuperació
Mòdul 4: Estructures de Fitxers
- Sistemes de Fitxers
- Estructures de Directoris
- Particions, Muntatge i Sistema de Fitxers Virtual
- Gestió de Fitxers
- Assignació d'Espai, Journaling i Integritat
- Seguretat i Permisos de Fitxers
Mòdul 5: Protecció i Seguretat del Sistema
- Principis de Protecció i Control d'Accés
- Usuaris, Autenticació i Escalada de Privilegis
- Amenaces Habituals i Enfortiment del Sistema
- Auditoria, Registres i Resposta a Incidents
Mòdul 6: Virtualització i Contenidors
- Virtualització: Hipervisors i Màquines Virtuals
- Contenidors: Namespaces i cgroups
- El Sistema Operatiu al Núvol
- Sistemes Operatius Mòbils i de Temps Real
