Set a parent shell's variable from a subshell

BashShellSubshell

Bash Problem Overview


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

a=3
(a=4)
echo $a

Bash Solutions


Solution 1 - Bash

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 and can be difficult to read.

Solution 2 - Bash

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

Solution 3 - Bash

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"

Solution 4 - Bash

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

Solution 5 - Bash

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

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

in calledScript.sh

a=4

Expected output

3
4

Solution 6 - Bash

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).

Solution 7 - Bash

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

Solution 8 - Bash

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.

Solution 9 - Bash

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

Solution 10 - Bash

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.

Solution 11 - Bash

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatt JoinerView Question on Stackoverflow
Solution 1 - BashcdarkeView Answer on Stackoverflow
Solution 2 - BashBeniBelaView Answer on Stackoverflow
Solution 3 - BashruakhView Answer on Stackoverflow
Solution 4 - BashmarioquarkView Answer on Stackoverflow
Solution 5 - BashCarl BoschView Answer on Stackoverflow
Solution 6 - BashkhachikView Answer on Stackoverflow
Solution 7 - BashIctusView Answer on Stackoverflow
Solution 8 - BashPaulView Answer on Stackoverflow
Solution 9 - BashciamejView Answer on Stackoverflow
Solution 10 - BashamphetamachineView Answer on Stackoverflow
Solution 11 - BashfcmView Answer on Stackoverflow