Issue
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.
#!/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
oruuidgen
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'sfib(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 largefib(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 thefib()
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)
Answered By - markp-fuso Answer Checked By - Marie Seifert (WPSolving Admin)