3

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 '19 at 13:38
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//+(\ )/+}"
    – ImHere
    Oct 3 '19 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 '19 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 '19 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.

4

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
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 '19 at 17:08
  • Or add records instead of fields: echo "$VAR" | awk '{a+=$0}END{print a}' RS=' ' Oct 3 '19 at 19:52
  • Or simply awk '{for (i = 1; i <= NF; ++i) sum += $i;print sum}' <<<$VAR
    – Paul Evans
    Oct 7 '19 at 11:00
  • 1
    Dear Shmuel - sana tova
    – yael
    Oct 12 '19 at 19:07
  • Thanks @yael you too.
    – Shmuel
    Oct 12 '19 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

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.