7

In bash, what's the most efficient way to assign a variable using piped input -- using only left to right syntax? Suppose the left side of the pipe is seq 3, so we'd want:

seq 3 | x=<put some code here>

NB: Not an answer, although probably functionally equivalent:

x=`seq 3`

...because seq 3 is not on the left side of a pipe.

For this Q, please ignore the possibility of exceeding the variable's memory, which pipes could certainly do.

agc
  • 7,973
  • 2
  • 29
  • 50
  • 1
    Which version of bash? In most, this is literally impossible, because the right-hand side of a pipe runs in a subprocess, which exits when the pipe completes. See [BashFAQ #24](http://mywiki.wooledge.org/BashFAQ/024). – Charles Duffy Mar 22 '17 at 21:59
  • 1
    ...and even in the versions of bash where this is *possible*, it's *unreliable*, depending on the `lastpipe` feature, which is only available when job control is inactive. – Charles Duffy Mar 22 '17 at 22:00
  • Why can't you do the `x=\`seq 3\`` (or `x=$(seq 3)`)? – Ludonope Mar 22 '17 at 22:00
  • 1
    @Ludonope, ...because the OP is asking a contrived example of a question? :) – Charles Duffy Mar 22 '17 at 22:01
  • 2
    This is, by the way, quite closely related to http://stackoverflow.com/questions/2746553/bash-script-read-values-from-stdin-pipe – Charles Duffy Mar 22 '17 at 22:16
  • @CharlesDuffy yeah but there is no real difference, since `x` will simply get the output of the previous command. – Ludonope Mar 22 '17 at 22:32

6 Answers6

18

To complement Charles Duffy's helpful answer with a focus on making it work in bash:

By default, and on Bash v4.1- invariably, any variable creations / modifications in a (multi-segment) pipeline happen in a subshell, so that the result will not be visible to the calling shell.

In Bash v4.2+, you can set option lastpipe to make the last pipeline segment run in the current shell, so that variable creations/modifications made there are visible to it.

For that to work in an interactive shell, you must additionally turn off job control with set +m.

Here's a complete example (Bash v4.2+):

$ unset x; shopt -s lastpipe; set +m; seq 3 | x=$(cat); echo "$x"
1
2
3

That said,

x=$(seq 3)

(the modern equivalent of your x=`seq 3`) is much simpler - it is POSIX-compliant and therefore works on older Bash versions too, and it requires no fiddling with global options.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
14

This is covered in detail in BashFAQ #24.

You can reliably use a variable collected on the right-hand side of a pipeline only if the code referencing it is also on the right-hand side of that pipeline.

#!/bin/bash
echo "hello" | { read -r var; echo "Read value: $var"; }
echo "After the pipeline exited, var contains: $var"

Typical output is:

Read value: hello
After the pipeline exited, var contains:

The POSIX sh specification neither requires nor precludes the right-hand side of a pipeline being executed in the same shell which later executes subsequent commands. Thus, a shell may execute the read on the second line in the same shell where it executes the echo on the third -- but bash, specifically, will not do so unless the lastpipe shell option is enabled.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
4

piped input

The simplest method to set a variable is to read it:

seq 3 | read -d '' x

Will also work in zsh but not in ksh (no NUL allowed for the -d option in ksh).

One way to use the value of x is to do so in the subshell created by the pipe:

$ seq 3 | { read -d '' x; echo "$x"; }
1
2
3

Note that the exit condition of read is of failure (because no '' character was found). More details could be found in Bash FAQ 24

In ksh and zsh the value could be used after the pipe has ended (using a char that is not in the input (but not NUL) to make ksh also work):

$ seq 3 | read -d ':' x
$ echo "$x"
1
2
3

That's because, originally in ksh (later in zsh) the last command on a pipe would run in the same process as the parent shell, keeping the value of the variable changed after the pipe has ended.

One way to emulate this capacity in bash is to use lastpipe (which will only work if JOB CONTROL is disabled (inside a non-interactive script or with set +m)):

$ set +m; shopt -s lastpipe
$ seq 3 | read -d '' x
$ echo "$x"
1
2
3

Capture command output

Using Process Substitution :

$ read -d '' x < <(seq 3)
$ echo "$x"
1
2
3

Or, using the old here-doc:

$ read -d '' x <<-Hello
> $(seq 3)
> Hello
$ echo "$x"
1
2
3

Or, using printf:

$ printf -v x "$(seq 3)"
$ echo "$x"
1
2
3
  • 1
    @agc Please read [2.2. Reading NUL-delimited streams](http://mywiki.wooledge.org/BashFAQ/005) and several other examples in there. –  Mar 23 '17 at 00:47
  • 1
    `zsh` can directly use it without creating a subshell. – Fırat Küçük Jun 03 '18 at 09:35
  • The first shell that had the last command in pipe running in the parent shell was **ksh**. That retained the value of `x`. That concept was later carried to `zsh` and (setting some options) to bash. Details added to the answer. –  Oct 27 '19 at 23:08
1

One (revised) method, maybe not the best though, and may fail the OP criteria depending how we look at it:

# load $x, echo it quoted, then unquoted.
seq 3 | { x=$(</dev/stdin)
         echo "$x"; echo $x; } 

Output:

1
2
3
1 2 3

This cat variant works in POSIX shells (yash, dash):

seq 3 | { n=$(cat /dev/stdin)
          echo "$n"; echo $n; }
agc
  • 7,973
  • 2
  • 29
  • 50
1

You can sort-of do this, if you're willing to be flexible about what you mean by "piped input" and "left to right syntax". Try this:

< <(seq 3) read -r -d '' var

I do not recommend this. At all. Just use var=$(seq 3).

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • 1
    Personally, btw, I'd put `seq 3 && printf '\0'` on the inside of your process substitution -- that way `read` returns true when `seq` succeeds. With the code as currently written it'll return false and thus trigger `set -e`, ERR traps, etc. due to the lack of a trailing delimiter. – Charles Duffy Oct 25 '21 at 11:59
0

You can use a temporary file:

seq 3 >/var/tmp/agc-bashvar1; x=$(cat /var/tmp/agc-bashvar1 ); echo x is $x

The following cleans up after use:

seq 3 >/var/tmp/agc-bashvar1; x=$(cat /var/tmp/agc-bashvar1 ); rm /var/tmp/agc-bashvar1

Then echo x is $x

Rationale: Based on @charles-duffy 's answer, you cannot survive a variable based on content received directly at the right hand side of a pipe. So why not use an external place to store the content it as a side effect (as opposed to a no-side-effect pipeline).

Make sure the temporary filename is not used by other programs.

Sohail Si
  • 2,750
  • 2
  • 22
  • 36
  • The Q. is very specific that there must be a *pipe* involved. Try to imagine a situation where the program had no file write access, or storage resources were low, too expensive, or significantly more unreliable than RAM. – agc Oct 25 '19 at 11:08
  • 1
    My answer may help another person. It fits the requirement specified above: from left pipe. Otherwise, this is impossible to send out the variables propagated from left to right. My answer is also more portable than the accepted answer, which is basically a hack (which didn't work on MacOS). In real case scenarios (for installation, etc) using a temporary file can be better and more POSIX compliant. It is a legitimate answer and did not deserve a negative vote. – Sohail Si Oct 25 '19 at 14:10
  • There were already portable answers, which *do* use a pipe, *.e.g.*: `seq 3 | { n=$(cat /dev/stdin) ; echo "$n" ; echo $n ; }`. – agc Oct 26 '19 at 14:24