Why can a bash function recursively call itself without using local variables? - bash

I am doing scripts in bash. It is said in this site (http://tldp.org/LDP/abs/html/recurnolocvar.html) that "A function may recursively call itself even without use of local variables." but it was not explained why.
There is a sample function involving the fibonacci sequence. He commented on the code that it doesnt need to be local and asked why, but did not answer. A part is shown below:
Fibonacci ()
{
idx=$1 # Doesn't need to be local. Why not?
if [ "$idx" -lt "$MINIDX" ]
then
echo "$idx" # First two terms are 0 1 ... see above.
else
(( --idx )) # j-1
term1=$( Fibonacci $idx ) # Fibo(j-1)
(( --idx )) # j-2
term2=$( Fibonacci $idx ) # Fibo(j-2)
echo $(( term1 + term2 ))
fi
}
The Fibonacci function has an "idx" variable which could have been modified by successive calls because the successive "idx" definitions are not declared local, hence it should affect the previous definitions.
The previous topic on that site (http://tldp.org/LDP/abs/html/localvar.html) demonstrates that if a variable is not declared as local (therefore defaults to global) then changing it would reflect changes in global scope.
Why can a bash function recursively call itself without using local variables?

Because the variables effectively are local.
The command in process substitution ($()) is run by a subshell. Variable values don't propagate back from subshell. So the recursive calls can't affect the parent call.
Commands run in subshell are:
process substitution ($(command))
both sides of a pipeline (command1 | command2)
explicit subshell ((command))
background jobs (command&)
(bash-specific) process redirections (>(command), <(command))
There is no way to propagate any variable values back from these.

It's because he calls the function recursively through a subshell:
term1=$( Fibonacci $idx ) # Fibo(j-1)
I actually find that inefficient For every level of recursion, a process is summoned and could cause overload to the system. It's better to use local variables.

Related

if statement, that calls a function and compares values to variables

I use i3 and want to control the volume of my sink using the volume keys, that call pactl. Since pactl does not support a parameter for max volume, but is adviced to use with pipewire, the script should get the current volume and compare, if the next volume_increment step would result in a higher value than a certain max_volume. My confusion is now related to an if statement, with the combination of calculating the difference between the volume_increment and max_volume, and compare it to the outcome of the current_volume function. So the volume will be raised, until the certain value, where the increment would result in a higher volume then the maximum value
#!/bin/bash
volume_increment=$1
max_volume=$2
function get_volume() {
pactl get-sink-volume #DEFAULT_SINK# | head -n1 | cut -d/ -f2 | tr -d ' %'
}
if (( get_volume >= $max_volume - $volume_increment )) ; then
pactl set-sink-volume #DEFAULT_SINK# +$volume_increment%
else
pactl set-sink-volume #DEFAULT_SINK# $max_volume%
fi
I've googled a lot and tried several variations of brackets in the if statement. I've managed to get it to work at some intermediate step. But then I've read that (()) brackets are not universal for POSIX shells.
Now I'm very confused and can't separate these various aspects in my head.
Would someone be so kind to explain the logic of calling a function in a statement and compare it to a difference of variables? Preferably for universal POSIX usage, nut bash specific.
Note that in arithmetic context, variables are implicitly expaned. Therefore, ((var)) uses $var automatically.
In your statement
(( get_volume >= $max_volume - $volume_increment ))
the $ is not needed - you could equally well have written max_volume - volume_increment, but you are refering to a variable get_volume and never set it. An unset variable is treated as zero, and therefore get_volume is always 0. Assuming that you can caluclate this value from the function current_volume, you could do a
get_volume=$(current_volume)
or run this function directly inside the arithmetic expression, i.e.
(( $(current_volume) >= $max_volume - $volume_increment ))
Of course, all this requires that you are restricting yourself to integer arithmetic in your script.

Trying to call a function within a function in Bash but it returns errors

I had looked on overflow and exchange, but I can't seem to find an answer for this. I am currently trying to make a recursive fibonacci function. However when I try to test it (using command line entry) it keeps returning the error
fork: retry: Resource temporarily unavailable
I had found a post saying it had to do with shell resource limits, but I felt like that probably wasn't my issue since I've seen separate posts with recursive functions being able to call their function fine enough.
I had broken up the operation into parts to see what exactly was happening, but I can't find a specific problem - it may have to do with how i'm calling my function, but I'm not sure, either.
I can pass the parameter I want just fine, though. It just prints the same error for whatever number I put in. If I enter 5, it echoes the fork error five times. It returns, but doesn't return the values...
For specification, I currently use Bash Version 4.4.20(1)
function fib_r
{
int=$1
for ((i=1; i<=int; i++))
do
f1=$(fib_r $((int-1)))
f2=$(fib_r $((int-2)))
fibo=$((f1+f2))
done
}
What I want to achieve is when you enter a number on command line, It does calculate that number, however it shows the calculated number at each step, rather than return the final value from start to end:
An example output:
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21
9 34
10 55
This Shellcheck-clean code fixes a few problems with the code in the question:
function fib_r
{
local -r n=$1
if (( n == 0 )); then
echo 0
elif (( n == 1 )); then
echo 1
else
local -r f1=$(fib_r "$((n-1))")
local -r f2=$(fib_r "$((n-2))")
echo "$((f1+f2))"
fi
return 0
}
It adds code for base cases (0 and 1) to stop the recursion. The original code had no base cases so the recursion continued until resources were exhausted.
The function echos the result so it is available to the caller. Command substitution ($(command)) is usually only useful if the command prints its result(s) to standard output.
The loop was removed because this is supposed to be a recursive function (and the loop wasn't useful).
local is used to make variables local to the function so they won't clash with variables used elsewhere in a program that uses the function. The -r (readonly) option is used because the variables never need to be changed within the function and it prevents them being accidentally changed by other functions.
The variable name int was changed to n because that is more conventional for functions like this (and int seems really strange to people who know C, or related programming languages).
Note that this function is very slow. That is partly because it uses command substitution (which runs an expensive sub-process every time) to return results, but mostly because this particular recursive algorithm is very inefficient (exponential complexity, see Computational complexity of Fibonacci Sequence). Much faster recursive implementations are possible.
The question was updated to request a function that prints all of the Fibonacci numbers up to a given number. This is a recursive function that does that:
function fib_r
{
local -r n=$1
local -r depth=${2-1}
local -r f1=${3-1}
local -r f2=${4-0}
if (( depth <= n )); then
printf '%2d %d\n' "$depth" "$f1"
fib_r "$n" "$((depth+1))" "$((f1+f2))" "$f1"
fi
return 0
}
This uses a much more efficient algorithm (O(n)), so it can calculate all of the Fibonacci numbers that can be represented by a 64-bit integer in a fraction of a second. Run fib_r 92 to do that.

Bash local variable scope best practice

I've seen that some people when writing bash script they define local variables inside an if else statement like example 1
Example 1:
#!/bin/bash
function ok() {
local animal
if [ ${A} ]; then
animal="zebra"
fi
echo "$animal"
}
A=true
ok
For another example, this is the same:
Example 2:
#!/bin/bash
function ok() {
if [ ${A} ]; then
local animal
animal="zebra"
fi
echo "$animal"
}
A=true
ok
So, the example above printed the same result but which one is the best practice to follow. I prefer the example 2 but I've seen a lot people declaring local variable inside a function like example 1. Would it be better to declare all local variables on top like below:
function ok() {
# all local variable declaration must be here
# Next statement
}
the best practice to follow
Check your scripts with https://shellcheck.net .
Quote variable expansions. Don't $var, do "$var". https://mywiki.wooledge.org/Quotes
For script local variables, prefer to use lowercase variable names. For exported variables, use upper case and unique variable names.
Do not use function name(). Use name(). https://wiki.bash-hackers.org/scripting/obsolete
Document the usage of global variables a=true. Or add local before using variables local a; then a=true. https://google.github.io/styleguide/shellguide.html#s4.2-function-comments
scope best practice
Generally, use the smallest scope possible. Keep stuff close to each other. Put local close to the variable usage. (This is like the rule from C or C++, to define a variable close to its usage, but unlike in C or C++, in shell declaration and assignment should be on separate lines).
Note that your examples are not the same. In the case variable A (or a) is an empty string, the first version will print an empty line (the local animal variable is empty), the second version will print the value of the global variable animal (there was no local). Although the scope should be as smallest, animal is used outside of if - so local should also be outside.
The local command constrains the variables declared to the function scope.
With that said, you can deduce that doing so inside an if block will be the same as if you did outside of it, as long as it's inside of a function.

How to read argument value inside for loop range for shell scripting [duplicate]

I'm working on getting accustomed to shell scripting and ran across a behavior I found interesting and unexplained. In the following code the first for loop will execute correctly but the second will not.
declare letters=(a b c d e f g)
for i in {0..7}; do
echo ${letters[i]}
done
for i in {0..${#letters[*]}}; do
echo ${letters[i]}
done
The second for loop results in the following error:
syntax error: operand expected (error token is "{0..7}")
What confuses me is that ${#letters[*]} is clearly getting evaluated, correctly, to the number 7. But despite this the code fails even though we just saw that the same loop with {0..7} works perfectly fine.
What is the reason for this?
I am running OS X 10.12.2, GNU bash version 3.2.57.
The bracket expansion happens before parameter expansion (see EXPANSIONS in man bash), therefore it works for literals only. In other words, you can't use brace expansion with variables.
You can use a C-style loop:
for ((i=0; i<${#letters[#]}; i++)) ; do
echo ${letters[i]}
done
or an external command like seq:
for i in $(seq 1 ${#letters[#]}) ; do
echo ${letters[i-1]}
done
But you usually don't need the indices, instead one loops over the elements themselves, see #TomFenech's answer below. He also shows another way of getting the list of indices.
Note that it should be {0..6}, not 7.
Brace expansion occurs before parameter expansion, so you can't use a variable as part of a range.
Expand the array into a list of values:
for letter in "${letters[#]}"; do
echo "$letter"
done
Or, expand the indices of the array into a list:
for i in ${!letters[#]}; do
echo "${letters[i]}"
done
As mentioned in the comments (thanks), these two approaches also accommodate sparse arrays; you can't always assume that an array defines a value for every index between 0 and ${#letters[#]}.

Is there file scope in bash programming?

I want to make this variable
local to="$HOME/root_install/grunt"
be available to the entire file
makeGrunt(){
# set paths
local to="$HOME/root_install/grunt"
cd $to
sudo npm install -g grunt-init
sudo git clone https://github.com/gruntjs/grunt-init-gruntfile.git ~/.grunt-init/gruntfile
sudo grunt-init gruntfile
}
In POSIX-like shells - unless you use nonstandard constructs such as local, typeset, or declare - variables created implicitly through
assignment have global scope in the shell at hand.
Thus, to="$HOME/root_install/grunt" will make variable $to available anywhere in the current shell - unless you're inside a function and that variable was explicitly marked as local.
andlrc's helpful answer demonstrates the pitfalls associated with subshells - subshells are child processes that are clones of the original shell - they see the same state, but cannot modify the original shell's environment.
Bash shells use dynamic
scopes
which means that all variables are available for all called functions, commands,
etc. Consider this:
var=1
a() {
local var=2
b
}
b() {
echo "$var"
}
a # 2
b # 1
a # 2
When using the local keyword a variable will be available for the function, in
where it's defined, but also within all functions called from that function.
The same applies when a variable is created without the local keyword. With
that exception that it will also be available outside the function.
One more thing to be aware of is that whenever a subshell is created a variable
will not be able to "leave" it, i.e. when a pipe is involved. Consider this:
sum=0
seq 3 | while read -r num; do
sum=$((sum + num))
echo "$sum" # will print 1, 3 and 6
done
echo "$sum" # 0 huh? 1 + 2 + 3 = 0?

Resources