257

I have a script like that

genhash --use-ssl -s $IP -p 443 --url $URL | grep MD5 | grep -c $MD5

I want to get stream generated by genhash in a variable. How do I redirect it into a variable $hash to compare inside a conditional?

if [ $hash -ne 0 ]
  then echo KO
  exit 0
else echo -n OK
  exit 0
fi
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
markcial
  • 9,041
  • 4
  • 31
  • 41
  • 4
    This question is the older than the linked answer. Therefore the linked answer is the duplicate, and should be marked as "asked before", not this one. – Breandán Dalton Aug 27 '19 at 11:45
  • Bash and Shell are different things, hence it's no duplicate. Shell doesn't support all of bash's features. – matfax Aug 15 '20 at 12:03

8 Answers8

395

Use the $( ... ) construct:

hash=$(genhash --use-ssl -s $IP -p 443 --url $URL | grep MD5 | grep -c $MD5)
  • 71
    This captures the output of a command, but it does not use a redirect. If you actually need to use a redirect because of a more complex need, See my answer. Google brought you here, right? Why go somewhere else to find the answer you searched for? – Bruno Bronosky Aug 05 '14 at 22:25
  • 13
    actually not an answer on OP question – Dmitry Zagorulkin Oct 17 '14 at 12:24
  • 19
    @BrunoBronosky this is what worked for me: `result=$(( $your_command ) 2>&1)` – Katie Sep 16 '15 at 23:07
  • 4
    @Kayvar when you use `$( ... )` it uses new subshell, so the result of command can be not the same as in the primary shell – Andrey Izman Apr 22 '17 at 03:10
  • 1
    It would be helpful to others if you made it clear whether this only captures STDOUT or if it also captures STDERR. It would also be very helpful if you could show how to print the exit code of the command inside `$(...)` – Olsgaard Oct 01 '20 at 06:54
283

TL;DR

To store "abc" into $foo:

echo "abc" | read foo

But, because pipes create forks, you have to use $foo before the pipe ends, so...

echo "abc" | ( read foo; date +"I received $foo on %D"; )

Sure, all these other answers show ways to not do what the OP asked, but that really screws up the rest of us who searched for the OP's question.

The answer to the question is to use the read command.

Here's how you do it

# I would usually do this on one line, but for readability...
series | of | commands \
| \
(
  read string;
  mystic_command --opt "$string" /path/to/file
) \
| \
handle_mystified_file

Here is what it is doing and why it is important:

  1. Let's pretend that the series | of | commands is a very complicated series of piped commands.

  2. mystic_command can accept the content of a file as stdin in lieu of a file path, but not the --opt arg therefore it must come in as a variable. The command outputs the modified content and would commonly be redirected into a file or piped to another command. (E.g. sed, awk, perl, etc.)

  3. read takes stdin and places it into the variable $string

  4. Putting the read and the mystic_command into a "sub shell" via parenthesis is not necessary but makes it flow like a continuous pipe as if the 2 commands where in a separate script file.

There is always an alternative, and in this case the alternative is ugly and unreadable compared to my example above.

# my example above as a oneliner
series | of | commands | (read string; mystic_command --opt "$string" /path/to/file) | handle_mystified_file

# ugly and unreadable alternative
mystic_command --opt "$(series | of | commands)" /path/to/file | handle_mystified_file

My way is entirely chronological and logical. The alternative starts with the 4th command and shoves commands 1, 2, and 3 into command substitution.

I have a real world example of this in this script but I didn't use it as the example above because it has some other crazy/confusing/distracting bash magic going on also.

Community
  • 1
  • 1
Bruno Bronosky
  • 66,273
  • 12
  • 162
  • 149
  • 3
    I too believe this is to be the right answer. There are situations where $(...) does not work while piping is accepted everywhere. – Pierre Nov 29 '13 at 18:14
  • Don't forget that semicolon at the end! :) – Pat May 17 '14 at 01:14
  • @Pat, semicolon to go where? I don't have a handy mystic command but it looks right? – AnneTheAgile Aug 05 '14 at 21:36
  • At first I thought @Pat was suggesting that I put a `;` after the mystic_command` or maybe that I remove the one after the `read`. Because of the smiley, I figured that @Pat was just teasing me about the inconsistency. The reason it is inconsistent is that it is the minimum needed if you condense it to a one-liner. I did test this with a utility I call arg_barf. It is basically a stand in for any command that I use when prototyping a bash script. It will accept stdin and any arguments, write them to a temp file, and optionally cat a file to stdout/stderr and return a non-zero status. – Bruno Bronosky Aug 05 '14 at 22:22
  • Sorry, I meant the semicolon at the end of `read string;` – Pat Aug 07 '14 at 15:32
  • 3
    @RichardBronosky Thanks for posting this answer. Can one access the variable in the parent process (e.g., farther along in the pipe line, say at the `handle_mystified_file` portion)? This seems to be impossible since `read` is operating in a child process, which cannot affect variables in the parent process. I am trying to split the output from a process using `tee` and "reconnect" the `tee`-divided processes later, by reading the output of one as an argument in the other. – user001 Aug 11 '14 at 00:02
  • Same question as above i.e. accessing variable in parent process. Also, is there any way for the variable to store a multi line string? – atlantis Oct 30 '14 at 02:24
  • 1
    Doesn't work for me: `echo "test" | read ROLE_X; echo $ROLE_X` doesn't print anything. – Phương Nguyễn Jan 30 '15 at 13:34
  • 3
    @PhươngNguyễn, That doesn't work because pipes create process forks. That another reason that I had to add parenthesis. `echo "test" | (read ROLE_X; echo $ROLE_X )` Read more about this forking at http://stackoverflow.com/a/13764018/117471 – Bruno Bronosky Apr 07 '15 at 21:10
  • 3
    Still doesn't work with parenthesis. `ls | (read $x; echo $x)` prints just the first line of the ls output. 'read' can't read more than one line of output. – Phil Goetz Sep 02 '15 at 16:19
  • 1
    @PhilGoetz That's because you have `read $x;`. The `$` makes it a variable access (so you saying "read into this variable access"). It should be `ls | (read x; echo $x)`. – wspurgin Mar 17 '16 at 16:15
  • 5
    If your stdin is going to have multiple lines, you can use `read -d '' string`. – Bruno Bronosky Mar 09 '17 at 22:56
  • @BrunoBronosky, I'm not sure why you can say words like "all these other answers show ways to not do what the OP asked". As I understand `$(...)` were the one that were accepted. Please consider describing your case so it will be clear in which case `read` is the only viable option. Note that you still can have redirects inside of `echo $(cat <<< "works")` and you can use `echo "$(pstree)"` to get multi-line strings. – ony Jul 28 '17 at 16:07
  • @BrunoBronosky "If your stdin is going to have multiple lines, you can use read -d '' string." Please consider putting this into your answer, at first I'd found your solution completely unusable in my case. The thing about the parentheses being required is important too (variable to which read assigns is empty in a following command without parentheses). – macieksk Jan 01 '18 at 19:26
  • 1
    I added a quick TL;DR for the impatient Googler. I hope you don't mind. – Mateen Ulhaq Feb 24 '18 at 11:00
  • @ony I say those words because the title of the question is "How do I redirect output to a variable in shell?" and my answer was the first to actually "**redirect** output to a variable". I LOVE "command substitution" and use it all the time (as does any half decent hack). But, when I need to redirect output instead, I usually have to Google for it **even though I wrote this answer** because I don't do it very often. This is the top hit on Google, so I wanted it to have the answer to the question asked. – Bruno Bronosky Feb 25 '18 at 04:08
  • @BrunoBronosky, it is good to have alternatives. I just trying to say that it would be more useful if you'll describe why you had to use this approach over the accepted one. And I should confess that your words were a bit bold from my point of view. People often say one thing but in the end they probably wanted to say something else and it is part of communication to look through words. This is especially true when people are speaking about something they don't know yet. Apparently substitution was fine since it was accepted. Would be nice to know your case were this answer is more suitable. – ony Feb 26 '18 at 07:06
  • `echo "abc" | (read foo; echo $foo)` works fine in the terminal but for some reason fails when used in a bash script. – BiBi Apr 23 '18 at 13:03
  • @BiBi I'm not sure why you are having that issue. I tried pasting your line into a file tested it many ways. https://gist.github.com/RichardBronosky/65a8307152363416803bc1969d6a67d9 – Bruno Bronosky Apr 24 '18 at 18:48
  • How do I _append_ to `$foo` though? – Martynas Jusevičius Aug 30 '18 at 20:50
  • @MartynasJusevičius That is going to be very tricky because of the way pipes create forks. you can read more about this here https://stackoverflow.com/a/13764018/117471 I could append in many ways, but without knowing your specifics it would most certainly be suboptimal. I'm fairly certain there is no way to accomplish my "chronological and logical" objective when trying to append. For example call this a few times: `read foo < <( echo -n $foo; date +%s, ); echo "value of foo: '$foo'"` – Bruno Bronosky Aug 30 '18 at 21:14
  • I used this to create a bash function I can reuse more simply. `tovar(){read $1; echo $"$1";}` then `echo 'content' > file.txt; cat file.txt | tovar somevariable` then `echo $somevariable => content` – Mario Olivio Flores Dec 03 '18 at 08:57
  • @MarioOlivioFlores, that doesn't work. I can't even tell what it's supposed to do. – Bruno Bronosky Dec 04 '18 at 04:39
  • the spacing makes it really hard to read. Basically, I just created a function so I could pipe to a variable. If my desired variable name were `myvar`, I could do `echo 'value' | tovar myvar` Useful for my usecase. – Mario Olivio Flores Dec 04 '18 at 10:11
  • What advantage does this `read` construct provide over just using `xargs`? – philraj Feb 12 '22 at 04:18
82
read hash < <(genhash --use-ssl -s $IP -p 443 --url $URL | grep MD5 | grep -c $MD5)

This technique uses Bash's "process substitution" not to be confused with "command substitution".

Here are a few good references:

Bruno Bronosky
  • 66,273
  • 12
  • 162
  • 149
Zombo
  • 1
  • 62
  • 391
  • 407
  • 8
    This needs more upvote. The only solution that worked for me these days. – Phương Nguyễn Jan 30 '15 at 13:38
  • 7
    This is useful when the subcommands must not be run in a subshell. Thanks! – ensonic Jul 28 '15 at 11:24
  • 2
    I wish more people understood the use of `<(…)` it's pure awesomeness. OP should have given an explanation. Basically, it runs a subprocess, but instead of piping it, it returns a file descriptor. This allows you to use commands in places where you need files. For example (as root) `vimdiff /etc/sysconfig/iptables <(iptables-save)` which ends up opening a filename similar to `/proc/23565/fd/63`. Yes, `vim` can accept `-` as a filename and read from `stdin` but that's just an example. May things don't use `-` or are much more legible with the command order that `<()` allows. – Bruno Bronosky Jun 13 '17 at 06:05
  • 2
    @IntrepidDude I edited the answer to include what you (and others) need to know. Thanks for asking. This will benefit everyone. – Bruno Bronosky Sep 22 '17 at 14:48
  • It looks like this only captures STDOUT and not also STDERR – Olsgaard Oct 01 '20 at 07:01
  • got `syntax error near unexpected token `<'`, running on macos Catalina – Jerry Green Nov 05 '20 at 17:35
  • This one works perfectly and it's easy to read. – MaXi32 Nov 28 '20 at 17:57
21

I guess compatible way:

hash=`genhash --use-ssl -s $IP -p 443 --url $URL | grep MD5 | grep -c $MD5`

but I prefer

hash="$(genhash --use-ssl -s $IP -p 443 --url $URL | grep MD5 | grep -c $MD5)"
ony
  • 12,457
  • 1
  • 33
  • 41
  • 1
    Thanks for mentioning compatibility. That was the method that worked on my environment, when the others didn't. – jlsecrest May 25 '13 at 11:39
  • 1
    There's no need for quotes around `$(...)`. – nikolay Oct 07 '13 at 06:26
  • 1
    Yes, there IS a need. Eg. if there is a * in the output, depending on how you handle it later, it'll try to expand it as a glob. – Koshinae Jun 25 '14 at 09:09
  • 2
    @Koshinae, it will try to expand for `echo $hash`. But it will keep `*` as-is if you'll do `hash=$(echo '*'0); echo "$hash"` – ony Jun 30 '14 at 18:51
  • If the result is multiple words, it will create an array no? So by surrounding it with quotes it creates a string instead of an array of strings. Not really sure, relatively new to bash – Mathieu Dumoulin Jul 23 '17 at 18:01
  • @MathieuDumoulin, when I need to store words in array I use `ab=($(echo a b))` . Otherwise it will assign as a string. – ony Jul 28 '17 at 15:24
9

If a pipeline is too complicated to wrap in $(...), consider writing a function. Any local variables available at the time of definition will be accessible.

function getHash {
  genhash --use-ssl -s $IP -p 443 --url $URL | grep MD5 | grep -c $MD5
}
hash=$(getHash)

http://www.gnu.org/software/bash/manual/bashref.html#Shell-Functions

Noldorin
  • 144,213
  • 56
  • 264
  • 302
Huuu
  • 492
  • 5
  • 9
  • 1
    You might even want to write a complicated pipeline into a shell script of its own and call it `hash=$(getHash.sh)` -- shell scripts would be available in a new terminal, and even after a reboot. – Able Mac Aug 14 '19 at 16:03
5

You can do:

hash=$(genhash --use-ssl -s $IP -p 443 --url $URL)

or

hash=`genhash --use-ssl -s $IP -p 443 --url $URL`

If you want to result of the entire pipe to be assigned to the variable, you can use the entire pipeline in the above assignments.

codaddict
  • 445,704
  • 82
  • 492
  • 529
3

I got error sometimes when using $(`code`) constructor.

Finally i got some approach to that here: https://stackoverflow.com/a/7902174/2480481

Basically, using Tee to read again the ouput and putting it into a variable. Theres how you see the normal output then read it from the ouput.

is not? I guess your current task genhash will output just that, a single string hash so might work for you.

Im so neewbie and still looking for full output & save into 1 command. Regards.

Community
  • 1
  • 1
m3nda
  • 1,986
  • 3
  • 32
  • 45
2

Create a function calling it as the command you want to invoke. In this case, I need to use the ruok command.

Then, call the function and assign its result into a variable. In this case, I am assigning the result to the variable health.

function ruok {
  echo ruok | nc *ip* 2181
}

health=echo ruok *ip*