LinuxParty
Realizar operaciones matemáticas en Bash: cálculos con enteros y decimales
Bash incorpora operadores para sumar, restar, multiplicar y comparar números enteros, mientras que herramientas como bc y awk permiten trabajar con decimales y operaciones más avanzadas
Tarde o temprano, cualquier script de Bash necesita realizar algún cálculo: incrementar un contador, comprobar el espacio disponible, obtener un porcentaje, calcular el tiempo transcurrido o decidir si un valor supera determinado límite.
Bash permite realizar directamente numerosas operaciones matemáticas, pero presenta una limitación importante: su aritmética interna trabaja con números enteros. Si necesitamos utilizar decimales, raíces cuadradas, logaritmos u otras operaciones avanzadas, tendremos que recurrir a programas externos como bc o awk.
En este tutorial veremos las principales alternativas y explicaremos cuál conviene utilizar en cada situación.
La forma recomendada: $(( expresión ))
La expansión aritmética es generalmente la forma más clara y práctica de efectuar cálculos enteros en Bash.
Su sintaxis es:
resultado=$(( expresion ))
Por ejemplo:
resultado=$((5 + 3)) echo "$resultado"
El resultado será:
8
Dentro de una expresión aritmética no es obligatorio anteponer $ al nombre de las variables:
numero1=15 numero2=4 resultado=$((numero1 + numero2)) echo "$resultado"
También sería válido escribir:
resultado=$(($numero1 + $numero2))
Pero la primera forma resulta más limpia y suele ser preferible.
Operadores aritméticos disponibles
Bash admite los operadores matemáticos habituales:
+para sumar.-para restar.*para multiplicar./para dividir.%para obtener el resto de una división.**para calcular una potencia.++para incrementar un valor.--para reducir un valor.
Podemos probarlos mediante:
a=20 b=6 echo "Suma: $((a + b))" echo "Resta: $((a - b))" echo "Multiplicación: $((a * b))" echo "División: $((a / b))" echo "Resto: $((a % b))" echo "Potencia: $((a ** 2))"
La salida será:
Suma: 26 Resta: 14 Multiplicación: 120 División: 3 Resto: 2 Potencia: 400
La división 20 / 6 devuelve 3 y no 3,333, porque Bash descarta la parte decimal.
Diferencia entre $(( )) y (( ))
Aunque ambas construcciones trabajan con expresiones aritméticas, no se utilizan exactamente de la misma manera.
La expresión $(( )) devuelve el resultado para poder mostrarlo o asignarlo:
resultado=$((10 * 5)) echo "$resultado"
La construcción (( )), en cambio, evalúa la operación y suele emplearse para modificar variables o realizar comparaciones:
contador=10 ((contador++)) ((contador += 5)) echo "$contador"
El resultado será:
16
También podemos realizar una asignación dentro de los dobles paréntesis:
((total = 25 + 15)) echo "$total"
Operadores abreviados de asignación
Bash admite operadores que permiten modificar una variable sin repetir su nombre:
numero=10 ((numero += 5)) echo "$numero" ((numero -= 3)) echo "$numero" ((numero *= 2)) echo "$numero" ((numero /= 4)) echo "$numero"
Estas operaciones equivalen a:
numero=$((numero + 5)) numero=$((numero - 3)) numero=$((numero * 2)) numero=$((numero / 4))
La forma abreviada suele resultar más cómoda para contadores y acumuladores.
Incrementar y reducir contadores
Para incrementar un contador podemos utilizar:
contador=0 ((contador++))
O bien:
((++contador))
Las dos expresiones aumentan el valor en una unidad, pero existe una diferencia en el resultado de la propia expresión.
El postincremento devuelve primero el valor anterior:
contador=5 echo "$((contador++))" echo "$contador"
La salida será:
5 6
El preincremento aumenta primero la variable y después devuelve el resultado:
contador=5 echo "$((++contador))" echo "$contador"
La salida será:
6 6
Precaución con set -e y ((contador++))
La construcción (( expresión )) devuelve un estado de ejecución 0 cuando la expresión produce un resultado distinto de cero. Si el resultado es cero, devuelve el estado 1.
Esto puede causar una finalización inesperada en scripts que utilicen:
set -e
Por ejemplo:
#!/usr/bin/env bash set -e contador=0 ((contador++)) echo "Este mensaje podría no mostrarse"
Como contador++ evalúa primero el valor anterior, que era cero, la expresión devuelve el estado 1 y set -e puede detener el script.
Una alternativa más segura es:
((++contador))
También podemos impedir explícitamente que ese estado detenga el script:
((contador++)) || true
Comparaciones numéricas mediante (( ))
Los dobles paréntesis también resultan cómodos para incluir comparaciones dentro de estructuras condicionales:
edad=20 if ((edad >= 18)); then echo "Es mayor de edad" else echo "Es menor de edad" fi
Podemos utilizar los operadores:
==igual que.!=distinto de.<menor que.<=menor o igual que.>mayor que.>=mayor o igual que.&&condición lógica Y.||condición lógica O.!negación.
Por ejemplo:
temperatura=32 humedad=70 if ((temperatura > 30 && humedad >= 60)); then echo "Hace calor y existe una humedad elevada" fi
Dentro de (( )) podemos utilizar directamente < y >, sin necesidad de escaparlos.
Utilizar operaciones en un bucle
La aritmética de Bash se integra perfectamente en los bucles de estilo C:
for ((i = 1; i <= 5; i++)); do echo "Iteración número $i" done
También podemos sumar una serie de valores:
total=0 for ((i = 1; i <= 10; i++)); do ((total += i)) done echo "La suma es: $total"
El resultado será:
La suma es: 55
Declarar variables enteras con declare -i
Bash permite indicar que una variable debe tratarse como un número entero mediante:
declare -i numero
A partir de ese momento, cualquier asignación será evaluada como una expresión aritmética:
declare -i numero=10 numero=numero+5 echo "$numero"
El resultado será:
15
También podemos combinar diferentes atributos:
declare -ir limite=100
En este caso:
-ideclara la variable como entera.-rla convierte en una variable de solo lectura.
Si intentamos modificarla:
limite=200
Bash mostrará un error porque la variable es inmutable.
Dentro de una función, declare crea normalmente una variable local. Si necesitamos declarar una variable global desde una función, podemos utilizar declare -g, siempre que la versión de Bash lo permita:
declare -gi contador_global=0
Realizar cálculos con let
let es una orden interna de Bash que evalúa expresiones aritméticas:
numero=10 let numero=numero+5 echo "$numero"
También admite incrementos y operadores abreviados:
let numero++ let numero+=10 let numero*=2
Cuando la expresión contiene espacios o caracteres que la shell podría interpretar, es recomendable entrecomillarla:
let "resultado = 5 * 8"
Aunque let continúa siendo válido, los dobles paréntesis suelen producir un código más legible:
((resultado = 5 * 8))
El veterano comando expr
expr es una herramienta tradicional de Unix que permite evaluar expresiones:
resultado=$(expr 5 + 3) echo "$resultado"
Los espacios entre los números y el operador son obligatorios:
expr 5 + 3
Si escribimos:
expr 5+3
el programa interpreta el argumento como una cadena y devuelve:
5+3
El operador de multiplicación debe escaparse para evitar que Bash lo interprete como un comodín:
expr 5 \* 3
Aunque expr puede encontrarse en scripts antiguos y mantiene su utilidad para determinadas operaciones con cadenas, en código Bash moderno suele ser preferible utilizar:
echo $((5 * 3))
Cómo trabajar con números decimales
Bash no realiza directamente cálculos en coma flotante. Si ejecutamos:
echo $((5 / 2))
obtendremos:
2
Para conseguir un resultado decimal podemos utilizar bc, una calculadora de precisión arbitraria que funciona desde la línea de comandos.
En Debian, Ubuntu y derivados podemos instalarla mediante:
sudo apt install bc
En Fedora:
sudo dnf install bc
En openSUSE:
sudo zypper install bc
En Arch Linux:
sudo pacman -S bc
Operaciones básicas con bc
Podemos enviar una expresión a bc mediante una tubería:
echo '4.1 + 5.2' | bc
El resultado será:
9.3
Para multiplicar:
echo '4.1 * 5.2' | bc
Dependiendo de las reglas de precisión utilizadas, el resultado puede mostrarse con menos decimales de los esperados. Para establecer explícitamente la precisión debemos asignar un valor a la variable scale:
echo 'scale=2; 4.1 * 5.2' | bc
El resultado será:
21.32
Para realizar una división con cuatro decimales:
echo 'scale=4; 10 / 3' | bc
Obtendremos:
3.3333
La variable scale establece el número de decimales utilizado en determinadas operaciones, especialmente en las divisiones.
No debemos confundirla con la función scale(x), que consulta cuántos dígitos decimales tiene un valor:
echo 'scale(12.345)' | bc
El resultado será:
3
Guardar el resultado de bc
Podemos almacenar el resultado mediante una sustitución de comandos:
resultado=$(echo 'scale=2; 25 / 4' | bc) echo "$resultado"
La salida será:
6.25
También podemos utilizar una cadena de entrada de Bash:
resultado=$(bc <<< 'scale=2; 25 / 4')
Esta sintaxis evita crear un proceso adicional para echo, aunque es específica de Bash y de otras shells compatibles.
Cuando intervienen variables:
precio=19.95 cantidad=4 total=$(bc <<< "scale=2; $precio * $cantidad") echo "Total: $total"
El resultado será:
Total: 79.80
Si los valores proceden de la entrada del usuario, debemos validarlos antes de incorporarlos a una expresión.
Calcular porcentajes
Un ejemplo frecuente consiste en calcular un porcentaje:
cantidad=375 total=500 porcentaje=$(bc <<< "scale=2; $cantidad * 100 / $total") echo "Porcentaje: $porcentaje %"
La salida será:
Porcentaje: 75.00 %
También podemos calcular un descuento:
precio=120 descuento=15 importe_descuento=$(bc <<< "scale=2; $precio * $descuento / 100") precio_final=$(bc <<< "scale=2; $precio - $importe_descuento") echo "Descuento: $importe_descuento" echo "Precio final: $precio_final"
Comparar números decimales con bc
Los operadores tradicionales de test y [ ] están pensados principalmente para enteros. Para comparar números decimales podemos utilizar bc.
En sus comparaciones, bc devuelve 1 cuando la condición es verdadera y 0 cuando es falsa:
echo '5.7 > 3.2' | bc
El resultado será:
1
Podemos utilizarlo dentro de un condicional:
temperatura=37.8 if (( $(bc <<< "$temperatura > 37.5") )); then echo "La temperatura supera el límite" else echo "La temperatura está dentro del límite" fi
Es importante comprobar previamente que la variable contiene un número válido.
Funciones matemáticas de bc
Al ejecutar bc con la opción -l, se carga su biblioteca matemática:
bc -l
Esto permite utilizar funciones como:
s(x)para calcular el seno en radianes.c(x)para calcular el coseno en radianes.a(x)para calcular el arcotangente.l(x)para obtener el logaritmo natural.e(x)para calcular la exponencial.sqrt(x)para calcular una raíz cuadrada.
Por ejemplo:
echo 'scale=4; sqrt(2)' | bc -l
La salida será aproximadamente:
1.4142
Para calcular una potencia también podemos utilizar:
echo '2^10' | bc
El resultado será:
1024
Utilizar awk para cálculos decimales
awk constituye otra alternativa muy útil, especialmente cuando ya estamos procesando columnas o archivos de texto.
Para realizar una división:
awk 'BEGIN { printf "%.2f\n", 10 / 3 }'
El resultado será:
3.33
Con variables de Bash:
precio=19.95 cantidad=4 awk -v p="$precio" -v c="$cantidad" \ 'BEGIN { printf "%.2f\n", p * c }'
awk trabaja con números en coma flotante y permite controlar el formato mediante printf.
Para muchos cálculos administrativos sencillos puede resultar suficiente. Sin embargo, cuando necesitamos precisión decimal arbitraria, bc suele ofrecer más control.
Los cálculos monetarios delicados requieren una estrategia bien definida de redondeo. Otra posibilidad consiste en trabajar siempre con unidades enteras, como céntimos, evitando así determinados problemas de representación en coma flotante.
Cuidado con los ceros iniciales
Bash puede interpretar los números que comienzan por cero como valores octales.
Por ejemplo:
echo $((010))
El resultado será:
8
El número 010 se interpreta como octal y no como decimal.
Esto puede provocar errores al procesar horas, días o meses:
mes=08 echo $((mes + 1))
Bash puede mostrar un error porque el dígito 8 no existe en el sistema octal.
Para forzar la interpretación decimal podemos anteponer 10#:
mes=08 resultado=$((10#$mes + 1)) echo "$resultado"
La salida será:
9
Esta precaución resulta especialmente importante cuando los números proceden de date:
mes=$(date +%m) mes_decimal=$((10#$mes))
Evitar divisiones por cero
Una división por cero provoca un error:
divisor=0 resultado=$((10 / divisor))
Antes de dividir debemos comprobar el valor:
dividendo=10 divisor=0 if ((divisor == 0)); then echo "Error: no se puede dividir por cero" >&2 exit 1 fi resultado=$((dividendo / divisor)) echo "$resultado"
La misma precaución debe aplicarse al utilizar bc.
Un ejemplo completo
El siguiente script solicita el precio y la cantidad de un producto, calcula el subtotal, aplica un descuento y muestra el resultado:
#!/usr/bin/env bash read -r -p "Precio del producto: " precio read -r -p "Cantidad: " cantidad read -r -p "Descuento (%): " descuento patron='^[0-9]+([.][0-9]+)?$' if [[ ! $precio =~ $patron || ! $cantidad =~ ^[0-9]+$ || ! $descuento =~ $patron ]]; then echo "Error: introduce valores numéricos válidos." >&2 exit 1 fi subtotal=$(bc <<< "scale=2; $precio * $cantidad") rebaja=$(bc <<< "scale=2; $subtotal * $descuento / 100") total=$(bc <<< "scale=2; $subtotal - $rebaja") printf 'Subtotal: %s €\n' "$subtotal" printf 'Descuento: %s €\n' "$rebaja" printf 'Total: %s €\n' "$total"
Este ejemplo valida los valores antes de entregarlos a bc y mantiene separadas las diferentes operaciones para facilitar su lectura.
¿Qué método deberíamos elegir?
Para cálculos con números enteros, la expansión aritmética es normalmente la opción más recomendable:
resultado=$((a + b))
Para modificar variables, crear contadores o realizar comparaciones:
((contador += 1))
Para números decimales y cálculos de precisión configurable:
resultado=$(bc <<< "scale=2; $a / $b")
Cuando ya estamos procesando texto o queremos aplicar rápidamente un formato decimal:
awk 'BEGIN { printf "%.2f\n", 10 / 3 }'
let continúa siendo válido, mientras que expr aparece principalmente en scripts antiguos o destinados a entornos muy específicos.
Bash ofrece suficientes recursos para resolver la mayoría de los cálculos habituales de un script. La clave consiste en recordar que su aritmética interna trabaja con enteros y elegir una herramienta externa cuando necesitemos decimales, mayor precisión o funciones matemáticas avanzadas.



