Assigning to a variable in a parent context in Bash

1k Views Asked by At

I want to write a function similar to the read built-in, where I pass a variable name as an argument, and the function returns its result into the variable named.

I tried doing it like this:

#!/bin/bash
FIB_CALLS=0

# usage: fib $variable $index
# puts fibonacci number $index in the variable named $variable
fib() {
    (( FIB_CALLS++ ))

    local -n result="$1"
    local i="$2"
    local a
    local b

    if (( i < 2 )); then
        result="$i"
    else
        fib a $(( i - 1 ))
        fib b $(( i - 2 ))
        (( result = a + b ))
    fi
}

i=15
fib val $i
echo "fib($i) = $val"
echo "$FIB_CALLS calls to fib"

The above doesn’t work. If I call fib with the first argument i, a or b, the assignment becomes to the local variable defined inside fib, and the caller does not receive the result at all; and of course, if I happen to name the result variable result, I get circular reference errors. In effect, this leaks the implementation of fib. Because I perform recursion here, I cannot just rename the variable; the variable name from level above will inevitably clash with the one at the current level. So not even ____i_DO_NOT_USE_OR_YOU_WILL_BE_FIRED will work. I cannot instead echo the result and capture it in a subshell, because I want to keep being able to modify global variables from within the function, which subshells cannot do.

Is there a way to assign to a dynamically-named variable as defined in the context of the caller?

In other words, I am looking for the bash equivalent of Tcl’s upvar, which allows me to write this:

#!/usr/bin/env tclsh
variable fib_calls 0

proc fib {result_var i} {
    global fib_calls
    incr fib_calls

    upvar $result_var result

    if {$i < 2} {
        set result $i
    } else {
        fib a [expr {$i - 1}]
        fib b [expr {$i - 2}]
        set result [expr {$a + $b}]
    }
}

set i 15
fib val $i
puts "fib($i) = $val"
puts "$fib_calls calls to fib"

Of course this is a rather contrived example; in normal Tcl one would just use return. This is just to demonstrate the principle.

(Editorial note: the example in the question was previously different.)

5

There are 5 best solutions below

2
markp-fuso On BEST ANSWER

NOTE: following is in response to the newly updated question (as of 5 Nov 2022) ...

Since OP has ruled out the use of subproces calls (x=$((fib ...))), and assuming OP is not open to a different algorithm (eg, this array based solution) or a different language (eg, c++, awk, perl, etc), this leaves us with trying to devise a (convoluted?) approach in bash ...

As I see it each invocation of the fib() function needs the ability to update the 'global' variable at the parent level while at the same time needing to maintain a separate 'local' version of the variable (which in turn becomes the 'global' variable for the child fib() calls).

One approach would be to dynamically generate new variable names in each invocation of the fib() function. The key issue will be to insure each new variable name is unique:

  • uuid or uuidgen would be ideal but would require a subprocess call (not allowed per OP's rules) in order to capture the output (eg, new_var_name=var_$(uuidgen))
  • $RANDOM is not very 'random'; in unit testing it's not uncommon to generate duplicates within a batch of 1000 references; since OP's fib(15) algorithm will require a few thousand $RANDOM calls this is a non-starter
  • if running bash 5.1+ the new $SRANDOM would serve this purpose well
  • or we can just piggyback off a variable we know has a different value on each call to fib() (eg, $FIB_CALLS)

Making a few changes to OP's current code:

fib() {
    (( FIB_CALLS++ ))

    # for all local variables we assume a '_' prefix is sufficient to escape conflicts (otherwise pick a different prefix)

    local -n _result=$1
    local    _i="$2"

    # if using bash 5.1+ we can make use of $SRANDOM
    # local -n _a=var_$SRANDOM             # use a nameref to associate local variable '_a' with a new random variable name
    # local -n _b=var_$SRANDOM             # use a nameref to associate local variable '_b' with a new random variable name

    # if bash 5.1+ is not available we can make use of $FIB_CALLS
    local -n _a=var_a_${FIB_CALLS}         # use a nameref to associate local variable '_a' with a new random variable name
    local -n _b=var_b_${FIB_CALLS}         # use a nameref to associate local variable '_b' with a new random variable name

    if (( "$_i" < 2 ))
    then
        _result="$_i"
    else
        fib ${!_a} $(( _i -1 ))            # instead of passing '_a' we pass the associated nameref ('var_$SRANDOM')
        fib ${!_b} $(( _i -2 ))            # instead of passing '_b' we pass the associated nameref ('var_$SRANDOM')
        (( _result = _a + _b ))
    fi

    unset ${!_a} ${!_b}                    # release memory
}

Taking for a test drive:

FIB_CALLS=0
i=15
fib val $i
echo "fib($i) = $val"
echo "$FIB_CALLS calls to fib"

This generates:

fib(15) = 610
1973 calls to fib

NOTES:

  • see fiddle (using $SRANDOM to create new variable names)
  • see fiddle (using $FIB_CALLS to create new variable names)
  • for fib(15) we end up generating/populating 3900+ variables; memory usage in this case will be negligible; for excessively large fib(X) calls where memory usage becomes an issue it would make (more) sense to look at a new algorithm or use a different language

While this is an interesting technical exercise I wouldn't recommend this solution (or any solution based on this algorithm) for a production environment.

For a fib(15) calculation:

  • this highly-redundant algorithm (recursively calculating previous fibs) requires 1973 calls of the fib() function
  • a tail-recursion algorithm (building fibs from 0/1 to desired final fib) would require 15 fib() function calls as well as eliminate the need to dynamically generate new variables on each pass through the fib() function
  • a simple looping construct (building fibs from 0/1 to desired final fib) will perform about the same as a tail-recursion algorithm but without the overhead of maintaining the recursion stack
  • each of these algorithms (highly-redundant, tail-recursion, simple-loop) will run faster in a more appropriate language (eg, perl, python, awk, etc)
1
dstromberg On

I suspect you'll end up with something like:

foo() {
    local result
    result="some computation with $1"
    echo "$result"
}

thing=$(set -eu; foo)
0
seeker On

If I understand correctly, the constraints for the target function outlined in the question are as follows:

  1. Assigning a value to a dynamically-named variable in a parent context;
  2. Using function scope variable name as parameter for recursion;
  3. Modifying global variable.

The initial OP's implementation coupled with advices to resolve constraint #2 revealed the "circular name reference" problem. Let's not correct the initial implementation, but change the idea of the variables by adding another parent level. And use this local builtin feature:

it makes the variable name have a visible scope restricted to that function and its children.

GLOBAL_VAR=0
INDENT="          "
CALL_LEVEL=0

foo() {
  ((CALL_LEVEL++))
  echo "${INDENT:0:(($CALL_LEVEL*2))}foo START: var = $var; dynamically_named_var = $dynamically_named_var" 
  local result=0
  while [ $GLOBAL_VAR -lt 3 ]; do
    (( GLOBAL_VAR++ ))
    (( var-- ))
    echo "${INDENT:0:(($CALL_LEVEL*2))}Recursive function call #$GLOBAL_VAR"
    foo var
  done
  result=$(( $GLOBAL_VAR + $var ))
  var=$result
  echo "${INDENT:0:(($CALL_LEVEL*2))}foo END: var = $var; dynamically_named_var = $dynamically_named_var"
  ((CALL_LEVEL--))
}

runFoo() {
  ((CALL_LEVEL++))
  local -n var=$1
  echo "${INDENT:0:(($CALL_LEVEL*2))}runFoo START: var = $var; GLOBAL_VAR = $GLOBAL_VAR"
  foo var
  echo "${INDENT:0:(($CALL_LEVEL*2))}runFoo END: var = $var; GLOBAL_VAR = $GLOBAL_VAR"
  ((CALL_LEVEL--))
}

dynamically_named_var=10
echo "${INDENT:0:(($CALL_LEVEL*2))}GLOBAL context: GLOBAL_VAR = $GLOBAL_VAR; dynamically_named_var = $dynamically_named_var"
runFoo dynamically_named_var
echo "${INDENT:0:(($CALL_LEVEL*2))}GLOBAL context: GLOBAL_VAR = $GLOBAL_VAR; dynamically_named_var = $dynamically_named_var"

Output:

GLOBAL context: GLOBAL_VAR = 0; dynamically_named_var = 10
  runFoo START: var = 10; GLOBAL_VAR = 0
    foo START: var = 10; dynamically_named_var = 10
    Recursive function call #1
      foo START: var = 9; dynamically_named_var = 9
      Recursive function call #2
        foo START: var = 8; dynamically_named_var = 8
        Recursive function call #3
          foo START: var = 7; dynamically_named_var = 7
          foo END: var = 10; dynamically_named_var = 10
        foo END: var = 13; dynamically_named_var = 13
      foo END: var = 16; dynamically_named_var = 16
    foo END: var = 19; dynamically_named_var = 19
  runFoo END: var = 19; GLOBAL_VAR = 3
GLOBAL context: GLOBAL_VAR = 3; dynamically_named_var = 19
2
Philippe On

Nameref combined with indirect variable (by !) should achieve what you wanted :

#!/usr/bin/env bash

recursive(){
    declare -n not_used_by_caller_name=$1
    local      not_used_by_caller_level=${2-0} # This is just for controlling recursion
    test $not_used_by_caller_level -lt 3 || return
    # ${!not_used_by_caller_name} passes `var` to next level
    recursive ${!not_used_by_caller_name} $((not_used_by_caller_level+1))
    not_used_by_caller_name=$(( (not_used_by_caller_level+1) ** 3 ))
    printf "Level %s returning %2d, var=%2d\n"\
        $not_used_by_caller_level $not_used_by_caller_name $var
}
recursive var
echo "Final value $var"

All assignments to not_used_by_caller_name assign to variable var

Output :

Level 2 returning 27, var=27
Level 1 returning  8, var= 8
Level 0 returning  1, var= 1
Final value 1

fib version :

#!/usr/bin/env bash

fib(){
    declare -n unused_identifier_array=$1
    local unused_identifier_nth=$2
    if  test $unused_identifier_nth -lt ${#unused_identifier_array}; then
        return # As already calculated
    fi
    if  test $unused_identifier_nth -lt 2; then
        unused_identifier_array[unused_identifier_nth]=$unused_identifier_nth
        return
    fi
    fib ${!unused_identifier_array} $((unused_identifier_nth-2))
    fib ${!unused_identifier_array} $((unused_identifier_nth-1))
    unused_identifier_array[unused_identifier_nth]=$((
        unused_identifier_array[unused_identifier_nth-2]
       +unused_identifier_array[unused_identifier_nth-1]))
}

declare -a sequence
fib sequence 15
declare -p sequence
# Output :
# declare -a sequence=([0]="0" [1]="1" [2]="1" [3]="2" [4]="3" [5]="5" [6]="8" [7]="13" [8]="21" [9]="34" [10]="55" [11]="89" [12]="144" [13]="233" [14]="377" [15]="610")
1
Fravadona On

Is there a way to assign to a dynamically-named variable as defined in the context of the caller?

Once a variable is defined as local in the current context, there's no builtin that allows you to modify it in the parent context directly. The intuitive solution of using declare -g "$varname"=... doesn't work, as it reaches the global context (not the parent one).

There's a work-around though. The following example illustrates the method, and shows that calling unset directly doesn't work (is it a bug?), you need to wrap unset in a function:

#!/bin/bash

funset() { unset "$@"; }

myfun1() { local "$1";  unset "$1"; printf -v "$1" %s 'myfun1'; }
myfun2() { local "$1"; funset "$1"; printf -v "$1" %s 'myfun2'; }

var=global; echo "$var"
myfun1 var; echo "$var"
myfun2 var; echo "$var"
global
global
myfun2

For your recursive fib and most nameref-functions, you can just use conditional declarations and make sure to address the arguments as $1 $2 ..., that avoids any possible name clash and doesn't leak any unwanted variable:

#!/bin/bash

FIB_CALLS=0

# fib varname integer
fib() {

    (( FIB_CALLS++ ))

    [[ $1 != a ]] && local a
    [[ $1 != b ]] && local b

    if (( $2 < 2 ))
    then
        (( $1 = $2 ))
    else
        fib a "$(( $2 - 1 ))"
        fib b "$(( $2 - 2 ))"
        (( $1 = a + b ))
    fi
}

b=15
fib a "$b"

echo "fib($b) = $a"
echo "$FIB_CALLS calls to fib"
fib(15) = 610
1973 calls to fib

remarks: Please notice that you can now use ANY variable name as first argument of fib, even a and b (which at some point in the recursion will be locally defined in fib). You can also see that the global variable b keeps its prior value after the call to fib.