En tancar el Mòdul 5 va quedar anotada la primera mancança del toolkit: cada vegada que cal fer comptes per columnes sobre enviaments.csv, informe-diari.sh encadena cut, sort i uniq, i rellegeix el fitxer un cop per cada dada que vol. Sumar imports per ciutat, comptar per estat i calcular la mitjana per repartidor són avui tres canonades diferents i tres lectures del disc. awk resol les tres en una sola passada, perquè no és una ordre més: és un petit llenguatge dissenyat per a text organitzat en registres i camps. Aquesta lliçó el presenta com a tal.

Contingut

  1. Per què awk no és una ordre més
  2. El model d'execució: patró { acció }, BEGIN i END
  3. Registres, camps i variables integrades
  4. Separadors d'entrada i de sortida
  5. Patrons
  6. Accions: print, printf, condicionals i bucles
  7. Variables d'usuari i la seva inicialització automàtica
  8. Arrays associatius: agregar en una sola passada
  9. Recórrer i ordenar el resultat
  10. Funcions integrades
  11. Coma flotant de franc i variables de Bash amb -v
  12. Programes llargs, fitxers .awk i sabors
  13. Aplicació: el nou nucli d'informe-diari.sh

  1. Per què awk no és una ordre més

grep decideix si una línia li interessa, cut extreu una columna, sort i uniq -c compten. Cadascun fa una cosa i per això s'encadenen. awk fa les quatre alhora perquè té el que a ells els falta: memòria entre línies (variables i arrays que sobreviuen d'un registre al següent) i aritmètica.

Aquesta és la diferència de fons: una canonada és un flux sense estat; un programa awk és un bucle amb estat. Tan bon punt la pregunta deixa de ser «quines línies?» i passa a ser «quant suma això agrupat per allò?», la canonada creix i awk es queda igual de curt.

  1. El model d'execució: patró { acció }, BEGIN i END

Un programa awk és una llista de regles. Per a cada línia de l'entrada, awk recorre totes les regles en ordre: si el patró es compleix, executa l'acció. A awk '/ERROR/ { print $0 }' /var/log/veloz/app.log, el patró és /ERROR/ i l'acció { print $0 }: awk llegeix una línia, comprova, imprimeix si escau i passa a la següent. No hi ha cap bucle escrit, el bucle és implícit, i és el primer que cal interioritzar. Les dues parts són opcionals:

Escrius Significa
awk '/ERROR/' Patró sense acció → l'acció per defecte és { print $0 }
awk '{ print $3 }' Acció sense patró → patró buit: es compleix a totes les línies
awk '/ERROR/ { print $3 }' Tots dos: el camp 3 només de les línies amb ERROR

Dos patrons especials no depenen de cap línia: BEGIN { } s'executa un cop abans de llegir res (configurar separadors, imprimir capçaleres) i END { } un cop després de l'última línia, que és on s'aboquen els totals.

awk '/ERROR/ { n++ } END { print "Errors:", n }' /var/log/veloz/app.log   # -> Errors: 47

Aquí ja tens el patró que domina la lliçó: acumular durant el recorregut, imprimir a END. El programa va sempre entre cometes simples: a dins hi ha $1, $NF… i amb cometes dobles Bash els expandiria als arguments de l'script (03-06) abans que awk els veiés.

  1. Registres, camps i variables integrades

awk parteix l'entrada en registres (per defecte, línies) i cada registre en camps (per defecte, espais o tabuladors). $0 és el registre complet, $1, $2… els camps comptant des d'1, NF el nombre de camps i per tant $NF l'últim i $(NF-1) el penúltim. Els parèntesis són obligatoris: $NF-1 significa «l'últim camp, menys un», una resta.

Variable Significat Per defecte
NR Número de registre global (comptant tots els fitxers)
FNR Número de registre dins del fitxer actual
NF Nombre de camps del registre actual
FS / OFS Separador de camps d'entrada / de sortida Espais / un espai
RS / ORS Separador de registres d'entrada / de sortida Salt de línia
FILENAME Nom del fitxer que s'està llegint

$NF és or quan la línia té un nombre variable de camps: a acces.log els bytes són sempre l'últim, encara que la petició tingui més o menys paraules, així que awk '{ s += $NF } END { printf "%.2f MB\n", s/1048576 }' /var/log/veloz/acces.log funciona sense comptar columnes.

NR i FNR es diferencien només amb diversos fitxers: en començar el segon, FNR torna a 1 i NR continua pujant. D'aquí ve l'idioma per saltar capçaleres CSV: awk -F, 'FNR>1' enviaments.csv salta totes les capçaleres, mentre que NR>1 només saltaria la del primer fitxer. Amb historic/2026/*/enviaments.csv, aquesta diferència és un error de dades silenciós.

  1. Separadors d'entrada i de sortida

Per al CSV cal dir-li a awk que el separador és la coma, i hi ha dues formes equivalents: l'opció awk -F, '{ print $3 }' enviaments.csv i l'assignació awk 'BEGIN { FS="," } { print $3 }' enviaments.csv. -F, és l'habitual; FS a BEGIN s'usa quan el separador és complex, perquè admet una expressió regular: FS="[,;]" accepta coma o punt i coma, i FS="[[:space:]]+" col·lapsa espais múltiples.

awk -F, 'BEGIN { OFS="\t" } FNR>1 { print $3, $6 }' /srv/veloz/dades/enviaments.csv | head -2
Valencia	34.50
Sevilla	18.90

La coma de print $3, $6 significa «separa'ls amb OFS». Sense coma, print $3 $6 concatena: Valencia34.50. És la confusió número u amb awk. A més, OFS només s'aplica quan awk reconstrueix el registre; si imprimeixes $0 sense tocar-lo surt tal qual, i el truc per forçar-ho és assignar-se un camp a si mateix ({ $1=$1; print }).

  1. Patrons

El patró pot ser qualsevol expressió que awk avaluï com a certa:

Patró Selecciona
/ERROR/ Línies que contenen ERROR (regex ERE, la de 05-04)
$5 == "incidencia" Registres el camp 5 dels quals és exactament aquest text
$5 ~ /^inc/ Camp 5 que casa amb la regex (!~ per al contrari)
$6 > 50 Comparació numèrica sobre el camp 6
NR == 1 Només la primera línia
/inici/,/fi/ Rang: des de la línia que casa amb la primera fins a la que casa amb la segona
$3=="Madrid" && $5=="incidencia" Combinació lògica amb &&, || i !
!/ERROR/ Negació: línies que no contenen ERROR
(buit) Totes les línies
awk -F, '$3 == "Madrid" && $5 == "incidencia" && $6 > 50 { print $1, $4, $6 }' \
    /srv/veloz/dades/enviaments.csv

Aquesta línia substitueix un grep | grep | cut o un while read de set línies. awk decideix sol si compara com a número o com a text: $6 > 50 és numèric perquè 50 ho és; $3 == "Madrid" és textual.

  1. Accions: print, printf, condicionals i bucles

Dins de { } hi cap un llenguatge complet amb sintaxi semblant a C: assignacions, if/else, for i while.

awk -F, 'FNR > 1 {
    if ($5 == "incidencia")   printf "%-10s %-8s %8.2f  <-- REVISAR\n", $3, $4, $6
    else if ($6 > 100)        printf "%-10s %-8s %8.2f  (alt)\n",       $3, $4, $6
}' /srv/veloz/dades/enviaments.csv

printf funciona com el de Bash que vas veure a 04-04 (%-10s alinea text a l'esquerra en 10 columnes, %8.2f un número amb 2 decimals), amb una diferència important: el \n s'ha de posar sempre, perquè no l'afegeix; print sí que l'afegeix. El for amb sintaxi de C serveix per recórrer els camps d'un registre: for (i=1; i<=NF; i++) if ($i == "") print FILENAME": linia "FNR" camp "i detecta camps buits al CSV.

  1. Variables d'usuari i la seva inicialització automàtica

A awk no es declaren variables: s'usen. I el que més codi estalvia és que una variable nova val 0 si la tractes com a número i cadena buida si la tractes com a text.

A awk -F, 'FNR>1 { total += $6; n++ } END { print n, total, total/n }' enviaments.csv, total i n no existeixen abans de la primera línia i tot i així total += $6 funciona: awk les crea valent 0, mentre que a Bash hauries d'escriure total=0; n=0 abans del bucle. El revers: un nom mal escrit no dona error, val 0. Si acumules a totl i imprimeixes total, el resultat serà 0 sense cap queixa.

  1. Arrays associatius: agregar en una sola passada

Aquest és el motiu de la lliçó. Els arrays d'awk són sempre associatius —l'índex és una cadena, com els de 04-03— i també es creen sols:

awk -F, 'FNR>1 { import[$3] += $6 } END { for (c in import) print c, import[c] }' \
    /srv/veloz/dades/enviaments.csv     # -> Sevilla 4820.30 / Valencia 6944.10 / Madrid 8210.55

Llegeix-ho a poc a poc: per a cada registre, agafa el camp 3 (la ciutat), fes-lo servir com a índex i suma-li el camp 6. No cal saber per endavant quines ciutats hi ha, ni ordenar, ni recórrer dues vegades. L'estructura és sempre { acumulador[clau] += valor } durant el recorregut i END { for (k in acumulador) ... } al final. I com que les regles són independents, les tres preguntes del principi caben en un programa i una sola lectura:

awk -F, 'FNR > 1 {
    import[$3] += $6                           # suma d'imports per ciutat
    estat[$5]++                                # recompte per estat
    suma_rep[$4] += $6; n_rep[$4]++            # per a la mitjana per repartidor
}
END {
    for (c in import)   printf "ciutat  %-10s %10.2f\n", c, import[c]
    for (e in estat)    printf "estat   %-12s %6d\n",    e, estat[e]
    for (r in suma_rep) printf "mitjana %-8s %8.2f\n",   r, suma_rep[r]/n_rep[r]
}' /srv/veloz/dades/enviaments.csv

Un array es pot indexar a més per la combinació de dos camps, i això dona taules creuades de franc: cella[$3, $5] += $6 suma imports per ciutat i estat alhora.

  1. Recórrer i ordenar el resultat

for (k in array) no garanteix cap ordre: awk recorre la seva taula hash com li convé. Si necessites un ordre, el que és portable és imprimir i ordenar per canonada amb el sort de 02-02:

awk -F, 'FNR>1 { i[$3] += $6 } END { for (c in i) printf "%.2f\t%s\n", i[c], c }' enviaments.csv | sort -rn

Fixa't que s'imprimeix primer el número, perquè sort -rn ordeni per ell sense opcions estranyes. GNU awk té PROCINFO["sorted_in"] i asorti(), però no són portables; la canonada sí.

  1. Funcions integrades

Funció Què fa Exemple
length(s) Longitud de la cadena (o de $0 si s'omet) length($4)
substr(s, i, n) Subcadena des de la posició i (comença a 1) substr($2,1,7)2026-08
split(s, arr, sep) Parteix s a l'array arr; retorna quants trossos split($2,f,"-")
sub(re, rep) Substitueix la primera coincidència a $0 (o al 3r argument) sub(/^ +/, "")
gsub(re, rep) Substitueix totes; retorna quantes gsub(/,/, ".", $6)
index(s, t) Posició de t dins de s, o 0 index($0,"ERROR")
toupper(s) / tolower(s) Canvia de caixa toupper($3)
int(x) Trunca a enter (no arrodoneix) int(4.9)4
sprintf(fmt, ...) Com printf, però retorna la cadena k = sprintf("%s/%s",$3,$5)

Combinades amb els arrays donen agregacions que a Bash costarien un bucle sencer: awk -F, 'FNR>1 { mes[substr($2,1,7)] += $6 } END { for (m in mes) print m, mes[m] }' enviaments.csv | sort factura per mes extraient l'AAAA-MM de la data.

  1. Coma flotant de franc i variables de Bash amb -v

A 04-06 va quedar clar que $(( )) només fa enters i que per als decimals calia sortir a bc -l. A awk tota l'aritmètica és en coma flotant, sense instal·lar res:

awk -F, 'FNR>1 { t++; if ($5=="lliurat") ok++ }
         END { printf "Taxa de lliurament: %.1f%%\n", 100*ok/t }' enviaments.csv  # -> 87.3%

Això converteix awk en la calculadora natural d'un script: pct=$(awk -v a="$ok" -v b="$tot" 'BEGIN { printf "%.1f", 100*a/b }') substitueix bc sense canonada. Compte amb %%: a printf, un percentatge literal s'escriu duplicat.

Aquest -v és la manera correcta de ficar un valor de Bash dins del programa, ja que les cometes simples impedeixen qualsevol expansió. La temptació és escriure awk -F, "\$3 == \"$ciutat\" { n++ }", interpolant la variable. No ho facis, per dos motius. Primer, es trenca amb no-res: un valor amb espais, cometes o barres destrossa la sintaxi. Segon, és injecció de codi: el que hi ha a la variable passa a ser programa, així que si ve d'un argument o d'un fitxer, qui controli aquest valor controla el que awk executa —el mateix risc que eval (05-06), tractat a fons a 08-03—. Amb -v c="$ciutat" el valor entra com a dada. Dos matisos: -v processa les seqüències d'escapada, i les seves variables ja estan disponibles a BEGIN, cosa que no passa amb la sintaxi awk '...' var=valor fitxer.

  1. Programes llargs, fitxers .awk i sabors

Un programa pot ocupar diverses línies dins de les mateixes cometes simples, amb sagnat normal. Quan passa d'unes quinze línies o es reutilitza, es treu a un fitxer i s'invoca amb awk -f ~/veloz-ops/lib/resum.awk enviaments.csv:

# ~/veloz-ops/lib/resum.awk — Resum diari d'enviaments.
BEGIN { FS="," }
FNR > 1 { import[$3] += $6; estat[$5]++ }
END { for (c in import) printf "ciutat\t%s\t%.2f\n", c, import[c] }

Un .awk admet comentaris amb #, es versiona a Git i és molt més llegible que un pedaç entre cometes: és a awk el que lib/comu.sh és a Bash.

awk és un estàndard POSIX amb diverses implementacions: a Ubuntu 24.04 /usr/bin/awk sol ser mawk (ràpid, POSIX pur), a Fedora gawk (amb extensions com gensub(), asorti(), RS com a regex o --csv) i a macOS/BSD l'awk original. Tot el d'aquesta lliçó és POSIX i funciona als tres; si uses una extensió de gawk, invoca gawk explícitament perquè la fallada en una altra màquina sigui «gawk no està instal·lat» i no una sortida silenciosament diferent. Un últim avís: awk parteix per comes a seques, així que un camp entre cometes que contingui una coma ("Madrid, centre") trenca el recompte. enviaments.csv no té aquest cas; si el tingués, la resposta és gawk --csv, no una regex més complicada.

  1. Aplicació: el nou nucli d'informe-diari.sh

En tancar el Mòdul 5, el càlcul eren tres canonades, tres lectures del fitxer i cap decimal: tail -n +2 "$CSV" | wc -l per al total, cut -d, -f5 "$CSV" | grep -c '^lliurat$' per als lliurats i cut -d, -f3 "$CSV" | tail -n +2 | sort | uniq -c | sort -rn per al repartiment per ciutat. Així queda ara com a funció de lib/informe.sh, amb una sola passada i un quadre creuat de ciutat per estat que abans no era viable:

# veloz_resum_csv — Aboca el resum del CSV com a linies clau<TAB>valor.
veloz_resum_csv() {
    local csv="${1:?falta el CSV}"
    awk -F, '
        FNR == 1 { next }                       # capcalera
        { total++; import[$3] += $6; estat[$5]++; cella[$3 SUBSEP $5]++ }
        END {
            if (total == 0) { print "csv sense dades" > "/dev/stderr"; exit 65 }
            print "total\t" total
            printf "taxa_lliurament\t%.1f\n", 100 * estat["lliurat"] / total
            for (c in import)
                printf "ciutat\t%s\t%.2f\t%d\n", c, import[c], cella[c SUBSEP "incidencia"] + 0
        }
    ' "$csv"
}

Tres detalls mereixen explicació. SUBSEP és el separador que awk fa servir internament per als índexs compostos —cella[$3, $5] i cella[$3 SUBSEP $5] són el mateix— i és un caràcter que no apareixerà mai a les dades, més segur que inventar-se un "|". El + 0 força que una cel·la inexistent s'imprimeixi com a 0 i no com a cadena buida. I exit 65 dins d'END acaba awk amb aquest codi, que set -euo pipefail (05-03) converteix en una fallada de l'script; 65 és EX_DATAERR, el convencional per a dades d'entrada incorrectes.

El consumidor a Bash llegeix aquesta sortida amb el bucle de 04-01, sense tornar a tocar el CSV:

while IFS=$'\t' read -r clau a b c; do
    case "$clau" in
        total)           veloz_log_info "Enviaments processats: $a" ;;
        taxa_lliurament) veloz_log_info "Taxa de lliurament: ${a}%" ;;
        ciutat)          printf '  %-10s %10s EUR  (%s incidencies)\n' "$a" "$b" "$c" ;;
    esac
done < <(veloz_resum_csv "$CSV")

La substitució de processos < <( ) és la de 05-05: manté el bucle al shell actual perquè les variables que assigni sobrevisquin. El repartiment de responsabilitats queda clar: awk calcula, Bash orquestra i presenta.

Errors Habituals i Consells

  • Cometes dobles al voltant del programa. awk "{ print $1 }" imprimeix línies buides perquè Bash ha expandit $1 abans. Programa entre cometes simples; els valors entren amb -v.
  • Oblidar la coma a print. print $3, $6 separa amb OFS; print $3 $6 concatena.
  • Confondre $NF amb NF. NF és el nombre de camps; $NF és el contingut de l'últim. El penúltim necessita parèntesis: $(NF-1).
  • Usar NR>1 amb diversos fitxers. Salta només la primera capçalera de totes; amb comodins o historic/, usa FNR>1.
  • Esperar ordre a for (k in array). No n'hi ha: si importa, ordena amb una canonada a sort.
  • printf sense \n. A diferència de print, no l'afegeix i tota la sortida surt enganxada.
  • Comparar text que sembla número. Un camp 007 es compara com a 7 davant d'un número i com a text davant d'una cadena; força el tipus amb $1+0 o $1 "" quan en depengui.
  • Consell: awk no sempre és la resposta. Per filtrar línies sense més, grep és més ràpid i més clar; awk guanya quan hi ha camps, comptes o memòria entre línies. I LC_ALL=C awk ... accelera el processament de fitxers grans en ASCII (ho veurem a 06-03).

Exercicis

Exercici 1. Amb una sola ordre awk sobre /srv/veloz/dades/enviaments.csv, imprimeix per a cada repartidor el seu nom, nombre d'enviaments, import total i import mitjà, ordenat de major a menor import total i en columnes alineades.

Exercici 2. Sobre /var/log/veloz/acces.log (el codi HTTP és el penúltim camp i els bytes l'últim), obtén amb un sol awk el nombre de peticions i els megabytes servits per codi de resposta, marcant els 5xx amb <-- ATENCIO.

Exercici 3. Escriu una funció veloz_mitjana_import per a lib/comu.sh que rebi el CSV i una ciutat, retorni l'import mitjà d'aquesta ciutat amb dos decimals passant la ciutat de forma segura, i surti amb codi 1 si aquesta ciutat no té enviaments.

Solucions

Solució 1.

awk -F, 'FNR>1 { n[$4]++; s[$4] += $6 }
         END { for (r in n) printf "%-8s %5d %10.2f %8.2f\n", r, n[r], s[r], s[r]/n[r] }' \
    /srv/veloz/dades/enviaments.csv | sort -k3 -rn

Dos arrays amb el mateix índex (n compta, s suma) són el patró per calcular mitjanes, i la divisió es fa a END, mai durant el recorregut. L'ordre es resol fora amb sort -k3 -rn sobre la columna de l'import total, perquè for (r in n) no en garanteix cap.

Solució 2.

awk '{ n[$(NF-1)]++; b[$(NF-1)] += $NF }
     END { for (c in n) {
               marca = (c >= 500 && c < 600) ? "  <-- ATENCIO" : ""
               printf "%-5s %7d peticions %9.2f MB%s\n", c, n[c], b[c]/1048576, marca
           } }' /var/log/veloz/acces.log | sort   # -> 503  47 peticions  0.12 MB  <-- ATENCIO

$(NF-1) i $NF eviten comptar camps a mà en un format on la petició té longitud variable. L'operador ternari condició ? a : b existeix a awk igual que a C i estalvia un if/else de quatre línies; c >= 500 funciona perquè awk tracta c com un número en comparar-lo amb un.

Solució 3.

# veloz_mitjana_import — Import mitja d'una ciutat. Us: veloz_mitjana_import <csv> <ciutat>
veloz_mitjana_import() {
    local csv="${1:?falta el CSV}" ciutat="${2:?falta la ciutat}"
    awk -F, -v c="$ciutat" '
        FNR>1 && $3 == c { s += $6; n++ }
        END { if (n == 0) exit 1; printf "%.2f\n", s/n }
    ' "$csv"
}

La ciutat entra amb -v c="$ciutat", així que un valor amb cometes o espais és una dada inofensiva i no part del programa. L'exit 1 dins d'END propaga la fallada al codi de sortida de la funció —awk és una ordre més i $? funciona com amb qualsevol altra (03-01)—, de manera que qui la crida pot escriure mitjana=$(veloz_mitjana_import "$CSV" Madrid) || veloz_log_error "sense dades". Sense l'if (n == 0), la divisió donaria un error o un nan segons la implementació.

Conclusió

awk és un llenguatge amb bucle implícit: per a cada registre recorre les seves regles patró { acció }, amb BEGIN i END com els dos moments que no depenen de cap línia. Trosseja cada registre en camps accessibles com $1, $NF o $(NF-1) i descriu la seva posició amb NR, FNR, NF i FILENAME; -F, o FS fixen com parteix l'entrada i OFS com la uneix en sortir. Els patrons van d'una regex a comparacions sobre camps, rangs i combinacions lògiques, i les accions inclouen print, printf, condicionals i bucles. Però el que justifica la lliçó són dos trets que cap canonada no té: variables que s'inicialitzen soles a 0 i arrays associatius, que converteixen acumulador[clau] += valor seguit de for (k in acumulador) a END en el patró universal d'agregació —suma per ciutat, recompte per estat, mitjana per repartidor i fins i tot taules creuades amb SUBSEP, tot en una lectura i amb coma flotant de franc—. Els valors de Bash entren amb -v, mai interpolats, i els programes llargs es treuen a un fitxer .awk amb -f.

informe-diari.sh ja no rellegeix el CSV tres vegades: awk calcula i Bash orquestra. Però awk llegeix i resumeix; no és l'eina per reescriure text conservant-ne la forma. Quan calgui anonimitzar les IP d'acces.log abans de compartir-lo, normalitzar els separadors d'un fitxer mal exportat o canviar una clau de veloz-ops.conf sense obrir un editor, fa falta un editor de flux. Això és sed, i és la lliçó següent (06-02): les mateixes expressions regulars que ja domines, però aplicades a transformar en lloc de a filtrar.

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