Categories
bash

The `lastpipe` Maneuver

Have you ever wanted to construct a long pipeline with a while read loop or a mapfile at the end of it? It’s so common that it’s practically a shell idiom.

Have you then (re)discovered that all pipeline components are run in separate shell environments?

#!/bin/bash
seq 20 | mapfile -t results
declare -p results  # => bash: declare: results: not found (WTF!)

Then someone told you to use a process substitution to get around it?

#!/bin/bash
mapfile -t results < <(seq 20)
declare -p results  # => declare -a results=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5" [5]="6" [6]="7" [7]="8" [8]="9" [9]="10" [10]="11" [11]="12" [12]="13" [13]="14" [14]="15" [15]="16" [16]="17" [17]="18" [18]="19" [19]="20")

And you go away unsatisfied, because:

mapfile -t results < <(gargantuan | pipeline | that | stretches | into | infinity | and | beyond)

just looks bass-ackward?

That’s what the lastpipe shell option solves. From the bash man page:

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.

So this works just fine:

#!/bin/bash
shopt -s lastpipe
seq 20 | mapfile -t results
declare -p results  # => declare -a results=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5" [5]="6" [6]="7" [7]="8" [8]="9" [9]="10" [10]="11" [11]="12" [12]="13" [13]="14" [14]="15" [15]="16" [16]="17" [17]="18" [18]="19" [19]="20")

There’s a catch: You can’t do this on the command line, because job control (a.k.a. suspend/resume) is active in interactive mode:

$ shopt -s lastpipe
$ seq 20 | mapfile -t results
$ declare -p results  # => bash: declare: results: not found

But in your scripts, you can now write your while read/mapfile pipelines the logical way.

Conversely, if you really want the code in your while read consumer loop to not pollute your main shell environment, you can explicitly turn off lastpipe just in case:

#!/bin/bash -x
# Check current state of lastpipe option...
[[ $BASHOPTS == *lastpipe* ]] && old_lastpipe="-s" || old_lastpipe="-u"
# ...then force it off
shopt -u lastpipe

cmd=(echo)
seq 20 | while read n; do
  cmd+=($n)
  "${cmd[@]}"
done
# => 1
# => 1 2
# ...
# => 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

# Restore lastpipe state, in case you want to add more code below
shopt "${old_lastpipe}" lastpipe

declare -p cmd  # => declare -a cmd=([0]="echo")

2 replies on “The `lastpipe` Maneuver”

If “mapfile -t results < <(long_command)” is bass-ackward, then how about:

< <(long_command) mapfile -t results

Frankly, I’ve never been a fan of starting command lines with a redirection. It may be syntactically valid, but it just looks wrong. “Wait, this process substitution’s output goes where now?”

Leave a Reply

Your email address will not be published. Required fields are marked *