4

I am calling a function with a for loop and saving the returned value to a variable. When I run the code, I get a command not found error. What is wrong?

#!/bin/bash

check_1()
{
    x_$1=$(check_2 $1)
}
check_2()
{
    ans=$((3+$1))
    echo $ans
}
for((i=1; i<=2;i++))
do
  check_1 $i
  tmp=x_$i
  echo ${!tmp}
done

If I run the script above I get:

sample.sh: line 5: x_1=4: command not found
sample.sh: line 5: x_2=5: command not found
terdon
  • 242,166
shiva
  • 45

3 Answers3

4

You can't define variable names that way. You are trying to build your variable's name by using another variable's value: x_$1=foo and that won't work. A much better way would be to use an array instead:

#!/bin/bash

check_1()
{
    x[$1]=$(check_2 "$1")

}
check_2()
{
    ans=$((3+$1))
    echo "$ans"
}
for((i=1; i<=2;i++))
do
  check_1 "$i"
  echo "${x[$i]}"
done

The above returns:

$ sample.sh
4
5
terdon
  • 242,166
3

You should, as terdon says in his answer, use bash's associative arrays instead.

But, if you insist, and if you have a new enough bash, you can use a nameref (that's the magic word to search for in the manpage, by the way).

i=1
declare -n tmp="foo_$i" # this is the nameref line
tmp="fooval"            # actually sets $foo_1
echo "$foo_1"           # prints fooval

If inside a function, use local -n instead of declare -n to get a function-local variable.

derobert
  • 109,670
1

If you weren't working with just math I'd suggest the only other safe/portable method I know of one that doesn't require very stringent testing:

var1=string1
export "$var1=string2"
echo "$var1"
echo "$string1"

OUTPUT

string1
string2

But you are just working with math, and so you're problem is that you are echoing the variable's value into a subshell when you should be concurrently defining it and evaluating it in the current shell.

i=0 ; until [ $((i=$i+1)) -ge 3 ]
do  echo "\$x_$i = $((x_$i=3+$i))"
done     
echo $x_1 $x_2

OUTPUT

$x_1 = 4
$x_2 = 5
4 5

So what you're doing is just math. The interesting thing about shell arithmetic is that you can use it to safely twice evaluate a variable in the current shell - no subshells.

POSIX has this to say about it:

Arithmetic expansion provides a mechanism for evaluating an arithmetic expression and substituting its value. The format for arithmetic expansion shall be as follows:

$((expression))

The expression shall be treated as if it were in double-quotes, except that a double-quote inside the expression is not treated specially. The shell shall expand all tokens in the expression for parameter expansion, command substitution, and quote removal.

Perhaps more interestingly, it also has this to say:

All changes to variables in an arithmetic expression shall be in effect after the arithmetic expansion, as in the parameter expansion "${x=value}".

If the shell variable x contains a value that forms a valid integer constant, optionally including a leading plus or minus sign, then the arithmetic expansions "$((x))" and "$(($x))" shall return the same value.

So if a variable is defined within the context of an arithmetic substitution, that definition persists in the current environment. Like:

echo $((x=1)); echo $x
1
1

But if you add to that the other parts - specifically that the shell shall expand all tokens and that "$((x))" and "$(($x))" shall return the same value. You might be able to see how the above until loop works.

So:

$((x_ #just a string
     $i #integer value
        = #assignment operator
          3 + #addition
              $i )) #same integer value

You see the shell has to expand the $i in both contexts - that of the variable shell token as a string before the assignment operator, and that of the integer value on which it performs the addition operation, the sum of which is assigned to x_$i.

Arrays can be handy, but, not counting one form, they are not portable and all come with their own implementation-specific snaffus. And besides - this is the answer to the question you asked.

This is why the following function works:

defv() {
    [ -n "${1##*[!0-9]*}" ] && # verify $1 is numeric only
    v=x_$1 &&                  # redefine caller loop var
    : $((x_$1=5+$1))           # : do nothing but expand arg
}

for v in 1 2 3 4 5             # init array
do  defv $v                    # defv() $indirection
    echo "$v = $(($v))"        # \$x_$indirection = $x_$indirection
done

OUTPUT

x_1 = 6
x_2 = 7
x_3 = 8
x_4 = 9
x_5 = 10

Your functions:

check_1()
{
    check_2 x_$1 $1
}
check_2()
{
    : $(($1=3+$2))
}
for((i=1; i<=2;i++))
do
  check_1 $i
  tmp=x_$i
  echo ${!tmp}
done

OUTPUT

4
5
mikeserv
  • 58,310