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
- Per què awk no és una ordre més
- El model d'execució:
patró { acció },BEGINiEND - Registres, camps i variables integrades
- Separadors d'entrada i de sortida
- Patrons
- Accions:
print,printf, condicionals i bucles - Variables d'usuari i la seva inicialització automàtica
- Arrays associatius: agregar en una sola passada
- Recórrer i ordenar el resultat
- Funcions integrades
- Coma flotant de franc i variables de Bash amb
-v - Programes llargs, fitxers
.awki sabors - Aplicació: el nou nucli d'
informe-diari.sh
- 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.
- El model d'execució:
patró { acció }, BEGIN i END
patró { acció }, BEGIN i ENDUn 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.
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.
- 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.
- 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.
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 }).
- 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.csvAquesta 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.
- Accions:
print, printf, condicionals i bucles
print, printf, condicionals i buclesDins 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.csvprintf 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.
- 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.
- 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.55Llegeix-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.csvUn 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.
- 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 -rnFixa'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í.
- 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.
- Coma flotant de franc i variables de Bash amb
-v
-vA 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.
- Programes llargs, fitxers
.awk i sabors
.awk i saborsUn 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.
- Aplicació: el nou nucli d'
informe-diari.sh
informe-diari.shEn 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$1abans. Programa entre cometes simples; els valors entren amb-v. - Oblidar la coma a
print.print $3, $6separa ambOFS;print $3 $6concatena. - Confondre
$NFambNF.NFés el nombre de camps;$NFés el contingut de l'últim. El penúltim necessita parèntesis:$(NF-1). - Usar
NR>1amb diversos fitxers. Salta només la primera capçalera de totes; amb comodins ohistoric/, usaFNR>1. - Esperar ordre a
for (k in array). No n'hi ha: si importa, ordena amb una canonada asort. printfsense\n. A diferència deprint, no l'afegeix i tota la sortida surt enganxada.- Comparar text que sembla número. Un camp
007es compara com a 7 davant d'un número i com a text davant d'una cadena; força el tipus amb$1+0o$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. ILC_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 -rnDos 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
- 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
