从文件和标准input添加数字

如何在shell中使用whilefor循环来一起添加数字?

我只想要一个非常简单的程序,与标准input和文件一起工作。

例:

 $ echo 1 2 | sh myprogram 3 

如果一个文件myfile包含一个数字列表,我想能够做到这一点:

 sh myprogram myfile 

并得到数字的总和作为输出。

虽然这个问题的核心是一个相关问题的重复,但它确实提出了额外的要求 (无论这些要求是否完全是由OP实施的):

  • 解决方案应该打包成一个脚本

  • 该解决方案应符合POSIX(问题是一般标签外壳 )

  • 输入应该来自一个文件,如果指定的话,或者来自标准输入默认。

  • 在一个输入行上可以多个数字(例如, echo 1 2 )。

  • 解决方案应该使用一段whilefor循环,即纯粹的外壳解决方案。

下面的解决方案解决了这些需求, 除了最后一个 – 这可能是OP的一个破坏者,但也许其他人会发现它是有用的。

通过使用外部实用程序偏离这一要求意味着解决方案将在大量输入数据的情况下运行良好 – shell代码中的循环很慢。

如果你仍然需要一个shell while -loop的解决方案,请看这篇文章的底部; 它还包括输入验证。


myprogram内容 (POSIX兼容,但需要一个表示标准输入的文件系统为/dev/stdin ):

请注意, 不会执行输入验证 – 输入中的所有令牌都被假定为十进制数(正数或负数)。 该脚本将打破任何其他输入。 请参阅下面的一个更复杂的解决方案,以滤除非十进制数字标记。

 #!/bin/sh { tr -s ' \t\n' '+'; printf '0\n'; } < "${1-/dev/stdin}" | bc 
  • ${1-/dev/stdin}使用第一个参数( $1 ,假定是一个文件路径)(如果指定的话)或/dev/stdin (代表标准输入stdin)。

  • tr -s ' \t\n' '+'用一个tr -s ' \t\n' '+'替换输入中的任何空格(空格,制表符,换行符); 实际上,这会导致<num1>+<num2>+...+ – 注意结尾处悬挂的+ ,稍后再讨论。

    • 请注意,正是这种处理空白的方法使解决方案能够处理任何一行一行和多行一行的混合输入
  • printf '0\n'追加一个0以便上面的表达式成为一个有效的加法运算。

    • 分组( { ...; ...; }trprintf命令使它们充当管道( | )的单个输出源。
  • bc是一个可以执行(任意精度)算术的POSIX实用程序 。 它评估输入表达式并输出结果。

通过输入验证 :只需忽略不是十进制数的输入令牌。

 #!/bin/sh { tr -s ' \t\n' '\n' | grep -x -- '-\{0,1\}[0-9][0-9]*' | tr '\n' '+'; printf '0\n'; } < "${1-/dev/stdin}" | bc 
  • tr -s ' \t\n' '\n'将所有单独的标记放在输入中,不管它们在同一行上还是在它们自己的行上。
  • grep -x -- '-\{0,1\}[0-9][0-9]*'只匹配只包含十进制数字的行。
  • 命令的其余部分与未经验证的解决方案类似。

例子:

注意:如果你使myprogram本身可执行 – 例如使用cmod +x myprogram ,你可以直接调用它 – 例如.\myprogram而不是sh myprogram

 # Single input line with multiple numbers $ echo '1 2 3' | sh myprogram 6 # Multiple input lines with a single number each { echo 1; echo 2; echo 3; } | sh myprogram 6 # A mix of the above $ sh myprogram <<EOF 1 2 3 EOF 6 

符合POSIX标准的基于循环的解决方案用于测试和省略总和中的非数字

注意:这是David C. Rankin的答案,以演示一个强有力的替代方案。
但是,请注意,除了小输入文件外,此解决方案将比上述解决方案慢得多。

 #!/bin/sh ifile=${1:-/dev/stdin} ## read from file or stdin sum=0 while read -ri; do ## read each token [ $i -eq $i 2>/dev/null ] || continue ## test if decimal integer sum=$(( sum + i )) ## sum done <<EOF $(tr -s ' \t' '\n' < "$ifile") EOF printf " sum : %d\n" "$sum" 
  • 该解决方案避免使用for来循环单个输入行,因为对未加引号的字符串变量使用导致令牌受到路径名扩展(globbing)的影响 ,这可能导致带有令牌的意外结果,例如*

    • 但是,可以使用set -f禁用globbing,并使用set +f来重新启用它。
  • 为了使用一个while循环,输入令牌首先被分割,以便每个令牌都在它自己的行上,通过在here-document内涉及tr的命令替换。

    • 使用here-document(而不是管道)提供输入while while语句可以在当前 shell中运行,因此循环内的变量在循环结束后保持在作用域中(如果通过管道, while会运行在一个子shell中 ,当循环退出时,所有的变量都会超出范围)。
  • sum=$(( sum + i ))使用算术扩展来计算总和,这比调用外部实用程序expr更高效。


如果你真的,真的想要这样做, 而不需要调用任何外部工具 – 我不明白为什么你会这样做:

 #!/bin/sh ifile=${1:-/dev/stdin} ## read from file or stdin sum=0 while read -r line; do ## read each line # Read the tokens on the line in a loop. rest=$line while [ -n "$rest" ]; do read -ri rest <<EOF $rest EOF [ $i -eq $i 2>/dev/null ] || continue ## test if decimal integer sum=$(( sum + i )) ## sum done done < "$ifile" printf " sum : %d\n" "$sum" 

如果您不介意通过set -f / set +f盲目地禁用和重新启用路径名扩展(globbing),则可以简化为:

 #!/bin/sh ifile=${1:-/dev/stdin} ## read from file or stdin sum=0 set -f # temp.disable pathname expansion so that `for` can safely be used while read -r line; do ## read each line # Read the tokens on the line in a loop. # Since set -f is in effect, this is now safe to do. for i in $line; do [ $i -eq $i 2>/dev/null ] || continue ## test if decimal integer sum=$(( sum + i )) ## sum done done < "$ifile" set +f # Re-enable pathname expansion printf " sum : %d\n" "$sum" 

此解决方案需要Bash,因为以下功能不是POSIX外壳兼容的:数组,正则表达式,这里是字符串,复合[[ ]]条件操作符。 对于POSIX兼容解决方案,请参阅David的答案 。

假设我们有一个空格分隔的数字,我们想总结一下。 为此,我们用read -a将它们read -a入一个数组nums ,然后我们循环得到sum

 read -a nums for num in "${nums[@]}"; do (( sum += num )) done echo $sum 

这适用于从标准输入或管道输入到脚本的单行:

 $ echo -e "1 2 3\n4 5 6" | ./sum 6 

注意第二行是如何被忽略的。 现在,对于多行,我们在一个while循环中包装它:

 while read -a nums; do for num in "${nums[@]}"; do (( sum += num )) done done echo $sum 

现在它适用于多行传递给脚本:

 $ echo -e "1 2 3\n4 5 6" | ./sum 21 

为了使这个从文件中读取,我们可以使用

 while read -a nums; do # Loop here done < "$1" 

将作为参数给定的文件重定向到标准输入:

 $ cat infile 1 2 3 4 5 6 $ ./sum infile 21 

但现在,管道已停止工作!

 $ ./sum <<< "1 2 3" ./sum: line 7: : No such file or directory 

为了解决这个问题,我们使用参数扩展 。 我们说“ 如果参数设置为非空 ,则从文件重定向,否则从标准输入读取”:

 while read -a nums; do # Loop here done < "${1:-/dev/stdin}" 

现在,标准输入和文件参数都起作用了:

 $ ./sum infile 21 $ ./sum < infile 21 

如果我们遇到的不是实际的数字,我们可以添加一个支票来投诉。 所有在一起的脚本:

 #!/bin/bash re='^[0-9]+$' # Regex to describe a number while read -a line; do for num in "${line[@]}"; do # If we encounter a non-number, print to stderr and exit if [[ ! $num =~ $re ]]; then echo "Non-number found - exiting" >&2 exit 1 fi (( sum += num )) done done < "${1:-/dev/stdin}" echo $sum 

要在一个while循环内进行求和,需要一种方法将每行的值分开,并确认它们是整数值,然后再将它们加到总和中。 脚本形式的POSIX shell的一种方法是:

 #!/bin/sh ifile=${1:-/dev/stdin} ## read from file or stdin sum=0 while read -ra || test -n "$a" ; do ## read each line for i in $a ; do ## for each value in line [ $i -eq $i >/dev/null 2>&1 ] || continue ## test if integer sum=$(expr $sum + $i) ## sum done done <"$ifile" printf " sum : %d\n" "$sum" exit 0