How to store the output of command in a variable without creating a subshell [Bash <v4]

Here's another way to do it, which is different enough that it warrants a separate answer. I think this method is subshell-free and bash sub-process free:

ubuntu@ubuntu:~$ bar () { echo "$BASH_SUBSHELL $BASHPID"; }
ubuntu@ubuntu:~$ bar
0 8215
ubuntu@ubuntu:~$ mkfifo /tmp/myfifo
ubuntu@ubuntu:~$ exec 3<> /tmp/myfifo
ubuntu@ubuntu:~$ unlink /tmp/myfifo
ubuntu@ubuntu:~$ bar 1>&3
ubuntu@ubuntu:~$ read -u3 a
ubuntu@ubuntu:~$ echo $a
0 8215
ubuntu@ubuntu:~$ exec 3>&-
ubuntu@ubuntu:~$

The trick here is to use exec to open the FIFO in read-write mode with an FD, which seems to have the side-effect of making the FIFO non-blocking. Then you can redirect your command to the FD without it blocking, then read the FD.

Note that the FIFO will be a limited-size buffer, probably around 4K, so if your command produces more output than this, it will end up blocking again.


Here's what I could come up with - its a bit messy, but foo is run in the top-level shell context and its output is provided in the variable a in the top-level shell context:

#!/bin/bash

foo () { echo ${BASH_SUBSHELL}; }

mkfifo /tmp/fifo{1,2}
{
    # block, then read everything in fifo1 into the buffer array
    i=0
    while IFS='' read -r ln; do
        buf[$((i++))]="$ln"
    done < /tmp/fifo1
    # then write everything in the buffer array to fifo2
    for i in ${!buf[@]}; do
        printf "%s\n" "${buf[$i]}"
    done > /tmp/fifo2
} &

foo > /tmp/fifo1
read a < /tmp/fifo2
echo $a

rm /tmp/fifo{1,2}

This of course assumes two things:

  • fifos are allowed
  • The command group that is doing the buffering is allowed to be put into the background

I tested this to work in these bash versions:

  • 3.00.15(1)-release (x86_64-redhat-linux-gnu)
  • 3.2.48(1)-release (x86_64-apple-darwin12)
  • 4.2.25(1)-release (x86_64-pc-linux-gnu)

Addendum

I'm not sure the mapfile approach in bash 4.x does what you want, as the process substitution <() creates a whole new bash process (though not a bash subshell within that bash process):

$ bar () { echo "$BASH_SUBSHELL $BASHPID"; }
$ bar
0 2636
$ mapfile -t bar_output < <(bar)
$ echo ${bar_output[0]}
0 60780
$ 

So while $BASH_SUBSHELL is 0 here, it is because it is at the top level of the new shell process 60780 in the process substitution.


The easiest way is to drop the function and pass the variable directly, e.g.:

declare -a foo_output
mapfile -t foo_output <<<${BASH_SUBSHELL}
subshell_depth=${foo_output[0]} # Should be zero.

Otherwise given two items in the function:

foo () { echo "$BASH_SUBSHELL $BASHPID"; }

you can use read (modify IFS as needed) like one of the following commands:

cat < <(foo) | read subshell_depth pid # Two variables.
read -r subshell_depth pid < <(foo) # Two separate variables.
read -a -r foo_arr < <(foo) # One array.

or using readarray/mapfile (Bash >4):

mapfile -t foo_output < <(foo)
readarray -t foo_output < <(foo)

then convert the output back into array:

foo_arr=($foo_output)
subshell_depth=${foo_arr[0]} # should be 0