Unconventional pipe captures
This is part of Underappreciated Bash idioms, which see.
Let's talk about Bash pipes.
Conventional capture
We often use pipes right away:
printf thequux | sed "s/^.../& /" | tr tq TQ #⇒ The Quux # │ ⬆│ ⬆ # └──────────────────┘└──────────┘
and, as above, it's usually implicit that the incoming contents come last, although some functions do require the use of - as a placeholder — which others, such as paste, can use repeatedly:
seq -w 42 | paste -d\ - - - - - - -
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
But what if we wanted the input to be somewhere other than at the last position?
Capture in any position
Ok, so now we're piping not thequux, but the sed expression itself. How do we use it?
The simplest recipe is this:
printf "s/^.../& /" | sed "$(cat)" <<< thequux | tr tq TQ #⇒ The Quux # │ ⬆ │ ⬆ # └──────────┘ └──────────┘
See, the input of that sed command comes from the Here string <<< thequux.
But that cat is not complete: cat what? So the pipe's contents go there, and they're processed just like any regular Command Substitution.
Purists who don't like piping to cat can do this — same thing, but faster, but also noisier:
printf "s/^.../& /" | sed "$(</dev/stdin)" <<< thequux | tr tq TQ #⇒ The Quux # │ ⬆ │ ⬆ # └──────────┘ └──────────┘
Capture with echo or printf
Here's another example. Suppose we wanted The Quux to be blue. How?
Either this printf or this echo -e would do it:
BLUE="\e[0;34m" NOCOLOR="\e[0m" # These work printf "$BLUE%s$NOCOLOR\n" "$(printf thequux | sed "s/^.../& /" | tr tq TQ)" echo -e "$BLUE$(printf thequux | sed "s/^.../& /" | tr tq TQ)$NOCOLOR"
But none of the printf or echo below would work, because neither of them accepts "implicit pipe for missing last element", nor do they recognize - as its placeholder:
BLUE="\e[0;34m" NOCOLOR="\e[0m" # None of these work printf thequux | sed "s/^.../& /" | tr tq TQ | printf "$BLUE%s$NOCOLOR\n" printf thequux | sed "s/^.../& /" | tr tq TQ | printf "$BLUE%s$NOCOLOR\n" - printf thequux | sed "s/^.../& /" | tr tq TQ | echo -e "$BLUE" printf thequux | sed "s/^.../& /" | tr tq TQ | echo -e "$BLUE" -
Yet you want to use the pipe! You want to pipe to printf or echo. How?
BLUE="\e[0;34m" NOCOLOR="\e[0m" # These work printf thequux | sed "s/^.../& /" | tr tq TQ | printf "$BLUE%s$NOCOLOR\n" "$(cat)" printf thequux | sed "s/^.../& /" | tr tq TQ | echo -e "${BLUE}$(cat)${NOCOLOR}" # │ ⬆│ ⬆│ ⬆ # └──────────────────┘└──────────┘└─────────────────────┘
The "cat solution" minimizes the amount of stuff that you see inside $(…).
But if you want to avoid a "useless cat" and you don't like the large </dev/stdin, and you prefer the shortest possible line, and you don't mind using the last part of your pipe, then you can replace the above with:
BLUE="\e[0;34m" NOCOLOR="\e[0m" # These work printf thequux | sed "s/^.../& /" | printf "$BLUE%s$NOCOLOR\n" "$(tr tq TQ)" printf thequux | sed "s/^.../& /" | echo -e "${BLUE}$(tr tq TQ)${NOCOLOR}" # │ ⬆│ ⬆ # └──────────────────┘└─────────────────────┘
Here's a progression from maximum to minimum stuff inside "$(…)" (Command Substitution):
BLUE="\e[0;34m" NOCOLOR="\e[0m" # All of these work echo -e "${BLUE}$(printf thequux | tr tq TQ | sed "s/^.../& /")${NOCOLOR}" printf thequux | echo -e "${BLUE}$(tr tq TQ | sed "s/^.../& /")${NOCOLOR}" printf thequux | sed "s/^.../& /" | echo -e "${BLUE}$(tr tq TQ)${NOCOLOR}" printf thequux | sed "s/^.../& /" | tr tq TQ | echo -e "${BLUE}$(cat)${NOCOLOR}"
You'd normally use the first one, right?
You then progressively move the first piped expression to "the outside", until you run out of | symbols inside it. Up to there, the length would have been maintained. Moving the last one to the outside comes at the "cost" of an extra " | cat" and six characters. This is less "economical", and the cat is "useless" — but it's also arguably more generalizable (you can always use the cat), and therefore more readable and universably recognizable, and it's also "cleaner" in that it minimizes stuff inside "$(…)", which tends to look noisy.
Capture in a group command
Now you have more stuff to do with The Quux. In particular, you'll use it more than once.
But there's a snag:
Everything flows and nothing stays.
You cannot step twice into the same Bash pipe.
— Heraclitus, 5th century BC
So outside some exceptional cases where using tee is feasible (and not too convoluted), you'll want to do the cat trick inside braces — a group command:
printf thequux | sed "s/^.../& /" | tr tq TQ | { qx=$(cat) cut -c7- <<<"\ ${qx: -4}ian Ventures specialize in cutting-edge operations to leverage your ${qx: -4} capitalization requirements. We're market leaders in: - ${qx^^} - ${qx^} - ${qx~} - ${qx~~} - ${qx,,} - ${qx,} - ${qx: -4}-related derivatives Sign up to our newsletter and receive the latest trends of ${qx,,}osphere!" ;} echo "→${qx}←"
which should output:
Quuxian Ventures specialize in cutting-edge operations to leverage your Quux capitalization requirements. We're market leaders in: - THE QUUX - The Quux - the Quux - tHE qUUX - the quux - the Quux - Quux-related derivatives Sign up to our newsletter and receive the latest trends of the quuxosphere! →←
Notice the empty arrows at the end? A Bash trap: that's a pipeline, so the group command is in a subshell. Once you leave a subshell, any variables defined there are gone.
The canonical solution is to put the whole block at "top level" and use < <(…) (Process Substitution) as input.
So this would fix it: same output, but ending with →The Quux←:
{ qx=$(cat) cut -c3- <<<"\ ${qx: -4}ian Ventures specialize in cutting-edge operations to leverage your ${qx: -4} capitalization requirements. We're market leaders in: - ${qx^^} - ${qx^} - ${qx~} - ${qx~~} - ${qx,,} - ${qx,} - ${qx: -4}-related derivatives Sign up to our newsletter and receive the latest trends of ${qx,,}osphere!" } < <(printf thequux | sed "s/^.../& /" | tr tq TQ) echo "→${qx}←"
Incidentally, this is also the preferable structure for processing lines in a loop when you want to preserve for later use some variable that you modified there:
x=() while read -r line do x+=("tag$line") done < <(seq -w 10) printf "%s:" ":${x[@]}"
:tag01:tag02:tag03:tag04:tag05:tag06:tag07:tag08:tag09:tag10:
Splitting the variable
Here's qx=$(cat) again:
printf thequux | sed "s/^.../& /" | tr tq TQ | { qx=$(cat) echo "$qx and ${qx^^} are our specialties." ;}
The Quux and THE QUUX are our specialties.
An alternative to it would be this:
printf thequux | sed "s/^.../& /" | tr tq TQ | { read -rd'\0' qx < <(cat) echo "$qx and ${qx^^} are our specialties." ;}
or this:
printf thequux | sed "s/^.../& /" | tr tq TQ | { read -rd'\0' qx < /dev/stdin echo "$qx and ${qx^^} are our specialties." ;}
or, actually, just this:
printf thequux | sed "s/^.../& /" | tr tq TQ | { read -rd'\0' qx echo "$qx and ${qx^^} are our specialties." ;}
and since here the piped contents come in a single line, simply this will do:
printf thequux | sed "s/^.../& /" | tr tq TQ | { read -r qx echo "$qx and ${qx^^} are our specialties." ;}
I mention it because read is handy when you want to split the input into more than one variable:
printf thequux | sed "s/^.../& /" | tr tq TQ | { read -r t q echo "Really, I think I found ${t^^} best $q today." ;}
Really, I think I found THE best Quux today.
which you could likewise rearrange as:
{ read -r t q echo "Really, I think I found ${t^^} best $q today." } < <(printf thequux | sed "s/^.../& /" | tr tq TQ)
if you want to reuse either $t or $q later on outside the braces.
Ditching the variable
In some cases, you don't need to assign a new qx variable, and can use a : and $_ combo.
One such case is when the many uses of the contents happen at once, in the same expression, as above:
printf thequux | sed "s/^.../& /" | tr tq TQ | { : "$(cat)"; echo "$_ and ${_^^} are our specialties." ;}
Another case is when you want to use it only once, but want to modify the piped contents with Parameter Expansion.
To get a Quu from that last pipe, you could do this, of course:
printf thequux | sed "s/^.../& /" | tr tq TQ | grep -oP '...(?=.$)'
But also this:
printf thequux | sed "s/^.../& /" | tr tq TQ | { : "$(cat)"; echo "${_: -4: -1}" ;}
This may seem silly, but comes handy when you have many transformations:
printf thequux | sed "s/^.../& /" | tr tq TQ | { : "$(cat)" : "${_: -4: -1}" echo "${_~~} has ${#_} letters." ;}
qUU has 3 letters.
Also remember: in these last examples, we're "forcing ourselves" to use the given pipe sequence as it comes. If that's not a requirement, and you prefer it shorter, you can absorb the last pipe to get rid of the cat:
printf thequux | sed "s/^.../& /" | { : "$(tr tq TQ)" : "${_: -4: -1}" echo "${_~~} has ${#_} letters." ;}
qUU has 3 letters.
Ditching the braces
In this example:
printf thequux | sed "s/^.../& /" | tr tq TQ | { qx=$(cat); echo "$qx and ${qx^^} are our specialties." ;}
although we use the variable more than one, we don't need the braces.
Why? Because we can capture that pipe and assign qx at the same time.
Why? Because the first use of $qx doesn't modify it, so:
printf thequux | sed "s/^.../& /" | tr tq TQ | echo "${qx=$(cat)} and ${qx^^} are our specialties."
or like this (remember?):
printf thequux | sed "s/^.../& /" | echo "${qx=$(tr tq TQ)} and ${qx^^} are our specialties."
All three of which return:
The Quux and THE QUUX are our specialties.
Capture in a conditional
What if we want to use the input in an if…then…fi?
The idea is similar to group commands — but we can add tests.
Into the if
The if part accepts many commands, and uses the status of the last:
if x=10; y=2; false; z=3; false; true then echo "True!" fi
So we can do this:
printf thequux | sed "s/^.../& /" | tr tq TQ | if qx=$(cat); grep -q the.Quux <<< "${qx,}" then echo "We have $qx in our pipes!" else echo "Where's my quux?" fi
We have The Quux in our pipes!
That previous logic also applies here: if you need $qx later on, you need to put if…fi at the "top level":
if qx=$(cat); grep -q the.Quux <<< "${qx,}" then echo "We got the $qx out of the subshell." else echo "Where's my quux?" fi < <(printf thequux | sed "s/^.../& /" | tr tq TQ) echo "So $qx survived!"
We got the The Quux out of the subshell. So The Quux survived!
And as strange as it might seen, the same logic of replacing the cat with the last piped command (remember?) would naturally work here as well:
if qx=$(tr tq TQ); grep -q the.Quux <<< "${qx,}" then echo "We got the $qx out of the subshell." else echo "Where's my quux?" fi < <(printf thequux | sed "s/^.../& /") echo "So $qx survived!"
We got the The Quux out of the subshell. So The Quux survived!
Elsewhere
You don't need to capture the pipe into the if: you can shuttle it into the then — or, in the next example, the else:
printf thequux | sed "s/^.../& /" | tr tq TQ | if (("$(date +%Y)" == 2042)) then echo "We're in the future!" else qx=$(cat) echo "$qx and ${qx^^} are our specialties." fi
Or, as we've seen, capture the pipe and assign:
printf thequux | sed "s/^.../& /" | tr tq TQ | if (("$(date +%Y)" == 2042)) then echo "We're in the future!" else echo "${qx=$(cat)} and ${qx^^} are our specialties." fi
The Quux and THE QUUX are our specialties.
Which is a good introduction to our last part.
Delayed pipe capture
To recap:
- First, we saw a way to capture the pipe in a position other than last item.
- Then we saw that this also works with functions that can't do "implicit last item capture", such as
printfandecho. - Then we saw how to capture at once the contents of the last pipe, and then use that repeatedly, at our leisure.
- Then we saw how to split that into more than one variable, using
read. - Then we saw how to ditch the variable using
:and$_, which is fine when the contents are used immediately. - Then we saw how to ditch the braces by assigning to the captured pipe:
"${quux=$(cat)}". - Then we saw how to capture in an
if…then…fi— and it'd also work withcase…esacand others.
In all these cases, we captured the pipe into the first command that showed up. Sort of.
(Sort of?)
That if is actually a shell keyword.
As is { — so if it worked with else...
A question remains: what if we don't want to capture that pipe immediately?
Answer: it'll wait for you. Leave it clogged for a while. Take your time.
printf thequux | sed "s/^.../& /" | tr tq TQ | { Sp=sphere echo "Welcome, Ladies and Gentlemen." echo "Glad that you could be here." echo "Make yourselves comfortable." ((foo++)) echo "We'll get to our topics when it's time to." echo "It's there, waiting for us." ((bar--)) echo "There's a lot to talk about — but no hurry:" echo "we can all breathe and take our time." sleep 10 echo "(Could you bring me a glass of water? Thank you.)" >/dev/null echo "Everyone seated? Good. Let's start, then."; echo : "$(grep -o Q.*)" # ←guess who's being captured down here? echo "$_ and the ${_,}o$Sp are tonight's" "topics" echo "and each ${_:: -1} has its charms." ;} # ^new $_
Welcome, Ladies and Gentlemen. Glad that you could be here. Make yourselves comfortable. We'll get to our topics when it's time to. It's there, waiting for us. There's a lot to talk about — but no hurry: we can all breathe and take our time. Everyone seated? Good. Let's start, then. Quux and the quuxosphere are tonight's topics and each topic has its charms.
📆 2025-11-08