【问题标题】:Add numbers from file and standard input [duplicate]从文件和标准输入中添加数字[重复]
【发布时间】:2016-05-07 21:08:18
【问题描述】:

如何在 shell 中使用 whilefor 循环将数字相加?

我只想要一个非常简单的程序,可以处理标准输入和文件。

例子:

$ echo 1 2 | sh myprogram
3

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

sh myprogram myfile

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

【问题讨论】:

  • 如果某个答案解决了您的问题,请单击旁边的大复选标记 (✓) 接受它。如果您发现其他答案有帮助,请给他们投票。接受和投票的答案不仅可以帮助那些回答的人,也可以帮助未来的读者。请参阅the relevant help-center article。如果您的问题尚未得到完全解答,请提供反馈。

标签: shell loops unix while-loop add


【解决方案1】:

虽然此问题的核心是链接问题的副本,但它确实说明了附加要求(无论它们是否完全由 OP 设计):

  • 解决方案应打包为脚本

  • 解决方案应符合 POSIX(问题通常标记为

  • 输入应该来自文件(如果指定)或默认来自标准输入。

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

  • 解决方案应使用whilefor 循环,即纯shell解决方案。

下面的解决方案解决了这些要求,除了最后一个 - 这很可能会破坏 OP 的交易,但也许其他人会发现它很有用。

使用外部实用程序偏离该要求意味着解决方案将在大量输入数据时表现良好 - shell 代码中的循环很慢。

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


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

注意 no 输入验证被执行 - 输入中的所有标记都假定为十进制数(正数或负数);该脚本将因任何其他输入而中断。请参阅下面的 - 更复杂的 - 过滤掉非十进制数字标记的解决方案。

#!/bin/sh

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

  • tr -s ' \t\n' '+' 用单个+ 替换输入中的任何空格(空格、制表符、换行符);实际上,这会导致&lt;num1&gt;+&lt;num2&gt;+...+ - 请注意最后悬空的+,稍后会解决。

    • 请注意,正是这种空白处理方法允许该解决方案使用每行一个数字和每行多个数字的任意混合输入
  • printf '0\n' 附加一个0,这样上面的表达式就变成了一个有效的加法运算。

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

使用输入验证:简单地忽略不是十进制数字的输入标记。

#!/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 的 while-loop 基于解决方案,它测试并省略总和中的非数字

注意:这是对 David C. Rankin's answer 的改编,以展示一个强大的替代方案。
但是请注意,除了小的输入文件外,此解决方案将比上述解决方案慢得多。

#!/bin/sh

ifile=${1:-/dev/stdin}  ## read from file or stdin

sum=0
while read -r i; 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 循环单个输入行,因为在未引用的字符串变量上使用for 会使生成的标记服从pathname expansion (globbing),这可能会导致使用诸如@ 之类的标记的意外结果987654359@.

    • 但是,可以使用set -f 禁用通配,并使用set +f 重新启用它。
  • 要启用单个 while 循环,输入标记首先被拆分,以便每个标记位于其自己的行上,通过涉及此处文档内tr 的命令替换.

    • 使用 here-document(而不是管道)向 while 提供输入允许 while 语句在 current shell 中运行,因此循环内的变量可以循环结束后保持在范围内(如果通过管道提供输入,while 将在 subshel​​l 中运行,并且在循环退出时其所有变量都将超出范围)。
  • sum=$(( sum + i )) 使用arithmetic expansion 计算总和,比调用外部实用程序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 -r i 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 盲目地禁用和重新启用路径名扩展(通配),您可以简化为:

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

【讨论】:

  • 这是一个很好的解释和例子。它还举例说明了 S.O. 答案的权衡。最好和最丰富的信息需要时间来制作。谢谢。
  • 感谢您的反馈,@DavidC.Rankin。
【解决方案2】:

此解决方案需要 Bash,因为以下功能与 POSIX shell 不兼容:数组、正则表达式、此处为字符串、复合 [[ ]] 条件运算符。有关 POSIX 兼容的解决方案,请参阅David's answer

假设我们有一行以空格分隔的数字,我们想总结它们。为此,我们使用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

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

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

【讨论】:

  • bash 解决方案很好,但[[..]]=~arrayshere-strings 是非 POSIX,不会在 Unix shell 中工作。请参阅POSIX Programmers Guide 注意: 这可能是 OP 正在寻找的内容,但问题被标记为 [shell](只是需要注意的东西)
  • @David 好点,我的解决方案实际上只是 Bash。我会修改的。
【解决方案3】:

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

#!/bin/sh

ifile=${1:-/dev/stdin}  ## read from file or stdin
sum=0

while read -r a || 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

【讨论】:

  • 请不要解析带有for的未知令牌列表;考虑如果$a 包含1 2 * 会发生什么。
  • 我很欣赏这个指针,但我试图理解你关注的症结所在。是的,我了解'*' 的问题,但这是否也同样适用于您所建议的myprogram 传递给bctr 的任何未知列表?其次,无论包含什么,即使'*' 出于某种原因在数字列表中传递(拉入文件名等),如果它不是int,它将被跳过(除非文件名是@987654332 @)。在将未知列表传递给tr 并用+ 重写spaces 时,您看到了哪些额外的保护?真的只是古玩。
  • 暂且不说允许* 扩展到所有文件名,然后在以后通过单独的测试将它们淘汰,unless filenames were ### 正是重点:如果这些文件名恰好是有效数字,您会将它们包含在您的计算中,因此这不是一个可靠的解决方案。通常,将for 与不带引号的变量引用一起使用是一种不好的做法,不鼓励这样做,至少不解释其中的缺陷。
  • 我的解决方案中的 tr / bc 组合不易容易受到文件名扩展的影响。它也没有尝试清除非数字,因为这不是 OP 所要求的。很高兴您选择解决这个额外的问题,但您并没有很好地解决它。
  • 这真的是那些该死的人之一,如果你不这样做的话,如果你没有在你必须将你的解决方案适应手头的输入集的情况下,那该死的。无论是我的循环还是您的 tr / bc 都不能保证在所有情况下都能正常工作。如果我们将讨论限制在数字列表中,那么两者都不会受到任何问题的影响。不要误会我的意思,我确实明白、理解并同意你的观点,但如果我们不考虑非数字,那么这只是一个学术观点。
猜你喜欢
  • 2021-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-08
相关资源
最近更新 更多