21

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)
  • 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
  • 1
    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
26

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"
| improve this answer | |
  • Thank you so much for explaining! I can't get the 'message=$(</dev/stdin)' solution to work. This is what would work best for me. Using Bash. Any ideas or something similar? ..hmm, this worked for some reason: echo 'hi' | echo $(</dev/stdin), but not your line from above. – alchemy Mar 24 at 19:25
  • @alchemy That's the original user's problematic code. Read the text for why it doesn't work, and what you could possibly do to make it work (the bit about lastpipe). – Kusalananda Mar 24 at 19:28
  • Thanks, that was the second read through and I skipped past the OP. So reading the comments from the OP, they seem to conclude that a command can read from a subshell but a variable cannot. Which is what 'read' is doing. I did get that solution to work, but it is difficult to debug because I am using the entire command in a subshell echoing to a file or email, so I was trying to simplify without success. If only there were a way to redirect back out of a subshell to stdout to console. You obviously know much more about it an it seems there isn't. I'm surprised a pipe cant be directed to a var. – alchemy Mar 25 at 4:27
  • @alchemy Since I can't see your code, it's difficult to come with suggestions or point out issues. It would be better if you asked a new question. In the worst case scenario, it would be marked as a duplicate (which still means you would probably get some sort of resolution). – Kusalananda Mar 25 at 7:14
  • basically, in pseudocode, it is: (tail log) | grep "keyword" | (read var | echo var | mail ) ..I wasn't getting the var in the email message, so was trying to get it to the stdout console to debug. Its working now, but I still wish I had the alert in the console. I may try to tee it somehow, or use the echo $(</dev/stdin), which works, before the read. It would have been easier not having to put read in a subshell. Fyi, the tail is in a subshell so the grep can finish.. not sure that is necessary, but what I read on StackEx. I could put it in a script, but the CLI should do everything a script – alchemy Mar 25 at 16:06
7

Use command substitution:

myvar=`echo foo`

or

myvar=$(echo foo)
| improve this answer | |
  • 2
    `echo foo` is just the deprecated alternative to $(echo foo), isn’t it? – Parckwart Jan 17 '17 at 10:19
  • 1
    @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
  • 2
    The OP actually said "I don't want to do that...." – Asfand Qazi Jan 31 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<<< ''
| improve this answer | |
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.

| improve this answer | |
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
| improve this answer | |
  • 1
    And the result of echo $echo_out after running this...? – roaima Jul 6 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.