Capture the output of a shell function without a subshell
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).
Related videos on Youtube
dgmike
Updated on September 18, 2022Comments
-
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 simplevar=FOOBAR
.-
Yunus over 7 yearsif you wanna call rbenv in current shell ; then you need at laest a named pip ; cmd > fifo; var="
<fifo
" ! -
Kusalananda over 7 yearsAny reason for not wanting to use
$(...)
or temporary file? -
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 over 7 yearsCan't you just parse the
.ruby-version
file in the current directory? BTW, I can not find anything that saysrbenv local
changes anything. It's supposed to only report the local version according to github.com/rbenv/rbenv#rbenv-local -
dgmike over 7 years@Kusalananda in case of
rvm
(the shell script will prevent therbenv
andrvm
) some variables are set, likeRUBY_VERSION
. But I think I can usecat .ruby-version
. :-) -
dgmike over 7 years@JigglyNaga I really don't wan't to use
tmp
file. It must exist another way. -
George Vasiliou about 7 yearsIf 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 about 7 yearsYou can use
read variable < <(rbenv local)
witch as far as i know works on the same shell using process substitution.
-
-
ton over 6 yearsI really don't understand why this answer was downvoted.
-
Niklas Holm almost 6 years
IFS= read -r -d $'\0' myvar
to read null-delimited data -
Alexander Mills over 5 yearswhat is a? is that a variable?
-
ton over 5 years
read a
, wherea
in this case is a variable where theread command
you put the content readed. The commandread
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 isREPLY
, so you can read the content from$REPLY
, usehelp read
for more details. -
Mark Haferkamp almost 5 yearsYour first method has a subshell, breaking variable assignment. So
read a < <(foo=bar; echo baz); echo "$a:$foo"
outputsbaz:
instead ofbaz:bar
. Your second method doesn't change$a
's value. Maybeshopt -u lastpipe
is broken for me? -
GiovaLomba over 4 yearsRunning
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 over 4 yearsTo 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.