14

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.

10
  • if you wanna call rbenv in current shell ; then you need at laest a named pip ; cmd > fifo; var="<fifo" !
    – Yunus
    Jan 3, 2017 at 14:17
  • Any reason for not wanting to use $(...) or temporary file?
    – Kusalananda
    Jan 3, 2017 at 14:24
  • 1
    @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.
    – dgmike
    Jan 3, 2017 at 14:28
  • 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
    – Kusalananda
    Jan 3, 2017 at 14:36
  • @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
    Jan 3, 2017 at 14:57

3 Answers 3

13

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.

3
  • 2
    IFS= read -r -d $'\0' myvar to read null-delimited data Aug 29, 2018 at 15:47
  • anyone have an idea on how to get this or something similar working in zsh?
    – Chris
    Aug 9, 2022 at 3:42
  • try something like this: coproc cat; echo test >&p ; read -p var; echo $var ... and look at this
    – ton
    Aug 9, 2022 at 18:15
5

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

6
  • I really don't understand why this answer was downvoted.
    – ton
    Mar 15, 2018 at 14:44
  • what is a? is that a variable? Dec 22, 2018 at 4:55
  • 1
    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.
    – ton
    Jan 18, 2019 at 16:45
  • 2
    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? Aug 7, 2019 at 23:57
  • 1
    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
    Mar 25, 2020 at 11:44
5

If on Linux, with bash versions prior to 5.1 (or zsh), 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 (at least when the contents of the herefile/herestring is small enough to fit in a pipe buffer). With those, you can always create the temp file by hand:

tmpfile=$(mktemp) || exit
{
  rm -f -- "$tmpfile"
  rbenv local >&3
  IFS= read -rd '' -u4 variable
} 3> "$tmpfile" 4< "$tmpfile"

(deleting the tmpfile early like <</<<< do to minimise the risk of it being left lying around if the script is killed or rbenv exits the shell).

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
}

That one is also currently available in development versions of bash and zsh.

Beware that contrary to the IFS= read -rd '' approach, that removes the trailing newline characters in the output¹ (like in regular command substitution).

To achieve that, internally, current (as of 2024) versions of ksh93 use a pre-deleted tempfile (in /dev/shm on my Debian system), mksh as well (in /tmp on my system), zsh-dev same except the file is deleted later and bash-dev an anonymous memfd tempfile where supported and a pre-deleted tempfile in /tmp where not.

In older versions of zsh, you leverage the =(...) form of process substitution to get an automatically deleted temp file (here passed as argument to an anonymous function):

() { rbenv local > $1; IFS= read -rd '' variable <$1; } =()

¹ though in the case of zsh, whether to do that or not or only when ${ cmd } is left unquoted is currently being debated.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .