Variable affectation from function not working with $() - bash

I'm trying to define a function which behaviour that will, the first time it has been called, ask the user for some choice and then remember thoses choice in order not to ask the user again.
Using a variable to store the state (initialized or not) works well when the function is called the normal way, however if I want this function to return results, capturing thoses results with $() breaks the behaviour I'm trying to achieve
Here is a simple reproductible exemple:
#!/bin/bash
initialized=false
function givevalue
{
echo "init = $initialized" 1>&2
if ! $initialized;
then
echo "a b c"
initialized=true
else
echo "d e f"
fi
}
for i in $(givevalue); do echo "run 1 : $i"; done
for i in $(givevalue); do echo "run 2 : $i"; done
the result I get is
init = false
run 1 : a
run 1 : b
run 1 : c
init = false
run 2 : a
run 2 : b
run 2 : c
while I was expecting
init = false
run 1 : a
run 1 : b
run 1 : c
init = true
run 2 : d
run 2 : e
run 2 : f

The contents of $(...) are run as a separate process and changes to variables are not propagated to the parent shell. This cannot be changed.

Related

bash: set -o errexit is not exiting when a command has an error. how do you change code to make it exit when there is an error? [duplicate]

In the bash man page, it states:
Exit immediately if a pipeline (which may consist of a single simple command),
a subshell command enclosed in parentheses, or one of the commands executed as
part of a command list enclosed by braces...
So I assumed that a function should be considered a command list enclosed by braces. However, if you apply a conditional to the function call, errexit no longer persists inside the function body and it executes the entire command list before returning. Even if you explicitly create a subshell inside the function with errexit enabled for that subshell, all commands in the command list are executed. Here is a simple example that demonstrates the issue:
function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }
( set -Eex ; a )
+ a
+ b
+ false
( set -Eex ; ap )
+ ap
+ b
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
Now if I apply a conditional to each of them...
( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
You started to quote the manual but then you cut the bit that explained this behaviour, which was in the very next sentence:
-e Exit immediately if a pipeline, which may consist of a single simple command, a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.
bug-bash mailing list has an explanation by Eric Blake more explicit about functions:
Short answer: historical compatibility.
...
Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive. But
it is standardized, so we have to live with it.
And some words about whether set -e can be exploited to achieve the wanted behavior:
Because once you are in a context that ignores 'set -e', the historical
behavior is that there is no further way to turn it back on, for that
entire body of code in the ignored context. That's how it was done 30
years ago, before shell functions were really thought about, and we are
stuck with that poor design decision.
Not an answer to the original question, but a work-around for the underlying problem: set up a trap on errors:
function on_error() {
echo "error happened!"
}
trap on_error ERR
echo "OK so far"
false
echo "this line should not execute"
The reason for the behavior itself is properly explained in other answers (basically it's expected bash behavior as per the manual and POSIX): https://stackoverflow.com/a/19789651/1091436
not an answer but you might fix this counter intuitive behaviur by defining this helper function:
fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }
then call your functions via fixerrexit.
example:
f1()
{
mv unimportant-0.txt important.txt
rm unimportant-*.txt
}
set -e
if fixerrexit f1
then
echo here is the important file: important.txt
echo unimportant files are deleted
fi
if outer context has errexit on, then fixerrexit turns on errexit inside f1() as well, so you dont need to worry about commands being executed after a failure occurs.
the only downside is you can not set variables since it runs f1 inside a subshell.

get empty value when passing two parametres to shell script

I am trying to extract two variables which I passed to shell script like :
/XDRI_RV_LOAD.KSH 20220617 rv_tiers
Inside the shell script, I write :
date_ctrlm=$1
branche=$2
echo "TEST 1 : " $date_ctrlm
echo " TEST 2 :" $branche
So that I get this
TEST 1 : 20220617
TEST 2 :
I don't know why the job does not extract the 2 param. In fact, I should get also
TEST 2 : rv_tiers
Thanks
The solution is to replace :
jobInit ${0} "$#"
By
jobInit ${0} "$*"

bash recursion automatically ends after single level

Why is this make_request function ending just after a single traversal?
make_request(){
path="${1//' '/'%20'}"
echo $path
mkdir -p $HOME/"$1"
$(curl --output $HOME/"$1"/$FILE_NAME -v -X GET $BASE_URL"/"$API_METHOD"/"$path &> /dev/null)
# sample response from curl
# {
# "count":2,
# "items": [
# {"path": "somepath1", "type": "folder"},
# {"path": "somepath2", "type": "folder"},
# ]
# }
count=$(jq ".count" $HOME/"$1"/$FILE_NAME)
for (( c=0; c<$count; c++ ))
do
child=$(jq -r ".items[$c] .path" $HOME/"$1"/$FILE_NAME);
fileType=$(jq -r ".items[$c] .type" $HOME/"$1"/$FILE_NAME);
if [ "$fileType" == "folder" ]; then
make_request "$child"
fi
done
}
make_request "/"
make_request "/" should give the following output:
/folder
/folder/folder1-1
/folder/folder1-1/folder1-2
/folder/foler2-1
/folder/folder2-1/folder2-2
/folder/folder2-1/folder2-3 ...
but I am getting the following:
/folder
/folder/folder1-1
/folder/folder1-1/folder1-2
You are using global variables everywhere. Therefore, the inner call changes the loop variables c and count of the outer call, resulting in bogus.
Minimal example:
f() {
this_is_global=$1
echo "enter $1: $this_is_global"
((RANDOM%2)) && f "$(($1+1))"
echo "exit $1: $this_is_global"
}
Running f 1 prints something like
enter 1: 1
enter 2: 2
enter 3: 3
exit 3: 3
exit 2: 3
exit 1: 3
Solution: Make the variables local by writing local count=$(...) and so on. For your loop, you have to put an additional statement local c above the for.
As currently written all variables have global scope; this means that all function calls are overwriting and referencing the same set of variables, this in turn means that when a child function returns to the parent the parent will find its variables have been overwritten by the child, this in turn leads to the type of behavior seen here.
In this particular case the loop variable c leaves the last child process with a value of c=$count and all parent loops now see c=$count and thus exit; it actually gets a bit more interesting because count is also changing with each function call. The previous comment to add set -x (aka enable debug mode) before the first function call should show what's going on with each variable at each function call.
What OP wants to do is insure each function is working with a local copy of a variable. The easiest approach is to add a local <variable_list> at the top of the function, making sure to list all variables that should be treated as 'local', eg:
local path count c child fileType
change variables to have local scope instead of global.
...
local count; # <------ VARIABLE MADE LOCAL
count=$(jq ".count" $HOME/"$1"/$FILE_NAME)
local c; # <------ VARIABLE MADE LOCAL
for (( c=0; c<$count; c++ ))
do
....
done
...

Retry a command only once : when a command fails (in bash)

for ( i=3; i<5; i++)
do
execute some command 1
if command 2 is successful then do not run the command 1 (the for loop should continue)
if command 2 is not successful then run command 1 only once (like retry command 1 only once, after this the for loop should continue)
done
This is to note that command 2 is dependent on command 1 and command 2 can only be executed after command 1
for example:
for ( i=3; i<5; i++)
do
echo "i" >> mytext.txt ---> command 1
if "check the content of mytext.txt file to see if the value of i is actually added" ---> command 2
if it is not added then execute echo "i" >> mytext.txt (command 1) again and only once.
if i value is added to the file .. then exit and continue the loop
done
Since the "command 1" is quite big and not just an example echo statement here.I do not want to add "command 1" twice .. once outside and once inside the if condition. I want this logic in an optimized way with no redundancy of code.
Per a comment it sounds like the OP may need to invoke command 1 up to 2 times for a given $i value, but only wants to type command 1 once in the script.
Siddhartha's suggestion to use a function is probably good enough but depending on the actual command 1 (OP mentions that it's 'quite big') I'm going to play devil's advocate and assume there could be additional issues with passing some args to the function (eg, a need to escape some characters ... ??).
The general idea is to have an internal loop that can be executed at most 2 times, with logic in the loop that will allow for an 'early' exit (eg, after just one pass through the loop).
Since we're using pseudo-code I'll use the same ...
for ( i=3; i<5; i++ )
do
pass=1 # reset internal loop counter
while ( pass -le 2 )
do
echo "i" >> mytext.txt # command 1
if ( pass -eq 1 ) # after first 'command 1' execution
&& ( value of 'i' is in mytext.txt ) # command 2
then
break # break out of inner loop; alternatively ...
# pass=10 # ensure pass >= 2 to force loop to exit on this pass
fi
pass=pass+1 # on 1st pass set pass=2 => allows another pass through loop
# on 2nd pass set pass=3 => will force loop to exit
done
done
you can declare functions like
function command
{
your_command -f params
}
for ( i=3; i<5; i++)
do
if command ; then
echo "success"
else
echo "retry"
command
fi
done

Does Powershell have an equivalent to the bash subshell?

One thing that's really great in linux bash shell is that you can define variables inside of a subshell and after that subshell completes the (environment?) variables defined within are just gone provided you define them without exporting them and within the subshell.
for example:
$ (set bob=4)
$ echo $bob
$
No variable exists so no output.
I was also recently writing some powershell scripts and noticed that I kept having to null out my variables / objects at the end of the script; using a subshell equivalent in powershell would clear this up.
I've not heard of such functionality before, but you can get the same effect by running something like the following:
Clear-Host
$x = 3
& {
$a = 5
"inner a = $a"
"inner x = $x"
$x++
"inner x increment = $x"
}
"outer a = $a"
"outer x = $x"
Output:
inner a = 5
inner x = 3
inner x increment = 4
outer a =
outer x = 3
i.e. this uses the call operator (&) to run a script block ({...}).

Resources