While this question is at its core a duplicate of the linked question, it does state additional requirements (whether they were all fully intended by the OP or not):
The solution should be packaged as a script.
The solution should be POSIX-compliant (question is generically tagged shell)
Input should either come from a file, if specified, or from stdin by default.
There can be multiple numbers on a single input line (e.g., echo 1 2
).
The solution should use a while
or for
loop, i.e. a pure shell solution.
The solution below addresses these requirements, except for the last one - which may well be a deal-breaker for the OP, but perhaps others will find it useful.
Deviating from that requirement by using external utilities means the solution will perform well with large sets of input data - loops in shell code are slow.
If you still want a shell while
-loop solution, see the bottom of this post; it also includes input validation.
Contents of myprogram
(POSIX-compliant, but requires a filesystem that represents the standard input as /dev/stdin
):
Note that no input validation is performed - all tokens in the input are assumed to be decimal numbers (positive or negative); the script will break with any other input. See below for a - more complex - solution that filters out non-decimal-number tokens.
#!/bin/sh
{ tr -s ' \t\n' '+'; printf '0\n'; } < "${1-/dev/stdin}" | bc
${1-/dev/stdin}
uses either the first argument ($1
, assumed to be a file path), if specified, or /dev/stdin
, which represents stdin, the standard input.
tr -s ' \t\n' '+'
replaces any run of whitespace in the input (spaces, tabs, newlines) with a single +
; in effect, this results in <num1>+<num2>+...+
- note the dangling +
at the end, which is addressed later.
- Note that it is this approach to whitespace handling that allows the solution to work with any mix of one-number-per-line and multiple-numbers-per-line input
printf '0\n'
appends a 0
so that the above expression becomes a valid addition operation.
- Grouping (
{ ...; ...; }
) the tr
and printf
commands makes them act as a single output source for the pipeline (|
).
bc
is a POSIX utility that can perform (arbitrary-precision) arithmetic. It evaluates the input expression and outputs its result.
With input validation: Simply ignores input tokens that aren't decimal numbers.
#!/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'
puts all individual tokens in the input - whether they are on the same line or on their own line - onto individual lines.
grep -x -- '-\{0,1\}[0-9][0-9]*'
only matches lines containing nothing but a decimal number.
- The remainder of the command works analogously to the solution without validation.
Examples:
Note: If you make myprogram
itself executable - e.g., using cmod +x myprogram
, you can invoke it directly - e.g., .\myprogram
rather than 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
A POSIX-compliant while
-loop based solution that tests for and omits non-numbers from the sum:
Note: This is an adaptation of David C. Rankin's answer to demonstrate a robust alternative.
Note, however, that this solution will be much slower than the solution above, except for small input files.
#!/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"
The solution avoids use of for
to loop over a single input line, as using for
on an unquoted string variable makes the resulting tokens subject to pathname expansion (globbing), which can lead to unexpected results with tokens such as *
.
- It is, however, possible to disable globbing with
set -f
, and to reenable it with set +f
.
To enable use of a single while
loop, the input tokens are first split so that each token is on its own line, via a command substitution involving tr
inside a here-document.
- Using a here-document (rather than a pipeline) to provide input to
while
allows the the while
statement to run in the current shell and thus for the variables inside the loop to remain in scope after the loop ends (if input were provided via a pipeline, while
would run in a subshell, and all its variables would go out of scope when the loop exits).
sum=$(( sum + i ))
uses arithmetic expansion to calculate the sum, which is more efficient than calling external utility expr
.
If you really, really want do this without calling any external utilities - I don't see why you would - try this:
#!/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"
If you don't mind blindly disabling and re-enabling pathname expansion (globbing) with set -f
/ set +f
, you can simplify to:
#!/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"