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
- Bash només fa enters
$(( ))i(( )): valor davant d'ordre- Operadors disponibles
- Bases numèriques i el parany del
08 declare -i,letiexpr- Decimals amb
bc - Decimals amb
awk, i comparativa - Percentatges i arrodoniment
RANDOM,SECONDSi aritmètica de dates- Límits de 64 bits
informe-diari.shcomplet
- 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àlidBash 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.
$(( )) i (( )): valor davant d'ordre
$(( )) i (( )): valor davant d'ordreSó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 139El $ 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.
- 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.
- Bases numèriques i el parany del
08
08En 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.
declare -i, let i expr
declare -i, let i exprdeclare -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 avisarAquest ú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.
- Decimals amb
bc
bcbc é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" | bcdona2. Cal fixarscale=Nal principi de l'expressió, o fer servir-l(que carrega la llibreria matemàtica i posascale=20); si amb-lnomés vols dos decimals, combinabc -l <<< "scale=2; 10/4"o formata després ambprintf '%.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:
bcfa servir punt, sempre. Si les teves dades vénen amb coma, converteix-les amb${v/,/.}(04-04). bcno és un builtin: cada crida és un procés. Dins d'un bucle de 10.000 línies, això es nota.
- Decimals amb
awk, i comparativa
awk, i comparativaawk 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 | Sí | Sí |
| 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 | Sí |
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.
- 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 multiplicarPer 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 enterI 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.
RANDOM, SECONDS i aritmètica de dates
RANDOM, SECONDS i aritmètica de datesRANDOM (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 datesConvertir 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.
- 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.
informe-diari.sh complet
informe-diari.sh completAfegim 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 * 100dona 0 gairebé sempre. Semprea * 100 / b. - El
08octal. Tot número que vingui dedateo d'un fitxer:10#$n. (( comptador++ ))ambset -e. El post-increment retorna codi 1 la primera vegada i mata l'script. Fes servir++comptador.- Descuidar l'
scaleabc. Sense ell la divisió és entera, igual que a Bash, i et preguntaràs per què10/4dona 2. - No comprovar el divisor. Un dia sense enviaments fa que
totalvalgui 0; a$(( ))això avorta amb «division by 0». - Cridar
bcdins d'un bucle de milers d'iteracions. Acumula en enters i converteix al final, o passa tota la feina aawk. - 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
printffinal 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"; fiSense 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
- 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
