Vam tancar el Mòdul 4 dient que a informe-diari.sh li falta robustesa, i vam prometre començar per aquí: find, xargs, tar i fitxers temporals segurs. No és casualitat que la robustesa comenci pels fitxers. Gairebé tot el que un script d'operacions fa malament ho fa en triar sobre quins fitxers actuar —esborra de més, es deixa un directori, s'ennuega amb un nom que porta un espai— o en escriure en un temporal que un altre procés pot trepitjar. En aquesta lliçó aprendràs a seleccionar fitxers amb precisió quirúrgica, a actuar-hi sense que un nom estrany trenqui res, i a crear arxius temporals i comprimits que no es converteixin en un forat de seguretat.
Contingut
- Per què el globbing no n'hi ha prou
- Anatomia de
find - Criteris de selecció
- Combinar criteris:
-a,-o,!i parèntesis -maxdepthi-mindepth: per què van al davant- Accions:
-print,-delete,-execi-execdir - Noms amb espais:
-print0ixargs -0 xargsa fons- Temporals segurs amb
mktemp - Arxivar i comprimir:
tar,gzip,zcat,zgrep - Enllaços durs i simbòlics
rsyncen dues línies
- Per què el globbing no n'hi ha prou
A 02-05 vas veure que *.log l'expandeix el shell abans que l'ordre arrenqui, i que amb shopt -s globstar fins i tot **/*.log recorre subdirectoris. Per a què cal find, doncs?
| Necessitat | Globbing? | find |
|---|---|---|
| Fitxers per nom en un directori | Sí | Sí |
| Recórrer subdirectoris | Amb globstar |
Natiu |
| Filtrar per data, mida, permisos, propietari | No | Sí |
| Distingir fitxer / directori / enllaç | Amb trucs | -type |
| Limitar la profunditat | No | -maxdepth |
| Milers de resultats | Falla: Argument list too long |
Sense límit |
Aquest últim punt és el que mata scripts en producció: el globbing construeix una línia d'ordres amb tots els noms, i el nucli la rebutja a partir d'uns dos megabytes. find processa els fitxers d'un en un. La regla pràctica: globbing per a un grapat de fitxers coneguts d'un directori; find per buscar per criteris o recórrer arbres.
- Anatomia de
find
findTres parts, sempre en aquest ordre: on buscar, què seleccionar i què fer amb el que s'ha trobat. Si ometes l'acció, find assumeix -print. Si ometes els criteris, mostra tot el que hi ha sota la ruta.
Dos detalls que causen sorpreses des del primer dia:
- El patró
'*.log'va entre cometes. Sense cometes, el shell l'expandiria abans de llançarfind, amb els fitxers del directori actual, ifindrebria una cosa que no esperava. És el mateix principi de cometes de 03-06: aquí el patró és per afind, no per al shell. findrecorre recursivament des de la ruta indicada.find .pot trigar minuts en un arbre gran.
- Criteris de selecció
find /srv/veloz/dades -name 'enviaments*.csv' # per nom, sensible a majúscules
find /srv/veloz/dades -iname 'ENVIAMENTS*.CSV' # -i: ignora majúscules → troba enviaments.csv
find /srv/veloz -path '*/historic/2026/*' # el patró s'aplica a la RUTA sencera
find /var/log/veloz -type f # només fitxers regulars
find /srv/veloz -type d -name 'historic' # només directoris
find ~/veloz-ops -type l # només enllaços simbòlicsCompte amb la diferència entre -name i -path: -name compara només el nom final (el basename de 04-04), mentre que -path compara la ruta sencera tal com find la va generant, així que els seus * sí que travessen barres.
Els criteris de mida i temps són els que fan find insubstituïble:
| Criteri | Significat | Exemple |
|---|---|---|
-size +100M |
més gran de 100 MiB (k, M, G; c = bytes) |
logs desbocats |
-size -1k |
més petit d'1 KiB | informes buits |
-mtime +30 |
modificat fa més de 30 dies | purga d'històric |
-mtime -1 |
modificat en les últimes 24 h | informe d'avui |
-mmin -15 |
modificat en els últims 15 minuts | vigilància |
-newer f |
més recent que el fitxer f |
canvis des de l'últim informe |
-user joan |
propietat d'aquest usuari | auditoria |
-perm 600 |
permisos exactament 600 | comprovar veloz-ops.conf |
-perm -o=w |
escrivible per altres (perill) | auditoria de seguretat |
-empty |
fitxer buit o directori sense contingut | neteja |
El signe importa molt: + és «més de», - és «menys de», i sense signe és exactament. Un -mtime 30 significa «modificat el dia 30 comptant cap enrere», gairebé mai el que vols. I -mtime compta en blocs de 24 hores truncant: per a finestres fines, fes servir -mmin.
find ~/veloz-ops/logs -type f -name 'informe-*.txt' -mtime +30
# /home/joan/veloz-ops/logs/informe-2026-06-15.txt
# /home/joan/veloz-ops/logs/informe-2026-06-14.txtAquí tens la purga d'informes vells que necessita el toolkit: tots els informes de més d'un mes.
- Combinar criteris:
-a, -o, ! i parèntesis
-a, -o, ! i parèntesisQuan poses dos criteris seguits, find els uneix amb un I implícit. Per a la resta hi ha operadors:
find ~/veloz-ops/logs -name '*.txt' -o -name '*.log' # O lògic
find /var/log/veloz -type f ! -name '*.gz' # NO: els no comprimits
find /srv/veloz -type f \( -name '*.csv' -o -name '*.tsv' \) -mtime +90Els parèntesis s'escapen (\\( i \\)) o es posen entre cometes ('('), perquè per al shell els parèntesis sense escapar signifiquen subshell. I són imprescindibles així que barreges -o amb un altre criteri, perquè -a té més prioritat que -o: sense parèntesis, aquest últim exemple voldria dir «els .csv (de qualsevol data) o els .tsv de més de 90 dies».
Aquest error és especialment greu combinat amb -delete. Comprova sempre amb -print abans de substituir-lo per una acció destructiva.
-maxdepth i -mindepth: per què van al davant
-maxdepth i -mindepth: per què van al davantfind /srv/veloz/dades -maxdepth 1 -name '*.csv' # només el nivell superior
find /srv/veloz/dades/historic -mindepth 2 -maxdepth 2 -type d # els AAAA/MM-maxdepth 0 és la mateixa ruta de partida; 1 són els seus fills directes. -mindepth 1 exclou la ruta de partida, útil per buidar un directori sense esborrar-lo.
Aquestes dues opcions han d'anar abans que qualsevol altre criteri. Tècnicament no són criteris sinó opcions globals: afecten tot el recorregut, no un fitxer concret. Si escrius find . -name '*.log' -maxdepth 1, find avisa amb warning: -maxdepth is a global option i l'aplica igualment, però és un senyal que no has entès l'ordre i algun dia et mossegarà amb opcions que no perdonen.
- Accions:
-print, -delete, -exec i -execdir
-print, -delete, -exec i -execdirfind ~/veloz-ops/logs -name 'informe-*.txt' -mtime +30 -print # llistar (per defecte)
find ~/veloz-ops/logs -name 'informe-*.txt' -mtime +30 -delete # esborrar
find /var/log/veloz -name '*.log.[0-9]' -exec gzip {} \; # executar per fitxer
find /var/log/veloz -name '*.log.[0-9]' -exec gzip {} + # executar per lots-delete és més segur i ràpid que -exec rm {} \; perquè no llança processos i no té problemes amb noms estranys, però ha d'anar al final: find . -delete -name '*.txt' esborra tot l'arbre abans de mirar el nom.
La diferència entre \; i + és la que costa més rendiment ignorar. El {} és el marcador on find insereix el nom del fitxer, i el terminador decideix com s'agrupen:
| Terminador | Invocacions | Amb 5.000 fitxers | Quan fer-lo servir |
|---|---|---|---|
\; |
Una per fitxer | 5.000 processos (~30 s) | L'ordre només accepta un argument, o vols el codi de sortida individual |
+ |
Una per lot (milers d'arguments) | 2-3 processos (<1 s) | Gairebé sempre: grep, gzip, rm, chmod |
El \; va escapat pel mateix motiu que els parèntesis: sense escapar, el ; acaba l'ordre per al shell.
-execdir és la variant segura —find /srv/veloz/dades -name '*.tmp' -execdir rm -- {} \;—: executa l'ordre dins del directori on és cada fitxer i li passa ./nom en comptes de la ruta sencera. Això evita atacs en què algú reanomena un directori a mitja passada i evita que un fitxer anomenat -rf s'interpreti com una opció.
- Noms amb espais:
-print0 i xargs -0
-print0 i xargs -0Aquest és el clàssic que separa qui copia receptes de qui entén el shell: find ~/veloz-ops/logs -name '*.txt' | xargs rm està trencat.
find imprimeix un nom per línia i xargs separa per espais en blanc. Un fitxer anomenat informe juliol.txt arriba com a dos arguments: xargs intenta esborrar informe i juliol.txt. Un nom amb salt de línia encara és pitjor. La solució canònica fa servir l'únic byte que no pot aparèixer en un nom de fitxer, el nul:
-print0 separa amb \0 en comptes de \n; -0 li diu a xargs que esperi aquest separador. No canalitzis mai find cap a xargs sense aquesta parella. Si l'ordre ho permet, -exec ... + fa el mateix sense canonada i sense risc.
xargs a fons
xargs a fonsxargs llegeix de l'entrada estàndard i construeix línies d'ordres amb el que ha llegit. Les seves opcions útils:
| Opció | Efecte |
|---|---|
-0 |
Separador nul (sempre, amb find -print0) |
-n N |
Com a màxim N arguments per invocació |
-I{} |
Substitueix {} per cada element, un a un (implica -n 1) |
-P N |
Executa fins a N invocacions en paral·lel |
-t |
Mostra a stderr l'ordre abans d'executar-la (depuració) |
-r |
No executar res si l'entrada és buida (GNU; vital en scripts) |
# Comprimir els CSV de l'històric, 4 alhora, mostrant què fa
find /srv/veloz/dades/historic -name '*.csv' -print0 \
| xargs -0 -r -t -P 4 -n 1 gzipSense -r, si el find no troba res, xargs executa gzip sense arguments i es queda esperant a l'entrada estàndard: l'script es penja sense explicació. És una errada real i freqüent en tasques programades.
-I{} serveix quan el nom no va al final de l'ordre —xargs -0 -I{} cp -- {} /srv/veloz/copia/—, però no agrupa: una invocació per fitxer, amb el cost que ja coneixes. Fes-lo servir només quan la posició de l'argument ho exigeixi.
- Temporals segurs amb
mktemp
mktempinforme-diari.sh necessita un fitxer intermedi per ordenar la taula abans d'imprimir-la. La temptació és tmp=/tmp/informe.$$, i està malament: $$ és el PID, que ni és secret ni és impredictible. /tmp el pot escriure qualsevol. Dos atacs clàssics: la condició de cursa (un atacant crea el fitxer entre la teva comprovació i la teva escriptura) i l'atac per enllaç simbòlic (crea /tmp/informe.12345 apuntant a ~/.ssh/authorized_keys, i el teu script hi escriu amb els teus permisos). La solució és delegar en el sistema:
tmp=$(mktemp) # /tmp/tmp.aX9k2Lp0
tmpdir=$(mktemp -d) # directori, permisos 700
tmp=$(mktemp -t informe-diari.XXXXXX) # amb prefix reconeixible
tmp=$(mktemp -p ~/veloz-ops/logs inf.XXXXXX) # en un directori concretmktemp crea el fitxer atòmicament, amb nom aleatori i permisos 600 (700 per a directoris). No hi ha finestra de cursa i ningú més no el pot llegir. Les XXXXXX (mínim sis) són on s'insereix la part aleatòria.
Falta l'altra meitat: esborrar-lo passi el que passi, també si l'script mor a mitges. Això es fa amb trap, i és la primera línia de 05-03: tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT. Queda't amb la parella: qui crea un temporal, a la línia següent en programa la neteja.
- Arxivar i comprimir:
tar, gzip, zcat, zgrep
tar, gzip, zcat, zgreptar agrupa molts fitxers en un (.tar); gzip comprimeix un fitxer. Junts donen el .tar.gz de tota la vida:
cd /srv/veloz/dades # clau: ens situem a l'arrel de l'arbre
tar -czf ~/veloz-ops/logs/historic-2026-07.tar.gz historic/2026/07
tar -tzf ~/veloz-ops/logs/historic-2026-07.tar.gz | head -3 # llistar sense extreure
tar -xzf ~/veloz-ops/logs/historic-2026-07.tar.gz -C /tmp/restaurar # extreureLes lletres: c crear, x extreure, t llistar, z passar per gzip, f fitxer destí, -C canviar de directori abans d'actuar. j fa servir bzip2 i J fa servir xz (més lent, comprimeix més).
Fes servir sempre rutes relatives. Si arxives /srv/veloz/dades/historic, tar avisa amb Removing leading '/' i, en versions antigues o amb l'opció -P, l'extracció escriuria directament sobre /srv/veloz/dades en qualsevol màquina, esclafant el que hi hagués. Amb rutes relatives decideixes on restaurar amb -C. El patró correcte és el cd previ (o tar -C /srv/veloz/dades -czf ... historic/2026/07).
--exclude accepta globs i es pot repetir. I per als logs rotats de /var/log/veloz no cal descomprimir res:
tar -czf copia.tar.gz --exclude='*.tmp' --exclude='logs/*' veloz-ops
zcat /var/log/veloz/acces.log.2.gz | wc -l # com cat, però per a .gz
zgrep -c ' 500 ' /var/log/veloz/acces.log.*.gz # com grep, sobre comprimits
gzip -d acces.log.2.gz # descomprimeix i ESBORRA el .gz
gzip -k acces.log.1 # -k conserva l'originalzgrep sobre els rotats és el que permet a informe-diari.sh respondre «quants errors 500 hi va haver la setmana passada?» sense ocupar disc extra.
- Enllaços durs i simbòlics
ln /srv/veloz/dades/enviaments.csv /srv/veloz/dades/enviaments-avui.csv # dur
ln -s /srv/veloz/dades/enviaments.csv ~/veloz-ops/enviaments-actual.csv # simbòlic
readlink -f ~/veloz-ops/enviaments-actual.csv # /srv/veloz/dades/enviaments.csv
find ~/veloz-ops -type l ! -exec test -e {} \; -print # enllaços trencatsEnllaç dur (ln) |
Enllaç simbòlic (ln -s) |
|
|---|---|---|
| Què és | Un altre nom per a les mateixes dades | Un fitxer que conté una ruta |
| Entre discos | No | Sí |
| A directoris | No | Sí |
| Si esborres l'original | Les dades segueixen vives | L'enllaç queda trencat |
ls -l mostra |
Un fitxer normal | enllaç -> destí |
A la pràctica: simbòlics per al 95 % dels casos (apuntar a l'«últim informe», canviar de versió activa), durs per a còpies barates dins del mateix sistema de fitxers. readlink -f resol la cadena sencera fins a la ruta real, i és la manera fiable de saber a què apunta una cosa.
rsync en dues línies
rsync en dues líniesrsync -a --delete origen/ desti/ sincronitza arbres copiant només el que ha canviat, funciona sobre SSH i és l'eina correcta per a còpies de seguretat incrementals. El seu lloc natural és 07-03; de moment queda't amb que existeix i amb que la barra final de l'origen canvia el significat.
Errors Habituals i Consells
- Oblidar les cometes a
-name '*.log'. El shell expandeix el patró abans ifindbusca una altra cosa. -deleteabans dels criteris. Esborra l'arbre sencer. Posa'l sempre al final i prova abans amb-print.find ... | xargssense-print0/-0. Un espai en un nom i esborres el que no tocava.xargssense-r. Amb entrada buida executa l'ordre sense arguments: penjada o desastre.-osense parèntesis. La precedència de-acanvia el sentit de l'expressió sense avisar.-mtime 30sense signe. Significa «exactament el dia 30», no «més de 30 dies».taramb rutes absolutes. Restaurar esclafa el sistema original. Fes servircdo-Ci rutes relatives.- Consell: construeix els
finddestructius en dos passos. Primer amb-print, revises la llista, i només llavors canvies l'acció. En un script, desa la llista en un temporal demktempi actua-hi.
Exercicis
Exercici 1. Escriu una funció purgar_informes() que esborri de ~/veloz-ops/logs els fitxers informe-*.txt de més de 30 dies i els fitxers buits de qualsevol antiguitat, informant de quants n'ha esborrat. Ha de funcionar amb noms que continguin espais.
Exercici 2. Arxiva l'històric del mes anterior (/srv/veloz/dades/historic/AAAA/MM/) a ~/veloz-ops/logs/historic-AAAA-MM.tar.gz excloent-ne els .tmp, verifica que l'arxiu es pot llistar i només llavors esborra els originals.
Exercici 3. Compta quantes peticions amb codi 500 hi ha a tots els acces.log* de /var/log/veloz, inclosos els comprimits, fent servir un fitxer temporal segur per al resultat intermedi.
Solucions
Solució 1.
purgar_informes() {
local dir="${1:-$HOME/veloz-ops/logs}" n=0
while IFS= read -r -d '' f; do
rm -- "$f"; (( ++n ))
done < <(find "$dir" -maxdepth 1 -type f \
\( -name 'informe-*.txt' -mtime +30 -o -empty \) -print0)
printf 'Purgats %d fitxers de %s\n' "$n" "$dir"
}read -d '' llegeix fins al byte nul, que és el que emet -print0: la parella exacta del patró de la secció 7. Els parèntesis agrupen la condició «(vell I amb aquest nom) O buit», i -maxdepth 1 impedeix que la purga s'esmunyi cap als subdirectoris. El -- abans de "$f" protegeix de noms que comencin per guionet.
Solució 2.
mes=$(date -d 'last month' +%m); any=$(date -d 'last month' +%Y)
desti=~/veloz-ops/logs/historic-$any-$mes.tar.gz
tar -C /srv/veloz/dades --exclude='*.tmp' -czf "$desti" "historic/$any/$mes"
if tar -tzf "$desti" > /dev/null; then
find "/srv/veloz/dades/historic/$any/$mes" -type f -delete
printf 'Arxivat i purgat %s/%s\n' "$any" "$mes"
else
printf "ERROR: l'arxiu %s no es pot llegir, no s'esborra res\n" "$desti" >&2
fiEl date -d 'last month' ve de 04-06. L'ordre és innegociable: verificar abans d'esborrar. El tar -tzf cap a /dev/null no mostra res però retorna un codi diferent de zero si l'arxiu està corromput, i l'if ho aprofita exactament com a 03-04.
Solució 3.
tmp=$(mktemp -t veloz500.XXXXXX)
zgrep -h ' 500 ' /var/log/veloz/acces.log* > "$tmp"
printf 'Errors 500 en total: %d\n' "$(wc -l < "$tmp")"
cut -d' ' -f7 "$tmp" | sort | uniq -c | sort -rn | head -5 # rutes més afectades
rm -f "$tmp"zgrep processa indistintament els fitxers plans i els .gz, i -h suprimeix el prefix amb el nom del fitxer perquè cut vegi sempre la mateixa estructura de columnes. El rm final és correcte, però només s'executa si l'script arriba viu fins aquí: a 05-03 el substituiràs per un trap que garanteixi la neteja fins i tot davant d'una fallada.
Conclusió
find és el cercador que el globbing no pot substituir: selecciona per nom, ruta, tipus, mida, data, propietari, permisos o buidor, combina criteris amb -a, -o, ! i parèntesis escapats, acota el recorregut amb -maxdepth/-mindepth —que van al davant per ser opcions globals— i actua amb -print, -delete, -exec ... \;, -exec ... + (milers de vegades més barat) o -execdir. Quan cal canalitzar, la parella -print0 amb xargs -0 és obligatòria, i -r, -n, -I{}, -P i -t completen l'instrumental. mktemp i mktemp -d creen temporals atòmics, privats i impredictibles, en comptes del /tmp/fitxer.$$ que convida que et trepitgin. tar amb rutes relatives arxiva i zcat/zgrep llegeixen els logs rotats sense descomprimir-los. I els enllaços, durs o simbòlics, donen noms alternatius al que ja existeix.
Amb això informe-diari.sh ja sap triar sobre quins fitxers treballa i on deixa els seus intermedis. Però un script d'operacions no només toca fitxers: llança processos, i de vegades uns quants alhora. És viva la veloz-api abans de demanar-li dades? Puc calcular les quatre ciutats en paral·lel en comptes d'una darrere l'altra? Què passa si l'informe de les 6:00 encara corre quan arrenca el de les 6:05? A 05-02 entrem en la gestió de processos: PIDs, ps, pgrep, treballs en segon pla, wait, senyals, timeout i el bloqueig amb flock que impedeix que dos informes es trepitgin.
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
