8

I want my script to perform the product of all its integer arguments. Instead of performing a loop I tried to replace blanks with * and then compute the operation. But I got the following result which I don't understand:

#!/bin/bash
# product.sh

echo $(( ${*// /*} ))  # syntax error with ./product.sh 2 3 4
args=$*
echo $(( ${args// /*} ))  # ./product.sh 2 3 4 => outputs 24

How is it that the first one produces an error while using an intermediate variable works fine?

The Thonnu
  • 3,578
  • 2
  • 8
  • 30
qouify
  • 3,698
  • 2
  • 15
  • 26

4 Answers4

10

How is it that the first one produces an error:

From the Bash Reference Manual:

If parameter is ‘@’ or ‘*’, the substitution operation is applied to each positional parameter in turn

(emphasis mine)
That is, the expression ${*// /*} replaces spaces inside positional parameters, not the spaces separating positional parameters. That expression expands to 2 3 4 (which gives a syntax error when used in an arithmetic context), since the parameters itself don't contain a space. Try with

./product '2 ' '3 ' 4

and you will see the difference.

M. Nejat Aydin
  • 9,597
  • 1
  • 7
  • 17
6

You may utilize IFS:

#!/bin/bash
# product.sh

# set IFS to *
IFS='*'

# use IFS in output
echo "$*"

# perform arithmetic
echo "$(( $* ))";

Output:

2*3*4
24
anubhava
  • 761,203
  • 64
  • 569
  • 643
  • No `$(( ${args// /*} ))` is not a nested curly bracket expression. Arithmetic context `$(...))` is applied after resolving inner bracket expression – anubhava Oct 28 '22 at 09:53
  • _"Reason why echo $(( ${*// /*} )) doesn't work because bash doesn't allow nested expressions."_ -- no, it's not. – ilkkachu Oct 29 '22 at 09:46
  • @ilkkachu: Try: `s='123#aaa.bbb'; echo "${#${s%.*}}"` – anubhava Oct 29 '22 at 09:50
  • @anubhava, yes, so? Bash doesn't support nested parameter expansions like that, but that's not the reason the original didn't work: the original didn't _have_ a nested parameter expansion. It had a parameter expansion within an arithmetic expansion, which actually works. Try e.g. `foo=x; echo $(( 3 ${foo/x/*} 5 ))`. The problem was that the expansion in the question didn't produce that asterisk, which is explained in the other answers, and which you can verify by printing the result of that expansion alone, without the arithmetic. – ilkkachu Oct 29 '22 at 10:11
  • I have already commented `$(( ${args// /*} )) is not a nested curly bracket expression` yesterday – anubhava Oct 29 '22 at 10:17
  • 1
    `local` outside a function is illegal. – chepner Oct 29 '22 at 13:59
  • Thanks, since I was testing using a function I just copy pasted from there, fixed now. – anubhava Oct 29 '22 at 14:17
6

In your example, the value $* does not actually contain any literal spaces, so ${*// /*} does not do anything.

If it did, those asterisks would be subject to wildcard expansion, so the idea of performing a substitution would seem to be rather brittle even if it worked.

I would simply create a function to process the arguments, instead of rely on trickery with substitutions -- these tend to have icky corner cases when one of the arguments is a variable or etc.

mul () {
    case $# in
      [01]) echo "$@";;
      *) local n=$1; shift; echo $((n * $(mul "$@")));;
    esac
}
tripleee
  • 175,061
  • 34
  • 275
  • 318
  • 3
    An expanded value with `*` inserted _used directly in a command_ is at risk from wildcard (formally pathname expansion), but not in arithmetic `$(( ))` – dave_thompson_085 Oct 29 '22 at 05:57
  • @dave_thompson_085 You're right, that consideration can be ignored in the OP's example ... but would still be tricky to pick apart and debug in isolation, for instance. – tripleee Oct 29 '22 at 07:19
  • a recursive function? How very Lisp. Wouldn't just `mul() { s=1; for a in "$@"; do s=$((s*a)); done; echo "$s"; }` do and be possibly easier to read? – ilkkachu Oct 29 '22 at 10:17
  • @ilkkachu Sure it would; matter of taste. Don't forget to make your variables `local` though. – tripleee Oct 29 '22 at 10:28
5

Or use printf, like this:

echo $(( $(printf '%s*' $*)1 ))
Ivan
  • 6,188
  • 1
  • 16
  • 23