116

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)
Parckwart
  • 1,161
  • 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
  • $(</dev/stdin) creates a subshell with empty stdin. – Ipor Sircer Jan 17 '17 at 10:27
  • Well it is, or would be if you were piping to a program that had an stdin but you seem to be attempting to pipe to a variable and that doesn't make sense. Are you just looking for myvar="foo"? if you want to assign the output of a command to a variable, then use var=$(command). There's nothing wrong with that (in fact, it is the one correct way of doing it). – terdon Jan 17 '17 at 10:27
  • @IporSircer Why does echo foo | echo $(</dev/stdin) work then? – Parckwart Jan 17 '17 at 10:29
  • 3
    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 http://stackoverflow.com/a/5760832/3701431 – Sergiy Kolodyazhnyy Jan 17 '17 at 10:35
  • All of your examples (if they worked) use a sub shell. – ctrl-alt-delor Jan 17 '17 at 10:36
  • @Parckwart because echo is a command that can read from stdin. It isn't a variable. You are comparing apples to oranges. – terdon Jan 17 '17 at 10:53
  • @richard With myvar=$(complex_function) the function is in a subshell. With complex_function | myvar=$(</dev/stdin) it’s not. – Parckwart Jan 17 '17 at 10:54
  • 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

8 Answers8

129

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 the 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"

Kusalananda
  • 333,661
  • and what if later I wold like to pass multiline output from variable to another command? E. g. files=$(ls); echo $files | subcommand breaks original output and produces all items in one line while ls | subcommand sends files by one – oxfn Nov 25 '21 at 15:54
  • 1
    @oxfn echo "$files". Without the quoting, the shell would split the value on whitespace etc. (but even when quoting, echo may change the contents of the string, see Why is printf better than echo?). Note that saving the output of ls in a variable is pretty useless as there is no way of safely accessing the individual filenames (valid filenames can contain newlines). See Why *not* parse `ls` (and what to do instead)? and also When is double-quoting necessary? – Kusalananda Nov 25 '21 at 16:38
  • How would one use this with sed? E.g. echo $RANDOM | md5sum | head -c 20 | { read val; sed -i 's/somevalue/$var/g' myFile - doesn't work, it replaces with the string "$var" not the actual value. – geoidesic Jan 24 '22 at 21:45
  • @geoidesic Because the shell does not expand variables inside single-quoted strings. Use double quotes instead. – Kusalananda Jan 24 '22 at 22:20
  • I have a command which I don't control or want to modify (it is part of an alias that comes from a git repo). The only thing I can do is expand the command line with whatever I want. In this case, command substitution is not a solution. Expanding the line with ie. > tempfile would work, but I want a variable instead of a tempfile. – karatedog Feb 02 '22 at 10:21
  • @karatedog Any simple or compound command could be used in a command substitution. I'd be surprised if your command could not be used in a command substitution. In theory, I could write a whole script in a command substitution with no problem. – Kusalananda Feb 02 '22 at 13:03
  • What exactly is the meaning of { code }? – Erwann Feb 23 '22 at 23:56
  • 1
    @Erwann It's a compound command. Or more exactly, it is exactly a single compound command. Since it is one command, input can be piped or redirected into it and its output may be piped or redirected elsewhere. I'm using it in my answer to read a string arriving over a pipe and then to output that string. It would not be possible without using { ... }. You could see the contents (code) within { ... } as a separate script if that helps, at least the way I've used it above. – Kusalananda Feb 24 '22 at 07:34
  • hmm, actually the shopt -s lastpipe doesnt seem to be working in bash on Ubuntu20.04 – alchemy Apr 05 '22 at 02:38
  • @alchemy A shell option does not depend on the operating system but on the type of shell you use. So, for example, if you use a shell other than bash, I'd expect it not to work, as it's a bash-specific option. If you're using bash and it does not work, you would have to give an example of what you're doing, what you expect to achieve, and what happens. You would do this best with a new question on this site rather than with a comment here. – Kusalananda Apr 05 '22 at 05:43
  • yeah, thats correct.. I was just filling a bunch of bugs with KDE and Ubuntu so included it.. it doesnt hurt though.. anyway, the example is literally the one you gave with the shopt.. granted I tried this not within a script, but as commands. Does that matter? Any ideas why it wouldnt work if not? – alchemy Apr 10 '22 at 04:23
  • @alchemy Um, did you read the very end of my answer where I said that it would not work in an environment where job control is active? Job control is active in interactive shells. This is also documented in the bash manual if you look for lastpipe there. – Kusalananda Apr 10 '22 at 06:34
  • Interesting, so all I need to do is set +m to solve the problem? I had a similar question to the OP a couple years ago. Had finally surrendered to using this form var3=$(somecommand (var2=$(somecommand $(var1=somecommand)))), when I really just wanted to used pipes as they seem to be intended to be used.. somewhere along the way it looks like job control and lastpipe disabling must have conflicted with the pure pipeline pattern (Gang of Four). I'm glad there is a way to undo that. The history and logic must be interesting. TBH, I thought interactive meant using the -i flag in scripts. – alchemy Apr 10 '22 at 20:20
  • However, I do see that your solution using brackets to group commands also works with multiple piped commands echo 'hello' | { read message; echo "$message"; } | { read message2; echo $message2" world"; } without any shell options, so may be more universal to cli and scripts environments. Thanks, maybe I can put away all the extra xargs baggage xargs -I % command % as well as xargs not working on all commands. – alchemy Apr 10 '22 at 21:18
14

Use command substitution:

myvar=`echo foo`

or

myvar=$(echo foo)
6

Given a function that modifies a global variable and outputs something on stdout:

global_variable=old_value
myfunction() {
  global_variable=new_value
  echo some output
}

In ksh93 or mksh R45 or newer you can use:

var=${
  myfunction
}

Then:

$ print -r -- "$global_variable, $var"
new_value, some output

${ ...; } is 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. mksh uses a temporary file instead.

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

fish's command substitution also behaves like ksh93's ${ ...; }:

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

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

myfunction > file
var=$(cat file) # can be optimised to $(<file) with some shells

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

{
  myfunction > /dev/fd/3 &&
  var=$(cat<&3)
} 3<<< ''

In zsh, you can also do:

() {
   myfunction > $1
   var=$(<$1)
} =(:)

In Korn-like shells such as ksh, zsh or bash, command substitution, whether the $(cmd...) standard form or the $(<file) or ${ cmd...; } variants strip all trailing newline characters (from file or the output of cmd). See shell: keep trailing newlines ('\n') in command substitution for how to work around that.

In fish, set var (cmd) assigns each line of the output of cmd to separate elements of the $var array. $var will contain the same thing whether cmd outputs foo or foo<newline>. Since version 3.4.0, fish also supports set var "$(cmd)" which behaves like in Korn-like shells (removes all trailing newline characters).

  • interesting about the <<< temp file.. you can also write to "global" shell variables after using the shell options shopt -s lastpipe && set +m – alchemy Apr 10 '22 at 22:27
  • @alchemy, I don't see how lastpipe would help. Even if you meant myfunction | IFS= read -rd '' var, myfunction would still run in a subshell. – Stéphane Chazelas Apr 11 '22 at 06:46
  • those two options do actually allow changing shell variables: see my answer https://unix.stackexchange.com/a/698694/346155 – alchemy Apr 12 '22 at 02:13
  • @alchemy, see if my latest edit makes it clearer what I actually meant. – Stéphane Chazelas Apr 15 '22 at 17:58
  • Sure, makes sense to me. So on Bash, does using { } allow changing a global variable? It didnt in my tests. – alchemy Apr 15 '22 at 18:30
  • @alchemy, { ...; } is for grouping commands. If { ...; } is part of a pipeline, a subshell will still be introduced (caused by the piping, not by {...;}. Redirection (as with myfunction > /dev/fd/3 or { ...; } 3<<< '') don't cause a subshell in bash (it did in the Bourne shell). – Stéphane Chazelas Apr 15 '22 at 18:33
  • Edited: That is interesting. I cant read the syntax very easily, (maybe you could add some explanation as inline comments), but it looks like myfunction output is sent to a 'file descriptor' and then to a var. I dont know what the temp file is doing, maybe clearing the fd, but it looks like the new_value is set just in running myfunction inside { }. Does it need all the other stuff? – alchemy Apr 15 '22 at 19:05
  • 1
    (1) +1, because complex_function > tmpfile; myvar=$(<tmpfile) is probably the best answer to the question for bash (with the caveat that you may need to use myvar=$(cat tmpfile) in some other shells). How on earth did nobody else suggest this in four months? (1b) I’m surprised that you didn’t even link to an answer explaining how to preserve multiple newlines at the end of a command substitution. … (Cont’d) – G-Man Says 'Reinstate Monica' Apr 24 '22 at 06:40
  • (Cont’d) …  (2) The /dev/fd/3 answer fails for Bash 4.1.17 under Cygwin.  I was able to get it to work by changing var=$(cat<&3) to var=$(cat /dev/fd/3).  Perhaps this is a result of the way Cygwin handles dup?  (3) What is print?  A ksh builtin? – G-Man Says 'Reinstate Monica' Apr 24 '22 at 06:40
  • Does it make a difference whether =(:) or =() is used in zsh? – ak2 Jul 10 '23 at 11:37
  • 1
    @ak2, I don't expect there be. Is suppose I hadn't realised =() also worked or possibly it didn't work in older versions. – Stéphane Chazelas Jul 17 '23 at 09:27
4

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.

  • (1) You totally missed the point of the question.  It says “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.”  Your (misnamed!) “complexFunction” is a single command!  Try adding othervar=$(date) to your function, and then do echo "$othervar" after doing your command with <(complexFunction).  Or add a cd to your function, and then do pwd after doing your command.  When you do <(…), you are running a subshell, just like when you do $(…). … (Cont’d) – G-Man Says 'Reinstate Monica' Apr 24 '22 at 06:43
  • (Cont’d) … (2) The braces in your { read -d '' message; } command are not necessary, since read is a single, simple command, and not a compound or complex command.  (3) Since you aren’t using read -r, backslashes in the data will cause problems.  (4) Since you aren’t using IFS=, leading and trailing whitespace can be lost. – G-Man Says 'Reinstate Monica' Apr 24 '22 at 06:43
  • @G-ManSays'ReinstateMonica' Good point. What the author wants - to be able to set environment variables in multiple segments of a pipeline, in bash, is impossible. Yet a selected answer exists - setting shopt -s lastpipe in bash, or using ksh. This < < syntax is actually better than shopt -s lastpipe because the effect doesn't linger, so it is helpful. While you are correct, I can't see why you posted here and not the selected answer. – Craig Hicks Apr 24 '22 at 19:32
  • (5) What do you mean by “the selected answer”?  There is no accepted answer. (6) What do you mean by “This < < syntax is actually better than shopt -s lastpipe because the effect doesn't linger, so it is helpful.”?  Do you mean “<<”?  And the OP wants a persistent variable, so how is a transient result helpful? – G-Man Says 'Reinstate Monica' Apr 24 '22 at 19:45
  • "selected"=>"most highly upvoted". The syntax is < <, not <<. The LHS is executed in the current shell, and the output from the RHS is redirected to input of the LHS. It is effectively an inverted two-stage pipeline. Did you run it in bash to check? It runs. – Craig Hicks Apr 24 '22 at 19:55
  • The syntax "<(something)" is process substitution. https://tldp.org/LDP/abs/html/process-sub.html. There must be a space between the first < and <(something). The value of $message is not transient, it is persistent. – Craig Hicks Apr 24 '22 at 20:12
  • Well, I believe that Kusalananda’s answer is not great, either, but I believe that it does a better job of addressing the question than yours does.  And I know what <(…) does.  Saying < < when you mean < <(…) is like saying “I need a ride to the airp.” — people might be able to figure out what you mean, but you’re being imprecise and unclear — especially since your answer also includes a <<.  And *you said* “it is helpful”, and I’m asking you to explain what you meant by that (although I guess I may have figured it out). – G-Man Says 'Reinstate Monica' Apr 25 '22 at 00:47
  • @G-ManSays'ReinstateMonica' - To quote you [ Do you mean “<<”? ]. You spoke a lot to justify your downvote, but in the end had nothing to say, and didn't even know what you were talking about. – Craig Hicks Apr 25 '22 at 07:22
  • I have exactly the same question as OP, except that it is not in a pipe. I have figured out the solution using a temporary file, and I want to find some more idiomatic way (in bash). I downvote this answer because it does not solve the core problem of exposing the side effects of setting the global variables in the current shell in execution complexFunction. Process substitution does introduce a subshell should be clearly avoided here, so it does not help, and, quite misleading. A waste of time is not interesting at all. – FrankHB Sep 18 '23 at 08:50
2

I would use a temporary file to store the complex function result, and then read the value from the temp file for processing.

# Create a temp file for storage
tmppath=$(mktemp)
# run the command and get the output to the temp file
complex_command > "${tmppath}"
# read the file into variable
message=$(cat "${tmppath}")
# use the variable
echo "$message"

rm -f "${tmppath}"

The usage of mktemp can refer to How create a temporary file in shell script?

Kusalananda
  • 333,661
  • Double-quote your variables when you use them so that their contents isn't parsed and word-split by the shell. For example echo $message should become echo "$message". (The curly braces are mostly unneeded.) Note that if $message begins with a dash (hyphen) all bets are off when you try to use echo anyway – Chris Davies May 07 '21 at 06:53
  • 2
    Shouldn't message=$(echo ${tmppath}) be message=$(cat "$tmppath") to get the contents of the temporary file rather then its name – Chris Davies May 07 '21 at 06:56
  • (1) I’m giving you a +1 for posting the best answer, *complete with* mktemp andrm, even though Stéphane Chazelas posted the bare bones of that answer four years earlier. Please get into the habit of reading all the existing answers before you post a new one. It’s OK to post a new answer improving on a previous post (IMO, you did that), but you should cite any such previous posts. … (Cont’d) – G-Man Says 'Reinstate Monica' Apr 24 '22 at 06:51
  • (Cont’d) … (2) @roaima is right: you should double-quote all your variables (e.g., complex_command > "$tmppath" and rm -f "$tmppath"), and you don’t need any of those curly braces.  (3) You could improve this answer by testing whether mktemp succeeded before you use "$tmppath". – G-Man Says 'Reinstate Monica' Apr 24 '22 at 06:51
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
sampop
  • 1
0

You could just use the read command and Command Grouping with curley braces: echo foo | { read myvar; echo $myvar; } except you want to use "global variables", which I think you mean "shell variables" (declare -p to list, or set | grep myvar or set -o posix; set). https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

Using Command Grouping executes them in the "current shell context", however that is still a subshell as soon as a pipe is used, even if the braces surround the entire line { echo foo | read myvar; echo $myvar; }.

So you have to use shell options. (it seems, because the command line is interactive by default):

shopt -s lastpipe      # sets shell option for lastpipe on
set +m                 # turns job control off, part of interactive shell
echo foo | read myvar; echo $myvar
# foo

With those shell options, chaining works read works echo foo | read myvar; echo $myvar" bar" | read myvar2; echo $myvar2, to produce foo bar.

PS, you can put those shell options in your .bashrc, but I'm not sure if there are downsides to that beside possible incompatibility with scripts that use interactive shell flag -i.

alchemy
  • 597
0

Notes to editors: Please don't change the code. I'll report it.

I hope you remember your username and password because lot of people found that answer useful.. But not me, no Sr. and that's because in a Makefile rules are slightly different.

In my case I'm encoding some JSON data and providing that to curl so I'm going to share just the basic idea because it is already encoded if you use the right type.

# Remember, this is a Makefile!
# Assuming your environment knows the variables in brackets

more_tests: @LABORADO=$$(echo -n "{&quot;user&quot;:&quot;$(ALWAYS)&quot;,&quot;know&quot;:&quot;$(NOTHING)&quot;}" | base64) &&
echo $(SHELL) &&
echo &quot;$$LABORADO&quot;

And this is the output:

> make more_tests 
/bin/sh
"eyJ1c2VyIjoic2hvdWxkIiwia25vdyI6ImhpbXNlbGYifQ=="

The shell variable was just a random environment variable that I picked as an example because I know whatever your environment is it will know that value (I hope).

Remember.. I know $ALWAYS, $NOTHING, but I know my environment :D)

Karmavil
  • 233