22

ksh has a really interesting construct to do this, detailed in this answer: https://stackoverflow.com/a/11172617/636849

Since Bash 4.0, there is a builtin mapfile builtin command that should solve this problem: http://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html

But strangely, it doesn't seem to work with process substitution:

foo () { echo ${BASH_SUBSHELL}; }
mapfile -t foo_output <(foo) # FAIL: hang forever here
subshell_depth=${foo_output[0]} # should be 0

But how to do this in Bash v3.2 ?

Community
  • 1
  • 1
Lucas Cimon
  • 1,859
  • 2
  • 24
  • 33
  • 1
    Out of curiosity, how would you use `mapfile`? As far as I know, there is no `bash` equivalent, in any version. – chepner Feb 07 '14 at 16:07
  • `mapfile -t foo_output <(foo)` - this is a process substitution - `foo` is run in a whole new process, which I don't think is what you want. See the addendum to my answer. – Digital Trauma Feb 07 '14 at 18:39
  • 1
    I'm not familiar with `mapfile`, but as it seems to read its standard input rather than a file, you might have to write `< <(cmd)` rather than just `<(cmd)`, as `<(cmd)` ultimately gets replaced with something like `/dev/fd/63` (try `echo <(echo)` to check that). At least it does not seem to hang that way. – Alice M. Sep 11 '18 at 13:57

4 Answers4

18

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.

Digital Trauma
  • 15,475
  • 3
  • 51
  • 83
  • 4
    Wooo, the dummy FD / FIFO trick is awesome ! Yes it looks like it does the trick. You could even define an "assign" function: `assign () { local var=$1; shift; "$@" > /tmp/myfifo; read ${var} < /tmp/myfifo; }`. I tested, it works: `foo () { local b; assign b bar; echo $b; }` – Lucas Cimon Feb 08 '14 at 15:35
  • @LucasCimon Note that it will work only for a single-line output and while it's probably better than a subshell if `/tmp` is mounted on a memory, it's definitely worse if it isn't as it creates a disk access. – EvgenKo423 Feb 18 '21 at 14:13
5

This question comes up very often while looking how to just capture output of any "printing" command into variable. So for anyone looking it's possible (since bash v3.1.0) with:

printf -v VARIABLE_NAME "whatever you need here: %s" $ID

If you tweak your scripts for speed then you can use pattern of setting some global variable at the end of functions instead of just "echoing" it - use this with care, it's sometimes criticized as leading to hard to maintain code.

yatsek
  • 855
  • 1
  • 10
  • 19
  • indeed, not an answer to the question, but good to know. Note that this applies to the bash built-in function. GNU coreutils printf (I have 8.3) does not offer the `-v` flag. `type -a printf` to see what `printf`s you have available. `builtin printf -v VARTOSET "put me in the var"` to invoke the built-in explicitly. – Jack Wasey Jan 22 '21 at 10:16
4

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

Community
  • 1
  • 1
Digital Trauma
  • 15,475
  • 3
  • 51
  • 83
  • Really interesting answer, thanks ! I thought about using `/dev/shm `, but named pipes are a really good idea. A few questions though: 1) where does `buf ` comes from ? 2) Why incrementing `i` in the first loop ? Also, your explanation for `mapfile` is perfect ! – Lucas Cimon Feb 07 '14 at 18:48
  • @LucasCimon - `buf` is simply the name of a bash array I am using to buffer the stream between fifo1 and fifo2. – Digital Trauma Feb 07 '14 at 18:52
  • From the point of view of the shell, `<(cmd)` looks like a file. So you need an extra `<` to do the redirection into `mapfile`. That is why `mapfile` is blocking - its actually blocking on stdin. Type `^D` and it'll unblock. – Digital Trauma Feb 07 '14 at 18:56
  • Oh, I didn't know that arrays don't need to be declared. Still, you need one more background process continuously running. What about no process/subshell AT ALL ? – Lucas Cimon Feb 07 '14 at 18:57
  • Indexed arrays don't need to be declared explicitly. Associative arrays do `declare -A array`. With no `fork` at all is definitely going to me more of a challenge. – Digital Trauma Feb 07 '14 at 18:58
  • @LucasCimon `i` is just an index into the buffer. It is incremented so that we can write to the the next line to the next array element in the buffer. I guess I could have just appended lines to a long string instead, but the array approach seemed cleaner. – Digital Trauma Feb 07 '14 at 19:22
3

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
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • Very nice answer. I do not understand why this has not been accepted. Does this solution only work for version 4? Thank you, @kenorb! – SomeStupid Dec 24 '20 at 11:26
  • 1
    process substitution `< <(cmd)` creates a subshell... it's a particular disconnected subshell, but nonetheless.. that's probably why it is not 'accepted'. – Brian Chrisman Oct 22 '21 at 18:40