如何在bash中使用浮点数算术?

342

我正在尝试在Bash脚本中计算两个图像宽度的商,但是Bash给出了0作为结果:

RESULT=$(($IMG_WIDTH/$IMG2_WIDTH))
我已经学习了Bash指南,知道应该使用bc。在网上的所有例子中,他们都使用bc。在echo中,我尝试将相同的内容放入我的SCALE中,但它没有起作用。
这是我在教程中找到的例子:
echo "scale=2; ${userinput}" | bc 

我该如何让Bash输出一个像 0.5 这样的浮点数?


11
对于每个在脚本中尝试使用浮点数算术的人,你需要问自己一个问题:_我真的需要使用浮点数算术吗?_有时候可以完全不用。例如,请参考BashFAQ/022的最后一部分。 - gniourf_gniourf
1
可以教授bash,例如浮点结果的整数除法。 - Cyrus
24个回答

6

我知道这个问题很老,但还是有一些诱惑。答案是:你不能... 但你有点可以。让我们尝试这个:

$IMG_WIDTH=1024
$IMG2_WIDTH=2048

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

像这样,在纯bash中得到小数点后2位,截断(称为向下舍入)(无需启动其他进程)。当然,如果您只需要小数点后一位,则乘以10并取模10。
这是什么意思:
第一个$ ((...)) 进行整数除法; 第二个$ ((...)) 对100倍的某些数字进行整数除法,本质上将您的2个数字移动到小数点左侧,然后(%)通过取模仅获取这2个数字。
额外福利:bc版本× 1000在我的笔记本电脑上花费了1.8秒,而纯bash版本只花费了0.016秒。

你的“解决方案”在“第二部分”的结果小于10时将无法工作。例如,尝试使用IMG_WIDTH=103IMG2_WIDTH=100 - FedKad

5

虽然在Bash中不能使用浮点数除法,但可以使用定点数除法。您需要做的就是将整数乘以10的某个幂,然后去掉整数部分并使用模运算获取小数部分。根据需要四舍五入。

#!/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))

4

这并不是真正的浮点数,但如果您想要在一次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

计算半径为 $1$ 的圆的面积和直径


4

对于那些想要使用已接受的答案计算百分比但丢失精度的人:

如果您运行以下代码:

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

你只得到了99.00,这意味着你失去了精度。
如果你按照下面的方式运行它:
echo "result = (100/180) * 180; scale=2; result / 1" | bc -l

现在你获得了99.99

因为你只在打印的时候才进行缩放。

请参考这里


4

有一些情况下你不能使用 bc,因为它可能不存在,比如在某些简化版的 Busybox 或嵌入式系统中。无论如何,限制外部依赖总是一个好习惯,这样你就可以给被除数(分子)加上零,这相当于乘以10的幂次方(根据需要选择一个10的幂次方),这将使得除法输出一个整数。一旦你获得了这个整数,把它作为字符串处理,并将小数点(从右向左移动)移动与你所乘的10的幂次方相等的次数。这是一个只使用整数来获得浮点结果的简单方法。


1
即使Busybox也有Awk。也许这里应该有一个更突出的Awk答案。 - tripleee

3
使用calc。我发现这是最简单的方法。 示例:

计算 1+1

 2

calc 1/10

 0.1

2
Bash可以在没有其他程序的情况下很好地计算浮点结果。
Bash甚至可以独立准确地计算到小数点后第九位的π
例如:
calc=104348/33215

accuracy=9

calc99p9=$((10**$accuracy))*$calc
result99p9=$((calc99p9))
result=${result99p9: -${#result99p9}: -$accuracy}.${result99p9: -$accuracy}

echo Bash calculated pi to be $result

结果是
Bash calculated pi to be 3.141592653

如果答案应该在-1 ... 0范围内,则此操作失败。 - Mikko Rantalainen
使用calc=-104348/332150作为例子,我得到的结果是-0.314159265。因此,在-1到0的范围内,它似乎工作得很好。 - Code ninetyninepointnine
我承认错误。我不确定我尝试了什么样的例子,但是我再也无法重现范围在-1到0之间的数字问题了。我猜测在最初的测试中可能出现了某种打字错误。 - Mikko Rantalainen
是的!就是这个! - undefined

2
如其他人所指出的,Bash没有内置的浮点运算符。
即使不使用计算器程序如bc和awk或任何外部程序,您也可以在Bash中实现浮点数。
我在我的项目shellmath中正是这样做的,分为三个基本步骤:
1. 将数字分解为其整数和小数部分 2. 使用内置的整数运算符分别处理这些部分,同时注意位值和进位 3. 重新组合结果
作为一个引子,我添加了一个演示脚本,使用以x=0为中心的Taylor级数计算e。
如果您有时间,请查看它。我欢迎您的反馈!

1
这听起来是一个有趣的练习,但它是否实用呢?通常更喜欢使用shell脚本本地处理数据处理任务而不是使用外部程序的目的是为了避免启动新进程的开销。但这与许多此类任务的基本缓慢性相抵触。在什么情况下您的shell实现会胜过bc(例如)? - John Bollinger
2
谢谢您的问题。我已经在shellmath README中添加了一些关于这个问题的信息。我认为它是实用的,因为我进行了时间实验,并将其捕获在timingData.txt中。首先,我一直小心使用高效的脚本编写方法(项目中提供了详细信息),但我认为真正的关键是您可以在不分叉子shell的情况下运行shellmathshellmath是一组函数,它们将其结果写入shell变量。另一方面,由于bc是一个可执行文件,您必须对其进行子shell处理才能捕获结果。 - Clarity_20
有点酷!我喜欢这个想法。 - lumpidu

2

** 在bash / shell中进行注入安全的浮点数运算 **

注意:本答案的重点是提供在bash(或其他shell)中执行数学运算的注入安全解决方案的想法。当然,同样可以进行高级字符串处理等操作,只需进行微调。

大多数提出的解决方案都会构建小的脚本,使用外部数据(变量、文件、命令行、环境变量)。外部输入可以用于将恶意代码注入引擎中,其中许多方法都有此风险。

以下是使用各种语言执行基本的浮点数计算(结果为浮点数)的比较。它计算A + B * 0.1。

所有解决方案都尝试避免创建动态脚本,这些脚本非常难以维护,而是使用静态程序,并将参数传递到指定的变量中。它们将安全地处理具有特殊字符的参数-减少了代码注入的可能性。例外情况是“BC”,它不提供输入/输出功能。

唯一的例外是“bc”,它不提供任何输入/输出,所有数据都通过stdin传递给程序,并且所有输出都通过stdout进行。所有计算都在沙盒中执行,不允许副作用(打开文件等)。理论上,它是按设计注入安全的!

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"

针对Python3的任意参数:
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

1

除数 = 被除数 × 商 + 余数

我们只需要计算商和余数,然后将这些字符串连接在一个变量中。

新的方法仅适用于对数为小数的除数:

function main() {
  bar=10030
  divisor=100
  # divisor=50

  quotient=$((bar / divisor))
  # remainder=$((bar - v_int * divisor))
  remainder=$((bar % divisor))
  remainder_init=$remainder

  printf "%-15s --> %s\n" "quotient" "$quotient"
  printf "%-15s --> %s\n" "remainder" "$remainder"

  cnt=0
  while :; do
    remainder=$((remainder * 10))
    aux=$((remainder / divisor))
    printf "%-15s --> %s\n" "aux" "$aux"
    [[ aux -ne 0 ]] && break
    ((cnt += 1))
    printf "%-15s --> %s\n" "remainder" "$remainder"
  done
  printf "%-15s --> %s\n" "cnt" "$cnt"
  printf "%-15s --> %s\n" "aux" "$aux"

  printf $quotient
  printf "."
  for i in $(seq 1 $cnt); do printf "0"; done
  printf $remainder_init
}
clear
main

旧的错误方法:

bar=1234 \
&& divisor=1000 \
    && foo=$(printf "%s.%s" $(( bar / divisor )) $(( bar % divisor ))) \
    && printf "bar is %d miliseconds or %s seconds\n" $bar $foo

输出:bar是1234毫秒或1.234秒


这将会错误地计算小数部分。例如,当 bar=103,divisor=100 时,应该输出 1.03 而不是 1.3。 - Mikko Rantalainen
1
你是对的 @Mikko,谢谢!我已经修复了它,但只针对除数10、100、1000等……实际上我会选择使用Python。 - Xopi García

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接