Matt Joiner
Matt Joiner

Reputation: 118480

Set a parent shell's variable from a subshell

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

a=3
(a=4)
echo $a

Upvotes: 95

Views: 102706

Answers (11)

cdarke
cdarke

Reputation: 44344

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.

Upvotes: 87

Carl Bosch
Carl Bosch

Reputation: 1281

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

Upvotes: 9

Ictus
Ictus

Reputation: 1557

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

Upvotes: 12

amphetamachine
amphetamachine

Reputation: 30595

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.

Upvotes: 3

fcm
fcm

Reputation: 1331

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

Upvotes: 0

ciamej
ciamej

Reputation: 7068

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

Upvotes: 7

Paul
Paul

Reputation: 582

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.

Upvotes: 3

khachik
khachik

Reputation: 28693

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

Upvotes: 5

marioquark
marioquark

Reputation: 427

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

Upvotes: 25

BeniBela
BeniBela

Reputation: 16907

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

Upvotes: 39

ruakh
ruakh

Reputation: 183251

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"

Upvotes: 30

Related Questions