98

How do I set a variable in the parent shell, from a subshell?

a=3 (a=4) echo $a 
3

11 Answers 11

88

The whole point of a subshell is that it doesn't affect the calling session. In bash a subshell is a child process, other shells differ but even then a variable setting in a subshell does not affect the caller. By definition.

Do you need a subshell? If you just need a group then use braces:

a=3 { a=4;} echo $a 

gives 4 (be careful of the spaces in that one). Alternatively, write the variable value to stdout and capture it in the caller:

a=3 a=$(a=4;echo $a) echo $a 

avoid using back-ticks ``, they are deprecated, can be difficult to read and are known to cause issues in certain circumstances.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you for pointing out the curly braces. I had seen them, but never knew what they did nor use them. Good advice!
Is there any way to use this when piping? E.g. { false; a=$?;} | true will not set the global(?) a to the desired value :(
@ArchimedesMP: A pipe is used to pass data between processes using standard streams, there are no processes involved here (except the shell). What is it you are trying to achieve?
I wanted to filter a meaningless warning from a third party tools stderr inside a script, without messing up stderr/stdout and while returning the original exit code. E.g. { some-tool 2>&1 1>&3; ec=$?; } | grep -v "Ignore some warning" 3>&1 1>&2; exit $ec (the X>&Y might be a bit off since I can not access the script right now). [& thanks for the reply on a 4y old comment!]
@ArchimedesMP: sounds a bit complex to do in one line, I would go for a wrapper script or a function to filter it out (you can redirect a function, e.g. myfunc() { echo stuff and nonsense; } > gash.txt)
|
39

There is the gdb-bash-variable hack:

gdb --batch-silent -ex "attach $$" -ex 'set bind_variable("a", "4", 0)'; 

although that always sets a variable in the global scope, not just the parent scope

4 Comments

Although less dangerous, I'd say this should be used about as frequently as a fork bomb in an actual script.
I had to ..."attach $PPID"... in order to make this work - $$ contains the PID of the current shell.
@dingalapadum not within OP's parenthesis. Probably the subshell runs in the same process and just resets the variables afterwards
@BeniBela in a () subshell, $$ remains the same. The actual subshell's PID can be seen with $BASHPID.
32

You don't. The subshell doesn't have access to its parent's environment. (At least within the abstraction that Bash provides. You could potentially try to use gdb, or smash the stack, or whatnot, to gain such access clandestinely. I wouldn't recommend that, though.)

One alternative is for the subshell to write assignment statements to a temporary file for its parent to read:

a=3 (echo 'a=4' > tmp) . tmp rm tmp echo "$a" 

8 Comments

Not pretty, but pretty much the better option in some cases. Thanks.
The file approach is quite useful if you're using a subshell to collect the values for multiple variables that the parent shell will need later, particularly in cases where not using a subshell is nontrivial.
I had to use this because I use "set -u" and "set -e" at the start of my main script, but I needed to run some commands to set a variable that exit with a non-zero exit code (and thus triggering the "-e" to kill the script).
can this be done more elegant with file descriptors? Are file descriptors accessible from sub shell and parent shell the like?
I am looking for a more elegant way similar to this approach too. How to use file descriptors for this? Is it the same way as khachik's answer below writing to output streams? I suppose that limits to one variable only.
|
27

If the problem is related to a while loop, one way to fix this is by using Process Substitution:

 var=0 while read i; do # perform computations on $i ((var++)) done < <(find . -type f -name "*.bin" -maxdepth 1) 

as shown here: https://stackoverflow.com/a/13727116/2547445

2 Comments

The problem is that Process Substitution is only working with bash and not sh (POSIX).
Another problem is that when the pipeline being read is complicated (or you have nested loops) this gets really hard to read compared to putting the pipeline ahead of the while. I whish there was a better solution.
14

By reading the answer from @ruakh (thank you) with a temporary file approach and the comments asking for a file descriptors solution, I got the following idea:

a=3 . <(echo a=4; echo b=5) echo $a echo $b 
  • It allows returning different variables at once (which could be an issue in the subshell variant of the accepted answer).
  • No iteration is needed,
  • No temporary file to take care of.
  • Close to the syntax proposed by the OP.

Result:

4 5 

With xtrace enabled is visible that we are sourcing from the file descriptor created for the output of the subshell:

+ a=3 + . /dev/fd/63 # <-- the file descriptor ;) ++ echo a=4 ++ echo b=5 ++ a=4 ++ b=5 + echo 4 4 + echo 5 5 

1 Comment

I am loving it.
9

To change variables in a script called from a parent script, you can call the script preceded with a "."

(EDIT - for explanation) In most shells "." is an alias for "source". the source command just inserts the text of another file at that position in the executing script. In the context of this question this answer avoids a sub-shell

a=3 echo $a . ./calledScript.sh echo $a 

in calledScript.sh

a=4 

Expected output

3 4 

5 Comments

Hi @Carl. this works but can explanation to the answer?
@Ajeetkumar Please find a link to the unix.stackexchange.com/a/114301 . is a shortcut to source cammand ss64.com/bash/source.html
calledScript.sh is run in the current shell, so it doesn't qualify as "subshell".
Point taken ivan_pozdeev, technically I did not answer to the question as it is was asked
if you source the shell script there is no subshell anymore
7

Instead of accessing the variable from the parent shell, change the order of the commands and use the process substitution:

a=3 echo 5 | (read a) echo $a 

prints 3

a=3 read a < <(echo 5) echo $a 

prints 5

Another example:

let i=0 seq $RANDOM | while read r do let i=r done echo $i 

vs

let i=0 while read r do let i=r done < <(seq $RANDOM) echo $i 

Alternatively, when job control is inactive (e.g. in scripts) you can use the lastpipe shell option to achieve the same result without changing the order of the commands:

#!/bin/bash shopt -s lastpipe let i=0 seq $RANDOM | while read r do let i=r done echo $i 

1 Comment

Using read allows to pass several variables. The subshell inherits variables from the parent, modifies them as needed, before exiting prints them out. That way it's easy to insolate a large span of code, for example the body of a loop.
5

You can output the value in the subshell and assign the subshell output to a variable in the caller script:

# subshell.sh echo Value # caller myvar=$(subshell.sh) 

If the subshell has more to output you can separate the variable value and other messages by redirecting them into different output streams:

# subshell.sh echo "Writing value" 1>&2 echo Value # caller myvar=$(subshell.sh 2>/dev/null) # or to somewhere else echo $myvar 

Alternatively, you can output variable assignments in the subshell, evaluate them in the caller script and avoid using files to exchange information:

# subshell.sh echo "a=4" # caller # export $(subshell.sh) would be more secure, since export accepts name=value only. eval $(subshell.sh) echo $a 

The last way I can think of is to use exit codes but this covers the integer values exchange only (and in a limited range) and breaks the convention for interpreting exit codes (0 for success non-0 for everything else).

Comments

3

Unless you can apply all io to pipes and use file handles, basic variable updating is impossible within $(command) and any other sub-process.

Regular files, however, are bash's global variables for normal sequential processing. Note: Due to race conditions, this simple approach is not good for parallel processing.

Create an set/get/default function like this:

globalVariable() { # NEW-VALUE # set/get/default globalVariable if [ 0 = "$#" ]; then # new value not given -- echo the value [ -e "$aRam/globalVariable" ] \ && cat "$aRam/globalVariable" \ || printf "default-value-here" else # new value given -- set the value printf "%s" "$1" > "$aRam/globalVariable" fi } 

"$aRam" is the directory where values are stored. I like it to be a ram disk for speed and volatility:

aRam="$(mktemp -td $(basename "$0").XXX)" # temporary directory mount -t tmpfs ramdisk "$aRam" # mount the ram disk there trap "umount "$aRam" && rm -rf "$aRam"" EXIT # auto-eject 

To read the value:

v="$(globalVariable)" # or part of any command 

To set the value:

globalVariable newValue # newValue will be written to file 

To unset the value:

rm -f "$aRam/globalVariable" 

The only real reason for the access function is to apply a default value because cat will error given a non-existent file. It is also useful to apply other get/set logic. Otherwise, it would not be needed at all.

An ugly read method avoiding cat's non-existent file error:

v="$(cat "$aRam/globalVariable 2>/dev/null")" 

A cool feature of this mess is that you can open another terminal and examine the contents of the files while the program is running.

2 Comments

Prone to race conditions since access to files is not synchronized.
great answer ! deserve upvotes
3

While it's harder to get multiple variables out of a subshell, you can set multiple variables inside a function without using globals.

You can pass the name of a variable into a function that uses local -n to turn it into a special variable called a nameref:

myfunc() { local -n OUT=$1 local -n SIDEEFFECT=$2 OUT='foo' SIDEEFFECT='bar' } myfunc A B echo $A > foo echo $B > bar 

This is the technique I ended up using instead of getting subshell FOO=$(myfunc) working setting multiple variables.

Comments

0

A very simple and practical method that allows multiple variables is as follows, eventually may add parameters to the call:

function ComplexReturn(){ # do your processing... a=123 b=456 echo -n "AAA=${a}; BBB=${b};" } # ... this can be internal function or any subshell command eval $(ComplexReturn) echo $AAA $BBB 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.