Acabàvem la lliçó anterior amb un diagnòstic incòmode: els bucles ens havien tret la repetició entre execucions de l'script, però dins del fitxer continuàvem escrivint tres vegades el mateix bloc de validació i quatre vegades el mateix printf de format. En Bash no hi ha classes, ni mòduls, ni objectes: la funció és l'única unitat de reutilització que existeix. Tot allò que vulguis escriure una vegada i fer servir moltes —validar, formatar, registrar, calcular— ha de ser una funció. En aquesta lliçó aprendràs a definir-les, a passar-los dades, a recuperar-ne resultats (que no és el que sembla) i a documentar-les; i al final refactoritzaràs informe-diari.sh perquè deixi de ser una paret de codi i passi a ser un conjunt de peces amb nom.
Contingut
- Les dues sintaxis i quina preferir
- On es defineixen: definicions a dalt,
maina baix - Paràmetres: les funcions són mini-scripts
local, i el bug que t'espera si te'n descuidesreturnretorna un codi de sortida, no una dada- Com retornar dades de debò
- Funcions que fallen, i recursivitat
- Funcions davant d'scripts i àlies
- Documentar una funció
- Refactor d'
informe-diari.sh
- Les dues sintaxis i quina preferir
Bash admet dues maneres de declarar una funció, i una tercera que les barreja:
saludar() { echo "Hola des de Veloz Envíos"; } # forma POSIX: la recomanada
function saludar { echo "Hola"; } # paraula clau: Bash/ksh
function saludar() { echo "Hola"; } # barreja: vàlida però redundant| Aspecte | nom() { } |
function nom { } |
|---|---|---|
| Portabilitat | POSIX: funciona en sh, dash, zsh |
Només Bash, ksh, zsh |
| Llegibilitat | Els parèntesis marquen que és invocable | La paraula clau destaca més |
| Recomanació | Fes-la servir sempre | Evita-la llevat d'estil d'equip |
Farem servir nom() { ... } a tot el curs. És la que espera ShellCheck (08-05) i la que continua funcionant si algun dia l'script ha de córrer sota sh (08-07).
Regles de nom: lletres, dígits i guions baixos; per convenció, minúscules amb _. No posis parèntesis en cridar-la: s'invoca com una ordre qualsevol, saludar, no saludar().
- On es defineixen: definicions a dalt,
main a baix
main a baixBash llegeix l'script de dalt a baix. Una funció només existeix després que l'intèrpret hagi passat per la seva definició:
D'aquí neix el patró estructural que faràs servir a tots els teus scripts a partir d'ara:
#!/usr/bin/env bash
readonly CAMI_CSV="/srv/veloz/dades/enviaments.csv" # 1) capçalera i constants
us() { ... } # 2) TOTES les definicions
validar_entorn() { ... }
resum_ciutat() { ... }
main() { validar_entorn; resum_ciutat "$1"; } # 3) el flux principal
main "$@" # 4) única crida, última líniaAvantatges concrets d'aquest esquelet: el lector troba el flux del programa a main i no burxant entre 300 línies; res no s'executa fins a l'última línia, així que l'ordre de definició entre funcions deixa d'importar (quan main corre, totes estan carregades); i main "$@" reenvia els arguments de l'script a la funció, amb les cometes al voltant de $@ que 03-05 va declarar obligatòries.
- Paràmetres: les funcions són mini-scripts
Aquí ve la sorpresa per a qui ve d'altres llenguatges: els paràmetres no es declaren a la signatura. Una funció rep arguments exactament igual que un script: $1, $2, $#, "$@", shift.
comptar_estat() {
(( $# >= 2 )) || { echo "comptar_estat: falten arguments" >&2; return 2; }
local ciutat="$1" estat="$2"
grep -c ",${ciutat},[^,]*,${estat}," "$CAMI_CSV"
}
comptar_estat Valencia incidencia # es crida sense comes ni parèntesisDos matisos que sorprenen: $0 no canvia dins d'una funció (continua sent el nom de l'script; per al nom de la funció hi ha ${FUNCNAME[0]}), i $1 són els arguments de la funció, no els de l'script, així que si els necessites a dins cal passar-los-hi amb la_meva_funcio "$@".
local, i el bug que t'espera si te'n descuides
local, i el bug que t'espera si te'n descuidesPer defecte, totes les variables de Bash són globals, incloses les que assignes dins d'una funció. Això produeix un dels bugs més difícils de rastrejar del llenguatge:
comptar() { total=0; for x in 1 2 3; do total=$(( total + x )); done; echo "$total"; }
total=999
comptar # imprimeix 6
echo "$total" # imprimeix 6, no 999: la funció t'ha trepitjat la variableLa funció ha destruït una variable del programa principal sense avisar. La solució és declarar local tota variable de treball intern:
comptar() { local total=0 x; for x in 1 2 3; do total=$(( total+x )); done; echo "$total"; }
total=999; comptar; echo "$total" # imprimeix 6 i després 999 ✓Detalls de local: només és vàlid dins d'una funció; n'admet diverses alhora (local a b c=0); el seu àmbit és dinàmic, de manera que una funció cridada des d'una altra veu les local de qui l'ha cridada (no ho aprofitis). I compte amb local var=$(ordre): el $? que queda és el de local, sempre 0, no el de l'ordre; si necessites comprovar-lo, separa-ho en local var; var=$(ordre) || return 1.
Regla d'or: si una variable no està declarada local, és perquè vols deliberadament que sobrevisqui a la funció.
return retorna un codi de sortida, no una dada
return retorna un codi de sortida, no una dadareturn N acaba la funció i fixa $? amb N. És el mateix mecanisme dels codis de sortida de 03-01, amb les mateixes limitacions: un enter entre 0 i 255, on 0 significa èxit.
csv_utilitzable() {
[[ -f "$CAMI_CSV" ]] || return 3 # no existeix
[[ -r "$CAMI_CSV" ]] || return 4 # sense permís de lectura
[[ -s "$CAMI_CSV" ]] || return 5 # buit
}
if csv_utilitzable; then echo "CSV a punt"; else echo "CSV no utilitzable ($?)" >&2; fiCom que la funció retorna un codi, es pot fer servir directament en un if, en un &&, en un while… igual que qualsevol ordre. Aquesta és la raó per la qual les funcions de comprovació es bategen amb noms tipus pregunta (csv_utilitzable, es_ciutat_valida) i retornen 0/1.
El que no pots fer és return 3.5, return "Valencia" ni return 1000 (es trunca a 1000 % 256 = 232). Si omets el return, la funció retorna el codi de l'última ordre executada.
- Com retornar dades de debò
Hi ha tres tècniques, per ordre de preferència:
a) Escriure a stdout i capturar amb $( ). És la forma idiomàtica:
mitjana_import() { awk -F, -v c="$1" '$3==c {s+=$6;n++} END{printf "%.2f",s/n}' "$CAMI_CSV"; }
m=$(mitjana_import Valencia); echo "Mitjana a Valencia: $m €"El seu cost: $( ) crea un subshell, i la funció no pot escriure res més a stdout —missatges de progrés inclosos— sense contaminar el resultat. Per això els missatges informatius sempre van a stderr, com veuràs a log_info més avall.
b) Assignar a una variable global acordada (resultat=...). Ràpid i sense subshell, però acobla la funció a un nom concret.
c) Nameref amb local -n (Bash 4.3+). Qui crida decideix el nom de la variable de sortida i la funció hi escriu indirectament:
mitjana_import() {
local ciutat="$1"
local -n _sortida="$2" # _sortida és un ÀLIES de la variable anomenada a $2
_sortida=$(awk -F, -v c="$ciutat" '$3==c {s+=$6;n++} END{printf "%.2f",s/n}' "$CAMI_CSV")
}
mitjana_import Valencia mitjana_val
echo "$mitjana_val" # 26.34local -n és la versió amb àmbit de declare -n. És la tècnica que faràs servir per «retornar» arrays (04-03), on $( ) perdria la separació entre elements. Parany: el nom local no pot coincidir amb el que passa qui crida o Bash dona error de referència circular; per això es prefixa amb _.
- Funcions que fallen, i recursivitat
Encadenar funcions amb &&, || i $? funciona igual que amb ordres externes: validar_entorn || exit $? propaga el codi de la funció al sistema operatiu, i csv_utilitzable && processar_csv encadena. Ull: exit dins d'una funció mata l'script sencer, no només la funció. És correcte en funcions d'avortament com morir(), però en una funció reutilitzable val més return i deixar que decideixi qui la crida.
Bash admet recursivitat —factorial() { local n="$1"; (( n <= 1 )) && { echo 1; return; }; echo $(( n * $(factorial $(( n - 1 )) ) )); }— però cada nivell crea un subshell pel $( ), així que és lent; el límit el fixa FUNCNEST. En scripts d'operacions, un bucle és gairebé sempre més clar i més ràpid.
- Funcions davant d'scripts i àlies
| Criteri | Àlies | Funció | Script separat |
|---|---|---|---|
| Accepta arguments | No (només els enganxa al final) | Sí ($1, "$@") |
Sí |
Lògica amb if/bucles |
No | Sí | Sí |
| On viu | ~/.bashrc |
A l'script o a lib/ |
Fitxer propi a bin/ |
| Invocable des d'un altre programa | No | No (llevat d'export -f) |
Sí |
| Cost d'execució | Nul | Nul (mateix procés) | Un procés nou |
| Pot modificar el shell actual | Sí | Sí | No |
Traducció pràctica: els àlies són dreceres de teclat personals (alias ll='ls -l'), les funcions són la reutilització dins d'un programa, i els scripts són la unitat que s'invoca des de fora —cron, systemd, un altre script—. informe-diari.sh és un script; validar_entorn és una funció seva.
- Documentar una funció
Una funció sense capçalera obliga a llegir-ne el cos per saber com es fa servir. Adopta aquest format des d'avui:
# resum_ciutat — Imprimeix el resum d'enviaments d'una ciutat en una data.
# Ús: resum_ciutat <ciutat> <data>
# Arguments: $1 ciutat (Valencia|Sevilla|Bilbao|Madrid) $2 data (yyyy-MM-dd)
# Sortida: Una línia formatada a stdout
# Retorna: 0 si hi ha dades; 6 si la ciutat no té enviaments aquell dia
resum_ciutat() { ... }Quatre apartats fixos —ús, arguments, sortida, codi de retorn— i una línia de descripció. És barat d'escriure i elimina la meitat de les preguntes sobre l'script.
- Refactor d'
informe-diari.sh
informe-diari.shApliquem tot l'anterior. Aquest és l'esquelet de l'script després del refactor (n'ometem l'anàlisi d'opcions, que ja tens de 03-05):
#!/usr/bin/env bash
# informe-diari.sh — Informe diari d'enviaments de Veloz Envíos.
readonly CAMI_CSV="${VELOZ_CSV:-/srv/veloz/dades/enviaments.csv}"
readonly CAMI_LOG="${VELOZ_LOG:-/var/log/veloz/app.log}"
readonly CIUTATS=(Valencia Sevilla Bilbao Madrid)
# log_info/log_error — Registren un missatge amb marca de temps a STDERR.
log_info() { printf '%s [INFO] %s\n' "$(date '+%F %T')" "$*" >&2; }
log_error() { printf '%s [ERROR] %s\n' "$(date '+%F %T')" "$*" >&2; }
# validar_entorn — Comprova que existeixen i són llegibles el CSV i el registre.
# Retorna: 0 correcte | 3 falta el fitxer | 4 sense permís de lectura
validar_entorn() {
local f
for f in "$CAMI_CSV" "$CAMI_LOG"; do
[[ -f "$f" ]] || { log_error "No existeix: $f"; return 3; }
[[ -r "$f" ]] || { log_error "Sense lectura: $f"; return 4; }
done
log_info "Entorn validat"
}
# comptar_errors — Nre. de línies [ERROR] del registre en una data. Sortida: enter.
comptar_errors() { grep -c "^${1:?} .*\[ERROR\]" "$CAMI_LOG" || true; }
# resum_ciutat — Línia formatada amb els totals d'una ciutat.
# Ús: resum_ciutat <ciutat> <data> Retorna: 0 | 6 si no hi ha enviaments
resum_ciutat() {
local ciutat="${1:?}" data="${2:?}" total incid
total=$(grep -c "^[^,]*,${data},${ciutat}," "$CAMI_CSV")
(( total > 0 )) || { log_info "Sense enviaments a $ciutat"; return 6; }
incid=$(grep "^[^,]*,${data},${ciutat}," "$CAMI_CSV" | grep -c ',incidencia,')
printf '%-10s %5d enviaments %5d incidències\n' "$ciutat" "$total" "$incid"
}
main() {
local data="${1:-$(date +%F)}" ciutat
validar_entorn || exit $?
log_info "Informe del $data"
printf '%-10s %12s %16s\n' CIUTAT ENVIAMENTS INCIDENCIES
for ciutat in "${CIUTATS[@]}"; do resum_ciutat "$ciutat" "$data"; done
log_info "Errors al registre: $(comptar_errors "$data")"
}
main "$@"Què hi hem guanyat: un sol lloc que valida (si demà cal comprovar el fitxer de configuració, es toca validar_entorn i res més); missatges separats de les dades, perquè log_info escriu a stderr i així informe-diari.sh > informe.txt desa la taula neta mentre els avisos continuen a pantalla —la separació de fluxos de 02-04 aplicada amb criteri—; un bucle de quatre línies en comptes de quatre blocs idèntics, de manera que afegir Zaragoza és afegir una paraula a CIUTATS; i un main que cap en una pantalla i es llegeix com l'enunciat del problema.
El || true de comptar_errors mereix explicació: grep -c retorna codi 1 quan compta 0 coincidències, cosa que en un script amb set -e (05-03) avortaria el programa. || true força un codi 0 sense alterar la sortida.
Aquestes funcions són genèriques i les voldràs també en altres scripts del toolkit. A 05-06 les mouràs a ~/veloz-ops/lib/comu.sh i les carregaràs amb source, convertint el toolkit en una llibreria de debò.
Errors Habituals i Consells
- Cridar la funció amb parèntesis:
resum_ciutat("Valencia")no és un error de sintaxi, és una altra cosa i fallarà de manera estranya. Es cridaresum_ciutat Valencia. - Descuidar el
local. El bug més car de la lliçó. Declara local tot el que sigui intern, sempre. - Barrejar missatges i resultat a stdout. Si la funció es captura amb
$( ), qualsevolechode progrés acaba dins del resultat. Missatges → stderr. - Creure que
returnretorna dades. Retorna un codi 0-255.return $(wc -l < fitxer)amb 300 línies retornaria 44. - Definir una funció després de fer-la servir. Definicions a dalt,
main "$@"al final. - Consell: bateja les funcions amb verb (
validar_,comptar_,generar_) i les de comprovació com a predicats (es_valida,existeix_). El nom ha de fer innecessari llegir-ne el cos.
Exercicis
Exercici 1. Escriu es_ciutat_valida() que rebi una ciutat i retorni 0 si és entre les quatre operatives i 1 en cas contrari, sense imprimir res. Fes-la servir en un if.
Exercici 2. Escriu enviaments_de() que rebi un repartidor i escrigui a stdout el seu nombre d'enviaments, i demostra que es captura amb $( ). Afegeix-hi la capçalera de documentació completa.
Exercici 3. Corregeix aquesta funció, que té dos defectes:
total_import() {
suma=0
while IFS=, read -r _ _ _ _ _ import; do suma=$(( suma + ${import%.*} )); done < "$1"
return $suma
}Solucions
Solució 1.
es_ciutat_valida() {
local ciutat="${1:?falta la ciutat}" c
for c in Valencia Sevilla Bilbao Madrid; do [[ "$ciutat" == "$c" ]] && return 0; done
return 1
}
if es_ciutat_valida "$1"; then echo "OK"; else echo "Ciutat desconeguda" >&2; fiA 04-05 veuràs que un case fa això en tres línies, i a 04-03 que un array associatiu ho fa en una.
Solució 2.
# enviaments_de — Compta els enviaments d'un repartidor.
# Ús: enviaments_de <repartidor> Arguments: $1 (alopez|mgarcia|jruiz)
# Sortida: nombre enter a stdout Retorna: 0 sempre
enviaments_de() { local r="${1:?falta el repartidor}"; grep -c ",${r}," "$CAMI_CSV" || true; }
n=$(enviaments_de alopez); echo "alopez ha fet $n enviaments"Solució 3. Els dos defectes són que suma és global i que return no pot retornar un total (es trunca a mòdul 256):
total_import() {
local fitxer="${1:?}" suma=0 import
while IFS=, read -r _ _ _ _ _ import; do suma=$(( suma + ${import%.*} )); done < "$fitxer"
echo "$suma" # el resultat surt per stdout, no per return
}
total=$(total_import "$CAMI_CSV")De passada: ${import%.*} retalla la part decimal —una expansió que dominaràs a 04-04— perquè Bash no suma decimals, cosa que resoldràs del tot a 04-06.
Conclusió
Les funcions converteixen un guió en un programa. Es defineixen amb nom() { }, es col·loquen totes a dalt amb un main "$@" com a última línia, reben arguments igual que un script mitjançant $1 i "$@", protegeixen el seu estat intern amb local, assenyalen èxit o fracàs amb return (un codi, mai una dada) i lliuren resultats escrivint a stdout perquè qui les crida els capturi amb $( ) —o mitjançant un nameref quan això no basta—. Amb validar_entorn, comptar_errors, resum_ciutat i les dues funcions de registre, informe-diari.sh ja té esquelet.
Li falta memòria. El nostre main ja fa servir "${CIUTATS[@]}" sense haver explicat què és aquesta sintaxi, i el bucle torna a llegir el CSV sencer una vegada per ciutat perquè no té on acumular un comptador per a cadascuna. Això són els arrays (04-03): llistes indexades i, encara millor, mapes de clau→valor que permeten comptar incidències per ciutat i per repartidor en una sola passada, substituint el vell sort | uniq -c que arrosseguem des del Mòdul 2.
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
