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.)
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 inbash...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 childfib()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:uuidoruuidgenwould 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))$RANDOMis not very 'random'; in unit testing it's not uncommon to generate duplicates within a batch of 1000 references; since OP'sfib(15)algorithm will require a few thousand$RANDOMcalls this is a non-starterbash 5.1+the new$SRANDOMwould serve this purpose wellfib()(eg,$FIB_CALLS)Making a few changes to OP's current code:
Taking for a test drive:
This generates:
NOTES:
$SRANDOMto create new variable names)$FIB_CALLSto create new variable names)fib(15)we end up generating/populating 3900+ variables; memory usage in this case will be negligible; for excessively largefib(X)calls where memory usage becomes an issue it would make (more) sense to look at a new algorithm or use a different languageWhile 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:fib()functionfib()function calls as well as eliminate the need to dynamically generate new variables on each pass through thefib()functionperl,python,awk, etc)