2

我正在尝试逐行读取文件并找到每行中数字的平均值。我收到错误消息: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
4

4 回答 4

4

通过一些小的更正,您的代码运行良好:

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
于 2015-10-03T02:03:14.210 回答
1

其他人已经指出这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=4\n(%s) / %d\n' $string $c | bc)
  printf "%s // avg=%s\n" "$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 // %s\n" "$line" "$average"
done

请注意,我的 shell 支持使用+=. 如果您没有,您可以根据需要进行调整。

在这两个示例中,我们将输出打印到小数点后四位——用scale=4bc 或4kdc。我们正在处理标准输入,因此如果您将这些脚本命名为“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.3f\n" "$line" "$average"

done

这里的重要位是: *declare告诉 bash 将其添加$total+=不是像字符串一样附加, * 两个average=赋值,第一个乘以$total1000,第二个将结果拆分为千列, 和 *printf其格式在其输出中强制执行三位小数的精度。

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

YMMV。我并不是说这就是你应该如何解决这个问题,只是它是一种选择。:)

于 2015-10-03T22:47:53.040 回答
1

使用 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
于 2015-10-03T02:27:57.487 回答
0

这是一篇相当老的帖子,但出现在我的谷歌搜索的顶部,所以我想分享一下我的想法:

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"
于 2019-01-28T17:20:46.147 回答