79

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

a=3
(a=4)
echo $a
codeforester
  • 39,467
  • 16
  • 112
  • 140
Matt Joiner
  • 112,946
  • 110
  • 377
  • 526
  • 1
    Something like this: http://stackoverflow.com/questions/15383082/pass-variable-from-a-child-to-parent-in-ksh/15383353#15383353 – Guru Mar 21 '13 at 07:05
  • Possible duplicate of [Can I export a variable to the environment from a bash script without sourcing it?](https://stackoverflow.com/q/16618071/608639), [Can a shell script set environment variables of the calling shell?](https://stackoverflow.com/q/496702/608639), etc. – jww Oct 29 '19 at 11:16

11 Answers11

83

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.

Roel Van de Paar
  • 2,111
  • 1
  • 24
  • 38
cdarke
  • 42,728
  • 8
  • 80
  • 84
  • Thank you for pointing out the curly braces. I had seen them, but never knew what they did nor use them. Good advice! – Christian Oct 22 '13 at 09:40
  • 1
    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 Jun 28 '18 at 10:49
  • @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? – cdarke Jun 28 '18 at 13:41
  • 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 Jun 29 '18 at 15:46
  • @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`) – cdarke Jun 29 '18 at 19:26
  • what about this workaround? https://stackoverflow.com/a/15383353/6466510 – andreagalle Feb 24 '22 at 09:19
36

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

BeniBela
  • 16,412
  • 4
  • 45
  • 52
  • 34
    Although less dangerous, I'd say this should be used about as frequently as a fork bomb in an actual script. – chepner Mar 21 '13 at 13:00
  • I had to `..."attach $PPID"...` in order to make this work - $$ contains the PID of the current shell. – dingalapadum Oct 07 '15 at 16:44
  • @dingalapadum not within OP's parenthesis. Probably the subshell runs in the same process and just resets the variables afterwards – BeniBela Dec 09 '16 at 22:44
  • @BeniBela in a `()` subshell, `$$` remains the same. The actual subshell's PID can be seen with `$BASHPID`. – ivan_pozdeev May 03 '19 at 04:30
27

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"
ruakh
  • 175,680
  • 26
  • 273
  • 307
  • 1
    Not pretty, but pretty much the better option in some cases. Thanks. – Benoit Duffez Oct 03 '14 at 09:38
  • 2
    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. – aroth Sep 02 '15 at 04:41
  • 1
    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). – dan_linder Mar 06 '18 at 22:58
  • can this be done more elegant with file descriptors? Are file descriptors accessible from sub shell and parent shell the like? – opinion_no9 Mar 07 '21 at 09:45
  • 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](https://stackoverflow.com/a/15541840/1884546) writing to output streams? I suppose that limits to one variable only. – midnite May 10 '22 at 18:15
  • Most linux systems have /dev/shm where you can write such files and it is written to memory not disk, so will be much quicker. – Cloudranger May 25 '23 at 06:15
21

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

Community
  • 1
  • 1
marioquark
  • 360
  • 2
  • 7
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
Jay M
  • 3,736
  • 1
  • 24
  • 33
Carl Bosch
  • 1,281
  • 16
  • 14
8

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
Ictus
  • 1,435
  • 11
  • 20
6

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
ciamej
  • 6,918
  • 2
  • 29
  • 39
  • 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. – Ale Jul 31 '21 at 14:16
4

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

Community
  • 1
  • 1
khachik
  • 28,112
  • 9
  • 59
  • 94
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.

Paul
  • 442
  • 4
  • 9
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.

amphetamachine
  • 27,620
  • 12
  • 60
  • 72
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
fcm
  • 1,247
  • 15
  • 28