Capture the output of a shell function without a subshell

6,794

Solution 1

There is a hack, but I think it just make sense if you need it in a loop.

you can open a cat coproc like this: coproc CAT { cat; }

This will start a cat command in background, and set two environment variables: CAT_PID and CAT. The CAT variable is an array with STDOUT and STDIN (in this order) file descriptor (pipes) used by cat.

So, you can execute anything writing the output to &${CAT[1]} that represents the STDIN, and use the builtin command read to set your variable reading from ${CAT[0]} that is the STDOUT of cat.

Here a sample:

coproc CAT { cat; }
echo 123 >&${CAT[1]}
read myvar <&${CAT[0]}

To test:

echo $myvar
123

Don't forget to stop the cat after use it. You can do it by by killing the process.

kill $CAT_PID

This makes a great difference in performance tuning.

Update: bash implements strings null delimited. So when dealing with binary data, read is really tricky. You can read with LC_ALL=C read -r -n1 -d $'\0' one byte at time, then the null will be empty strings on ${REPLY} variable.

Solution 2

With bash you can also do it like this :

read a < <(echo hello)
echo "$a"

Or like this :

shopt -s lastpipe
echo hello | read a
shopt -u lastpipe
echo "$a"

But you still have to launch a sub-process which will run ruby, so I don't really understand what you are trying to avoid...

Solution 3

If on Linux, with bash versions prior to 5.1, you could do:

{
  chmod u+w /dev/fd/3 # only needed in bash 5.0
  rbenv local > /dev/fd/3
  IFS= read -rd '' -u 3 variable
} 3<<< ''

That does use a temp file like every here-document or here-string, though that's hidden to you. bash 5.1 switched to using pipes instead of regular temp files.

If rbenv outputs less data than can fit in a pipe without blocking (typically 64KiB), still on Linux and Linux only, you can use a pipe instead of the temp file with:

{
  rbenv local > /dev/fd/3
  IFS= read -rd '' -u 3 variable
} 3< <(:)

With ksh93 or recent versions of mksh, use the form of command substitution that doesn't start a subshell:

variable=${
  rbenv local
}

Beware that contrary to the IFS= read -rd '', that removes the trailing newline characters in the output (like all command substitutions).

Share:
6,794

Related videos on Youtube

dgmike
Author by

dgmike

Updated on September 18, 2022

Comments

  • dgmike
    dgmike almost 2 years

    I have rbenv (ruby version manager) installed on machine and it works like that:

    $ rbenv local
    2.3.1
    

    Writing to stdout the local version of my ruby. I want to rescue this version and declare it in a variable to reuse in another occasion.

    $ declare -r RUBY_DEFINED_VERSION=$(rbenv local)
    $ echo Using ruby version $RUBY_DEFINED_VERSION
    Using ruby version 2.3.1
    

    It works!

    But I don't want to use a subshell to do the work (using $() or ``). I want to use the same shell and I don't want to create a tmp file to do the work.

    Is there a way to do this?

    Note: declare -r is not mandatory, it can be a simple var=FOOBAR.

    • Yunus
      Yunus over 7 years
      if you wanna call rbenv in current shell ; then you need at laest a named pip ; cmd > fifo; var="<fifo" !
    • Kusalananda
      Kusalananda over 7 years
      Any reason for not wanting to use $(...) or temporary file?
    • dgmike
      dgmike over 7 years
      @Kusalananda The rbenv local changes some variables and I want to use these variables. The shell script will run on various projects and I can't trust in /tmp or permissions. Some machines I just can write on /var/tmp.
    • Kusalananda
      Kusalananda over 7 years
      Can't you just parse the .ruby-version file in the current directory? BTW, I can not find anything that says rbenv local changes anything. It's supposed to only report the local version according to github.com/rbenv/rbenv#rbenv-local
    • dgmike
      dgmike over 7 years
      @Kusalananda in case of rvm (the shell script will prevent the rbenv and rvm) some variables are set, like RUBY_VERSION. But I think I can use cat .ruby-version. :-)
    • dgmike
      dgmike over 7 years
      @JigglyNaga I really don't wan't to use tmp file. It must exist another way.
    • George Vasiliou
      George Vasiliou about 7 years
      If we forget the temp files, why you don't want to use the $( ) method? Is a temp subshell that will only be used to return to your variable the output of the command....
    • George Vasiliou
      George Vasiliou about 7 years
      You can use read variable < <(rbenv local) witch as far as i know works on the same shell using process substitution.
  • ton
    ton over 6 years
    I really don't understand why this answer was downvoted.
  • Niklas Holm
    Niklas Holm almost 6 years
    IFS= read -r -d $'\0' myvar to read null-delimited data
  • Alexander Mills
    Alexander Mills over 5 years
    what is a? is that a variable?
  • ton
    ton over 5 years
    read a, where a in this case is a variable where the read command you put the content readed. The command read will create or replace this variable and you can put any non-reserved word here, If you do not want to choose a name, you can omit it and the default variable name is REPLY, so you can read the content from $REPLY, use help read for more details.
  • Mark Haferkamp
    Mark Haferkamp almost 5 years
    Your first method has a subshell, breaking variable assignment. So read a < <(foo=bar; echo baz); echo "$a:$foo" outputs baz: instead of baz:bar. Your second method doesn't change $a's value. Maybe shopt -u lastpipe is broken for me?
  • GiovaLomba
    GiovaLomba over 4 years
    Running bash -x yourscript.sh with your first code example makes very clear the problem. You are doing command substitution hence the command runs in a subshell. While second examples runs in the current shell so even running changing environment commands will work well. Thank you.
  • GiovaLomba
    GiovaLomba over 4 years
    To avoid you headaches remember that bash builtin man pages says: lastpipe: If set, and job control is not active, the shell runs the last command of a pipeline not executed in the background in the current shell environment.