En tancar 05-04 va quedar plantejat el problema: informe-diari.sh ja valida i extreu text amb precisió, però no sap dirigir la seva pròpia sortida. Imprimeix amb printf solts, i per desar l'informe en un fitxer cal redirigir l'script sencer des de fora —cosa que s'endú per davant també els missatges d'error—. No pot escriure a pantalla i a fitxer alhora, ni mantenir obert un registre mentre treballa, ni compondre una plantilla de vint línies sense vint printf. A 02-04 vas veure >, >>, 2>&1 i les canonades com a receptes que funcionen. Aquesta lliçó t'ensenya la maquinària que hi ha a sota, i amb ella la redirecció deixa de ser un conjunt de trucs memoritzats per convertir-se en una cosa que pots dissenyar.

Contingut

  1. Els descriptors són números
  2. Obrir, llegir i tancar descriptors propis
  3. Llegir dos fitxers alhora
  4. Duplicar descriptors i l'ordre de la redirecció
  5. Desar i restaurar la sortida estàndard
  6. Redirigir blocs, funcions i bucles
  7. Here-documents
  8. Here-strings
  9. Substitució de processos
  10. tee amb diversos destins
  11. Els fitxers especials de /dev
  12. L'informe amb plantilla

  1. Els descriptors són números

Un descriptor de fitxer és un número enter amb el qual un procés es refereix a un canal obert. No hi ha res màgic: és un índex en una taula que el nucli manté per procés. Tot procés arrenca amb tres: el 0 (stdin, el teclat), l'1 (stdout, la pantalla) i el 2 (stderr, també la pantalla).

El que no se sol explicar és que del 3 al 9 estan lliures perquè els facis servir tu, i que tota la sintaxi de redirecció que ja coneixes són operacions sobre aquesta taula:

ordre > fitxer          # equival a  ordre 1> fitxer
ordre < entrada.txt     # equival a  ordre 0< entrada.txt

El número va enganxat a l'operador: a ordre 2> errors.log el 2 enganxat al > significa «el descriptor 2 va a aquest fitxer», mentre que ordre 2 > f (amb espai) passa 2 com a argument i redirigeix stdout, un error silenciós. Per veure-ho amb els teus propis ulls, ls -l /proc/$$/fd llista els descriptors oberts del teu shell ara mateix, els tres apuntant al teu terminal (/dev/pts/3 o similar).

  1. Obrir, llegir i tancar descriptors propis

exec, sense una ordre al darrere, aplica les redireccions al shell actual en comptes de a una ordre concreta. Aquest és el mecanisme per obrir canals duradors:

exec 3< /srv/veloz/dades/enviaments.csv  # LECTURA
exec 4> ~/veloz-ops/logs/informe.txt     # ESCRIPTURA (trunca); 5>> per AFEGIR
exec 6<> /tmp/canal                      # lectura I escriptura

Es llegeix amb read -u N i s'escriu redirigint a >&N:

exec 3< /srv/veloz/dades/enviaments.csv
read -r -u 3 capcalera                   # consumeix només la primera línia
while IFS=, read -r -u 3 id data ciutat resta; do
    printf 'Enviament %s a %s\n' "$id" "$ciutat"
done
exec 3<&-                                # tancar: el &- significa "tanca aquest"

El tancament (exec 3<&- per a lectura, exec 4>&- per a escriptura) no és opcional en un script llarg: cada procés té un límit de descriptors oberts, i un que quedi obert per a escriptura pot retenir dades a la memòria intermèdia.

Per què molestar-se, si done < fitxer ja funciona? Per dos motius. Primer, eficiència: obrir un fitxer té cost, i si a cada iteració d'un bucle extern tornes a obrir el mateix fitxer, el pagues cada vegada. Amb exec 3< l'obres una vegada i el descriptor conserva la posició de lectura entre lectures —per això el read de la capçalera i el bucle posterior no llegeixen la mateixa línia—. Segon, i més important: permet llegir dos fitxers alhora.

  1. Llegir dos fitxers alhora

Un bucle while read normal consumeix stdin. Si necessites recórrer dos fitxers en paral·lel —comparar l'informe d'avui amb el d'ahir, línia a línia—, amb un sol canal és impossible. Amb descriptors propis és directe:

exec 3< ~/veloz-ops/logs/informe-2026-08-02.txt 4< ~/veloz-ops/logs/informe-2026-08-03.txt
while read -r ahir <&3 && read -r avui <&4; do
    [[ "$ahir" != "$avui" ]] && printf -- '- %s\n+ %s\n' "$ahir" "$avui"
done
exec 3<&- 4<&-       # diverses redireccions caben en un sol exec

read -r ahir <&3 redirigeix l'entrada d'aquell read concret al descriptor 3; és la forma alternativa a read -u 3, i totes dues són equivalents. El && fa que el bucle acabi quan qualsevol dels dos fitxers s'acabi.

  1. Duplicar descriptors i l'ordre de la redirecció

N>&M significa «fes que el descriptor N apunti on apunta ara M». És una còpia del destí, no un enllaç permanent, i aquesta distinció explica el clàssic que tothom memoritza sense entendre:

ordre > sortida.log 2>&1     # TOTS DOS al fitxer
ordre 2>&1 > sortida.log     # stderr a la PANTALLA, stdout al fitxer

Les redireccions es processen d'esquerra a dreta, i cadascuna copia el destí que l'altre descriptor té en aquell instant:

Ordre Pas 1 Pas 2 Resultat
> f 2>&1 1 → fitxer 2 → còpia d'1 (= fitxer) Tots dos al fitxer
2>&1 > f 2 → còpia d'1 (= pantalla) 1 → fitxer 2 a pantalla, 1 al fitxer

En el segon cas, quan 2>&1 s'executa, el descriptor 1 encara apunta a la pantalla; que 1 canviï després no arrossega el 2. La regla mnemotècnica: 2>&1 ha d'anar sempre després de la redirecció de stdout. I la drecera &> fitxer de 02-04 és exactament > fitxer 2>&1, escrita de manera que no admet aquest error. La duplicació en l'altre sentit és igual d'útil: printf 'Avis\n' >&2 escriu a stderr, i exec 3>&1 desa on apunta stdout ara.

  1. Desar i restaurar la sortida estàndard

Combinant l'anterior en surt un patró molt potent: redirigir tot el que l'script imprimeixi a partir de cert punt, i després tornar enrere.

exec 3>&1                                  # 3 recorda la pantalla
exec 1> ~/veloz-ops/logs/informe.txt       # a partir d'aquí, tot va al fitxer
taula_ciutats                              # els seus printf acaben a l'informe
exec 1>&3 3>&-                             # restaurar stdout i tancar el desat
printf 'Informe escrit a informe.txt\n'    # això ja es veu a la pantalla

Sense l'exec 3>&1 previ, no hi hauria manera de tornar: la referència original a la pantalla s'hauria perdut. Aquest patró és el que permet que una funció escrigui a fitxer sense que qui la va escriure ho hagi de saber, i és la base dels sistemes de registre que veuràs a 07-04.

  1. Redirigir blocs, funcions i bucles

No cal arribar a l'exec global. Qualsevol agrupació accepta una redirecció al final, que s'aplica a tot el seu contingut:

{                                           # bloc: s'executa en AQUEST shell
    printf 'INFORME VELOZ ENVÍOS\n'
    taula_ciutats
} > ~/veloz-ops/logs/informe.txt

( cd /srv/veloz/dades && ls -l ) > llistat.txt   # subshell: el cd no t'afecta
resum_ciutat Valencia > val.txt 2> val.err       # una funció, com una ordre
while IFS=, read -r id data ciutat resta; do     # i un bucle, al final del done
    printf '%s\n' "$ciutat"
done < /srv/veloz/dades/enviaments.csv > ciutats.txt

La diferència entre { } i ( ) és la del Mòdul 1: les claus executen al shell actual (les variables que assignis persisteixen) i els parèntesis creen un subshell (no persisteixen, però el cd no t'afecta). Sintaxi: { necessita espai després i ; o salt abans de }.

  1. Here-documents

Un here-document alimenta l'entrada estàndard d'una ordre amb un bloc de text escrit dins del mateix script:

cat << EOF
Informe de Veloz Envíos — Data: $(date +%F)
Total d'enviaments: $total
EOF

El delimitador (EOF per convenció, pot ser qualsevol paraula) marca el final, i ha d'aparèixer sol a la seva línia, sense espais al davant. Aquesta és la fallada número u amb here-documents: un delimitador indentat no tanca el bloc i l'script mor amb unexpected end of file.

Hi ha tres variants i la diferència és important:

Sintaxi Expandeix $var i $(...) Ús
<< EOF / <<- EOF (el - treu els tabuladors inicials) Plantilles amb dades; blocs indentats
<< 'EOF' No (literal) Ajuda, codi, text amb $
us() {
	cat <<- 'FI'
		Ús: informe-diari.sh [opcions] <subordre>
		Subordres: resum | detall | ciutats
		  --data AAAA-MM-DD   Dia a analitzar (per defecte, avui)
	FI
}

Dues decisions en aquest exemple. El delimitador entre cometes simples fa que el text sigui literal: sense elles, un $1 o un $HOME a l'ajuda s'expandirien. I <<- permet indentar el bloc al nivell del codi, traient els tabuladors inicials en imprimir —només tabuladors, no espais, i per això l'exemple està escrit amb tabuladors reals—.

Un here-document funciona amb qualsevol ordre que llegeixi de stdin, no només amb cat: bc -l << EOF seguit de scale=2 i l'expressió també és un ús perfectament normal.

  1. Here-strings

Quan el text cap en una línia, <<< evita el bloc sencer. Ja l'has fet servir a 04-06 amb bc:

read -r ciutat enviaments <<< "Valencia 128"
grep -c ERROR <<< "$contingut"
bc -l <<< "scale=2; $imp / 100"

Un here-string converteix una variable en l'entrada estàndard d'una ordre, i això resol el problema del subshell de la canonada que vas veure al Mòdul 4: echo "$x" | read var no funciona perquè el read corre en un subshell, mentre que read var <<< "$x" sí que assigna, perquè no hi ha canonada i per tant no hi ha subshell.

Compte: <<< afegeix sempre un salt de línia final, una cosa que es nota en comparar longituds (wc -c <<< "abc" dona 4).

  1. Substitució de processos

<(ordre) i >(ordre) són la construcció més potent de la lliçó: converteixen la sortida (o l'entrada) d'una ordre en un nom de fitxer. L'exemple canònic és diff <(sort ahir.txt) <(sort avui.txt).

Això no es pot fer amb canonades, i el motiu és estructural: diff necessita dos fitxers, i una canonada només alimenta un stdin. Bash ho resol creant un fitxer especial per cada substitució i passant-ne la ruta a l'ordre —echo <(echo hola) imprimeix /dev/fd/63, que és literalment el que l'ordre rep—.

L'ús més freqüent en scripts és el que ja va aparèixer al Mòdul 4, i ara entens per què funciona:

tail -n +2 "$CSV" | while IFS=, read -r ...; do (( total++ )); done
printf 'Total: %d\n' "$total"        # 0  ← el bucle ha corregut en un SUBSHELL

while IFS=, read -r ...; do (( total++ )); done < <(tail -n +2 "$CSV")
printf 'Total: %d\n' "$total"        # 450 ← correcte

En el primer cas, la canonada fica el bucle en un subshell i les seves variables moren amb ell. En el segon, < <(...) és una redirecció des d'un fitxer que resulta ser la sortida d'una ordre: el bucle corre al shell principal i les seves variables sobreviuen. L'espai entre < i <( és obligatori; sense ell, Bash llegeix << i espera un here-document.

>(ordre) funciona a l'inrevés, alimentant l'entrada d'una ordre: tar -czf - /srv/veloz/dades | tee >(sha256sum > copia.sha256) > copia.tar.gz arxiva i calcula la suma de verificació en una sola passada.

  1. tee amb diversos destins

tee (02-04) duplica l'entrada estàndard cap a stdout i cap als fitxers que li indiquis. Amb substitució de processos, aquests destins poden ser ordres:

informe-diari.sh ciutats | tee ~/veloz-ops/logs/informe-$(date +%F).txt \
  | tee >(grep -c incidencia > /tmp/n-incidencies) | column -t

L'informe es desa en un fitxer, es compta en paral·lel i es mostra formatat a la pantalla, tot en una passada. I si necessites que un bloc sencer es vegi i es desi, n'hi ha prou amb canalitzar-lo: { printf 'INFORME %s\n' "$(date +%F)"; taula_ciutats; } | tee ~/veloz-ops/logs/informe.txt.

Amb tee -a s'afegeix en comptes de truncar, i important per al mode estricte de 05-03: sota pipefail, el codi de sortida d'aquesta canonada és el pitjor dels tres graons, així que una fallada en escriure el fitxer sí que es detecta.

  1. Els fitxers especials de /dev

Fitxer Què és
/dev/null El forat negre: el que s'hi escriu es descarta, llegir-lo dona fi de fitxer
/dev/stdin, /dev/stdout, /dev/stderr, /dev/fd/N Noms de fitxer per a 0, 1, 2 i per al descriptor N (el que fa servir <(...))
/dev/tty El terminal real, encara que tot estigui redirigit
/dev/tcp/host/port Pseudofitxer de Bash: obre una connexió TCP

/dev/stdin serveix per a ordres que exigeixen un nom de fitxer: awk -f programa.awk /dev/stdin. I /dev/tty resol un cas real: preguntar a l'usuari encara que la sortida de l'script estigui redirigida a un fitxer, escrivint amb printf 'Continuar? [s/N] ' > /dev/tty i llegint amb read -r resposta < /dev/tty, que pren del teclat encara que stdin vingui d'una canonada.

/dev/tcp/host/port no existeix al disc: és una invenció de Bash que permet parlar TCP sense nc ni curl. El veuràs a fons a 06-04; de moment queda't amb que (echo > /dev/tcp/localhost/8080) 2>/dev/null és una comprovació de port en una línia.

  1. L'informe amb plantilla

Tot junt, aplicat a l'script vertebrador:

# generar_informe — Compon l'informe del dia. Ús: generar_informe <data> <desti>
generar_informe() {
    local data="${1:?}" desti="${2:?}" incid_pct
    incid_pct=$(percentatge "$total_inc" "$total_env")
    {
        cat << FI
  VELOZ ENVÍOS — Informe de repartiment del $data
  Generat per $(basename "$0") a $(hostname) a les $(date +%T)
FI
        taula_ciutats
        cat << FI
Resum: $total_env enviaments, $total_inc incidencies ($incid_pct %)
FI
    } | tee "$desti"
}

La plantilla viu en un here-document amb expansió, així que el text de l'informe es llegeix tal com sortirà, sense la boira de vint printf. El bloc { } agrupa la capçalera, la taula i el peu en un únic flux, i tee l'entrega simultàniament al fitxer i a la pantalla —el que l'script no sabia fer en començar la lliçó—. Els missatges de log_info/log_error segueixen anant a stderr i no contaminen l'informe.

Errors Habituals i Consells

  • 2>&1 abans de > fitxer. stderr es queda a la pantalla. Sempre després, o fes servir &>.
  • Delimitador del here-document indentat. Ha d'anar enganxat al marge, tret d'amb <<- i tabuladors.
  • Fer servir << EOF per a un text d'ajuda amb $. S'expandeix i surt buit. Fes servir << 'EOF'.
  • Escriure <<(ordre) sense espai. Bash ho llegeix com a here-document. És < <(ordre).
  • Oblidar de tancar descriptors, o fer servir echo "$x" | read var: el subshell s'endú la variable. Fes servir read var <<< "$x".
  • Redirigir cap a un fitxer que també es llegeix a la mateixa línia. sort f > f buida el fitxer: el shell el trunca abans que sort l'obri. Fes servir un temporal de mktemp (05-01) o sort -o f f.
  • Consell: exec 3>&1 abans de qualsevol redirecció global. És l'única manera de tornar, i costa una línia.

Exercicis

Exercici 1. Escriu una funció comparar_informes() que rebi dos fitxers i mostri les línies que difereixen, indicant el número de línia, llegint-los tots dos alhora amb descriptors propis i sense fer servir diff.

Exercici 2. Escriu capcalera_informe() que generi amb un here-document una capçalera amb data, servidor i usuari, i una funció us() amb un here-document literal indentat que no expandeixi res.

Exercici 3. Fes que informe-diari.sh escrigui el seu informe simultàniament a pantalla i a ~/veloz-ops/logs/informe-AAAA-MM-DD.txt, desi a part només les línies amb incidències a incidencies-AAAA-MM-DD.txt i mantingui els missatges de registre fora de tots dos fitxers.

Solucions

Solució 1.

comparar_informes() {                 # Ús: comparar_informes <fitxer1> <fitxer2>
    local a="${1:?}" b="${2:?}" l1 l2 n=0 dif=0
    [[ -r "$a" && -r "$b" ]] || { printf 'Fitxers il·legibles\n' >&2; return 66; }
    exec 3< "$a" 4< "$b"
    while read -r l1 <&3 && read -r l2 <&4; do
        (( ++n )); [[ "$l1" == "$l2" ]] && continue
        printf '%4d | -%s\n%4d | +%s\n' "$n" "$l1" "$n" "$l2"; (( ++dif ))
    done
    exec 3<&- 4<&-
    printf '%d línies comparades, %d diferències\n' "$n" "$dif"
    (( dif == 0 ))                     # codi 0 si són iguals, 1 si no
}

Un sol bucle recorre dos fitxers perquè cada read té el seu propi canal. L'última línia aprofita que (( )) fixa $? (04-06) perquè la funció retorni un codi útil sense un if explícit.

Solució 2.

capcalera_informe() {
    cat << FI
 VELOZ ENVÍOS · Informe del $(date +%F)
 Servidor: $(hostname)   Usuari: $USER
FI
}
us() {
	cat <<- 'FI'
		Ús: informe-diari.sh [--data AAAA-MM-DD] [--debug] <subordre>
		Variables: VELOZ_CSV, VELOZ_LOG_NIVELL
	FI
}

La primera fa servir << FI sense cometes perquè necessita que $(date), $(hostname) i $USER s'expandeixin. La segona fa servir <<- 'FI' perquè vol el text literal —si VELOZ_CSV no portés cometes simples, l'ajuda mostraria el seu valor en comptes del seu nom— i el guionet permet indentar el bloc amb tabuladors al nivell del codi.

Solució 3.

main() {
    local data="${1:-$(date +%F)}"
    local inf=~/veloz-ops/logs/informe-$data.txt
    local inc=~/veloz-ops/logs/incidencies-$data.txt
    {
        capcalera_informe
        taula_ciutats
    } | tee "$inf" | tee >(grep -i 'incidencia' > "$inc")
    log_info "Informe a $inf; incidències a $inc"
}

El bloc { } produeix un únic flux; el primer tee el desa complet i el continua passant; el segon el deriva cap a un grep mitjançant substitució de processos, que filtra cap al segon fitxer mentre el flux principal continua cap a la pantalla. Com que log_info escriu a stderr (decisió que vam prendre a 04-02), els seus missatges no entren a la canonada i per tant no apareixen en cap dels dos fitxers: aquest és el motiu real d'aquella convenció.

Conclusió

Els descriptors són números a la taula de canals oberts d'un procés: 0, 1 i 2 vénen donats i del 3 al 9 són teus. exec 3< f, exec 4> f i exec 6<> f els obren, read -u 3 o read <&3 en llegeixen, exec 3<&- els tanca, i el seu gran avantatge és obrir una vegada el que es llegeix moltes —i poder recórrer dos fitxers alhora—. N>&M duplica un destí en l'instant en què s'executa, i d'aquí surt l'explicació definitiva que > f 2>&1 enviï tot al fitxer mentre 2>&1 > f deixa stderr a la pantalla. Desar stdout al 3 i restaurar-lo després permet redirigir un tram sencer; { ...; } > f i done < f redirigeixen blocs, funcions i bucles sense tocar el shell global. Els here-documents donen plantilles llegibles amb expansió (<<EOF), text literal (<<'EOF') i indentació amb tabuladors (<<-EOF); els here-strings <<< fiquen una variable a stdin sense subshell. I la substitució de processos converteix ordres en fitxers: diff <(a) <(b) no té equivalent amb canonades, i done < <(ordre) és la manera correcta que un bucle conservi les seves variables.

Amb això, informe-diari.sh és un programa complet: robust, detallat quan se li demana, precís en validar i capaç de dirigir la seva sortida amb exactitud. I és també un fitxer de diversos centenars de línies on conviuen log_info, morir, validar_entorn, percentatge i mitja dotzena més de funcions que no tenen res a veure amb els informes i que qualsevol altre script del toolkit necessitaria. Copiar-les i enganxar-les al següent script seria el principi del final. A 05-06, la fita del mòdul, extraiem aquestes funcions a ~/veloz-ops/lib/comu.sh: source enfront d'executar, com localitzar la llibreria sense que les rutes relatives et traeixin, guardes d'inclusió, convenis de noms, fitxers de configuració amb el seu ordre de precedència i l'estructura definitiva del toolkit.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats