8

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.

6
  • 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. Commented Mar 22, 2017 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. Commented Mar 22, 2017 at 22:00
  • Why can't you do the x=`seq 3` (or x=$(seq 3))? Commented Mar 22, 2017 at 22:00
  • 1
    @Ludonope, ...because the OP is asking a contrived example of a question? :) Commented Mar 22, 2017 at 22:01
  • 2
    This is, by the way, quite closely related to stackoverflow.com/questions/2746553/… Commented Mar 22, 2017 at 22:16

6 Answers 6

21

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Not the most portable answer, but the most efficient for bash.
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.

Comments

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

3 Comments

@agc Please read 2.2. Reading NUL-delimited streams and several other examples in there.
zsh can directly use it without creating a subshell.
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.
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).

1 Comment

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.
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; }

Comments

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.

3 Comments

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.
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.
There were already portable answers, which do use a pipe, .e.g.: seq 3 | { n=$(cat /dev/stdin) ; echo "$n" ; echo $n ; }.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.