4

I have a variable set with numbers separated with space, of which first number can also be led by space, example:

VAR=" 2 1  34 3    2 "

I need to sum all these numbers. Easiest way would be replacing all spaces in between numbers with + and pipe on bc

I can do it with a for loop, paste and bc, but maybe someone knows easier way to do it? Maybe some calculation directly in bash on a VAR with bash built-in string replacements?

$ for i in $VAR;do echo $i;done|paste -sd+|bc
42

update: Thx for all proposal, I finally found quite short method with an array:

$ VAR=" 2 1  34 3    2 "
$ arr=( $VAR );echo "$((${arr[@]/%/+}0))"
42
$ VAR="$VAR -14"
$ arr=( $VAR );echo "$((${arr[@]/%/+}0))"
28
$
1
  • Please not that this will only work for integers. If you need to add float-values, pipe to bc. echo "${arr[@]/%/+}0" | bc
    – markgraf
    Oct 4, 2019 at 13:38

7 Answers 7

7

Your source string has repeated spaces and leading/trailing spaces.

A naive conversion of spaces to + will fail:

$ value='      2 1  34 3    2    '
$ echo "${value// /+}"
++++++2+1++34+3++++2++++

To colapse all repeated spaces and remove leading/trailing space you just need an echo (or printf) for an unquoted variable (assuming IFS is default):

value=$(echo $value)
echo "${value// /+}"
2+1+34+3+2

And that could be feed to bc:

$ echo "${value// /+}" | bc
42

All in one line, if you want that:

value=$(echo $value); echo "${value// /+}" | bc

Or, even using sed as a filter (no aditional variable, but slower):

echo $value | sed 's/ /+/g' | bc

A previous attempt at this with <<< had a problem:

$ ~/bin/b44sh -c 'value="      2 1  34 3    2    ";sed "s/ /+/g" <<<$value'
++++++2+1++34+3++++2++++

On bash since version 4.4. On previous versions it worked as this:

~/bin/b43sh -c 'value="      2 1  34 3    2    ";sed "s/ /+/g" <<<$value'
2+1+34+3+2

For any version of bash (and sed) we can do (a quite robust version but calls an external utility -sed):

sed "s/ \+/+/g" <<<"0 $value 0"     | tee /dev/tty     | bc
0+2+1+34+3+2+0
42

A pure shell solution (demands bash,ksh or zsh for the replacement ${//} part)could be:

value=$(echo $value); bc <<<"${value// /+}

And a more robust (enforce your assumptions™) and portable version:

  • avoid that changes of IFS affect the present shell. Use a sub shell (…).
  • ensure that IFS breaks values on spaces (IFS=" ")
  • ensure that even if unquoted, the string will not expand * et all (set -f).
  • ensure that parts are joined with a + (IFS=+).

( IFS=" "; set -f; set -- $value; IFS=+; echo "$*" | bc; )

A function version
1- if your shell doesn't allow local, use the slower sub-shell form
2- Some (correctly in POSIX) may complain about using an unquoted $*.

sum(){ local IFS=" "; set -f; set -- $*; IFS=+; echo "$*" | bc; }

Which adds arguments in many ways

$ value="      2 1  34 3    2    "
$ sum "$value"
42
$ sum $value    # beware of glob chars *, ? and [  and of odd IFS=123 settings
42
$ sum "      2 1  34 3    2    "
42
$ sum "      2"    "1  "    "34 3"    "    2    "
42
$ var=23
$ sum "      2"    "1  "    "34 3"    "    2    "   "$var"
65
0
4

Use parameter expansion.

#!/bin/bash
VAR=" 2 1  34 3    2 "

shopt -s extglob                # Enable the `+(...)` construct.
expression=${VAR#+(\ )}         # Remove leading spaces.
expression=${expression%+(\ )}  # Remove trailing spaces.
bc <<< ${expression//+(\ )/+}   # Replace strings of spaces by pluses.
1
  • Bash replacement is extremely slow, don't use it. Make the variable expression=$(printf "%.0sword " {1..100}) (just 100 words) and test: shopt -s extglob; time echo "${expression//+(\ )/+}"
    – user232326
    Oct 3, 2019 at 23:27
4

A few Perl tricks:

$ perl -lane '$t+=$_ for @F; print $t' <<<"$var"
42

Or

$ perl -pe 's/(\d)\s+(?=\d)/$1+/g' <<<"$var" | bc
42

Or

$ perl -lane 'print eval join "+", @F' <<<"$var"
42

Or GNU sed and coreutils:

$ tr -s ' ' '+' <<<$var | sed 's/^+//; s/+$//' | bc
42
2
  • Another one (relies on word splitting... not sure if this should be encouraged) printf '%d+' $VAR | dc -e0 -f- -ep Oct 3, 2019 at 18:32
  • @steeldriver oooh, nice! Sure, it's probably illegal in multiple territories, but it's still simple, elegant and works in this example. I'd post it!
    – terdon
    Oct 3, 2019 at 18:38
4

Similar to Chris' array solution:

$ var=" 2 1  34 3    2 "
$ var=$(echo $var); echo "$((${var// /+}))"
42

Trim and squeeze all space characters, then replace all space characters with + and evaluate.

2

bash scripts:

$ v="2 4 7 10 3"
$ s=0
$ for i in $v
> do
> s=$((s+i))
> done
$ echo $s
26
1

If you know the length of the variable you can use awk like this :

echo $VAR | awk '{print $1 + $2 + $3 + $4 + $5}'

Output:

42

Hope this will help.

5
  • 3
    Doing it in a loop from 1 to NF would make it a generic solution for an unknown number of numbers.
    – Kusalananda
    Oct 3, 2019 at 17:08
  • Or add records instead of fields: echo "$VAR" | awk '{a+=$0}END{print a}' RS=' ' Oct 3, 2019 at 19:52
  • Or simply awk '{for (i = 1; i <= NF; ++i) sum += $i;print sum}' <<<$VAR
    – Paul Evans
    Oct 7, 2019 at 11:00
  • 1
    Dear Shmuel - sana tova
    – yael
    Oct 12, 2019 at 19:07
  • Thanks @yael you too.
    – Shmuel
    Oct 12, 2019 at 19:15
0

As long as the constituent numbers are integers, this should work too (this is due to declare or typeset accepting an arithmetic expression to initialize an integer variable)

VAR=" 2 1  34 3    2 "
read -r VAR <<<$VAR #shed leading and trailing spaces, compact internal spaces
VAR=${VAR// /+}   #replace spaces with plusses
declare -i var=$VAR     #typeset var to an integer
echo $var
42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .