Portem tot el mòdul ajornant els números. informe-diari.sh ja sap recórrer ciutats, acumular comptadors en mapes i despatxar subordres, però quan arriba el moment de dir quin percentatge d'enviaments va acabar en incidència, o quin és l'import mitjà, es rendeix: hem anat truncant cèntims amb ${import%.*} perquè, i aquesta és la frase que resumeix la lliçó sencera, Bash només sap fer aritmètica entera. No és una limitació menor ni un detall: condiciona com s'escriu cada càlcul en un script i obliga a conèixer les sortides d'emergència. Aquí veuràs l'aritmètica nativa a fons, els seus paranys —inclòs un que trenca scripts de dates cada mes d'agost— i les eines externes per quan calen decimals de debò.

Contingut

  1. Bash només fa enters
  2. $(( )) i (( )): valor davant d'ordre
  3. Operadors disponibles
  4. Bases numèriques i el parany del 08
  5. declare -i, let i expr
  6. Decimals amb bc
  7. Decimals amb awk, i comparativa
  8. Percentatges i arrodoniment
  9. RANDOM, SECONDS i aritmètica de dates
  10. Límits de 64 bits
  11. informe-diari.sh complet

  1. Bash només fa enters

echo $(( 10 / 4 ))       # 2   ← no 2.5; trunca cap a zero, NO arrodoneix
echo $(( 1 / 3 ))        # 0
echo $(( 24.50 + 10 ))   # error de sintaxi: "24.50" no és un número vàlid

Bash treballa amb enters amb signe de 64 bits, i punt. La divisió trunca: 7/2 és 3, i -7/2 és -3 (cap a zero, no cap avall). Els literals amb punt decimal ni tan sols s'accepten com a entrada. Les tres conseqüències pràctiques que governen tota la resta: per a percentatges i mitjanes cal multiplicar abans de dividir o el resultat serà 0; els imports en euros es guarden millor en cèntims (enters) i es formaten al final; i quan de debò calen decimals al càlcul, es delega en bc o awk.

  1. $(( )) i (( )): valor davant d'ordre

Són dues construccions diferents que comparteixen sintaxi interna: n=$(( 3 + 4 )) és una expansió que se substitueix pel valor (n val 7), mentre que (( n > 5 )) és una ordre que no produeix sortida i només fixa $? (aquí, 0 perquè la comparació és certa).

Dins de totes dues, les variables no porten $, perquè el context aritmètic ja sap que són variables:

total=128; incidencies=11
echo $(( incidencies * 100 / total ))     # 8   ← sense $ a dins
echo $(( $incidencies * 100 / $total ))   # 8   ← amb $ funciona, però sobra
(( total++ )); (( total += 10 ))          # 129, després 139

El $ només és imprescindible per als paràmetres posicionals ($1) i els especials ($#), i per als elements d'arrays associatius amb clau variable: (( ENVIAMENTS[$c]++ )).

Ara la part contraintuïtiva. Com a ordre, (( )) inverteix el criteri de C:

Expressió Valor aritmètic $? (codi de sortida) En un if
(( 1 )) / (( 5 )) 1 / 5 0 cert
(( 0 )) / (( 3 > 5 )) 0 1 fals

És coherent: en aritmètica, «diferent de zero» és veritable; al shell, èxit és 0. (( )) tradueix entre tots dos mons. Però això crea un parany real amb set -e (ho veuràs a 05-03), que avorta l'script davant de qualsevol ordre que falli: amb comptador=0, el post-increment de (( comptador++ )) avalua al valor anterior, és a dir 0, així que $? val 1 i l'script mor. Les solucions, per ordre de preferència:

(( ++comptador )) (pre-increment: avalua a 1, codi 0), (( comptador++ )) || true, o comptador=$(( comptador + 1 )), que en ser una expansió no falla mai. Regla de butxaca: fes servir $(( )) per calcular i (( )) per a condicions; si fas servir (( )) per incrementar sota set -e, posa el ++ davant o afegeix || true.

  1. Operadors disponibles

Categoria Operadors Exemple
Aritmètics + - * / % ** $(( 2 ** 10 )) → 1024
Increment i assignació ++ --, = += -= *= /= %= (( ++i )), (( suma += imp ))
Comparació == != < <= > >= (( total > 100 ))
Lògics i ternari && || !, c ? a : b $(( n > 0 ? n : -n ))
Bits & | ^ ~ << >> $(( 1 << 3 )) → 8

Exemples: echo $(( 17 % 5 )) dona la resta 2; max=$(( a > b ? a : b )) guarda el més gran de dos; i (( errors > 0 && detallat )) && mostrar_detall combina dues condicions numèriques. Dins de (( )) pots fer servir < i > amb seguretat, cosa impossible a [ ], on serien redireccions. Per això per comparar números sempre (( )), i [[ ]] queda per a cadenes i fitxers. És la mateixa recomanació de 03-03, ara justificada.

  1. Bases numèriques i el parany del 08

En context aritmètic, Bash interpreta el prefix d'un número:

Escriptura Base Valor
42 / 0x2A Decimal / hexadecimal 42
052 Octal (zero inicial) 42
2#101010 Binària (base#numero) 42
10#08 Decimal forçada 8

El tercer és la bomba. Un zero a l'esquerra significa octal, i en octal no existeixen els dígits 8 i 9, així que mes="08"; echo $(( mes + 1 )) no imprimeix 9: avorta amb bash: 08: valor massa gran per a la base.

Aquest error apareix exactament on més fa mal: processant dates i hores, que porten zeros a l'esquerra de manera natural (08 d'agost, 09 de setembre, l'hora 08:00). Un script que ha funcionat onze mesos rebenta a l'agost. La solució és forçar la base amb 10#:

mes="08"; echo $(( 10#$mes + 1 ))          # 9 ✓
hora="09"
(( 10#$hora >= 8 && 10#$hora < 20 )) && echo "Horari de repartiment"

Fixa't que aquí el $ és obligatori: 10#$mes construeix textualment 10#08 abans d'avaluar-lo. L'alternativa és treure el zero amb una expansió de 04-04, ${mes#0}, tot i que falla si el valor és 00. Aplica 10# a tot número que vingui de fora —de date, d'un fitxer, d'un argument— perquè no controles si porta zeros al davant.

  1. declare -i, let i expr

declare -i marca una variable com a entera: qualsevol assignació s'avalua com a aritmètica.

declare -i comptador=0
comptador+=5; comptador="3 * 4"   # 5 (suma, no concatena), després 12 (la cadena s'avalua)
comptador="hola"                  # 0: el que no és numèric val zero, SENSE avisar

Aquest últim comportament silenciós és la raó per la qual moltes guies d'estil el desaconsellen: prefereixen variables normals i $(( )) explícit, que deixa visible on hi ha càlcul. Fes-lo servir per a acumuladors evidents (declare -i total=0) i no per a dades d'entrada.

let "n = 3 + 4" i n=$(expr 3 + 4) són formes heretades d'escriure n=$(( 3 + 4 )). expr és especialment dolent: crea un procés per cada operació, exigeix espais al voltant de cada operador i obliga a escapar l'* com a \*; existeix només per compatibilitat amb shells antics (08-07). let és intern i correcte, però (( )) fa el mateix amb millor sintaxi. Escriu (( )) i $(( )); reconeix let i expr quan els vegis en scripts vells.

  1. Decimals amb bc

bc és una calculadora de precisió arbitrària. Llegeix expressions per l'entrada estàndard i escriu el resultat:

bc <<< "24.50 * 3"                   # 73.50  ← here-string, més curt que echo | bc
echo "scale=2; 10 / 4" | bc          # 2.50
total=1487; lliurats=1362
printf 'Lliuraments: %.2f%%\n' "$(bc -l <<< "scale=2; $lliurats*100/$total")"  # 91.59%

Les claus:

  • Sense scale, la divisió és entera: echo "10/4" | bc dona 2. Cal fixar scale=N al principi de l'expressió, o fer servir -l (que carrega la llibreria matemàtica i posa scale=20); si amb -l només vols dos decimals, combina bc -l <<< "scale=2; 10/4" o formata després amb printf '%.2f' (on %% imprimeix un % literal).
  • Les variables de Bash s'expandeixen abans, així que van amb $ i entre cometes: bc <<< "scale=2; $a / $b".
  • Ull amb el separador decimal: bc fa servir punt, sempre. Si les teves dades vénen amb coma, converteix-les amb ${v/,/.} (04-04).
  • bc no és un builtin: cada crida és un procés. Dins d'un bucle de 10.000 línies, això es nota.

  1. Decimals amb awk, i comparativa

awk també calcula en coma flotant, i té un avantatge: pot llegir el fitxer i calcular alhora, en una sola passada i un sol procés. awk -F, 'NR>1 {s += $6; n++} END {printf "%.2f\n", s/n}' enviaments.csv retorna 26.34, l'import mitjà, sense cap bucle de Bash pel mig.

Criteri $(( )) natiu bc awk
Decimals No
Processos llançats 0 1 per crida 1 per crida
Velocitat en bucles Màxima Baixa Baixa (però 1 sola passada)
Precisió 64 bits enters Arbitrària Coma flotant (doble)
Pot llegir fitxers No No

Criteri de tria: enters → $(( )); un càlcul decimal solt → bc; agregar un fitxer sencer → awk. Aquest últim és el protagonista de 06-01, on veuràs que molts bucles d'aquest mòdul es redueixen a una línia.

  1. Percentatges i arrodoniment

Amb enters, la tècnica és multiplicar per 100 abans de dividir:

incidencies=11; total=128
echo $(( incidencies * 100 / total ))   # 8 ✓ correcte
echo $(( incidencies / total * 100 ))   # 0 ✗ la divisió trunca a 0 abans de multiplicar

Per aconseguir un decimal sense sortir de Bash, multiplica per 1000 i munta la cadena a mà; i per arrodonir a l'enter més pròxim en comptes de truncar, suma la meitat del divisor abans de dividir:

p=$(( incidencies * 1000 / total )); printf '%d.%d%%\n' $(( p/10 )) $(( p%10 ))  # 8.5%
echo $(( (incidencies * 100 + total / 2) / total ))  # 9 en comptes de 8: arrodoniment enter

I la versió neta amb dos decimals, que és la que farem servir: printf 'Incidències: %.2f%%\n' "$(bc -l <<< "scale=4; $incidencies * 100 / $total")" imprimeix Incidències: 8.59%. Fixa't en el scale=4 seguit de %.2f: es calcula amb més precisió de la necessària i s'arrodoneix en formatar, que és la manera correcta d'evitar errors d'arrodoniment acumulats.

  1. RANDOM, SECONDS i aritmètica de dates

RANDOM (vist a 03-02) retorna un enter pseudoaleatori entre 0 i 32767 a cada lectura. Amb el mòdul s'acota a un rang, útil per mostrejar:

index=$(( RANDOM % ${#CIUTATS[@]} ))            # una ciutat a l'atzar de l'array
echo "Auditant ${CIUTATS[index]}"

Amb l'operador % els valors baixos queden lleugeríssimament afavorits; per a una auditoria de mostreig és irrellevant, per a criptografia RANDOM no serveix en absolut (fes servir /dev/urandom).

SECONDS compta els segons des que va arrencar el shell o des de l'últim cop que se li va assignar un valor, així que SECONDS=0; treball; printf 'Processat en %d s\n' "$SECONDS" cronometra un bloc sense més. Per a mesures més fines, date +%s dona la marca de temps Unix en segons (i %s%3N en mil·lisegons), de manera que inici=$(date +%s) seguit de $(( $(date +%s) - inici )) mesura la durada exacta.

Aritmètica de dates. No la facis a mà: els mesos tenen durades diferents i existeixen els anys de traspàs. date -d (GNU) entén expressions relatives:

ahir=$(date -d 'yesterday' +%F)          # 2026-08-02
fa_7=$(date -d '7 days ago' +%F)         # 2026-07-27
dema=$(date -d "$data + 1 day" +%F)      # sobre una data concreta
dies=$(( ( $(date -d "$f2" +%s) - $(date -d "$f1" +%s) ) / 86400 ))  # dies entre dates

Convertir totes dues dates a segons Unix i restar és l'idioma universal per a «quants dies han passat». El faràs servir a 07-01 per programar tasques i a 07-03 per esborrar còpies de seguretat de més de N dies.

  1. Límits de 64 bits

Bash fa servir enters amb signe de 64 bits: de -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807. En passar-se, desborda en silenci: echo $(( 9223372036854775807 + 1 )) imprimeix -9223372036854775808. Cap error, cap avís: el número fa la volta. A la pràctica no hi arribaràs sumant imports, però sí que pot aparèixer amb marques de temps en nanosegons o mides en bytes multiplicades diverses vegades. Si sospites que un càlcul es pot acostar a aquell ordre de magnitud, fes servir bc, que té precisió arbitrària i no desborda mai.

  1. informe-diari.sh complet

Afegim els càlculs que faltaven. Els imports s'acumulen en cèntims —enters— i només es converteixen a euros en imprimir:

# acumular_dia guarda IMPORT[ciutat] en CÈNTIMS (enter)
acumular_dia() {
    local data="${1:?}" f ciutat estat import euros cent
    declare -gA ENVIAMENTS INCIDENCIES IMPORT
    while IFS=, read -r _ f ciutat _ estat import; do
        [[ "$f" == "$data" ]] || continue
        euros="${import%.*}"; cent="${import#*.}"                 # 04-04
        (( ENVIAMENTS["$ciutat"]++ ))
        (( IMPORT["$ciutat"] += 10#$euros * 100 + 10#$cent ))     # 10#: per si hi ha "05"
        [[ "$estat" == "incidencia" ]] && (( INCIDENCIES["$ciutat"]++ ))
    done < <(tail -n +2 "$CAMI_CSV")
}

# percentatge — Calcula a/b*100. Ús: percentatge <part> <total>
percentatge() {  # la guarda evita la divisió per zero
    (( ${2:?} == 0 )) && { echo "0.00"; return 0; }; bc -l <<< "scale=4; $1 * 100 / $2"
}

taula_ciutats() {
    local c env inc imp total_env=0 total_inc=0
    printf '%-10s %7s %7s %8s %10s %9s\n' CIUTAT ENVIAM. INCID. '%INCID' IMPORT MITJANA
    for c in "${CIUTATS[@]}"; do
        [[ -v ENVIAMENTS[$c] ]] || continue
        env="${ENVIAMENTS[$c]}"; inc="${INCIDENCIES[$c]:-0}"; imp="${IMPORT[$c]}"
        printf '%-10s %7d %7d %7.2f%% %9.2f€ %8.2f€\n' \
            "$c" "$env" "$inc" "$(percentatge "$inc" "$env")" \
            "$(bc -l <<< "scale=4; $imp/100")" \
            "$(bc -l <<< "scale=4; $imp/100/$env")"
        (( total_env += env, total_inc += inc ))
    done
    printf '%-10s %7d %7d %7.2f%%\n' TOTAL "$total_env" "$total_inc" \
        "$(percentatge "$total_inc" "$total_env")"
    printf 'Lliuraments correctes: %.2f%%\n' \
        "$(percentatge "$(( total_env-total_inc ))" "$total_env")"
}

Sortida:

CIUTAT     ENVIAM.  INCID.   %INCID     IMPORT   MITJANA
Valencia       128      11    8.59%   3204.55€    25.04€
Madrid         152      14    9.21%   4013.75€    26.41€
TOTAL          450      41    9.11%
Lliuraments correctes: 90.89%

I la variació respecte al dia anterior, que compara dues execucions:

variacio() {  # retorna "n/d" si ahir no hi va haver enviaments
    (( ${2:?} == 0 )) && { echo "n/d"; return 0; }; bc -l <<< "scale=2; ($1-$2)*100/$2"
}
ahir_env=$(grep -c ",$(date -d yesterday +%F)," "$CAMI_CSV")
printf 'Variació de volum: %+.2f%%\n' "$(variacio "$total_env" "$ahir_env")"  # +3.45%

Tres decisions de disseny que val la pena subratllar: els imports viuen com a enters en cèntims durant tot el càlcul, bc s'invoca una vegada per cel·la i no dins del bucle de lectura (que processa milers de línies), i percentatge() comprova el divisor abans de dividir, perquè una divisió per zero a bc no avorta però contamina la sortida.

Errors Habituals i Consells

  • Dividir abans de multiplicar. a / b * 100 dona 0 gairebé sempre. Sempre a * 100 / b.
  • El 08 octal. Tot número que vingui de date o d'un fitxer: 10#$n.
  • (( comptador++ )) amb set -e. El post-increment retorna codi 1 la primera vegada i mata l'script. Fes servir ++comptador.
  • Descuidar l'scale a bc. Sense ell la divisió és entera, igual que a Bash, i et preguntaràs per què 10/4 dona 2.
  • No comprovar el divisor. Un dia sense enviaments fa que total valgui 0; a $(( )) això avorta amb «division by 0».
  • Cridar bc dins d'un bucle de milers d'iteracions. Acumula en enters i converteix al final, o passa tota la feina a awk.
  • Consell: guarda diners, temps i magnituds en la unitat mínima entera (cèntims, segons, bytes). Tota l'aritmètica es torna exacta i només el printf final necessita decimals.

Exercicis

Exercici 1. Escriu pct_enter(), que calculi el percentatge arrodonit (no truncat) de $1 sobre $2 fent servir només aritmètica nativa i retorni 0 si el total és zero.

Exercici 2. Un script rep una hora en format HH (per exemple 08 o 19) i ha d'imprimir Horari de repartiment si és entre 8 i 19 inclosos, i Fora d'horari en cas contrari. Escriu-lo evitant el parany de l'octal.

Exercici 3. Calcula l'import mitjà per enviament d'enviaments.csv amb dos decimals de dues maneres: acumulant en cèntims amb Bash + bc, i amb una sola línia d'awk. Mesura quant triga cadascuna amb SECONDS.

Solucions

Solució 1.

pct_enter() {
    local part="${1:?}" total="${2:?}"
    (( total == 0 )) && { echo 0; return 0; }
    echo $(( (part * 100 + total / 2) / total ))
}
pct_enter 11 128                   # 9  (8.59 arrodonit)

El + total / 2 abans de dividir és el truc estàndard d'arrodoniment amb enters: suma mitja unitat del resultat final.

Solució 2.

hora="${1:?falta l'hora}"
if (( 10#$hora >= 8 && 10#$hora <= 19 )); then echo "Horari de repartiment"
else echo "Fora d'horari"; fi

Sense el 10#, l'entrada 08 provocaria valor massa gran per a la base i l'script moriria cada matí a les vuit. Amb ell, funciona amb 08, 8 i 19 igual de bé.

Solució 3.

readonly CSV=/srv/veloz/dades/enviaments.csv
SECONDS=0; suma=0; n=0
while IFS=, read -r _ _ _ _ _ import; do
    (( suma += 10#${import%.*} * 100 + 10#${import#*.} )); (( ++n ))
done < <(tail -n +2 "$CSV")
printf 'Bash: %.2f€ en %d s\n' "$(bc -l <<< "scale=4; $suma/100/$n")" "$SECONDS"

SECONDS=0
printf 'awk:  %s€ en %d s\n' \
    "$(awk -F, 'NR>1 {s+=$6; n++} END {printf "%.2f", s/n}' "$CSV")" "$SECONDS"

Totes dues donen 26.34€. Amb uns quants milers de línies les dues triguen menys d'un segon, però en créixer el fitxer la diferència es dispara a favor d'awk: una sola passada, un sol procés, coma flotant nativa. És l'eina que estrenaràs a 06-01.

Conclusió

Bash calcula amb enters de 64 bits i res més: la divisió trunca, els decimals no s'accepten i per això tot càlcul monetari es fa en cèntims i tot percentatge multiplica abans de dividir. $(( )) produeix el valor i (( )) actua com a ordre amb el codi de sortida invertit —zero aritmètic significa error—, dins de totes dues les variables van sense $, i 10#$n et salva que un 08 procedent d'una data rebenti l'script. Quan calen decimals de debò, bc -l amb el seu scale resol el càlcul solt i awk resol el fitxer sencer; printf %.2f hi posa el format final. RANDOM, SECONDS i date -d completen l'instrumental per mostrejar, mesurar i comparar dates.

Amb això es tanca el Mòdul 4 i, sobretot, es tanca la transformació d'informe-diari.sh: el que al Mòdul 3 era un guió lineal que calia invocar una vegada per ciutat és avui un programa amb bucles, funcions documentades, mapes de comptadors, format amb printf, subordres despatxades per case i estadístiques amb decimals. Té estructura.

El que encara no té és robustesa. Si el CSV està a mig escriure, si el disc s'omple, si algú el llança dues vegades alhora, l'script farà coses estranyes sense dir per què. Al Mòdul 5 fem aquest salt: find, xargs, tar i fitxers temporals segurs amb mktemp (05-01); gestió de processos i senyals (05-02); set -euo pipefail, trap i depuració amb set -x (05-03); expressions regulars de debò amb =~ (05-04); descriptors de fitxer i here-documents (05-05); i per fi la llibreria lib/comu.sh que anem prometent des de 04-02, on les teves funcions deixaran de viure dins d'un sol script per convertir-se en el toolkit compartit de Veloz Envíos (05-06).

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