bashvariablesscopedynamic-scopeupvar

Assigning to a variable in a parent context in Bash


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.)


Solution

  • 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:

    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:


    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: