2

In bash or zsh, I can use following syntax to read from pipe into variables:

echo AAA BBB | read X Y ; echo $X

which will print AAA

Why does the same not work in /bin/sh?

I am using /bin/sh -> /bin/dash dash in Debian

Martin Vegter
  • 358
  • 75
  • 236
  • 411
  • 2
    for me, it doesn't work in bash either. Also, please clarify if you expect an answer for sh or dash. – pLumo Sep 11 '18 at 07:06

4 Answers4

5

Why does the same not work in '/bin/sh' ?

Assigning variables in a pipe does not work as expected in sh and bash because each command of a pipe runs in a subshell. Actually, the command does work, X and Y get declared, but they are not available outside the pipe.

The following will work:

echo AAA BBB | { read X Y ; echo $X; }

But in your case:

try this,

read X Y <<< "AAA BBB"

or

read X Y < <(echo "AAA BBB")

Some useful links:

pLumo
  • 22,565
  • 2
    for me, it is not working in bash (4.3.48). – pLumo Sep 11 '18 at 06:53
  • Thanks, I made it clear that this answer is for sh. I don't use dash, so someone else might help. – pLumo Sep 11 '18 at 06:58
  • Re. the sh/dash thing — on your system sh appears to be bash, on Martin’s system sh is dash, so neither of your last two commands will work for him. – Stephen Kitt Sep 11 '18 at 07:08
  • I thought on my system sh is sh ?! – pLumo Sep 11 '18 at 07:09
  • 1
    "in Bash < 4.4"? Is there some reason to think that would work differently in Bash 4.4 or later? – ilkkachu Sep 11 '18 at 08:16
  • 1
    @Stephen Kitt said earlier in a now deleted comment that the command works for him in 4.4.x. Also, OP reports that it works for him in bash. Unfortunately, I did not find sources for that... – pLumo Sep 11 '18 at 08:19
  • Testing, I find the behaviour (of the variable being set) exhibited by the Z shell and the Debian and FreeBSD 93 Korn shells; and not by the Debian and FreeBSD PD Korn shells, the MirBSD Korn shell, the Debian and FreeBSD Almquist shells, the Bourne Again shell, the Watanabe shell, and the Heirloom Bourne shell. The Thompson shell has a third behaviour of its own. All invoked both under their real names and as sh, which made no difference in any case. – JdeBP Sep 11 '18 at 08:41
  • @JdeBP. Nice research :-) This leads me to the conclusion: Don't use variable assignment in pipes if you want only a tiny bit of portability ! – pLumo Sep 11 '18 at 08:43
  • 1
    I’m not sure what I was smoking earlier, the initial command doesn’t work for me on Bash 4.4 either now (and there’s nothing in the changes from 4.3 to 4.4 which would explain the difference in behaviour). – Stephen Kitt Sep 11 '18 at 08:47
  • 1
    The user was asking for a dash solution. –  May 11 '20 at 22:35
  • read X Y < <(echo "AAA BBB") will not work with dash that ships with Debian and is symlinked from sh, but using << EOF .... The here doc method seems to be the only POSIX compliant solution that allows to pipe in strings across the board. – Martin Braun Feb 23 '23 at 23:10
3

in bash or zsh, I can use following syntax to read from pipe into variables.

echo AAA BBB | read X Y ; echo $X

No, you can't. Not in Bash with the default settings.

$ ./bash5.0-alpha -c 'echo AAA BBB | read X Y ; echo "Bash=$BASH_VERSION X=\"$X\""'
Bash=5.0.0(1)-alpha X=""
$ bash -c 'echo AAA BBB | read X Y ; echo "Bash=$BASH_VERSION X=\"$X\""'
Bash=4.4.12(1)-release X=""

Bash runs all the commands in a pipeline in separate subshell environments, so the changes to shell variables aren't visible outside the pipeline. Dash is similar here.

Zsh and ksh (AT&T implementations, not pdksh or derivatives) run the last command of the pipeline in the main shell environment, so there that works:

$ zsh -c 'echo AAA BBB | read X Y ; echo "Zsh=$ZSH_VERSION X=\"$X\""'
Zsh=5.3.1 X="AAA"

In Bash, you can shopt -s lastpipe to have it do what ksh and zsh do (only works in non-interactive shells though):

$ bash -O lastpipe -c 'echo AAA BBB | read X Y ; echo "Bash=$BASH_VERSION X=\"$X\""'
Bash=4.4.12(1)-release X="AAA"

But I don't think there's such an option for Dash.

In Bash you could also use process substitution instead of the pipe, but that's not an option in Dash either.


The workarounds would revolve around making the right-hand side of the loop a compound statement or a function, and so using the value read from the pipe in the same environment it was read in.

$ dash -c 'echo AAA BBB | { read X Y ; echo "X=\"$X\""; } '
X="AAA"
$ dash -c 'f() { read X Y ; echo "X=\"$X\""; }; echo AAA BBB | f'
X="AAA"

Or use a here document:

read X Y << EOF
$(echo AAA BBB)
EOF
ilkkachu
  • 138,973
0

No, this does not set X and Y (after the semicolon).

echo AAA BBB | read X Y ; echo $X
$ bash -c 'echo AAA BBB | read X Y ; echo "<$X>"'
<>

The same happens in dash.

To get it to work in dash you need to resort to older solutions (here-doc):

$ read X Y <<_EOT_
> AAA BBB
> _EOT_
$ echo "<$X>"
<AAA>

As a command to try shells (sadly the newlines can not be removed (no one-liner)):

$ bash -c 'read X Y <<_EOT_
AAA BBB
_EOT_
echo "<$X>" '
<AAA>

That works exactly the same in dash, zsh, ksh, and many others:

$ dash -c 'read X Y <<_EOT_
AAA BBB
_EOT_
echo "<$X>" '
<AAA>

Newer alternatives (that do not work in dash) to the here-doc may be:

  1. here-string (works in bash, ksh, zsh):

    $ bash -c 'read X Y <<<"AAA BBB"; echo "<$X>" '
    <AAA>
    
  2. Process substitution (works in bash, ksh, zsh):

    $ bash -c 'read X Y < <(echo AAA BBB) ; echo "<$X>" '
    <AAA>
    

An alternative that prints the value and works in dash, but does not keep the variable set in dash or bash (but does in ksh and zsh), is:

  1. Group command:

    $ dash -c 'echo "AAA BBB" | { read X Y ; echo "<$X>"; }; echo "<$X>" '
    <AAA>
    <>
    

    Note that this last solution may be set to keep the variables set in bash with the lastpipe option (not for interactive use) or in ksh/zsh by default:

    $ bash -c 'shopt -s lastpipe;echo "AAA BBB"|{ read X Y;echo "1<$X>"; };echo "2<$X>"'
    1<AAA>
    2<AAA>
    
    $ ksh -c 'echo "AAA BBB"|{ read X Y;echo "1<$X>"; };echo "2<$X>"'
    1<AAA>
    2<AAA>
    
    $ zsh -c 'echo "AAA BBB"|{ read X Y;echo "1<$X>"; }; echo "2<$X>"'
    1<AAA>
    2<AAA>
    
  • Presumably, the OP is using the lastpipe option in bash. Try bash -O lastpipe -c 'echo AAA BBB | read X Y; echo "<$X>"' – Stéphane Chazelas Sep 11 '18 at 09:50
  • @StéphaneChazelas Yes, the user may use (may be using) lastpipe, but I didn't read any confirmation of that. Details added anyway. –  Sep 11 '18 at 10:01
-2

Be careful with variable assignments from a process in a pipeline. The POSIX standard does not require a specific behavior.

Modern shells like ksh93 and recent versions of the Bourne Shell let the main shell be the parent of both processes in your pipeline and in case the rightmost process is a builtin command, this command is even run in the main shell.

Another variant is to use the above method but to always run the rightmost command in another process.

The old version is how the original Bourne Shell worked: The shell forks and the forked process creates all other proceses from the pipe and finally converts into the rightmost process.

The last version needs a lot less code than the others but is slower. Because of the code size, this was used in 1976.

The first variant is the fastest variant but needs more code than the others, but it it the only variant that runs the variable assignment in the orginal shell process, which is required to have the modified variable value in the main shell.

schily
  • 19,173