12

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.

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
dgmike
  • 121
  • 1
  • 3
  • if you wanna call rbenv in current shell ; then you need at laest a named pip ; cmd > fifo; var="` – Yunus Jan 03 '17 at 14:17
  • Any _reason_ for not wanting to use `$(...)` or temporary file? – Kusalananda Jan 03 '17 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 03 '17 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 https://github.com/rbenv/rbenv#rbenv-local – Kusalananda Jan 03 '17 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 03 '17 at 14:57
  • Are any of `$TMP`, `$TMPDIR` or `$TEMP` set in your environment? These are the environment variables that Ruby itself checks when [finding the temp folder](http://ruby-doc.org/stdlib-2.4.0/libdoc/tmpdir/rdoc/Dir.html#method-c-tmpdir). – JigglyNaga Jan 03 '17 at 16:04
  • @JigglyNaga I really don't wan't to use `tmp` file. It must exist another way. – dgmike Jan 03 '17 at 18:35
  • 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 Mar 29 '17 at 14:59
  • You can use `read variable < <(rbenv local)` witch as far as i know works on the same shell using process substitution. – George Vasiliou Mar 29 '17 at 15:01
  • Related: https://stackoverflow.com/questions/23564995/how-to-modify-a-global-variable-within-a-function-in-bash – Ciro Santilli OurBigBook.com Nov 08 '22 at 11:43

3 Answers3

12

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.

ton
  • 361
  • 3
  • 8
  • 2
    `IFS= read -r -d $'\0' myvar` to read null-delimited data – Niklas Holm Aug 29 '18 at 15:47
  • anyone have an idea on how to get this or something similar working in zsh? – Chris Aug 09 '22 at 03:42
  • try something like this: `coproc cat; echo test >&p ; read -p var; echo $var` ... and look at [this](https://unix.stackexchange.com/questions/86270/how-do-you-use-the-command-coproc-in-various-shells) – ton Aug 09 '22 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...

Vouze
  • 829
  • 5
  • 8
  • I really don't understand why this answer was downvoted. – ton Mar 15 '18 at 14:44
  • what is a? is that a variable? – Alexander Mills Dec 22 '18 at 04: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 '19 at 16:45
  • 1
    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? – Mark Haferkamp Aug 07 '19 at 23:57
  • 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 '20 at 11:44
  • 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.``` – GiovaLomba Mar 25 '20 at 12:48
5

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

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501