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
- Els descriptors són números
- Obrir, llegir i tancar descriptors propis
- Llegir dos fitxers alhora
- Duplicar descriptors i l'ordre de la redirecció
- Desar i restaurar la sortida estàndard
- Redirigir blocs, funcions i bucles
- Here-documents
- Here-strings
- Substitució de processos
teeamb diversos destins- Els fitxers especials de
/dev - L'informe amb plantilla
- 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:
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).
- 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 escripturaEs 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.
- 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 execread -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.
- 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 fitxerLes 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.
- 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 pantallaSense 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.
- 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.txtLa 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 }.
- Here-documents
Un here-document alimenta l'entrada estàndard d'una ordre amb un bloc de text escrit dins del mateix script:
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 |
Sí (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.
- 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).
- 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 ← correcteEn 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.
tee amb diversos destins
tee amb diversos destinstee (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 -tL'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.
- Els fitxers especials de
/dev
/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.
- 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>&1abans 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
<< EOFper 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 servirread var <<< "$x". - Redirigir cap a un fitxer que també es llegeix a la mateixa línia.
sort f > fbuida el fitxer: el shell el trunca abans quesortl'obri. Fes servir un temporal demktemp(05-01) osort -o f f. - Consell:
exec 3>&1abans 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
- 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
