Comment utiliser la division à virgule flottante dans bash?

242

J'essaie de diviser deux largeurs d'image dans un script Bash, mais bash me donne 0le résultat:

RESULT=$(($IMG_WIDTH/$IMG2_WIDTH))

J'ai étudié le guide Bash et je sais que je devrais l'utiliser bc, dans tous les exemples sur Internet qu'ils utilisent bc. Dans echoJ'ai essayé de mettre la même chose dans mon SCALEmais il ne fonctionnait pas.

Voici l'exemple que j'ai trouvé dans les tutoriels:

echo "scale=2; ${userinput}" | bc 

Comment puis-je demander à Bash de me donner un flotteur 0.5?

Medya Gh
la source
9
Un commentaire pour tous ceux qui essaient de faire de l'arithmétique en virgule flottante dans votre script, demandez-vous: ai-je vraiment besoin d'une arithmétique en virgule flottante? parfois vous pouvez vraiment vous en passer. Voir, par exemple, la dernière partie de BashFAQ / 022 .
gniourf_gniourf

Réponses:

242

Tu ne peux pas. bash ne fait que des entiers; vous devez déléguer à un outil tel que bc.

Ignacio Vazquez-Abrams
la source
8
comment puis-je déléguer un outil comme bc pour mettre la réponse dans la variable RESULT?
Medya Gh
2
donc vous voulez dire comme VAR = $ (echo "scale = 2; $ (($ IMG_WIDTH / $ IMG2_WIDTH))" | bc)?
Medya Gh
54
@Shevin VAR=$(echo "scale=2; $IMG_WIDTH/$IMG2_WIDTH" | bc)ou VAR=$(bc <<<"scale=2;$IMG_WIDTH/$IMG2_WIDTH")sans $ (()) (doubles parenthèses); qui est développé par le bash avant d'exécuter la commande
Nahuel Fouilleul
4
Certes, mais awk est généralement plus susceptible d'être déjà installé dans le système.
CMCDragonkai
9
@NahuelFouilleul Vous avez la meilleure réponse ici. Devrait vraiment être sa propre réponse, et acceptée comme réponse. Cette ligne particulière était incroyablement utile: VAR = $ (bc <<< "scale = 2; $ IMG_WIDTH / $ IMG2_WIDTH")
David
197

tu peux le faire:

bc <<< 'scale=2; 100/3'
33.33

MISE À JOUR 20130926 : vous pouvez utiliser:

bc -l <<< '100/3' # saves a few hits
aayoubi
la source
8
... et ajoute de nombreux chiffres. Au moins sur ma machine ce yiels 33.33333333333333333333 tandis que l'ancien donne 33.33.
Andreas Spindler
12
@AndreasSpindler Une sorte de vieux message, mais au cas où quelqu'un voudrait le savoir, cela peut être changé en appliquant la commande scale par exemple. bc -l <<< 'scale=2; 100/3'
Martie
Faites attention si vous espérez obtenir un entier pour une utilisation ultérieure dans bash en utilisant scale = 0. Depuis la v1.06.95, bc, pour une raison quelconque, ignore la variable d'échelle lorsque les nombres en entrée ont une partie décimale. C'est peut-être dans les documents, mais je ne l'ai pas trouvé. Essayez: echo $ (bc -l <<< 'scale = 0; 1 * 3.3333')
Greg Bell
1
@GregBell La page de manuel indique Unless specifically mentioned the scale of the result is the maximum scale of the expressions involved.Et il y a une note supplémentaire pour l' /opérateur:The scale of the result is the value of the variable scale.
psmith
2
Merci @psmith. Fait intéressant, car / il est dit que "l'échelle du résultat est la valeur de l'échelle variable", mais pas pour la multiplication. Mes meilleurs exemples: l' bc <<< 'scale=1; 1*3.00001' échelle est vraiment 5 pour une raison quelconque, l' bc <<< 'scale=1; 1/3.000001' échelle est 1. Intéressant, la division par 1 met les choses au clair: l' bc <<< 'scale=1; 1*3.00001/1'échelle est 1
Greg Bell
136

frapper

Comme indiqué par d'autres, bashne prend pas en charge l'arithmétique à virgule flottante, bien que vous puissiez la simuler avec une supercherie décimale fixe, par exemple avec deux décimales:

echo $(( 100 * 1 / 3 )) | sed 's/..$/.&/'

Production:

.33

Voir la réponse de Nilfred pour une approche similaire mais plus concise.

Alternatives

Outre les options mentionnées bcet awkalternatives, il existe également les suivantes:

clisp

clisp -x '(/ 1.0 3)'

avec sortie nettoyée:

clisp --quiet -x '(/ 1.0 3)'

ou via stdin:

echo '(/ 1.0 3)' | clisp --quiet | tail -n1

dc

echo 2k 1 3 /p | dc

genius cli calculator

echo 1/3.0 | genius

ghostscript

echo 1 3 div = | gs -dNODISPLAY -dQUIET | sed -n '1s/.*>//p' 

gnuplot

echo 'pr 1/3.' | gnuplot

jq

echo 1/3 | jq -nf /dev/stdin

Ou:

jq -n 1/3

ksh

echo 'print $(( 1/3. ))' | ksh

lua

lua -e 'print(1/3)'

ou via stdin:

echo 'print(1/3)' | lua

maxima

echo '1/3,numer;' | maxima

avec sortie nettoyée:

echo '1/3,numer;' | maxima --quiet | sed -En '2s/[^ ]+ [^ ]+ +//p'

nœud

echo 1/3 | node -p

octave

echo 1/3 | octave

perl

echo print 1/3 | perl

python2

echo print 1/3. | python2

python3

echo 'print(1/3)' | python3

R

echo 1/3 | R --no-save

avec sortie nettoyée:

echo 1/3 | R --vanilla --quiet | sed -n '2s/.* //p'

rubis

echo print 1/3.0 | ruby

wcalc

echo 1/3 | wcalc

Avec sortie nettoyée:

echo 1/3 | wcalc | tr -d ' ' | cut -d= -f2

zsh

echo 'print $(( 1/3. ))' | zsh

unités

units 1/3

Avec une sortie compacte:

units --co 1/3

Autres sources

Stéphane Chazelas a répondu à une question similaire sur Unix.SX.

Thor
la source
9
Très bonne réponse. Je sais qu'elle a été publiée quelques années après la question, mais mérite davantage d'être la réponse acceptée.
Brian Cline
2
Si vous avez zsh disponible, alors il vaut la peine d'envisager d'écrire votre script en zsh au lieu de Bash
Andrea Corbellini
Bravo
Belle liste. Surtout echo 1/3 | node -pest court.
Johnny Wong
39

Améliorant un peu la réponse de marvin:

RESULT=$(awk "BEGIN {printf \"%.2f\",${IMG_WIDTH}/${IMG2_WIDTH}}")

bc ne vient pas toujours comme paquet installé.

adrianlzt
la source
4
Le script awk a besoin d'un exitpour l'empêcher de lire à partir de son flux d'entrée. Je suggère également d'utiliser les -vdrapeaux awk pour prévenir le syndrome du cure-dent penché. Donc:RESULT=$(awk -v dividend="${IMG_WIDTH}" -v divisor="${IMG2_WIDTH}" 'BEGIN {printf "%.2f", dividend/divisor; exit(0)}')
aecolley
2
Une façon plus awkish de le faire serait de lire les arguments du flux d'entrée: RESULT=$(awk '{printf("result= %.2f\n",$1/$2)}' <<<" $IMG_WIDTH $IMG2_WIDTH ".
jmster
1
bcfait partie de POSIX, il est généralement préinstallé.
fuz
cela a fonctionné pour moi en utilisant git bash dans windows 7 ... merci :)
The Beast
31

Vous pouvez utiliser bc par l' -loption (la lettre L)

RESULT=$(echo "$IMG_WIDTH/$IMG2_WIDTH" | bc -l)
user1314742
la source
4
Si je n'inclus pas le -lsur mon système, bc ne fait pas de calcul en virgule flottante.
starbeamrainbowlabs
28

Comme alternative à bc, vous pouvez utiliser awk dans votre script.

Par exemple:

echo "$IMG_WIDTH $IMG2_WIDTH" | awk '{printf "%.2f \n", $1/$2}'

Dans ce qui précède, "% .2f" indique à la fonction printf de renvoyer un nombre à virgule flottante avec deux chiffres après la décimale. J'ai utilisé l'écho pour diriger les variables sous forme de champs car awk fonctionne correctement sur elles. "$ 1" et "$ 2" font référence aux premier et second champs entrés dans awk.

Et vous pouvez stocker le résultat comme une autre variable en utilisant:

RESULT = `echo ...`
marvin
la source
Excellent! Merci. Ceci est utile pour les environnements embarqués où bc n'est pas présent. Vous m'avez fait gagner du temps de compilation croisée.
enthousiastegeek
19

C'est le moment idéal pour essayer zsh, un sur-ensemble (presque) bash, avec de nombreuses fonctionnalités supplémentaires, y compris les mathématiques en virgule flottante. Voici à quoi ressemblerait votre exemple dans zsh:

% IMG_WIDTH=1080
% IMG2_WIDTH=640
% result=$((IMG_WIDTH*1.0/IMG2_WIDTH))
% echo $result
1.6875

Cet article peut vous aider: bash - Vaut-il la peine de passer à zsh pour une utilisation décontractée?

Penghe Geng
la source
3
Je suis un grand fan de zsh et je l'utilise depuis 4 ans, mais l'utilisation interactive est un bon accent ici. Un script qui nécessite zsh ne sera généralement pas très portable sur un ensemble diversifié de machines car il n'est généralement pas standard, malheureusement (pour être juste, peut-être que ça va; OP n'a pas dit tout à fait comment il sera utilisé).
Brian Cline
17

Eh bien, avant float était un temps où la logique des décimales fixes était utilisée:

IMG_WIDTH=100
IMG2_WIDTH=3
RESULT=$((${IMG_WIDTH}00/$IMG2_WIDTH))
echo "${RESULT:0:-2}.${RESULT: -2}"
33.33

La dernière ligne est un bashim, si vous n'utilisez pas bash, essayez plutôt ce code:

IMG_WIDTH=100
IMG2_WIDTH=3
INTEGER=$(($IMG_WIDTH/$IMG2_WIDTH))
DECIMAL=$(tail -c 3 <<< $((${IMG_WIDTH}00/$IMG2_WIDTH)))
RESULT=$INTEGER.$DECIMAL
echo $RESULT
33.33

La raison d'être du code est la suivante: multipliez par 100 avant de diviser pour obtenir 2 décimales.

Nilfred
la source
5

Si vous avez trouvé la variante de votre préférence, vous pouvez également l'encapsuler dans une fonction.

Ici, j'emballe un bashisme dans une fonction div:

Bon mot:

function div { local _d=${3:-2}; local _n=0000000000; _n=${_n:0:$_d}; local _r=$(($1$_n/$2)); _r=${_r:0:-$_d}.${_r: -$_d}; echo $_r;}

Ou multi-ligne:

function div {
  local _d=${3:-2}
  local _n=0000000000
  _n=${_n:0:$_d}
  local _r=$(($1$_n/$2))
  _r=${_r:0:-$_d}.${_r: -$_d}
  echo $_r
}

Vous avez maintenant la fonction

div <dividend> <divisor> [<precision=2>]

et l'utiliser comme

> div 1 2
.50

> div 273 123 5
2.21951

> x=$(div 22 7)
> echo $x
3.14

MISE À JOUR J'ai ajouté un petit script qui vous fournit les opérations de base avec des nombres à virgule flottante pour bash:

Usage:

> add 1.2 3.45
4.65
> sub 1000 .007
999.993
> mul 1.1 7.07
7.7770
> div 10 3
3.
> div 10 3.000
3.333

Et voici le script:

#!/bin/bash
__op() {
        local z=00000000000000000000000000000000
        local a1=${1%.*}
        local x1=${1//./}
        local n1=$((${#x1}-${#a1}))
        local a2=${2%.*}
        local x2=${2//./}
        local n2=$((${#x2}-${#a2}))
        local n=$n1
        if (($n1 < $n2)); then
                local n=$n2
                x1=$x1${z:0:$(($n2-$n1))}
        fi
        if (($n1 > $n2)); then
                x2=$x2${z:0:$(($n1-$n2))}
        fi
        if [ "$3" == "/" ]; then
                x1=$x1${z:0:$n}
        fi
        local r=$(($x1"$3"$x2))
        local l=$((${#r}-$n))
        if [ "$3" == "*" ]; then
                l=$(($l-$n))
        fi
        echo ${r:0:$l}.${r:$l}
}
add() { __op $1 $2 + ;}
sub() { __op $1 $2 - ;}
mul() { __op $1 $2 "*" ;}
div() { __op $1 $2 / ;}
bebbo
la source
1
local _d=${3:-2}est plus simple
KamilCuk
4

Ce n'est pas vraiment une virgule flottante, mais si vous voulez quelque chose qui définit plus d'un résultat en une seule invocation de bc ...

source /dev/stdin <<<$(bc <<< '
d='$1'*3.1415926535897932384626433832795*2
print "d=",d,"\n"
a='$1'*'$1'*3.1415926535897932384626433832795
print "a=",a,"\n"
')

echo bc radius:$1 area:$a diameter:$d

calcule l'aire et le diamètre d'un cercle dont le rayon est donné en $ 1

user3210339
la source
4

Il existe des scénarios dans lesquels vous ne pouvez pas utiliser bc car il pourrait tout simplement ne pas être présent, comme dans certaines versions réduites de busybox ou de systèmes intégrés. Dans tous les cas, limiter les dépendances externes est toujours une bonne chose à faire, vous pouvez donc toujours ajouter des zéros au nombre divisé par (numérateur), ce qui revient à multiplier par une puissance de 10 (vous devez choisir une puissance de 10 selon la précision dont vous avez besoin), cela fera de la division un nombre entier. Une fois que vous avez cet entier, traitez-le comme une chaîne et positionnez le point décimal (en le déplaçant de droite à gauche) un nombre de fois égal à la puissance de dix, vous avez multiplié le numérateur par. Il s'agit d'un moyen simple d'obtenir des résultats flottants en utilisant uniquement des nombres entiers.

Daniel J.
la source
1
Même Busybox a Awk. Il devrait peut-être y avoir une réponse Awk plus importante ici.
tripleee
4

Bien que vous ne puissiez pas utiliser la division en virgule flottante dans Bash, vous pouvez utiliser la division en virgule fixe. Tout ce que vous devez faire est de multiplier vos entiers par une puissance de 10, puis de diviser la partie entière et d'utiliser une opération modulo pour obtenir la partie fractionnaire. Arrondi au besoin.

#!/bin/bash

n=$1
d=$2

# because of rounding this should be 10^{i+1}
# where i is the number of decimal digits wanted
i=4
P=$((10**(i+1)))
Pn=$(($P / 10))
# here we 'fix' the decimal place, divide and round tward zero
t=$(($n * $P / $d + ($n < 0 ? -5 : 5)))
# then we print the number by dividing off the interger part and
# using the modulo operator (after removing the rounding digit) to get the factional part.
printf "%d.%0${i}d\n" $(($t / $P)) $(((t < 0 ? -t : t) / 10 % $Pn))
G. Allen Morris III
la source
4

je sais que c'est vieux, mais trop tentant. donc, la réponse est: vous ne pouvez pas ... mais vous pouvez en quelque sorte. Essayons ça:

$IMG_WIDTH=1024
$IMG2_WIDTH=2048

$RATIO="$(( IMG_WIDTH / $IMG2_WIDTH )).$(( (IMG_WIDTH * 100 / IMG2_WIDTH) % 100 ))

comme ça, vous obtenez 2 chiffres après le point, tronqués (appelez-les arrondis au bas, haha) en bash pur (pas besoin de lancer d'autres processus). bien sûr, si vous n'avez besoin que d'un chiffre après le point, vous multipliez par 10 et faites modulo 10.

ce que cela fait:

  • premier $ ((...)) fait une division entière;
  • second $ ((...)) fait une division entière sur quelque chose de 100 fois plus grand, déplaçant essentiellement vos 2 chiffres vers la gauche du point, puis (%) vous obtenant seulement ces 2 chiffres en faisant modulo.

piste bonus : la version bc x 1000 a pris 1,8 secondes sur mon ordinateur portable, tandis que la version bash pure a pris 0,016 secondes.

івась тарасик
la source
3

Comment faire des calculs à virgule flottante dans bash:

Au lieu d'utiliser " ici des chaînes " ( <<<) avec la bccommande, comme le fait l'un des exemples les plus appréciés , voici mon bcexemple de virgule flottante préféré , directement dans la EXAMPLESsection des bcpages de manuel (voir man bcles pages de manuel).

Avant de commencer, sachez que pour une équation pi est: pi = 4*atan(1). a()ci-dessous est la bcfonction mathématique pour atan().

  1. Voici comment stocker le résultat d'un calcul à virgule flottante dans une variable bash - dans ce cas dans une variable appeléepi . Notez que scale=10définit le nombre de chiffres décimaux de précision à 10 dans ce cas. Tous les chiffres décimaux après cet endroit sont tronqués .

    pi=$(echo "scale=10; 4*a(1)" | bc -l)
  2. Maintenant, pour avoir une seule ligne de code qui imprime également la valeur de cette variable, ajoutez simplement la echocommande à la fin en tant que commande de suivi, comme suit. Notez la troncature à 10 décimales, comme commandé:

    pi=$(echo "scale=10; 4*a(1)" | bc -l); echo $pi
    3.1415926532
  3. Enfin, jetons quelques arrondis. Ici, nous utiliserons la printffonction pour arrondir à 4 décimales. Notez que les 3.14159...tours maintenant 3.1416. Puisque nous arrondissons, nous n'avons plus besoin d'utiliser scale=10pour tronquer à 10 décimales, nous allons donc simplement supprimer cette partie. Voici la solution finale:

    pi=$(printf %.4f $(echo "4*a(1)" | bc -l)); echo $pi
    3.1416

Voici une autre très bonne application et démonstration des techniques ci-dessus: mesurer et imprimer le temps d'exécution.

Notez que la valeur dt_minest arrondie de 0.01666666666...à 0.017:

start=$SECONDS; sleep 1; end=$SECONDS; dt_sec=$(( end - start )); dt_min=$(printf %.3f $(echo "$dt_sec/60" | bc -l)); echo "dt_sec = $dt_sec; dt_min = $dt_min"
dt_sec = 1; dt_min = 0.017

En relation:

Gabriel Staples
la source
1
une réponse solide avec des exemples détaillés et des cas d'utilisation et ronde à l'aide de printf
downvoteit
2

Utilisez calc. C'est l'exemple le plus simple que j'ai trouvé:

calc 1 + 1

 2

calc 1/10

 0.1
Camila Pereira
la source
2

Pour ceux qui essaient de calculer des pourcentages avec la réponse acceptée, mais qui perdent en précision:

Si vous exécutez ceci:

echo "scale=2; (100/180) * 180" | bc

Vous obtenez 99.00seulement, ce qui perd de la précision.

Si vous l'exécutez de cette façon:

echo "result = (100/180) * 180; scale=2; result / 1" | bc -l

Maintenant vous obtenez 99.99.

Parce que vous ne mettez à l'échelle qu'au moment de l'impression.

Se référer ici

Wadih M.
la source
1

** Mathématiques à virgule flottante sécurisées pour les injections dans bash / shell **

Remarque: L'objectif de cette réponse est de fournir des idées pour une solution sécurisée par injection pour effectuer des mathématiques dans bash (ou d'autres shells). Bien sûr, la même chose peut être utilisée, avec des ajustements mineurs pour effectuer un traitement avancé des chaînes, etc.

La plupart des solutions présentées ont été construites à la volée, à l'aide de données externes (variables, fichiers, ligne de commande, variables d'environnement). L'entrée externe peut être utilisée pour injecter du code malveillant dans le moteur, beaucoup d'entre eux

Ci-dessous est une comparaison sur l'utilisation des différents langages pour effectuer des calculs mathématiques de base, où le résultat en virgule flottante. Il calcule A + B * 0,1 (en virgule flottante).

Toutes les solutions tentent d'éviter de créer des scriptlets dynamiques, qui sont extrêmement difficiles à maintenir. À la place, ils utilisent un programme statique et transmettent les paramètres à la variable désignée. Ils géreront en toute sécurité les paramètres avec des caractères spéciaux - réduisant ainsi la possibilité d'injection de code. L'exception est «BC» qui ne fournit pas de fonctionnalité d'entrée / sortie

L'exception est 'bc', qui ne fournit aucune entrée / sortie, toutes les données proviennent de programmes dans stdin, et toutes les sorties vont à stdout. Tous les calculs s'exécutent dans un sandbox, ce qui ne permet pas d'effet secondaire (ouverture de fichiers, etc.). En théorie, l'injection est sûre par conception!

A=5.2
B=4.3

# Awk: Map variable into awk
# Exit 0 (or just exit) for success, non-zero for error.
#
awk -v A="$A" -v B="$B" 'BEGIN { print A + B * 0.1 ; exit 0}'

# Perl
perl -e '($A,$B) = @ARGV ; print $A + $B * 0.1' "$A" "$B"

# Python 2
python -c 'import sys ; a = float(sys.argv[1]) ; b = float(sys.argv[2]) ; print a+b*0.1' "$A" "$B"

# Python 3
python3 -c 'import sys ; a = float(sys.argv[1]) ; b = float(sys.argv[2]) ; print(a+b*0.1)' "$A" "$B"

# BC
bc <<< "scale=1 ; $A + $B * 0.1"
dash-o
la source
pour python3 avec des arguments arbitraires: python3 -c 'import sys ; *a, = map(float, sys.argv[1:]) ; print(a[0] + a[1]*0.1 + a[2])' "$A" "$B""4200.0" ==> 4205.63
WiringHarness
0

voici la commande awk: -F = séparateur de champ == +

echo "2.1+3.1" |  awk -F "+" '{print ($1+$2)}'
koolwithk
la source