[linux] Read values into a shell variable from a pipe

I am trying to get bash to process data from stdin that gets piped into, but no luck. What I mean is none of the following work:

echo "hello world" | test=($(< /dev/stdin)); echo test=$test
test=

echo "hello world" | read test; echo test=$test
test=

echo "hello world" | test=`cat`; echo test=$test
test=

where I want the output to be test=hello world. I've tried putting "" quotes around "$test" that doesn't work either.

This question is related to linux bash pipe

The answer is


This is another option

$ read test < <(echo hello world)

$ echo $test
hello world

In my eyes the best way to read from stdin in bash is the following one, which also lets you work on the lines before the input ends:

while read LINE; do
    echo $LINE
done < /dev/stdin

I think you were trying to write a shell script which could take input from stdin. but while you are trying it to do it inline, you got lost trying to create that test= variable. I think it does not make much sense to do it inline, and that's why it does not work the way you expect.

I was trying to reduce

$( ... | head -n $X | tail -n 1 )

to get a specific line from various input. so I could type...

cat program_file.c | line 34

so I need a small shell program able to read from stdin. like you do.

22:14 ~ $ cat ~/bin/line 
#!/bin/sh

if [ $# -ne 1 ]; then echo enter a line number to display; exit; fi
cat | head -n $1 | tail -n 1
22:16 ~ $ 

there you go.


Because I fall for it, I would like to drop a note. I found this thread, because I have to rewrite an old sh script to be POSIX compatible. This basically means to circumvent the pipe/subshell problem introduced by POSIX by rewriting code like this:

some_command | read a b c

into:

read a b c << EOF
$(some_command)
EOF

And code like this:

some_command |
while read a b c; do
    # something
done

into:

while read a b c; do
    # something
done << EOF
$(some_command)
EOF

But the latter does not behave the same on empty input. With the old notation the while loop is not entered on empty input, but in POSIX notation it is! I think it's due to the newline before EOF, which cannot be ommitted. The POSIX code which behaves more like the old notation looks like this:

while read a b c; do
    case $a in ("") break; esac
    # something
done << EOF
$(some_command)
EOF

In most cases this should be good enough. But unfortunately this still behaves not exactly like the old notation if some_command prints an empty line. In the old notation the while body is executed and in POSIX notation we break in front of the body.

An approach to fix this might look like this:

while read a b c; do
    case $a in ("something_guaranteed_not_to_be_printed_by_some_command") break; esac
    # something
done << EOF
$(some_command)
echo "something_guaranteed_not_to_be_printed_by_some_command"
EOF

read won't read from a pipe (or possibly the result is lost because the pipe creates a subshell). You can, however, use a here string in Bash:

$ read a b c <<< $(echo 1 2 3)
$ echo $a $b $c
1 2 3

But see @chepner's answer for information about lastpipe.


bash 4.2 introduces the lastpipe option, which allows your code to work as written, by executing the last command in a pipeline in the current shell, rather than a subshell.

shopt -s lastpipe
echo "hello world" | read test; echo test=$test

I'm no expert in Bash, but I wonder why this hasn't been proposed:

stdin=$(cat)

echo "$stdin"

One-liner proof that it works for me:

$ fortune | eval 'stdin=$(cat); echo "$stdin"'

The following code:

echo "hello world" | ( test=($(< /dev/stdin)); echo test=$test )

will work too, but it will open another new sub-shell after the pipe, where

echo "hello world" | { test=($(< /dev/stdin)); echo test=$test; }

won't.


I had to disable job control to make use of chepnars' method (I was running this command from terminal):

set +m;shopt -s lastpipe
echo "hello world" | read test; echo test=$test
echo "hello world" | test="$(</dev/stdin)"; echo test=$test

Bash Manual 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.

Note: job control is turned off by default in a non-interactive shell and thus you don't need the set +m inside a script.


Piping something into an expression involving an assignment doesn't behave like that.

Instead, try:

test=$(echo "hello world"); echo test=$test

The first attempt was pretty close. This variation should work:

echo "hello world" | { test=$(< /dev/stdin); echo "test=$test"; };

and the output is:

test=hello world

You need braces after the pipe to enclose the assignment to test and the echo.

Without the braces, the assignment to test (after the pipe) is in one shell, and the echo "test=$test" is in a separate shell which doesn't know about that assignment. That's why you were getting "test=" in the output instead of "test=hello world".


A smart script that can both read data from PIPE and command line arguments:

#!/bin/bash
if [[ -p /proc/self/fd/0 ]]
    then
    PIPE=$(cat -)
    echo "PIPE=$PIPE"
fi
echo "ARGS=$@"

Output:

$ bash test arg1 arg2
ARGS=arg1 arg2

$ echo pipe_data1 | bash test arg1 arg2
PIPE=pipe_data1
ARGS=arg1 arg2

Explanation: When a script receives any data via pipe, then the stdin /proc/self/fd/0 will be a symlink to a pipe.

/proc/self/fd/0 -> pipe:[155938]

If not, it will point to the current terminal:

/proc/self/fd/0 -> /dev/pts/5

The bash [[ -p option can check it it is a pipe or not.

cat - reads the from stdin.

If we use cat - when there is no stdin, it will wait forever, that is why we put it inside the if condition.


I wanted something similar - a function that parses a string that can be passed as a parameter or piped.

I came up with a solution as below (works as #!/bin/sh and as #!/bin/bash)

#!/bin/sh

set -eu

my_func() {
    local content=""
    
    # if the first param is an empty string or is not set
    if [ -z ${1+x} ]; then
 
        # read content from a pipe if passed or from a user input if not passed
        while read line; do content="${content}$line"; done < /dev/stdin

    # first param was set (it may be an empty string)
    else
        content="$1"
    fi

    echo "Content: '$content'"; 
}


printf "0. $(my_func "")\n"
printf "1. $(my_func "one")\n"
printf "2. $(echo "two" | my_func)\n"
printf "3. $(my_func)\n"
printf "End\n"

Outputs:

0. Content: ''
1. Content: 'one'
2. Content: 'two'
typed text
3. Content: 'typed text'
End

For the last case (3.) you need to type, hit enter and CTRL+D to end the input.


if you want to read in lots of data and work on each line separately you could use something like this:

cat myFile | while read x ; do echo $x ; done

if you want to split the lines up into multiple words you can use multiple variables in place of x like this:

cat myFile | while read x y ; do echo $y $x ; done

alternatively:

while read x y ; do echo $y $x ; done < myFile

But as soon as you start to want to do anything really clever with this sort of thing you're better going for some scripting language like perl where you could try something like this:

perl -ane 'print "$F[0]\n"' < myFile

There's a fairly steep learning curve with perl (or I guess any of these languages) but you'll find it a lot easier in the long run if you want to do anything but the simplest of scripts. I'd recommend the Perl Cookbook and, of course, The Perl Programming Language by Larry Wall et al.


The syntax for an implicit pipe from a shell command into a bash variable is

var=$(command)

or

var=`command`

In your examples, you are piping data to an assignment statement, which does not expect any input.


How about this:

echo "hello world" | echo test=$(cat)

Examples related to linux

grep's at sign caught as whitespace How to prevent Google Colab from disconnecting? "E: Unable to locate package python-pip" on Ubuntu 18.04 How to upgrade Python version to 3.7? Install Qt on Ubuntu Get first line of a shell command's output Cannot connect to the Docker daemon at unix:/var/run/docker.sock. Is the docker daemon running? Run bash command on jenkins pipeline How to uninstall an older PHP version from centOS7 How to update-alternatives to Python 3 without breaking apt?

Examples related to bash

Comparing a variable with a string python not working when redirecting from bash script Zipping a file in bash fails How do I prevent Conda from activating the base environment by default? Get first line of a shell command's output Fixing a systemd service 203/EXEC failure (no such file or directory) /bin/sh: apt-get: not found VSCode Change Default Terminal Run bash command on jenkins pipeline How to check if the docker engine and a docker container are running? How to switch Python versions in Terminal?

Examples related to pipe

Angular 4: InvalidPipeArgument: '[object Object]' for pipe 'AsyncPipe' Using Pipes within ngModel on INPUT Elements in Angular What are the parameters for the number Pipe - Angular 2 Limit to 2 decimal places with a simple pipe Pass a password to ssh in pure bash How to open every file in a folder Why does cURL return error "(23) Failed writing body"? How do I use a pipe to redirect the output of one command to the input of another? How to use `subprocess` command with pipes Pipe subprocess standard output to a variable