<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pipes · XB Field Notes</title><link>https://hk.xbcatm.com/tags/pipes/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/pipes/index.xml" rel="self" type="application/rss+xml"/><item><title>Shell pipes stream between processes</title><link>https://hk.xbcatm.com/posts/shell-pipes-stream-between-processes/</link><pubDate>Tue, 28 Sep 2027 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/shell-pipes-stream-between-processes/</guid><description>A pipeline connects the stdout of one command to the stdin of the next, feeding data a chunk at a time. That lets a long producer overlap its consumer.</description><content:encoded>&lt;p&gt;A shell pipeline connects one command’s standard output to the next command’s standard input. The commands can run concurrently, so the producer need not finish before the consumer starts. This supports incremental processing, but the pipe itself does not guarantee that either program processes data incrementally. Buffering, early termination, and the status of each stage determine whether the complete operation does what you intended.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-shell-pipes-stream-between-processes.detail.en.svg" alt="Vertical producer, finite kernel pipe, and consumer flow keeps stderr separate. Full-pipe writers and empty-pipe readers wait under the stated conditions. All writers closing and data draining leads to EOF; a write after all readers close triggers SIGPIPE or EPIPE. Separate stage rows show 7 and 0 aggregate to 0 by default and 7 with pipefail, while 7 and 4 aggregate to 4. Results must be saved immediately." width="720" height="2880"&gt;&lt;figcaption&gt;A pipe transports bytes between processes. Buffering, the lifetime of every open endpoint, and each stage’s status remain separate concerns.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="a-byte-stream-with-finite-capacity"&gt;A byte stream with finite capacity&lt;/h2&gt;
&lt;p&gt;A pipe transports bytes rather than records. A reader is not promised one read per line or one read per producer write. Programs must implement any framing they need. Standard error normally remains separate, which is why an upstream diagnostic can appear on the terminal without entering the downstream filter. Decide explicitly whether a consumer should receive diagnostics as data.&lt;/p&gt;
&lt;p&gt;The kernel provides finite buffering. A blocking writer waits when the pipe fills, and a reader waits when the pipe is empty while writers remain open. This backpressure limits how far a producer can advance through that particular pipe. Capacity depends on the operating system and configuration; it is not universally sixty-four kibibytes. Applications can still accumulate arbitrarily large internal data structures, so a pipeline alone does not bound total memory.&lt;/p&gt;
&lt;p&gt;User-space buffering adds another delay. A program that flushes every line to a terminal may buffer larger chunks when stdout is a pipe. An apparently idle consumer may therefore be waiting for data still buffered in the producer. Check the producer’s documented streaming and flush controls before changing kernel limits or adding arbitrary sleeps. Some transformations, such as a complete sort, inherently need more input before producing useful output.&lt;/p&gt;
&lt;p&gt;Atomic writing is a separate property from total capacity. Linux guarantees noninterleaving for appropriately sized writes up to PIPE_BUF, with blocking-mode qualifications; that does not create message boundaries for readers. Several short writes may be returned together, and one write may need several reads. Do not design a parser around observed chunk sizes.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A reader sees end-of-file only after every descriptor referring to the write end is closed and all buffered bytes have been read. Closing the writers does not discard data already in the pipe. A child that inherited an unused write descriptor can keep a consumer waiting even after the apparent producer exited. In the other direction, writing after all read ends are closed generates SIGPIPE, or yields an EPIPE error when the signal is ignored. A consumer that deliberately stops early can therefore cause an otherwise healthy producer to terminate unsuccessfully.&lt;/p&gt;
&lt;p&gt;By default, Bash returns the last stage’s status. An upstream failure followed by a successful cat can look successful overall. With pipefail, the aggregate result is the rightmost nonzero stage status, not the first failure in time or the leftmost failing command. The PIPESTATUS array retains individual stage results briefly. Capture it immediately: even an assignment or printf can replace the observation.&lt;/p&gt;
&lt;p&gt;Most Bash pipeline stages run in subshell environments. An assignment in a loop on the right side of a pipe normally does not update the caller’s variable. Bash’s lastpipe option can alter the final stage’s execution when job control is inactive, so state assumptions need an explicit shell configuration. A redirected loop can avoid a pipeline when the caller must retain the loop’s state.&lt;/p&gt;
&lt;p&gt;Finally, a foreground Bash pipeline waits for its stages to terminate. It does not return merely because the consumer read its first line. A producer that catches a broken pipe and continues unrelated work can delay completion. Treat a pipeline as several processes with connected descriptors and separate outcomes, rather than one command that necessarily succeeds or stops as a unit.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run the following Bash block with cat, yes, and head available. It compares default and pipefail statuses using a producer that deliberately exits seven. It then demonstrates two failing stages and an early-closing consumer. Errexit is disabled only while collecting expected nonzero results, then restored before assertions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bash --noprofile --norc &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;BASH&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;(printf &amp;#39;partial\n&amp;#39;; exit 7) | cat &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;plain=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${plain[*]} == &amp;#39;0 7 0&amp;#39; ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;(printf &amp;#39;partial\n&amp;#39;; exit 7) | cat &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;strict=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${strict[*]} == &amp;#39;7 7 0&amp;#39; ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;(exit 7) | (exit 4)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;multiple=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${multiple[*]} == &amp;#39;4 7 4&amp;#39; ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;yes | head -n 1 &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;early=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${early[0]} -ne 0 &amp;amp;&amp;amp; ${early[1]} -ne 0 &amp;amp;&amp;amp; ${early[2]} -eq 0 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;default: %s\npipefail: %s\nrightmost: %s\nearly close: %s\n&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;${plain[*]}&amp;#34; &amp;#34;${strict[*]}&amp;#34; &amp;#34;${multiple[*]}&amp;#34; &amp;#34;${early[*]}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;PASS: aggregate and individual stage outcomes\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each reported array starts with the aggregate status, followed by the stages in source order. Expect &lt;code&gt;0 7 0&lt;/code&gt;, &lt;code&gt;7 7 0&lt;/code&gt;, and &lt;code&gt;4 7 4&lt;/code&gt; for the first three. The final consumer succeeds while yes returns nonzero; the test deliberately does not require a platform-specific signal number. A production pipeline must distinguish this intentional early closure from lost required output. Measure throughput and memory separately; these status assertions do not measure buffering performance.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Pipelines.html"&gt;GNU Bash: Pipelines&lt;/a&gt;, process execution and aggregate status rules, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html"&gt;Linux man-pages: pipe(7)&lt;/a&gt;, byte streams, descriptor lifetime, capacity, and SIGPIPE, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html"&gt;GNU Bash: Special Parameters&lt;/a&gt;, the immediately preceding command’s status, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>