Search code examples
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:

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