1

I am trying to write a Bash script that takes a list of numbers as command line arguments and then outputs the sum of those numbers.

So the script would be run as script.sh 1 555 22 122 66 and would then sum them all up. I know they need to be passed to the $@ variable and then likely run through a loop, but I don't understand how to convert the contents of $@ to an int to do the math. I tried to do this:

#!/bin/bash
for i in $@
do
    $@+$@
    echo "the total is `$@`"
done
  • 1
    Note: I think you're confusing standard input with arguments. Arguments are a list of strings (in your case, strings that represent integers) that're supplied to the script (/command/function/whatever) as part of the command when it's started. Standard input is a stream of bytes that the script can optionally read from while it runs. In a shell script, you'd use the read command to read from standard input. – Gordon Davisson Jul 6 '18 at 5:44
  • A for loop and a while loop are two different things. – Charles Duffy Jul 6 '18 at 12:59
8

In general, an argument is converted to an integer automatically if used inside an arithmetic $((...)) expansion. This loop will sum all the arguments:

for x; do sum=$((sum+x)); done; echo "$sum"

The shell caches all the arguments in separated memory locations as a c program deals with an argv[] array. The shell user does not need to deal directly with that array, the shell helps by assigning them to $1, $2, $3, etc. The shell also abstract such list as "$@". And finally, the syntax for x is a shorthand for for x in "$@" to loop over all arguments.

That is assuming that arguments are decimal numbers that do not start with zero, octal numbers starting with zero or hexadecimal numbers that start with 0x, and that the total sum does not overflow (2^63-1 in 64 bit systems)

This list:

$ ./script 12 021 0xab

Will print 200 (the decimal result).

|improve this answer|||||
  • 3
    If you want to force the numbers to be taken as base-10 numbers, then do sum=$(( sum + 10#$x )); done; (works at least in Bash) – ilkkachu Jul 6 '18 at 9:27
  • 1
    @DanMan3395 Bourne-like shells do that. csh and its derivatives apparently do want you to mention an array. stackoverflow.com/a/1544361/3701431 But for the most part, you'll probably be dealing with a Bourne-like shell. – Sergiy Kolodyazhnyy Jul 6 '18 at 19:34
  • 1
    @ilkkachu Sometimes. That fails with any sign on the number. Try bash -c 'a=+000010; echo $((10#$a+20))' – Isaac Jul 8 '18 at 23:31
  • @Isaac, oh, bug! that's annoying. It would be nice if there was an easy way to prevent leading zeroes from being taken as octal, but I suppose that means it's more work to support negative numbers too. – ilkkachu Jul 9 '18 at 9:27
8

You can accomplish this with the following:

tr ' ' '+' <<<"$@" | bc

It takes all arguments passed and replaces whitespace with the + sign and then pipes that to bc.

|improve this answer|||||
  • haha, you keep coming up with ways to do things that don't even occur to me. This is actually really clever. – DanMan3395 Jul 6 '18 at 1:03
  • 7
    I'm sure there are all kinds of reasons not too that I haven't thought of, but one could do the same thing natively in the shell IFS=+; echo $(($*)) – steeldriver Jul 6 '18 at 1:05
  • ... or IFS=+; echo "$*" | bc to preserve your solution's advantage of handling floating point arguments – steeldriver Jul 6 '18 at 2:20
3

Take the arguments one by one:

total=0
while [ -n "$1" ]; do
  total=$((total + "$1"))
  shift
done

Or use for loop:

total=0
for argument; do
  total=$((total + "$argument"))
done
|improve this answer|||||
  • wont both of these examples only take one numeric argument? – DanMan3395 Jul 6 '18 at 0:27
  • They both loop over all the arguments; they work for any number of arguments, including no arguments at all. Try them. In production you should of course add error-checking etc. – AlexP Jul 6 '18 at 0:38
2

Besides $(()) like other answers have used, you can also use (()) and use += inside like this:

sum=0
for x; do
  (( sum += x ))
done
echo $sum

It might also help to explain what you were doing in the code you provided. Basically, you split each argument on whitespace (because you didn't double-quote $@ in for i in $@), and then you looped through the result. Then you ignored the assignment you did, and again split the arguments on whitespace twice more and joined the last argument of the first set and the first argument of the second set with a + then called the command named by the first argument with the rest of the arguments as arguments. So for example if this script was named sum and you called it like this: sum "1 2" 3, that $@+$@ would have tried to call the command 1 (even looking for /bin/1) like this:1 "2" "3+1" "2" "3". It would probably have resulted in bash warning something like bash: 1: command not found, 3 times (once for each argument). Then because you wrote the echo inside the loop, it would try to report the total once for each argument. In reality, it would try to execute 1 "2" "3" with 1 being interpreted as a command again because of the backticks in the string. This will also result in bash warning that the command was not found. Because there was no command, there will be no output and echo will just echo "the total is ". And so the complete result is:

$ sum "1 2" 3
bash: 1: command not found
bash: 1: command not found
the total is 
bash: 1: command not found
bash: 1: command not found
the total is 
bash: 1: command not found
bash: 1: command not found
the total is 
|improve this answer|||||
  • yeah i added that loop to crudely explain what I was trying to figure out. Turns out that $@ is automatically handled by the shell so this was quite simple. – DanMan3395 Jul 6 '18 at 3:44
1
#!/bin/sh

IFS='+'
printf '%s\n' "$*" | bc

Testing

$ ./script.sh 1 2 3 -1 30 0.1
35.1

The "$*" will expand to the positional parameters (the command line arguments) delimited by the first character of the IFS shell variable. We set IFS to a plus sign and pass the string off to bc for evaluation.

If only integer arithmetics is needed:

#!/bin/sh

IFS='+'
printf '%d\n' "$(( $* ))"
|improve this answer|||||

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.