informe-diari.sh valida el seu entorn i avisa quan alguna cosa se surt del normal, però continua fent sempre exactament el mateix: el dia d'avui, totes les ciutats. Si direcció et demana l'informe del dilluns passat, o només el de Bilbao, has d'editar el fitxer. Això és precisament el que separava un àlies d'una eina, i en aquesta lliçó ho resolem. En acabar, informe-diari.sh --data 2026-07-28 --ciutat Bilbao funcionarà, hi haurà un --help que expliqui com fer-lo servir, i l'script sabrà demanar per teclat allò que no se li hagi dit.

Contingut

  1. Paràmetres posicionals, $#, $@ i $*
  2. La diferència crítica entre "$@" i "$*"
  3. shift: consumir arguments
  4. Validar el nombre d'arguments i la funció us()
  5. Arguments amb nom: while + case
  6. getopts per a opcions curtes
  7. Valors per defecte i variables d'entorn
  8. Entrada interactiva amb read
  9. informe-diari.sh amb --data i --ciutat

  1. Paràmetres posicionals, $#, $@ i $*

Quan invoques un script amb arguments, Bash els deixa en variables numerades anomenades paràmetres posicionals. En invocar informe-diari.sh Bilbao 2026-07-28 detallat, $0 conté el camí de l'script, $1 val Bilbao, $2 val 2026-07-28, $3 val detallat i de $4 endavant són buits. Dins de l'script els faries servir com echo "Ciutat: $1 · Data: $2 · Mode: $3".

Hi ha un parany a partir del desè argument: $10 no significa «argument 10», sinó «$1 seguit d'un zero», perquè Bash només reconeix un dígit. A partir del desè cal fer servir claus —${10}—, la mateixa regla de 03-02 aplicada a noms numèrics. A la pràctica, un script que rep deu posicionals està mal dissenyat; a partir de tres, el correcte és passar a arguments amb nom (apartat 6).

$0 mereix un comentari: conté el nom tal com s'ha invocat. Si executes ./informe-diari.sh valdrà això mateix, i si el crides pel PATH valdrà el camí complet. Per mostrar només el nom als missatges d'ajuda es fa servir nom=$(basename "$0").

Tres variables especials completen el panorama: $# és el nombre d'arguments rebuts (sense comptar $0), $@ són tots els arguments com a llista i $* són tots com una sola cadena. Amb echo "Rebuts $# arguments: $@" dins de l'script, la invocació informe-diari.sh Bilbao 2026-07-28 imprimeix Rebuts 2 arguments: Bilbao 2026-07-28. $# és la clau per validar: (( $# == 0 )) detecta que no s'ha passat res, (( $# > 2 )) que sobren arguments.

  1. La diferència crítica entre "$@" i "$*"

Sense cometes, $@ i $* es comporten igual. Entre cometes dobles són radicalment diferents, i és una de les diferències que causa més bugs subtils a Bash:

Forma S'expandeix a
"$@" "$1" "$2" "$3" — cada argument, una paraula separada
"$*" "$1 $2 $3" — tots units en una sola paraula (separador: el primer caràcter d'IFS)

La demostració clàssica fa servir un fitxer amb espais al nom. Invocant ./demo.sh "enviaments juliol.csv" Bilbao:

for arg in "$@"; do echo "[$arg]"; done   # → [enviaments juliol.csv]  i  [Bilbao]
for arg in "$*"; do echo "[$arg]"; done   # → [enviaments juliol.csv Bilbao]

El primer bucle fa dues voltes i el segon només una. Amb "$@" es conserven dos arguments i el nom amb espai continua sencer. Amb "$*" tot es fon en una única cadena, i el receptor ja no pot saber on acabava un argument i començava el següent.

Regla professional: fes servir sempre "$@", amb cometes. És el correcte per reenviar arguments a una altra ordre: grep -c ERROR "$@" passa cada fitxer com un argument independent, mentre que grep -c ERROR "$*" li passa tots els noms enganxats com si fossin un de sol, i falla.

"$*" només té un ús legítim: construir un missatge llegible, com echo "Arguments rebuts: $*". Per a tota la resta, "$@".

  1. shift: consumir arguments

shift descarta $1 i desplaça tots els altres una posició: $2 passa a ser $1, $3 passa a ser $2, i $# disminueix en un. Partint de tres arguments:

shift;   echo "$# args: $@"      # 2 args: 2026-07-28 detallat
shift 2; echo "$# args: $@"      # 0 args:

shift N desplaça N posicions d'un sol cop. És el mecanisme que permet recórrer els arguments consumint-los un a un, i és la base del bucle de l'apartat 6. Un detall important: shift falla (retorna codi 1) si no queden arguments per desplaçar, cosa que serveix com a condició d'aturada.

  1. Validar el nombre d'arguments i la funció us()

Tot script que accepti arguments s'ha de saber explicar; la convenció universal és una funció us() que imprimeix la sintaxi:

us() { cat <<FI
Ús: $(basename "$0") [OPCIONS]
Genera l'informe diari d'activitat de Veloz Envíos.
  -f, --data AAAA-MM-DD    Data a analitzar (per defecte: avui)
  -c, --ciutat NOM         Filtrar per ciutat (per defecte: totes)
  -v, --detallat           Mostrar detall d'errors
  -h, --help               Mostrar aquesta ajuda i sortir
FI
}

Aquesta construcció cat <<FI ... FI és un here-document: un bloc de text literal que s'envia a cat com si fos un fitxer. És molt més còmode que vint echo seguits, i s'estudia a fons a 05-05; aquí n'hi ha prou de copiar el patró. La sintaxi de funcions és de 04-02, però la necessites ja.

Amb us() definida, validar és immediat: (( $# > 6 )) && { echo "ERROR: massa arguments" >&2; us >&2; exit 2; }. Fixa't en dues decisions. L'ajuda per error va a stderr (us >&2), perquè forma part del diagnòstic; però quan l'usuari demana --help explícitament, l'ajuda va a stdout i l'script surt amb codi 0, perquè això no és cap error. I el codi de sortida per a ús incorrecte és 2, seguint la convenció de la lliçó 03-01.

  1. Arguments amb nom: while + case

Els arguments posicionals tenen un límit obvi: cal recordar-ne l'ordre. informe-diari.sh Bilbao 2026-07-28 obliga a saber que la ciutat va primer. Els arguments amb nom eliminen aquest problema i són l'estàndard de qualsevol eina seriosa. El patró universal a Bash combina un bucle while amb case:

while [[ $# -gt 0 ]]; do
    case "$1" in
        -f|--data)     data="$2";     shift 2 ;;
        -c|--ciutat)   ciutat="$2";   shift 2 ;;
        -v|--detallat) detallat="si"; shift   ;;
        -h|--help)     us; exit 0 ;;
        --)            shift; break ;;      # fi de les opcions
        -*) echo "ERROR: opció desconeguda '$1'" >&2; us >&2; exit 2 ;;
        *)  echo "ERROR: argument inesperat '$1'" >&2; exit 2 ;;
    esac
done

Recorre el bloc a poc a poc, perquè l'escriuràs moltes vegades:

  • while [[ $# -gt 0 ]] continua mentre quedin arguments; case "$1" mira l'actual, i cada branca acaba en ;;. La barra vertical de -f|--data) accepta la forma curta o la llarga.
  • shift 2 a les opcions amb valor consumeix l'opció i el seu argument; shift a seques n'hi ha prou per als indicadors sense valor com -v.
  • -- és la convenció universal per dir «a partir d'aquí no hi ha més opcions», imprescindible quan un valor podria començar per guió. -*) captura qualsevol opció no reconeguda i falla explícitament. Ignorar en silenci una opció mal escrita és de les pitjors decisions possibles: --ciuatt Bilbao produiria un informe de totes les ciutats sense dir res. I *) recull els arguments solts que no esperaves.

case s'estudia formalment a 04-05; aquí el fem servir com l'eina pràctica que és.

Una errada freqüent: si l'usuari escriu --data sense valor, $2 serà buit i l'script continuarà endavant amb una data buida. La defensa és comprovar-ho a la branca mateixa, amb [[ -n "${2:-}" ]] || { echo "ERROR: --data requereix un valor" >&2; exit 2; } abans de l'assignació. Aquest ${2:-} és expansió amb valor per defecte (03-06).

  1. getopts per a opcions curtes

Bash porta un builtin dedicat a analitzar opcions curtes, getopts, de sintaxi molt compacta:

while getopts ":f:c:vh" opcio; do
    case "$opcio" in
        f) data="$OPTARG" ;;    c) ciutat="$OPTARG" ;;
        v) detallat="si"  ;;    h) us; exit 0 ;;
        \?) echo "ERROR: opció desconeguda -$OPTARG" >&2; exit 2 ;;
        :)  echo "ERROR: l'opció -$OPTARG requereix un valor" >&2; exit 2 ;;
    esac
done
shift $((OPTIND - 1))

Les peces d'aquesta cadena ":f:c:vh":

Element Significat
f: L'opció -f requereix un valor (els dos punts van al darrere)
v L'opció -v és un indicador sense valor
: inicial Mode silenciós: gestiones tu els errors amb \? i :
$OPTARG / $OPTIND Valor de l'opció actual / índex de l'argument següent

shift $((OPTIND - 1)) al final descarta totes les opcions processades i deixa a $1, $2… els arguments que no eren opcions.

Avantatges de getopts: agrupa indicadors (-vh equival a -v -h), admet -f2026-07-28 enganxat, i gestiona els errors per tu. I el seu límit decisiu: no admet opcions llargues; --data és impossible amb getopts. La balança queda així: getopts guanya en codi escrit i en agrupació d'indicadors, while + case guanya en l'únic punt que de debò decideix.

Criteri d'elecció: si vols opcions llargues —i en una eina d'operacions les vols, perquè --ciutat es llegeix i -c s'ha de recordar— fes servir while + case. És el que farem a informe-diari.sh.

  1. Valors per defecte i variables d'entorn

Un bon script funciona sense arguments, aplicant valors assenyats. La forma idiomàtica és assignar-los abans de processar els arguments, de manera que allò que no arribi per la línia d'ordres conservi el seu valor inicial sense necessitat de condicionals addicionals. I aquests valors poden venir al seu torn de variables d'entorn, reprenent el que vas veure a 03-02: és la via natural per a configuració persistent que no vols teclejar cada cop.

data=$(date +%F)                   # per defecte, avui
ciutat="${VELOZ_CIUTAT:-totes}"    # la variable d'entorn si existeix, si no "totes"

export VELOZ_CIUTAT=Bilbao
informe-diari.sh                   # ja filtra per Bilbao, sense arguments
informe-diari.sh --ciutat Sevilla  # l'argument mana sobre l'entorn

La convenció de precedència, de més a menys prioritat, és: argument de línia d'ordres > variable d'entorn > fitxer de configuració > valor per defecte. Com que el bucle d'arguments s'executa després d'aquestes assignacions, aquesta jerarquia surt gratis amb l'ordre correcte de les línies. L'expansió ${var:-valor} que hi veus s'explica a fons a 03-06.

Aquest és el paper de ~/veloz-ops/etc/veloz-ops.conf (02-03): un fitxer carregat amb source que fixa els valors habituals de l'equip, sobre els quals cada invocació pot imposar els seus.

  1. Entrada interactiva amb read

Quan falta una dada i l'script s'executa de manera interactiva, es pot preguntar a l'usuari amb read -r -p "Quina ciutat vols analitzar? " ciutat. Les seves opcions essencials:

Opció Efecte
-p "text" Mostra un prompt sense salt de línia
-r No interpreta la barra invertida com a escapament
-s Mode silenciós: no mostra el que es teclea (contrasenyes)
-t N / -n N Espera N segons i es rendeix / llegeix N caràcters sense esperar l'Enter
-a array Desa les paraules en un array (04-03)

Fes servir -r sempre. Sense ell, si l'usuari escriu un camí de Windows com C:\dades\enviaments.csv, Bash es menjarà les barres invertides i desarà C:dadesenviaments.csv. No hi ha cap cas en operacions on vulguis aquest comportament.

Ara bé: un script pensat per a cron no pot preguntar res, perquè no hi ha ningú a l'altre costat i es quedaria penjat per sempre. Per això cal comprovar si l'entrada és un terminal abans de preguntar:

read -rsp "Contrasenya de l'API: " clau; echo    # -s amaga; echo afegeix el salt
read -rt 10 -p "Continuar? [s/N] " resp || { echo "Sense resposta"; exit 1; }

if [[ -t 0 ]]; then read -r -p "Quina ciutat? " ciutat
else ciutat="totes"        # sense terminal: per defecte, sense blocar-se
fi

[[ -t 0 ]] és cert si el descriptor 0 (stdin) està connectat a un terminal. És la comprovació que evita que un script es pengi de matinada esperant una resposta que no arribarà mai.

La mateixa idea permet llegir de stdin quan no hi ha arguments, com fan els filtres clàssics d'Unix: if (( $# == 0 )) && [[ ! -t 0 ]]; then ciutats=$(cat); fi recull per canonada allò que no ha arribat per la línia d'ordres, de manera que echo Bilbao | informe-diari.sh funciona igual que passar l'argument. La lectura de fitxers línia a línia amb while read es veu a 04-01.

  1. informe-diari.sh amb --data i --ciutat

Ho reunim tot. La funció us() s'abreuja aquí perquè ja la vas veure completa a l'apartat 4:

#!/usr/bin/env bash
# informe-diari.sh - Resum diari d'activitat de Veloz Envíos
# Ús    : informe-diari.sh [--data AAAA-MM-DD] [--ciutat NOM] [-v] [-h]
# Codis : 0 correcte | 2 ús incorrecte | 3 log il·legible | 4 CSV invàlid
readonly CAMI_APP_LOG="/var/log/veloz/app.log"
readonly CAMI_CSV="/srv/veloz/dades/enviaments.csv"
readonly LLINDAR_ERRORS="${VELOZ_LLINDAR:-50}"
us() { echo "Ús: $(basename "$0") [-f AAAA-MM-DD] [-c CIUTAT] [-v] [-h]"; }

# --- Valors per defecte i processament d'arguments --------------------
data=$(date +%F)
ciutat="${VELOZ_CIUTAT:-totes}"
detallat="no"
while [[ $# -gt 0 ]]; do
    case "$1" in
        -f|--data)     data="$2";     shift 2 ;;
        -c|--ciutat)   ciutat="$2";   shift 2 ;;
        -v|--detallat) detallat="si"; shift   ;;
        -h|--help)     us; exit 0 ;;
        *) echo "ERROR: opció desconeguda '$1'" >&2; us >&2; exit 2 ;;
    esac
done

# --- Validació ---------------------------------------------------------
[[ "$data" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] \
    || { echo "ERROR: la data '$data' no té format AAAA-MM-DD" >&2; exit 2; }
[[ -r "$CAMI_APP_LOG" && -s "$CAMI_CSV" ]] || { echo "ERROR: sense dades" >&2; exit 3; }

# --- Informe -----------------------------------------------------------
echo "INFORME VELOZ ENVIOS  ·  data: $data  ·  ciutat: $ciutat"
total_errors=$(grep -c "^$data .*ERROR" "$CAMI_APP_LOG")
echo "Errors del dia: $total_errors (llindar: $LLINDAR_ERRORS)"
[[ $detallat == "si" ]] && grep "^$data .*ERROR" "$CAMI_APP_LOG" | tail -5

linies=$(grep ",$data," "$CAMI_CSV")
[[ $ciutat != "totes" ]] && linies=$(echo "$linies" | grep ",$ciutat,")
echo "-- Enviaments per estat --"
echo "$linies" | cut -d, -f5 | sort | uniq -c | sort -rn
exit 0
$ informe-diari.sh --ciutat Bilbao --data 2026-07-28 -v
INFORME VELOZ ENVIOS  ·  data: 2026-07-28  ·  ciutat: Bilbao
Errors del dia: 12 (llindar: 50)
2026-07-28 18:22:41 [ERROR] fallo al geocodificar dirección de Bilbao
-- Enviaments per estat --
     84 lliurat
     11 incidencia

El =~ de la validació de data és l'operador d'expressions regulars de [[ ]], que s'estudia a 05-04; aquí comprova que la data tingui quatre dígits, guió, dos dígits, guió i dos dígits. És una validació de forma, no de calendari: 2026-13-45 la passaria, i afinar-la és feina de més endavant. Fixa't també en com el llindar i la ciutat poden venir de l'entorn amb ${VELOZ_LLINDAR:-50} mentre que un --ciutat explícit guanya sempre, perquè el bucle s'executa després: la precedència de l'apartat 7 surt gratis amb l'ordre correcte de les línies.

Errors Habituals i Consells

  • Escriure $10 esperant el desè argument. És $1 seguit de 0. Fes servir ${10}.
  • Fer servir $@ o $* sense cometes, o confondre'ls. Sense cometes, els noms amb espais es parteixen; amb elles, "$*" fon tot en una sola cadena. Sempre "$@".
  • Oblidar el shift 2 a les opcions amb valor. El valor es processarà com si fos una altra opció, amb resultats desconcertants.
  • Ignorar les opcions desconegudes. Una errada com --ciuatt ha d'avortar amb codi 2, mai passar desapercebuda.
  • Fer servir read sense -r. Les barres invertides que teclegi l'usuari desapareixen.
  • Preguntar amb read en un script de cron. Es penjarà indefinidament. Comprova-ho abans amb [[ -t 0 ]].
  • Enviar --help a stderr o sortir amb codi ≠ 0. Demanar ajuda no és cap error: stdout i exit 0.

Exercicis

Exercici 1 — Demostrar "$@" davant de "$*". Escriu un script comptar-args.sh que imprimeixi quants elements veu en recórrer "$@" i quants en recórrer "$*", i invoca'l amb ./comptar-args.sh "enviaments juliol.csv" Bilbao 2026-07-28. Explica el resultat.

Exercici 2 — Un script amb opcions completes. Crea ~/veloz-ops/bin/buscar-enviaments.sh que accepti --repartidor NOM, --estat ESTAT, --limit N (per defecte 20) i --help, validi que l'estat sigui un de lliurat, en_repartiment o incidencia, i mostri les línies corresponents d'enviaments.csv. Ha de sortir amb codi 2 davant de qualsevol ús incorrecte.

Exercici 3 — Interactiu però automatitzable. Modifica el fragment de la ciutat perquè, si no s'ha passat --ciutat, pregunti per teclat només si hi ha terminal, amb un màxim de 15 segons, i faci servir totes si no hi ha terminal o s'exhaureix el temps.

Solucions

Solució a l'Exercici 1

n=0; for a in "$@"; do (( n++ )); done; echo "Amb \"\$@\": $n elements"
m=0; for a in "$*"; do (( m++ )); done; echo "Amb \"\$*\": $m elements"

La sortida és Amb "$@": 3 elements i Amb "$*": 1 element. "$@" conserva l'estructura: tres arguments, i el primer manté el seu espai intern intacte. "$*" els concatena en una única cadena fent servir el primer caràcter d'IFS (un espai per defecte), i amb això es perd irremeiablement la frontera entre arguments. Per això reenviar amb "$*" a una altra ordre és un error: grep ERROR "$*" buscaria un fitxer el nom complet del qual fos «enviaments juliol.csv Bilbao 2026-07-28».

Solució a l'Exercici 2

#!/usr/bin/env bash
readonly CAMI_CSV="/srv/veloz/dades/enviaments.csv"
us() { echo "Ús: $(basename "$0") [--repartidor N] [--estat E] [--limit N]"; }
repartidor=""; estat=""; limit=20

while [[ $# -gt 0 ]]; do            # mateix patró de l'apartat 5
    case "$1" in
        --repartidor) repartidor="$2"; shift 2 ;;
        --estat)      estat="$2";      shift 2 ;;
        --limit)      limit="$2";      shift 2 ;;
        -h|--help)    us; exit 0 ;;
        *) echo "ERROR: opció desconeguda '$1'" >&2; us >&2; exit 2 ;;
    esac
done
case "$estat" in
    lliurat|en_repartiment|incidencia|"") ;;    # vàlids, o sense filtre
    *) echo "ERROR: estat '$estat' no vàlid" >&2; exit 2 ;;
esac
[[ "$limit" =~ ^[0-9]+$ ]] || { echo "ERROR: --limit no és cap número" >&2; exit 2; }
resultat=$(tail -n +2 "$CAMI_CSV")
[[ -n $repartidor ]] && resultat=$(echo "$resultat" | grep ",$repartidor,")
[[ -n $estat ]]     && resultat=$(echo "$resultat" | grep ",$estat,")
echo "$resultat" | head -n "$limit"

Tres decisions dignes de comentari. La validació de l'estat fa servir case amb una branca que inclou "", perquè una cadena buida significa «sense filtre» i s'ha de considerar vàlida. El --limit es valida com a número abans de passar-lo a head, que fallaria amb un missatge poc clar si rebés text. I els filtres s'apliquen de manera acumulativa sobre una variable, cosa que permet combinar-los o fer-los servir per separat sense duplicar codi.

Solució a l'Exercici 3

if [[ -z $ciutat ]]; then
    [[ -t 0 ]] && { read -rt 15 -p "Quina ciutat? [totes] " ciutat || ciutat=""; }
    ciutat="${ciutat:-totes}"
fi

La lògica encaixa tres proteccions en quatre línies. [[ -t 0 ]] impedeix que l'script pregunti quan el llança cron, i evita un bloqueig etern. -t 15 limita l'espera encara que hi hagi terminal, per si l'operador se'n va a buscar cafè. I ${ciutat:-totes} cobreix alhora els tres camins que deixen la variable buida: sense terminal, temps exhaurit, o l'usuari prement Enter sense escriure res. Fixa't en el || ciutat="" després del read: quan expira el temps, read retorna un codi diferent de zero i podria deixar brossa a la variable, així que la buidem explícitament.

Conclusió

informe-diari.sh ja és una eina de línia d'ordres com les del sistema. Saps llegir paràmetres posicionals, inclòs el parany de ${10}; comptar amb $# i recórrer amb "$@", entenent per què "$*" destrueix la separació entre arguments; consumir-los amb shift; documentar l'ús amb una funció us() i un here-document; processar opcions llargues amb el patró while + case, fallant davant del que és desconegut en lloc d'ignorar-ho; coneixes getopts, el seu OPTARG, el seu OPTIND i el seu límit amb les opcions llargues; assignes valors per defecte; preguntes amb read -r sense penjar les tasques de cron gràcies a [[ -t 0 ]]; i acceptes configuració per variables d'entorn amb la precedència correcta.

Queda un cap solt, i és el més important del mòdul. Mira les línies que has escrit avui: "$1", "$@", "$data". Per què unes van entre cometes i altres no? Què passaria si algú executés informe-diari.sh --ciutat "Sant Sebastià", o si $ciutat arribés buida enmig d'un [[ ]]? Has escrit ${2:-} i "${VELOZ_LLINDAR:-50}" sense una explicació completa.

Això és la lliçó 03-06, que tanca el mòdul per on s'havia de tancar: l'ordre exacte en què Bash expandeix una línia, què fa cada tipus de cometes, per què la divisió en paraules passa després de substituir les variables, la família completa de ${var:-def}, ${var:?} i ${var:+}, el paper d'IFS, el desastre de for f in $(ls) i quan printf és millor que echo. És la lliçó que converteix els scripts que funcionen gairebé sempre en scripts que funcionen sempre.

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