33

I am trying to get the output of a pipe into a variable. I tried the following things:

echo foo | myvar=$(</dev/stdin)
echo foo | myvar=$(cat)
echo foo | myvar=$(tee)

But $myvar is empty.

I don’t want to do:

myvar=$(echo foo)

Because I don’t want to spawn a subshell.

Any ideas?

Edit: I don’t want to spawn a subshell because the command before the pipe needs to edit global variables, which it can’t do in a subshell. Can it? The echo thing is just for simplification. It’s more like:

complex_function | myvar=$(</dev/stdin)

And I don’t get, why that doesn’t work. This works for example:

complex_function | echo $(</dev/stdin)
11
  • 1
    I don't understand what you're trying to do since none of your examples are correct syntax. What pipe? What is myvar supposed to contain? Could you give an example with a real command and explain what output you want to save? And what do you have against subshells anyway? – terdon Jan 17 '17 at 10:22
  • I don’t even understand why $myvar does not contain foo in my examples. After all, foo should be in stdin. I simplified the example on purpose. The echo foo thing is actually a more complicated command changing global variables, which won’t work if it’s in a subshell. – Parckwart Jan 17 '17 at 10:26
  • 2
    Sorry, but you're pretty much stuck with using subshells, even if you don't want to use them. Each command in pipes is executed in subshells too, see stackoverflow.com/a/5760832/3701431 – Sergiy Kolodyazhnyy Jan 17 '17 at 10:35
  • 3
    Yes, you can edit variables in a subshell and no, you can't assign the output if a command to a variable without a subshell. This is what's known as an XY problem. Please edit your question and explain what you are actually trying to do. Give an example of code that reproduces your problem and we should be able to help you out. – terdon Jan 17 '17 at 10:55
  • 1
    @Parckwart no, all commands in a pipeline are executed in subshells. See the "Pipelines" section in man bash. Just give us a complete example and we can help you out. – terdon Jan 17 '17 at 10:57
38

The correct solution is to use command substitution like this:

variable=$(complex_command)

as in

message=$(echo 'hello')

(or for that matter, message=hello in this case).

Your pipeline:

echo 'hello' | message=$(</dev/stdin)

or

echo 'hello' | read message

actually works. The only problem is that the shell that you're using will run the second part of the pipeline in a subshell. This subshell is destroyed when the pipeline exits, so the value of $message is not retained in shell.

Here you can see that it works:

$ echo 'hello' | { read message; echo "$message"; }
hello

... but since the subshell's environment is separate (and gone):

$ echo "$message"

(no output)

One solution for you would be to switch to ksh93 which is smarter about this:

$ echo 'hello' | read message
$ echo "$message"
hello

Another solution for bash would be to set the lastpipe shell option. This would make the last part of the pipeline run in the current environment. This however does not work in interactive shells as lastpipe requires that job control is not active.

#!/bin/bash

shopt -s lastpipe
echo 'hello' | read message
echo "$message"
0
8

Use command substitution:

myvar=`echo foo`

or

myvar=$(echo foo)
4
  • 2
    `echo foo` is just the deprecated alternative to $(echo foo), isn’t it? – Parckwart Jan 17 '17 at 10:19
  • 2
    @Parckwart yes. Well, not actually "deprecated", it's still supported. it's just that $() is better almost always. – terdon Jan 17 '17 at 10:20
  • 2
    command substitution still spawns a subshell, so it won't help the OP here. – Stéphane Chazelas Oct 23 '18 at 10:37
  • 3
    The OP actually said "I don't want to do that...." – Asfand Qazi Jan 31 '20 at 10:22
4

In ksh93, you can use:

var=${
  my command that updates global variable
}

That's a form of command substitution that doesn't spawn a subshell. For commands that are builtin commands, instead of having them writing their output to a pipe (for which you'd need different processes to read and write on the pipe to avoid dead-locks), ksh93 just makes them not output anything but gather what they would have been outputting in to make up the expansion.

$ ksh -c 'a=${ b=123; echo foo;}; echo "$a $b"'
foo 123

fish's command substitution also behaves like that:

$ fish -c 'set a (set b 123; echo foo); echo $a $b'
foo 123

In most other shells, you'd use a temporary file:

my command that updates global variable > file
var=$(cat file) # can be optimised to $(<file) with some shells

On Linux, and with bash or zsh (that use temp files for <<<), you can do:

{ my command that updates global variable > /dev/fd/3 &&
  var=$(cat<&3); } 3<<< ''
1

Here is a simple way to do it in bash -

complexFunction(){
cat <<_endOfHereFile_
The lazy dog
jumped over 
the moon
_endOfHereFile_
}

{ read -d '' message; }< <(complexFunction)
echo "${message}"

The result of echo "${message}" is

The lazy dog
jumped over 
the moon

The complex function could be any function emitting to stdout. It happens to use a here file in this example, but that is not important.

The call read -d '' reads stdin up to the delimiter, which since it is the empty character `` means reading until the end of input.

The syntax {lhs;}< <(rhs) redirects the stdout of rhs to the stdin of lhs, where lhs enjoys the shared variable namespace so that echo ${message} works as desired.

0

I am not really an expert, but have you tried the following?

echo "hello world" \
| { echo_out=$(< /dev/stdin); echo "echo output is: $echo_out"; } \
| cut -d":" -f 2
1
  • 2
    And the result of echo $echo_out after running this...? – roaima Jul 6 '20 at 22:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.