bash- find average of numbers in line

前端 未结 4 1284
我寻月下人不归
我寻月下人不归 2021-01-22 01:54

I am trying to read a file line by line and find the average of the numbers in each line. I am getting the error: expr: non-numeric argument

I have narrowe

4条回答
  •  攒了一身酷
    2021-01-22 02:47

    Others have already pointed out that expr is integer-only, and recommended writing your script in awk instead of shell.

    Your system may have a number of tools on it that support arbitrary-precision math, or floats. Two common calculators in shell are bc which follows standard "order of operations", and dc which uses "reverse polish notation".

    Either one of these can easily be fed your data such that per-line averages can be produced. For example, using 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
    

    Of course, the only bc-specific part of this is the format for the notation and the bc itself in the third last line. The same basic thing using dc might look like like this:

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

    Note that my shell supports appending to strings with +=. If yours does not, you can adjust this as you see fit.

    In both of these examples, we're printing our output to four decimal places -- with scale=4 in bc, or 4k in dc. We are processing standard input, so if you named these scripts "calc", you might run them with command lines like:

    $ ./calc < inputfile.txt
    

    The set command at the beginning of the loop turns the $line variable into positional parameters, like $1, $2, etc. We then process each positional parameter in the for loop, appending everything to a string which will later get fed to the calculator.


    Also, you can fake it.

    That is, while bash doesn't support floating point numbers, it DOES support multiplication and string manipulation. The following uses NO external tools, yet appears to present decimal averages of your input.

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

    The important bits here are: * declare which tells bash to add to $total with += rather than appending it as if it were a string, * the two average= assignments, the first of which multiplies $total by 1000, and the second of which splits the result at the thousands column, and * printf whose format enforces three decimal places of precision in its output.

    Of course, input still needs to be integers.

    YMMV. I'm not saying this is how you should solve this, just that it's an option. :)

提交回复
热议问题