Bash -找到平均的数字在行的

  • 本文关键字:数字 Bash arrays bash expr
  • 更新时间 :
  • 英文 :


我正试图逐行读取文件并找到每行数字的平均值。我得到错误:expr: non-numeric argument

我已经把问题缩小到sum= expr $sum + $i,但我不确定为什么代码不工作。

while read -a rows
do
    for i in "${rows[@]}"
    do
        sum=`expr $sum + $i`
        total=`expr $total + 1`
    done
    average=`expr $sum / $total`
done < $fileName

文件看起来像这样(数字用制表符分隔):

1       1       1       1       1
9       3       4       5       5
6       7       8       9       7
3       6       8       9       1
3       4       2       1       4
6       4       4       7       7

经过一些小的修改,您的代码运行得很好:

while read -a rows
do
    total=0
    sum=0
    for i in "${rows[@]}"
    do
        sum=`expr $sum + $i`
        total=`expr $total + 1`
    done
    average=`expr $sum / $total`
    echo $average
done <filename
对于样例输入文件,产生的输出是:
1
5
7
5
2
5

请注意,答案是什么,因为expr只做整数运算。

使用sed对expr进行预处理

以上代码可以重写为:

$ while read row; do expr '(' $(sed 's/  */ + /g' <<<"$row") ')' / $(wc -w<<<$row); done < filename
1
5
7
5
2
5

使用bash的内置算术能力

expr是过时的。在现代bash中:

while read -a rows
do
    total=0
    sum=0
    for i in "${rows[@]}"
    do
        ((sum += $i))
        ((total++))
    done
    echo $((sum/total))
done <filename

使用awk进行浮点运算

因为awk做浮点运算,所以它可以提供更准确的结果:

$ awk '{s=0; for (i=1;i<=NF;i++)s+=$i; print s/NF;}' filename
1
5.2
7.4
5.4
2.8
5.6

使用IFS变量的相同技巧的一些变体

#!/bin/bash
while read line; do
    set -- $line
    echo $(( ( $(IFS=+; echo "$*") ) / $# ))
done < rows
echo
while read -a line; do
    echo $(( ( $(IFS=+; echo "${line[*]}") ) / ${#line[*]} ))
done < rows
echo
saved_ifs="$IFS"
while read -a line; do
    IFS=+
    echo $(( ( ${line[*]} ) / ${#line[*]} ))
    IFS="$saved_ifs"
done < rows

其他人已经指出expr仅限整数,并建议使用awk而不是shell编写脚本。

您的系统上可能有许多支持任意精度数学或浮点数的工具。shell中两个常见的计算器是bc,它遵循标准的"操作顺序",dc使用"反向波兰符号"。

这两种方法中的任何一种都可以很容易地输入您的数据,从而可以生成每行平均值。例如,使用bc:

#!/bin/sh
while read line; do
  set - ${line}
  c=$#
  string=""
  for n in $*; do
    string+="${string:++}$1"
    shift
  done
  average=$(printf 'scale=4n(%s) / %dn' $string $c | bc)
  printf "%s // avg=%sn" "$line" "$average"
done

当然,其中唯一与bc相关的部分是最后第三行中的符号格式和bc本身。使用dc的相同基本内容可能看起来像这样:

#!/bin/sh
while read line; do
  set - ${line}
  c=$#
  string="0"
  for n in $*; do
    string+=" $1 + "
    shift
  done
  average=$(dc -e "4k $string $c / p")
  printf "%s // %sn" "$line" "$average"
done

请注意,我的shell支持用+=添加字符串。如果没有,你可以根据自己的需要进行调整。

在这两个例子中,我们都将输出输出到小数点后四位——scale=4在bc中,4k在dc中。我们正在处理标准输入,因此,如果您将这些脚本命名为"calc",则可以使用以下命令行运行它们:

$ ./calc < inputfile.txt

循环开头的set命令将$line变量转换为位置参数,如$1$2等。然后,我们在for循环中处理每个位置参数,将所有内容附加到字符串中,该字符串稍后将提供给计算器。


也可以伪造

也就是说,虽然bash不支持浮点数,但它支持乘法和字符串操作。下面的代码没有使用外部工具,但是出现在中以表示输入的十进制平均值。

#!/bin/bash
declare -i total
while read line; do
  set - ${line}
  c=$#
  total=0
  for n in $*; do
    total+="$1"
    shift
  done
  # Move the decimal point over prior to our division...
  average=$(($total * 1000 / $c))
  # Re-insert the decimal point via string manipulation
  average="${average:0:$((${#average} - 3))}.${average:$((${#average} - 3))}"
  printf "%s // %0.3fn" "$line" "$average"
done

这里的重要部分是:* declare告诉bash将添加到$total+=中,而不是像添加字符串一样添加*两次average=赋值,第一次将$total乘以1000,第二次将结果分割到thousand列,并且* printf,其格式强制输出精度为小数点后三位

当然,输入仍然需要是整数

YMMV。我并不是说这是你应该解决这个问题的方法,只是说这是一种选择。:)

这是一篇相当古老的文章,但在我的谷歌搜索中名列前茅,所以我想分享一下我的想法:

while read line; do
    # Convert each line to an array
    ARR=( $line )
    # Append each value in the array with a '+' and calculate the sum
    #   (this causes the last value to have a trailing '+', so it is added to '0')
    ARR_SUM=$( echo "${ARR[@]/%/+} 0" | bc -l)
    # Divide the sum by the total number of elements in the array
    echo "$(( ${ARR_SUM} / ${#ARR[@]} ))"
done < "$filename"

相关内容

  • 没有找到相关文章

最新更新