I'm working with this:
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
I have a script like below:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
test1
echo "$e"
Which returns:
hello
4
But if I assign the result of the function to a variable, the global variable e
is not modified:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
ret=$(test1)
echo "$ret"
echo "$e"
Returns:
hello
2
I've heard of the use of eval in this case, so I did this in test1
:
eval 'e=4'
But the same result.
Could you explain me why it is not modified? How could I save the echo of the test1
function in ret
and modify the global variable too?
When you use a command substitution (ie the
$(...)
construct), you are creating a subshell. Subshells inherit variables from their parent shells, but this only works one way - a subshell cannot modify the environment of its parent shell. Your variablee
is set within a subshell, but not the parent shell. There are two ways to pass values from a subshell to its parent. First, you can output something to stdout, then capture it with a command substitution:Gives:
For a numerical value from 0-255, you can use
return
to pass the number as the exit status:Gives:
What you are doing, you are executing test1
$(test1)
in a sub-shell( child shell ) and Child shells cannot modify anything in parent.
You can find it in bash manual
Please Check: Things results in a subshell here
It's because command substitution is performed in a subshell, so while the subshell inherits the variables, changes to them are lost when the subshell ends.
Reference:
Summary
Your example can be modified as follows to archive the desired effect:
prints as desired:
Note that this solution:
e=1000
, too.$?
if you need$?
The only bad sideffects are:
bash
._
)_capture
just replace all occurances of3
with another (higher) number.The following (which is quite long, sorry for that) hopefully explains, how to adpot this recipe to other scripts, too.
The problem
outputs
while the wanted output is
The cause of the problem
Shell variables (or generally speaking, the environment) is passed from parental processes to child processes, but not vice versa.
If you do output capturing, this usually is run in a subshell, so passing back variables is difficult.
Some even tell you, that it is impossible to fix. This is wrong, but it is a long known difficult to solve problem.
There are several ways on how to solve it best, this depends on your needs.
Here is a step by step guide on how to do it.
Passing back variables into the parental shell
There is a way to pass back variables to a parental shell. However this is a dangerous path, because this uses
eval
. If done improperly, you risk many evil things. But if done properly, this is perfectly safe, provided that there is no bug inbash
.prints
Note that this works for dangerous things, too:
prints
This is due to
printf '%q'
, which quotes everything such, that you can re-use it in a shell context safely.But this is a pain in the a..
This does not only look ugly, it also is much to type, so it is error prone. Just one single mistake and you are doomed, right?
Well, we are at shell level, so you can improve it. Just think about an interface you want to see, and then you can implement it.
Augment, how the shell processes things
Let's go a step back and think about some API which allows us to easily express, what we want to do.
Well, what do we want do do with the
d()
function?We want to capture the output into a variable. OK, then let's implement an API for exactly this:
Now, instead of writing
we can write
Well, this looks like we haven't changed much, as, again, the variables are not passed back from
d
into the parent shell, and we need to type a bit more.However now we can throw the full power of the shell at it, as it is nicely wrapped in a function.
Think about an easy to reuse interface
A second thing is, that we want to be DRY (Don't Repeat Yourself). So we definitively do not want to type something like
The
x
here is not only redundant, it's error prone to always repeate in the correct context. What if you use it 1000 times in a script and then add a variable? You definitively do not want to alter all the 1000 locations where a call tod
is involved.So leave the
x
away, so we can write:outputs
This already looks very good.
Avoid changing
d()
The last solution has some big flaws:
d()
needs to be alteredxcapture
to pass the output.output
, so we can never pass this one back._passback
Can we get rid of this, too?
Of course, we can! We are in a shell, so there is everything we need to get this done.
If you look a bit closer to the call to
eval
you can see, that we have 100% control at this location. "Inside" theeval
we are in a subshell, so we can do everything we want without fear of doing something bad to the parental shell.Yeah, nice, so let's add another wrapper, now directly inside the
eval
:prints
However, this, again, has some major drawback:
!DO NOT USE!
markers are there, because there is a very bad race condition in this, which you cannot see easily:>(printf ..)
is a background job. So it might still execute while the_passback x
is running.sleep 1;
beforeprintf
or_passback
._xcapture a d; echo
then outputsx
ora
first, respectively._passback x
should not be part of_xcapture
, because this makes it difficult to reuse that recipe.$(cat)
), but as this solution is!DO NOT USE!
I took the shortest route.However, this shows, that we can do it, without modification to
d()
!Please note that we not neccessarily need
_xcapture
at all, as we could have written everyting right in theeval
.However doing this usually isn't very readable. And if you come back to your script in a few years, you probably want to be able to read it again without much trouble.
Fix the race
Now let's fix the race condition.
The trick could be to wait until
printf
has closed it's STDOUT, and then outputx
.There are many ways to archive this:
Following the last path could look like (note that it does the
printf
last because this works better here):outputs
Why is this correct?
_passback x
directly talks to STDOUT.>&3
.$("${@:2}" 3<&-; _passback x >&3)
finishes after the_passback
, when the subshell closes STDOUT.printf
cannot happen before the_passback
, regardless how long_passback
takes.printf
command is not executed before the complete commandline is assembled, so we cannot see artefacts fromprintf
, independently howprintf
is implemented.Hence first
_passback
executes, then theprintf
.This resolves the race, sacrificing one fixed file descriptor 3. You can, of course, choose another file descriptor in the case, that FD3 is not free in your shellscript.
Please also note the
3<&-
which protects FD3 to be passed to the function.Make it more generic
_capture
contains parts, which belong tod()
, which is bad, from a reusability perspective. How to solve this?Well, do it the desparate way by introducing one more thing, an additional function, which must return the right things, which is named after the original function with
_
attached.This function is called after the real function, and can augment things. This way, this can be read as some annotation, so it is very readable:
still prints
Allow access to the return-code
There is only on bit missing:
v=$(fn)
sets$?
to whatfn
returned. So you probably want this, too. It needs some bigger tweaking, though:prints
There is still a lot room for improvement
The solution pollutes a file descriptor by using it internally. In case you need it in your script, you need to be very careful not to use this. Perhaps there is a way to get rid of this and replace it with a dynamic (free) file descriptor.
Perhaps you want to capture STDERR of the called function, too. Or you want to even pass in and out more than one filedescriptor from and to variables.
Also do not forget:
This must call a shell function, not an external command.
Last words
This is not the only possible solution. It is one example to a solution.
As always you have many ways to express things in the shell. So feel free to improve and find something better.
The solution presented here is quite far from being perfect:
bash
, so probably is hard to port to other shells.However I think it is quite easy to use:
I had a similar problem, when I wanted to automatically remove temp files I had created. The solution I came up with was not to use command substitution, but rather to pass the name of the variable, that should take the final result, into the function. E.g.
So, in your case that would be:
Works and has no restrictions on the "return value".
You can always use an alias: