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?”