<?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>Shell · XB Field Notes</title><link>https://www.xbcatm.com/tags/shell/</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://www.xbcatm.com/tags/shell/index.xml" rel="self" type="application/rss+xml"/><item><title>Bash history is your searchable command log</title><link>https://www.xbcatm.com/posts/linux-bash-history-session/</link><pubDate>Mon, 28 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/linux-bash-history-session/</guid><description>Bash history supports command recall, but session memory, disk persistence, filters, and explicit imports determine what remains searchable.</description><content:encoded>&lt;p&gt;A useful command history saves retyping, but it is not a complete record of everything executed. Bash normally enables history in interactive shells, and settings can exclude entries or limit retention. The searchable list in a running shell and the history file on disk are separate states. That distinction explains why a command may be visible in one terminal, absent in another, and lost when an unwritten session ends unexpectedly.&lt;/p&gt;
&lt;h2 id="session-memory-differs-from-the-file"&gt;Session memory differs from the file&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HISTSIZE&lt;/code&gt; limits the in-memory history entries; &lt;code&gt;HISTFILESIZE&lt;/code&gt; limits the saved file&amp;rsquo;s lines. Multiline commands and timestamp records mean that line counts and command counts are not interchangeable. &lt;code&gt;HISTFILE&lt;/code&gt; selects the file, normally the user&amp;rsquo;s Bash history file. An unset or empty value prevents ordinary saving, and an unwritable location can leave apparently healthy in-memory recall with no persistent record.&lt;/p&gt;
&lt;p&gt;Bash records command text before parameter expansion but after history expansion, subject to &lt;code&gt;HISTCONTROL&lt;/code&gt; and &lt;code&gt;HISTIGNORE&lt;/code&gt;. A saved line containing a variable therefore does not necessarily reveal the value used during the original execution. Current directories, aliases, functions, environment values, and external files can all change before a command is recalled. History preserves useful input, not a reproducible execution environment or a trustworthy audit trail.&lt;/p&gt;
&lt;h2 id="appending-does-not-refresh-other-terminals"&gt;Appending does not refresh other terminals&lt;/h2&gt;
&lt;p&gt;On normal exit, Bash saves history; &lt;code&gt;histappend&lt;/code&gt; selects appending rather than overwriting the file. Without earlier writes, abnormal termination can lose all unsaved entries, not merely the final command. &lt;code&gt;history -a&lt;/code&gt; appends newly entered lines that this session has not already appended. Saving before exit reduces that loss window, but does not promise durable storage through every system failure.&lt;/p&gt;
&lt;p&gt;Another running shell has its own list. It needs an import such as &lt;code&gt;history -n&lt;/code&gt; to read newly appended lines; enabling &lt;code&gt;histappend&lt;/code&gt; alone does not make another terminal&amp;rsquo;s searches update live. &lt;code&gt;history -r&lt;/code&gt; reads the entire file into the existing list and can introduce duplicates. &lt;code&gt;history -w&lt;/code&gt; replaces the file with the current list and can discard other sessions&amp;rsquo; records. These operations should not be treated as interchangeable synchronization commands.&lt;/p&gt;
&lt;p&gt;If append and import operations are integrated into &lt;code&gt;PROMPT_COMMAND&lt;/code&gt;, preserve existing prompt hooks and any use of the preceding command&amp;rsquo;s status. Replacing a configured hook wholesale can break other shell behavior. Concurrent sessions also do not create a reliable global execution order merely by sharing a file. Keep separate operational logs when ordering, exit status, or accountability matters.&lt;/p&gt;
&lt;h2 id="search-inspect-then-execute"&gt;Search, inspect, then execute&lt;/h2&gt;
&lt;p&gt;In the usual Emacs-style Readline bindings, Ctrl-R searches backward incrementally, and pressing it again finds older matches. Use an editing key to leave the search and inspect the selected line before running it; Ctrl-G cancels the search. Enter accepts the command. Ctrl-J is also a newline and must not be described as a harmless preview key. Bindings can differ in another editing mode or a customized terminal.&lt;/p&gt;
&lt;p&gt;Inspect the hostname, directory, options, and intended target whenever reusing an old command. A command that was correct last month may address a different environment today. Prefer a distinctive search fragment and review the whole line, rather than trusting that the first matching command is the right operation.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Missing commands can result from filtering, retention limits, a different history file, failed saving, or an unrefreshed session. Check those causes separately. &lt;code&gt;ignoredups&lt;/code&gt; excludes consecutive duplicates, not every duplicate in the entire list. &lt;code&gt;ignoreboth&lt;/code&gt; combines that behavior with &lt;code&gt;ignorespace&lt;/code&gt;; it does not produce a globally unique command archive.&lt;/p&gt;
&lt;p&gt;A leading space excludes an entry only when the relevant setting is active. It does not remove terminal recordings, process arguments, or logs written elsewhere, so do not put credentials directly into command lines and rely on history filtering for secrecy. Diagnostic examples should use synthetic entries and a temporary history file, rather than printing someone&amp;rsquo;s real recent commands.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this with Python 3, Bash, and standard Unix utilities. The environment points Bash to a temporary file before it starts. Only synthetic entries are inspected. The brace group avoids recording the demonstration&amp;rsquo;s own control commands after history is enabled.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;set -eu
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;set -o history
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -c
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -s &amp;#39;demo original&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -w &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;first-file:\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="s1"&gt;cat &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -c
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -s &amp;#39;demo replacement&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -w &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;replacement-file:\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="s1"&gt;cat &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -r &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;memory-after-reread:\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="s1"&gt;history | sed &amp;#39;s/^[[:space:]]*[0-9][0-9]*[[:space:]]*//&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;unset HISTFILE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;}&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash-history-demo-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;history_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;history&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;HISTSIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;100&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HISTFILESIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;100&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTCONTROL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTIGNORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HISTTIMEFORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--noprofile&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--norc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first file contains &lt;code&gt;demo original&lt;/code&gt;; after replacement it contains only &lt;code&gt;demo replacement&lt;/code&gt;. Reading that file into a list already containing the replacement produces two identical entries in memory. This verifies overwrite and import semantics without executing the stored strings. It does not claim to test keyboard bindings or crash durability, which require their own observations.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Bash-History-Facilities"&gt;Bash history facilities&lt;/a&gt;, session lists and saving to the history file, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Bash-History-Builtins"&gt;Bash history builtins&lt;/a&gt;, append and replacement versus importing entries, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Commands-For-History"&gt;Readline history commands&lt;/a&gt;, search and command-acceptance bindings, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell word splitting turns unquoted variables into several arguments</title><link>https://www.xbcatm.com/posts/shell-word-splitting-inserts-separators/</link><pubDate>Fri, 31 Dec 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-word-splitting-inserts-separators/</guid><description>When the shell expands an unquoted variable on a command line, it re-splits the result on whitespace, so one variable can silently become many arguments.</description><content:encoded>&lt;p&gt;A shell variable holds a string, but an unquoted expansion can turn that string into several command arguments. In Bash, word splitting examines expansion results using IFS as the delimiter set. This is why a pathname that works when typed with quotes can fail after being stored in a variable. Debug the arguments the receiving command actually gets, including empty ones, before blaming the filesystem.&lt;/p&gt;
&lt;h2 id="splitting-depends-on-the-context"&gt;Splitting depends on the context&lt;/h2&gt;
&lt;p&gt;With the default IFS, runs of spaces, tabs, and newlines separate fields. Leading and trailing IFS whitespace is discarded. An unquoted value containing two words therefore becomes two arguments; an empty unquoted value normally contributes none. In contrast, &lt;code&gt;&amp;quot;$value&amp;quot;&lt;/code&gt; supplies one argument even when the string is empty. Empty and absent arguments can have completely different meanings to a program.&lt;/p&gt;
&lt;p&gt;Splitting applies to results of unquoted parameter expansion, command substitution, and arithmetic expansion in suitable contexts. It is not a second parse of the entire command line. An ordinary assignment such as &lt;code&gt;copy=$value&lt;/code&gt; stores the string without splitting or filename expansion. Later passing &lt;code&gt;$copy&lt;/code&gt; unquoted to a command reintroduces both hazards. Bash conditional expressions using double brackets also have different expansion rules from ordinary command arguments.&lt;/p&gt;
&lt;p&gt;IFS is a set of delimiter characters, not a multi-character separator or a regular expression. Non-whitespace delimiters behave differently from IFS whitespace: adjacent colons, for example, can produce an empty field. Setting IFS to an empty string disables splitting, but it does not turn an empty unquoted expansion into an explicit empty argument. Use a parser suited to the format when quoting, escaping, or structured records matter.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Disabling splitting does not disable the filename expansion that follows it. A variable containing a star can still become matching filenames when IFS is empty. The result can change merely because a file appeared in the working directory. Quoting the variable prevents both transformations; changing IFS alone only addresses one stage. Shell options controlling globbing are independent as well.&lt;/p&gt;
&lt;p&gt;A scalar string is also a poor representation of an argument list. Adding quote characters inside the string does not make Bash treat those characters as shell quotation on expansion. The quotation that controls parsing must be in the command&amp;rsquo;s source. Avoid eval as a repair: it introduces another shell parse and can execute text that was previously data. For Bash commands assembled incrementally, use an array and expand it as &lt;code&gt;&amp;quot;${args[@]}&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Some data is deliberately delimited, so splitting is not always a mistake. Make the format and delimiter explicit, keep changes to IFS local to the operation, and decide what empty fields mean. Do not use whitespace splitting to process arbitrary filenames or CSV records. These examples specifically target Bash; other shells, including zsh in its normal configuration, do not necessarily split scalar expansions the same way.&lt;/p&gt;
&lt;p&gt;For example, a semicolon obtained from a variable does not by itself become a command separator during ordinary expansion. It remains argument data, even though splitting can still damage the surrounding value. This distinction separates incorrect argument construction from deliberately reparsing a string as code.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;This self-contained Bash experiment checks argument counts rather than using echo, which hides the distinction between one spaced argument and several arguments. It creates its own two matching files, tests an empty value, and checks the difference between whitespace and colon delimiters. The temporary directory is removed at exit.&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 +f
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;unset GLOBIGNORE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -u nullglob failglob dotglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;work=$(mktemp -d)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;trap &amp;#39;rm -rf -- &amp;#34;$work&amp;#34;&amp;#39; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cd &amp;#34;$work&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;IFS=$&amp;#39; \t\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;value=&amp;#39;report final.md&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 -- $value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 2 ]]
&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 -- &amp;#34;$value&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;report final.md&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;value=&amp;#39;&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 -- $value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -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;set -- &amp;#34;$value&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;&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;touch a.txt &amp;#39;two words.txt&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;IFS=&amp;#39;&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;pattern=&amp;#39;*.txt&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 -- $pattern
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 2 ]]
&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 -- &amp;#34;$pattern&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;*.txt&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;IFS=:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;record=&amp;#39;red::blue&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 -- $record
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 3 &amp;amp;&amp;amp; $2 == &amp;#39;&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;printf &amp;#39;colon field &amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$@&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;args=(&amp;#39;report final.md&amp;#39; &amp;#39;&amp;#39; &amp;#39;*.txt&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 -- &amp;#34;${args[@]}&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;[[ $# -eq 3 &amp;amp;&amp;amp; $2 == &amp;#39;&amp;#39; &amp;amp;&amp;amp; $3 == &amp;#39;*.txt&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;printf &amp;#39;PASS: boundaries, empty values, globbing, delimiters\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;Expect three colon-field lines, with an empty middle field, followed by PASS. The assertions before those lines distinguish zero arguments from one empty argument and prove that an empty IFS still permits globbing. In a failing script, put a small argument-counting function at the call boundary and print each argument separately with Bash printf&amp;rsquo;s &lt;code&gt;%q&lt;/code&gt; format. That diagnostic shows boundaries and escaping; it does not turn a human-readable log into a safe command to execute later.&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/Word-Splitting.html"&gt;GNU Bash: Word Splitting&lt;/a&gt;, IFS and empty-field rules, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html"&gt;GNU Bash: Shell Expansions&lt;/a&gt;, splitting before filename expansion, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Arrays.html"&gt;GNU Bash: Arrays&lt;/a&gt;, preserving a list of arguments, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell positional arguments are a numbered list</title><link>https://www.xbcatm.com/posts/shell-positional-arguments-slot-the-cli/</link><pubDate>Mon, 29 Nov 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-positional-arguments-slot-the-cli/</guid><description>Inside a script, the arguments you pass are exposed as $1, $2, and so on, plus $@ and $# for the full list, so order and count are explicit.</description><content:encoded>&lt;p&gt;A shell script receives an ordered argument list, not a single command-line string that it must split again. Positional parameters expose that list as &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, and subsequent entries; &lt;code&gt;$#&lt;/code&gt; reports its size. A small fixed interface can use these positions directly. Optional settings and flags benefit from explicit parsing, but either design still needs to define the permitted count, empty values, and failure behavior.&lt;/p&gt;
&lt;h2 id="preserve-the-list-before-interpreting-it"&gt;Preserve the list before interpreting it&lt;/h2&gt;
&lt;p&gt;The script name in &lt;code&gt;$0&lt;/code&gt; is separate from the positional arguments and does not contribute to &lt;code&gt;$#&lt;/code&gt;. With &lt;code&gt;bash -c&lt;/code&gt;, the first argument after the command string becomes &lt;code&gt;$0&lt;/code&gt;, which can surprise wrappers expecting it to become &lt;code&gt;$1&lt;/code&gt;. Positions above nine require braces: &lt;code&gt;${10}&lt;/code&gt; means the tenth argument, whereas &lt;code&gt;$10&lt;/code&gt; means the first argument followed by a literal zero.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; preserves one word per supplied argument, including empty arguments and values containing spaces. An unquoted expansion can split values and expand wildcard characters, while &lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; joins the list into one string. Use the quoted list when forwarding or iterating. &lt;code&gt;set --&lt;/code&gt; replaces the current positional list, and &lt;code&gt;shift&lt;/code&gt; removes leading entries; neither operation changes the caller&amp;rsquo;s original list outside the relevant script or function scope.&lt;/p&gt;
&lt;p&gt;An empty argument still occupies a position. Checking &lt;code&gt;$#&lt;/code&gt; distinguishes an absent argument from one explicitly passed as &lt;code&gt;''&lt;/code&gt;. An expression such as &lt;code&gt;${1:?message}&lt;/code&gt; rejects both missing and empty values, which is appropriate only when the interface requires a nonempty value. Validate the count before reading required positions, then apply any separate rules about allowed contents.&lt;/p&gt;
&lt;h2 id="let-getopts-report-syntax-then-validate-meaning"&gt;Let getopts report syntax, then validate meaning&lt;/h2&gt;
&lt;p&gt;The portable &lt;code&gt;getopts&lt;/code&gt; interface handles short options. An option character followed by a colon in the option specification takes a value; it does not automatically provide support for long names such as &lt;code&gt;--user&lt;/code&gt;. A leading colon selects quiet error reporting: unknown options produce &lt;code&gt;?&lt;/code&gt;, and missing option values produce &lt;code&gt;:&lt;/code&gt;, with the offending option available in &lt;code&gt;OPTARG&lt;/code&gt;. The script must handle those cases explicitly.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OPTIND&lt;/code&gt; tracks parsing progress. Reset it to one before parsing a new argument list in the same shell, and after parsing use &lt;code&gt;shift &amp;quot;$((OPTIND - 1))&amp;quot;&lt;/code&gt; to expose the remaining operands. &lt;code&gt;--&lt;/code&gt; ends option processing so a leading-hyphen filename can be an operand. A non-option also ends the ordinary option scan; do not assume options placed after operands will be rearranged automatically.&lt;/p&gt;
&lt;p&gt;Parsing an option value does not establish that it is meaningful. An explicitly supplied empty string can satisfy the parser&amp;rsquo;s requirement for a value while violating the application&amp;rsquo;s requirement for a name. Likewise, defaults should apply only to omitted optional settings, not hide malformed input. Keep diagnostics on standard error and return a documented nonzero status before performing the requested work.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A common parser accepts the happy path but silently drops surplus operands. Another forgets to reset &lt;code&gt;OPTIND&lt;/code&gt;, so a function&amp;rsquo;s first invocation works while later invocations skip options. Unquoted forwarding can produce the same outward symptom by changing the argument count before the parser sees it. Test the received list and the parsing state separately rather than adding guessed shifts until one invocation works.&lt;/p&gt;
&lt;p&gt;The example below deliberately defines a narrow contract: optional &lt;code&gt;-n NAME&lt;/code&gt;, optional &lt;code&gt;-v&lt;/code&gt;, and exactly one input operand. Its name must be nonempty. The function uses Bash&amp;rsquo;s &lt;code&gt;local&lt;/code&gt; declaration, so the complete example is Bash-specific even though the option parser itself is portable. A script targeting another shell should use that shell&amp;rsquo;s supported scoping rules instead of assuming &lt;code&gt;local&lt;/code&gt; is standardized.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run the complete block in Bash. The first part demonstrates the tenth-position boundary and preservation of empty arguments. The parser then exercises valid input and four distinct invalid forms. All operations are local prints; no files or external commands are invoked by the demonstration function.&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;&lt;span class="nv"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&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 -- one 2 3 4 5 6 7 8 9 ten
&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;without-braces=&amp;lt;%s&amp;gt; tenth=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$10&amp;#34; &amp;#34;${10}&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 -- &amp;#39;&amp;#39; &amp;#39;two words&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;printf &amp;#39;count=%s\n&amp;#39; &amp;#34;$#&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;for demo_arg in &amp;#34;$@&amp;#34;; do printf &amp;#39;argument=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$demo_arg&amp;#34;; done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_cli() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local OPTIND=1 demo_option demo_name=guest demo_verbose=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; while getopts &amp;#39;:n:v&amp;#39; demo_option; do
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; case &amp;#34;$demo_option&amp;#34; in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; n) demo_name=$OPTARG ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; v) demo_verbose=yes ;;
&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;missing value for -%s\n&amp;#39; &amp;#34;$OPTARG&amp;#34; &amp;gt;&amp;amp;2; return 2 ;;
&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;unknown option -%s\n&amp;#39; &amp;#34;$OPTARG&amp;#34; &amp;gt;&amp;amp;2; return 2 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift &amp;#34;$((OPTIND - 1))&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; if [ &amp;#34;$#&amp;#34; -ne 1 ] || [ -z &amp;#34;$demo_name&amp;#34; ]; then
&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;need a nonempty name and exactly one input\n&amp;#39; &amp;gt;&amp;amp;2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; return 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; fi
&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;name=&amp;lt;%s&amp;gt; verbose=%s input=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$demo_name&amp;#34; &amp;#34;$demo_verbose&amp;#34; &amp;#34;$1&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_cli -v -n &amp;#39;Jane Doe&amp;#39; -- &amp;#39;-input file&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;if demo_cli -n 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;missing=%s\n&amp;#39; &amp;#34;$?&amp;#34;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if demo_cli -x 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;unknown=%s\n&amp;#39; &amp;#34;$?&amp;#34;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if demo_cli -n &amp;#39;&amp;#39; input 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;empty=%s\n&amp;#39; &amp;#34;$?&amp;#34;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if demo_cli one two 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;extra=%s\n&amp;#39; &amp;#34;$?&amp;#34;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_cli second
&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;Expect &lt;code&gt;one0&lt;/code&gt; versus &lt;code&gt;ten&lt;/code&gt;, followed by a count of two and two separately printed arguments. The valid invocation keeps &lt;code&gt;Jane Doe&lt;/code&gt; and &lt;code&gt;-input file&lt;/code&gt; intact. Missing values, unknown options, an empty name, and extra operands each return status two. The final call restores the defaults and accepts &lt;code&gt;second&lt;/code&gt;, proving that repeated parsing starts with fresh state.&lt;/p&gt;
&lt;p&gt;These observations verify the interface&amp;rsquo;s boundaries, not merely that a usage message exists. When extending a real command, add checks for the new option&amp;rsquo;s meaning and preserve the established treatment of quoting, empty values, and the end-of-options marker.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Positional-Parameters"&gt;Bash positional parameters&lt;/a&gt;, numbered parameters and their function scope, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/getopts.html"&gt;POSIX getopts&lt;/a&gt;, option syntax errors and resetting parsing state, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell parameter expansion rewrites values</title><link>https://www.xbcatm.com/posts/shell-parameter-expansion-rewrites/</link><pubDate>Tue, 16 Nov 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-parameter-expansion-rewrites/</guid><description>Parameter expansion supplies defaults and transforms text before command execution. Quoting, empty values, and assignment operators determine the result.</description><content:encoded>&lt;p&gt;Parameter expansion derives a value from a shell parameter before a command receives its arguments. It can provide a default, reject missing configuration, or remove part of a string without launching another process. This is expansion during execution, not an instruction to evaluate every variable when the script is first parsed: a function reads the current value when it runs.&lt;/p&gt;
&lt;h2 id="defaults-distinguish-missing-from-empty"&gt;Defaults distinguish missing from empty&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;${setting:-default}&lt;/code&gt; uses the default when &lt;code&gt;setting&lt;/code&gt; is unset or empty. &lt;code&gt;${setting-default}&lt;/code&gt; substitutes only when it is unset, preserving an intentional empty value. The colon therefore changes configuration semantics; it is not optional punctuation. &lt;code&gt;${setting:+enabled}&lt;/code&gt; produces &lt;code&gt;enabled&lt;/code&gt; only for a set, nonempty value, whereas &lt;code&gt;${setting+enabled}&lt;/code&gt; also accepts an empty one.&lt;/p&gt;
&lt;p&gt;Ordinary default expansion does not assign anything. &lt;code&gt;${setting:=default}&lt;/code&gt; additionally stores the selected default in the variable, so subsequent reads see it. &lt;code&gt;${setting:?message}&lt;/code&gt; reports an unset or empty value on standard error and exits a noninteractive shell unsuccessfully. Without the colon, both operators test only whether the variable is unset. Choose according to the caller&amp;rsquo;s contract, rather than silently replacing required configuration.&lt;/p&gt;
&lt;p&gt;Braces also separate a variable name from adjacent text. &lt;code&gt;&amp;quot;${prefix}_suffix&amp;quot;&lt;/code&gt; appends a suffix to &lt;code&gt;prefix&lt;/code&gt;, while &lt;code&gt;&amp;quot;$prefix_suffix&amp;quot;&lt;/code&gt; reads a different variable altogether. This distinction matters when constructing filenames from several settings: an apparently missing value can be a name-boundary mistake rather than absent configuration. Print each component separately before investigating the resulting path, and remember that the final string can be syntactically valid even when one component came from the wrong variable.&lt;/p&gt;
&lt;h2 id="patterns-transform-strings-not-files"&gt;Patterns transform strings, not files&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;${name#pattern}&lt;/code&gt; and &lt;code&gt;${name##pattern}&lt;/code&gt; remove the shortest and longest matching prefix. The corresponding suffix operators are &lt;code&gt;%&lt;/code&gt; and &lt;code&gt;%%&lt;/code&gt;. They use shell patterns, not regular expressions, and leave the original variable unchanged. If nothing matches, the result is the original string. A matching slash is just text here; these operations neither inspect directories nor resolve symbolic links.&lt;/p&gt;
&lt;p&gt;Double-quote the complete expansion when its result must remain one argument. Quoting the result does not disable the pattern inside the braces: &lt;code&gt;&amp;quot;${name##*/}&amp;quot;&lt;/code&gt; still removes through the last slash. These portable operators differ from Bash extensions such as substring offsets and global replacement. A script invoked as &lt;code&gt;sh&lt;/code&gt; must not assume those extensions exist merely because the author&amp;rsquo;s interactive shell supports them.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;An unquoted result can undergo field splitting and filename expansion. Braces delimit the variable name but do not protect argument boundaries. A value containing spaces or an asterisk can consequently turn into unrelated arguments. Also, a default expression is expanded only when selected; placing a command substitution there can execute a command on the missing-value branch. Keep defaults simple when side effects would be surprising.&lt;/p&gt;
&lt;p&gt;Parameter expansion is not a command templating language. Text obtained from a variable is not reparsed into shell operators or quoting syntax. Construct argument lists with arrays in Bash, or explicit positional parameters in portable shell, instead of storing a command line and applying &lt;code&gt;eval&lt;/code&gt;. &lt;code&gt;set -u&lt;/code&gt; catches some missing values but does not validate their meaning, and &lt;code&gt;pipefail&lt;/code&gt; has no bearing on these string operations.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this self-contained example with Bash. It changes only variables inside a child shell and prints both the substituted values and the resulting variable state. The brackets make empty output visible.&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;unset demo_setting
&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;unset=[%s]\n&amp;#39; &amp;#34;${demo_setting-default}&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;demo_setting=
&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;empty=[%s] colon=[%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;${demo_setting-default}&amp;#34; &amp;#34;${demo_setting:-default}&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;substitute=[%s] stored=[%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;${demo_setting:-fallback}&amp;#34; &amp;#34;$demo_setting&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;: &amp;#34;${demo_setting:=saved}&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;assigned=[%s]\n&amp;#39; &amp;#34;$demo_setting&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;demo_name=&amp;#39;reports/weekly report.tar.gz&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;printf &amp;#39;base=[%s] one=[%s] all=[%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;${demo_name##*/}&amp;#34; &amp;#34;${demo_name%.*}&amp;#34; &amp;#34;${demo_name%%.*}&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;original=[%s]\n&amp;#39; &amp;#34;$demo_name&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;show_value() { printf &amp;#39;current=[%s]\n&amp;#39; &amp;#34;$demo_setting&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;demo_setting=later
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;show_value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if (unset demo_setting; : &amp;#34;${demo_setting:?required for demo}&amp;#34;) 2&amp;gt;/dev/null; then
&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;unexpected success\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;else
&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;missing value rejected\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;fi
&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;Expect &lt;code&gt;unset=[default]&lt;/code&gt;, then &lt;code&gt;empty=[] colon=[default]&lt;/code&gt;. The substitution line leaves &lt;code&gt;stored=[]&lt;/code&gt;; the assignment line reports &lt;code&gt;saved&lt;/code&gt;. The basename is &lt;code&gt;weekly report.tar.gz&lt;/code&gt;, the shortest suffix removal leaves &lt;code&gt;reports/weekly report.tar&lt;/code&gt;, and the longest leaves &lt;code&gt;reports/weekly report&lt;/code&gt;. The original still contains its complete value.&lt;/p&gt;
&lt;p&gt;Finally, the function prints &lt;code&gt;later&lt;/code&gt;, proving that its variable was not frozen when the function was defined. The last line confirms rejection without terminating the outer demonstration. These observations distinguish substitution, assignment, and error handling; a successful command alone would not prove that an empty configuration value was preserved correctly.&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/Shell-Parameter-Expansion.html"&gt;Bash manual parameter expansion&lt;/a&gt;, defines expansion operators, checked 2026-06-10.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06_02"&gt;POSIX shell parameter expansion&lt;/a&gt;, unset versus null values and portable pattern removal, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Umask filters new permissions, with a default ACL exception</title><link>https://www.xbcatm.com/posts/umask-sets-the-default-file-mode/</link><pubDate>Sat, 09 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/umask-sets-the-default-file-mode/</guid><description>Verify bitwise masking, unchanged existing files, later chmod, and default ACL inheritance on an ACL-capable filesystem.</description><content:encoded>&lt;p&gt;A process&amp;rsquo;s umask removes permission bits when creating files and directories under the ordinary mode-based creation rule. The application first supplies a requested mode; the mask restricts that request. It is neither a complete access policy nor an instruction to rewrite existing files. A directory&amp;rsquo;s default access control list changes the creation calculation, so inspecting only the process mask can give an incomplete prediction.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Without a default ACL, the ordinary permission result is the requested mode bitwise AND the complement of umask. For requested 0666 and mask 0022, that gives 0644. This is not arithmetic subtraction. A request for 0600 remains 0600 under the same mask, and a request that omits execute permission does not acquire it because the mask is permissive.&lt;/p&gt;
&lt;p&gt;A restrictive umask does not repair an existing world-writable file. Opening and truncating that file changes content while preserving its existing mode. A later &lt;code&gt;chmod&lt;/code&gt; can also widen permissions regardless of the current mask. Treating umask as a permanent upper bound on permissions therefore leaves both existing-file and subsequent-permission-change cases unaccounted for.&lt;/p&gt;
&lt;p&gt;When the parent directory supplies a default ACL, Linux derives the new access ACL from that default and restricts it by the application&amp;rsquo;s requested mode; the process umask is not applied in the ordinary way. A shared directory can consequently produce group-accessible files even when the creator&amp;rsquo;s mask is 0077. The inherited ACL must be part of the diagnosis.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save this as &lt;code&gt;mask.py&lt;/code&gt; and run &lt;code&gt;python3 mask.py&lt;/code&gt; on Linux with Python 3 and &lt;code&gt;setfacl&lt;/code&gt; installed. The temporary-file directory must support default ACLs. The recorded run used Python 3.11.2, acl 2.3.1, and a separate ext4 volume selected through &lt;code&gt;TMPDIR&lt;/code&gt;. The initial tmpfs environment rejected default ACLs, so the complete experiment was rerun on ext4:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_CREAT&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_EXCL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S_IMODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o022&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;xbcatm-umask-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;data&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o644&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;executable&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o777&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o755&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;private&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;folder&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mo"&gt;0o777&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o755&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;~&lt;/span&gt;&lt;span class="mo"&gt;0o022&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;0o022&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;without default ACL: requested mode AND NOT umask; 0666 becomes 0644, 0777 becomes 0755&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o077&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;strict&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;existing&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_TRUNC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;restrictive mask leaves an existing 0666 file unchanged; chmod can widen permissions later&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;acl&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;setfacl&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;u::rwx,g::rwx,o::---&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inherited&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;inherited&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inherited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;restricted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;restricted&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restricted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inherited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o660&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restricted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;with default ACL and umask 0077: request 0666 yields 0660; request 0600 still yields 0600&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc/self/status&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Umask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o077&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc/self/status observes this process mask without temporarily changing it&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With mask 0022 and no inherited default ACL, requesting 0666 produces a 0644 file. Requesting 0777 produces 0755 for both a regular file and a directory, while requesting 0600 still produces 0600. The fixture also checks that arithmetic subtraction would give a different result for the restrictive request, exposing that common calculation error.&lt;/p&gt;
&lt;p&gt;After switching to mask 0077, a newly created file requested as 0666 becomes 0600. Another file is explicitly changed to 0666, then opened with truncation but without creation. Its mode stays 0666. This comparison distinguishes initial creation from later operations on an existing inode, rather than assuming every open reapplies creation policy.&lt;/p&gt;
&lt;p&gt;Next, the fixture installs a default ACL granting the owning user and group full permissions and granting others none. Under the still-active 0077 mask, a new file requested as 0666 receives mode 0660. The inherited ACL preserves group read/write permissions despite the restrictive mask, but the application&amp;rsquo;s request excludes execute permission.&lt;/p&gt;
&lt;p&gt;A neighboring file requested as 0600 remains 0600 under that same default ACL. The default does not invent permissions absent from the requested mode. These two requests establish both the default-ACL exception to ordinary masking and the continued role of the application&amp;rsquo;s explicit creation request.&lt;/p&gt;
&lt;p&gt;The script reads &lt;code&gt;Umask&lt;/code&gt; from &lt;code&gt;/proc/self/status&lt;/code&gt; and confirms that the process mask is still 0077. It restores the original mask in &lt;code&gt;finally&lt;/code&gt; and removes the temporary tree. If the selected filesystem cannot install the ACL, the command fails; skipping that step would leave the central exception untested.&lt;/p&gt;
&lt;h2 id="apply-creation-policy-at-the-right-layer"&gt;Apply creation policy at the right layer&lt;/h2&gt;
&lt;p&gt;Umask is inherited through normal process creation and preserved across execution of another program. Set it in the actual launch path for a service or command. An unrelated terminal&amp;rsquo;s mask does not retroactively change the state of an already running process, and shell startup files matter only when the launch path reads them.&lt;/p&gt;
&lt;p&gt;Because umask is process-wide for threads sharing the relevant filesystem state, temporarily changing it to discover its previous value can interfere with concurrent file creation. On Linux, reading the proc status field avoids that mutation. Applications needing a specific private file should request suitably restrictive permissions and verify their actual storage context.&lt;/p&gt;
&lt;p&gt;Inspect parent-directory access, ownership, ACLs, and the resulting file permissions together. A 0660 file inside an inaccessible parent is not automatically reachable by its group, and mode bits alone do not describe all named ACL entries. The experiment proves creation results; end-to-end access requires testing the intended identity through the complete path.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/umask.2.html"&gt;umask(2)&lt;/a&gt;, creation masks, inheritance, default ACL interaction, and proc inspection, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/acl.5.html"&gt;acl(5)&lt;/a&gt;, default ACL inheritance and requested-mode permission restrictions, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/open.2.html"&gt;open(2)&lt;/a&gt;, creation modes and opening or truncating an existing file, checked 2026-09-12.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell aliases shrink repeated typing</title><link>https://www.xbcatm.com/posts/shell-aliases-shrink-repeated-typing/</link><pubDate>Mon, 04 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-aliases-shrink-repeated-typing/</guid><description>An alias maps a short name to a longer command so common sequences become short. Interactive shortcuts are the point; scripts usually avoid them.</description><content:encoded>&lt;p&gt;An alias replaces a command word with shell text while Bash reads a command. It is useful for personal shortcuts such as &lt;code&gt;alias ll='ls -l'&lt;/code&gt;, but it is not an executable and has no independent argument scope. Noninteractive Bash disables alias expansion by default; a script can explicitly enable it. Understanding that distinction explains why a shortcut works at a prompt but disappears in automation.&lt;/p&gt;
&lt;h2 id="replacement-happens-when-input-is-read"&gt;Replacement happens when input is read&lt;/h2&gt;
&lt;p&gt;An unquoted word in command position is eligible for alias expansion. With &lt;code&gt;alias ll='ls -l'&lt;/code&gt;, &lt;code&gt;ll 'two words'&lt;/code&gt; becomes a command whose final argument still contains the space. Aliases do not inherently destroy quoting on arguments supplied by the caller. They simply cannot refer to their own &lt;code&gt;$1&lt;/code&gt; or rearrange a private argument list as functions can.&lt;/p&gt;
&lt;p&gt;The replacement can contain shell syntax, including pipelines or several commands, so aliases are not technically limited to one executable. That freedom also makes their behavior harder to inspect. Bash expands the first word of the replacement again, except when it is the alias already being expanded. Consequently, &lt;code&gt;alias ls='ls -F'&lt;/code&gt; does not recurse forever. A trailing blank in an alias value has another special effect: the following command word is also checked for alias expansion.&lt;/p&gt;
&lt;p&gt;Quoting the definition controls another timing boundary. Single quotes preserve dollar signs for later interpretation in the replacement text; double quotes can expand variables immediately while the alias is being defined. A shortcut meant to follow a changing directory variable can therefore retain an old value. Inspect the stored definition, not just the line originally typed, when diagnosing that discrepancy.&lt;/p&gt;
&lt;h2 id="scope-and-timing-are-different-questions"&gt;Scope and timing are different questions&lt;/h2&gt;
&lt;p&gt;Aliases belong to the current shell; they are not environment variables exported to a separately launched Bash process. A subshell created with parentheses inherits the shell state, which is a different operation. Startup files can define the same alias again, but that is configuration being loaded, not an alias travelling through the environment.&lt;/p&gt;
&lt;p&gt;In Bash, &lt;code&gt;shopt -s expand_aliases&lt;/code&gt; enables expansion in a noninteractive shell. Definitions must still precede the input that uses them. Bash reads a complete line or compound command before executing it, so defining an alias and invoking it later on the same line does not activate that new definition in time. The same issue applies inside a function body being parsed.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A function can capture an alias expansion when its definition is read. Replacing that alias afterward does not rewrite the already parsed function. This surprises users who expect every call to consult the latest alias table. Define functions directly when their behavior should be explicit, and use quoted &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; to forward arguments without losing empty values or spaces.&lt;/p&gt;
&lt;p&gt;Another failure is mistaking a shortcut for a command available to another program. A process launcher does not consult your interactive alias table. Diagnose the current shell with &lt;code&gt;type name&lt;/code&gt; and inspect a specific definition with &lt;code&gt;alias name&lt;/code&gt;; remove it with &lt;code&gt;unalias name&lt;/code&gt;. Quoting or backslash-escaping the command word suppresses alias expansion, but functions or builtins can still affect subsequent lookup. An explicit executable path selects that file.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;This Bash experiment uses only diagnostic names and builtins. It intentionally compares disabled expansion, enabled expansion, same-line definition, and a function parsed before an alias changes. Expected failures are captured immediately so a later successful print cannot hide their status.&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;shopt -u expand_aliases
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;alias demo_alias=&amp;#39;printf &amp;#34;alias=&amp;lt;%s&amp;gt;\n&amp;#34;&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;demo_alias hidden 2&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;printf &amp;#39;disabled=%s\n&amp;#39; &amp;#34;$?&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;shopt -s expand_aliases
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_alias &amp;#39;two words&amp;#39; &amp;#39;&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;alias demo_late=&amp;#39;printf &amp;#34;late\n&amp;#34;&amp;#39;; demo_late 2&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;printf &amp;#39;same-line=%s\n&amp;#39; &amp;#34;$?&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;demo_late
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;alias demo_version=&amp;#39;printf &amp;#34;old\n&amp;#34;&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;demo_function() { demo_version; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;alias demo_version=&amp;#39;printf &amp;#34;new\n&amp;#34;&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;demo_function
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_version
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;unalias demo_alias
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if command -v demo_alias &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
&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;unexpected command\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;else
&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;alias removed\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;fi
&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;Expect status &lt;code&gt;127&lt;/code&gt; while expansion is disabled, followed by &lt;code&gt;alias=&amp;lt;two words&amp;gt;&lt;/code&gt; and &lt;code&gt;alias=&amp;lt;&amp;gt;&lt;/code&gt; after enabling it. The same-line call also returns &lt;code&gt;127&lt;/code&gt;; the next line prints &lt;code&gt;late&lt;/code&gt;. The function then prints &lt;code&gt;old&lt;/code&gt;, while a direct invocation prints &lt;code&gt;new&lt;/code&gt;. Finally, lookup confirms removal.&lt;/p&gt;
&lt;p&gt;These results establish the actual boundaries: argument quoting can survive a simple alias, noninteractive expansion is configurable, and read-time substitution can freeze behavior inside a function. For shared scripts, put reusable logic in a function or executable that the script defines or loads explicitly. Keep aliases as visible conveniences for the shell session in which they were configured.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/posts/shell-functions-borrow-positional-parameters/"&gt;Shell functions borrow positional parameters&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Aliases"&gt;Bash reference manual: aliases&lt;/a&gt;, the maintainer-hosted manual on expansion timing and noninteractive behavior, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_03_01"&gt;POSIX alias substitution&lt;/a&gt;, command-word substitution and quoting rules, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Redirection reorders the streams</title><link>https://www.xbcatm.com/posts/redirection-reorders-the-streams/</link><pubDate>Wed, 29 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/redirection-reorders-the-streams/</guid><description>Redirections attach a command's standard streams to files before it starts. Order matters when stdout and stderr both change direction together.</description><content:encoded>&lt;p&gt;Redirection changes a command&amp;rsquo;s file descriptors before the command executes. The shell processes redirections from left to right, opening or duplicating a descriptor as it reaches each operation. Descriptors 0, 1, and 2 conventionally represent standard input, standard output, and standard error. Reading this sequence as concrete changes to those handles explains both misplaced diagnostics and files that become empty before their contents can be read.&lt;/p&gt;
&lt;h2 id="opening-a-file-is-already-a-side-effect"&gt;Opening a file is already a side effect&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;output&lt;/code&gt; opens a file for writing and normally truncates an existing regular file. &lt;code&gt;&amp;gt;&amp;gt;output&lt;/code&gt; opens it for appending. &lt;code&gt;&amp;lt;input&lt;/code&gt; opens a file for reading. These actions belong to the shell, not to the program named by the command. If opening a required target fails, the command is not executed; successful earlier redirections can still have created or truncated other files.&lt;/p&gt;
&lt;p&gt;Consequently, transforming a file with &lt;code&gt;sed expression input &amp;gt;input&lt;/code&gt; destroys the original before &lt;code&gt;sed&lt;/code&gt; can read it. Opening input first with &lt;code&gt;&amp;lt;input&lt;/code&gt; does not rescue the data if a later redirection truncates the same underlying file. Write to a separate temporary file, check the transformation&amp;rsquo;s status and result, and only then perform a deliberate replacement appropriate to the file&amp;rsquo;s ownership and filesystem.&lt;/p&gt;
&lt;h2 id="duplication-captures-the-current-destination"&gt;Duplication captures the current destination&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;log 2&amp;gt;&amp;amp;1&lt;/code&gt; first sends descriptor 1 to the log, then makes descriptor 2 refer to that same open destination. &lt;code&gt;2&amp;gt;&amp;amp;1 &amp;gt;log&lt;/code&gt; first duplicates the original standard output onto descriptor 2, then changes only descriptor 1. Standard error therefore remains at the original destination, such as a terminal or a capturing pipe. Duplication copies a descriptor reference, not file contents, and it does not create a live connection between descriptor numbers.&lt;/p&gt;
&lt;p&gt;Bash&amp;rsquo;s &lt;code&gt;&amp;amp;&amp;gt;log&lt;/code&gt; is shorthand for redirecting both streams, but &lt;code&gt;&amp;gt;log 2&amp;gt;&amp;amp;1&lt;/code&gt; also works in portable shell scripts. Opening the same filename twice with &lt;code&gt;&amp;gt;log 2&amp;gt;log&lt;/code&gt; is not equivalent: the two opens have independent offsets and can overwrite one another&amp;rsquo;s output. Even when descriptors share one destination, application buffering can affect the order in which messages become visible.&lt;/p&gt;
&lt;p&gt;Pipelines connect standard output to the next stage before command-specific redirections are applied. In &lt;code&gt;producer &amp;gt;file | consumer&lt;/code&gt;, the producer&amp;rsquo;s output consequently goes to the file, leaving the consumer without that data. Conversely, &lt;code&gt;producer 2&amp;gt;&amp;amp;1 | consumer&lt;/code&gt; sends diagnostics through the pipe too. When the consumer expects machine-readable records, merging human-readable errors into that stream can corrupt otherwise valid input.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Quote variable filenames so spaces do not turn a target into multiple words. Keep the descriptor number adjacent to its operator: &lt;code&gt;2&amp;gt;errors&lt;/code&gt; redirects standard error, while &lt;code&gt;2 &amp;gt;errors&lt;/code&gt; supplies an ordinary argument containing &lt;code&gt;2&lt;/code&gt; and redirects standard output. A misplaced space can therefore change both program input and output routing.&lt;/p&gt;
&lt;p&gt;Bash&amp;rsquo;s &lt;code&gt;set -C&lt;/code&gt; enables &lt;code&gt;noclobber&lt;/code&gt;, rejecting ordinary output redirection to an existing regular file. It is useful protection against accidental replacement, not a backup system or a universal guarantee about every file type. &lt;code&gt;&amp;gt;|&lt;/code&gt; explicitly overrides it. Also distinguish a command&amp;rsquo;s temporary redirections from &lt;code&gt;exec &amp;gt;log&lt;/code&gt;, which changes the current shell&amp;rsquo;s descriptors and can redirect every subsequent command. Local tests should not unexpectedly silence the surrounding session.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run the following in Bash with standard Unix utilities. Only freshly created temporary files are truncated or removed. The output compares both orderings, demonstrates same-file truncation, and checks that a failed redirection prevents a function body from running.&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 -eu
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_dir=$(mktemp -d)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;trap &amp;#39;rm -f &amp;#34;$demo_dir/both&amp;#34; &amp;#34;$demo_dir/out&amp;#34; &amp;#34;$demo_dir/input&amp;#34;; rmdir &amp;#34;$demo_dir&amp;#34;&amp;#39; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;emit() { printf &amp;#39;out\n&amp;#39;; printf &amp;#39;err\n&amp;#39; &amp;gt;&amp;amp;2; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;emit &amp;gt; &amp;#34;$demo_dir/both&amp;#34; 2&amp;gt;&amp;amp;1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_captured=$(emit 2&amp;gt;&amp;amp;1 &amp;gt; &amp;#34;$demo_dir/out&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;both:\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;cat &amp;#34;$demo_dir/both&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;out-file=[%s] captured=[%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;$(cat &amp;#34;$demo_dir/out&amp;#34;)&amp;#34; &amp;#34;$demo_captured&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;seed\n&amp;#39; &amp;gt; &amp;#34;$demo_dir/input&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;sed &amp;#39;s/seed/replaced/&amp;#39; &amp;#34;$demo_dir/input&amp;#34; &amp;gt; &amp;#34;$demo_dir/input&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;if [ ! -s &amp;#34;$demo_dir/input&amp;#34; ]; then printf &amp;#39;input truncated\n&amp;#39;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if (set -C; emit &amp;gt; &amp;#34;$demo_dir/both&amp;#34;) 2&amp;gt;/dev/null; then
&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;unexpected overwrite\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;else
&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;noclobber rejected overwrite\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;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_ran=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;mark_run() { demo_ran=yes; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if mark_run 2&amp;gt;/dev/null &amp;gt; &amp;#34;$demo_dir/missing/output&amp;#34;; then
&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;unexpected redirection\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;else
&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;function-ran=%s\n&amp;#39; &amp;#34;$demo_ran&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;fi
&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;The combined file prints &lt;code&gt;out&lt;/code&gt; and &lt;code&gt;err&lt;/code&gt;. Reversed ordering leaves only &lt;code&gt;out&lt;/code&gt; in the output file and captures &lt;code&gt;err&lt;/code&gt; separately. The transformation reports an empty input because truncation preceded execution. The remaining lines confirm that &lt;code&gt;noclobber&lt;/code&gt; rejected replacement and the failed open left &lt;code&gt;function-ran=no&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These checks observe file contents and whether the command actually ran, rather than relying on a successful final print. In a real script, retain the producer&amp;rsquo;s exit status alongside its logs: a perfectly routed error message does not turn a failed operation into success. Keeping output routing and operation status separate makes recovery decisions reliable.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/notes/redirects-and-error-codes-should-preserve-the-story/"&gt;Redirects and error codes should preserve the story&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Redirections"&gt;Bash reference manual: redirections&lt;/a&gt;, left-to-right descriptor changes and noclobber behavior, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_07"&gt;POSIX shell redirection&lt;/a&gt;, portable redirection operators and file-opening semantics, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell pipes stream between processes</title><link>https://www.xbcatm.com/posts/shell-pipes-stream-between-processes/</link><pubDate>Tue, 28 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.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&amp;rsquo;s standard output to the next command&amp;rsquo;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;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&amp;rsquo;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;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. 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&amp;rsquo;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&amp;rsquo;s variable. Bash&amp;rsquo;s lastpipe option can alter the final stage&amp;rsquo;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&amp;rsquo;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&amp;rsquo;s status, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Job control moves terminal foreground ownership between groups</title><link>https://www.xbcatm.com/posts/job-control-splits-the-terminal/</link><pubDate>Mon, 27 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/job-control-splits-the-terminal/</guid><description>Use a real pseudo-terminal to stop and resume a pipeline, interrupt it, and observe a background reader stop for terminal input.</description><content:encoded>&lt;p&gt;Interactive job control lets a shell manage process groups that share a controlling terminal. The terminal has one foreground process group at a time, while other jobs can run or remain stopped in the background. Foreground ownership, running state, and the shell&amp;rsquo;s job table are distinct. A background job is not automatically suspended, and a stopped job has not necessarily terminated.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Typing a configured interrupt or suspend character causes the terminal driver to signal the foreground group when terminal signal processing is enabled. It does not simply signal whichever individual process last printed something. Remapped control characters, altered terminal modes, or a program&amp;rsquo;s signal handlers can change the observed behavior, so key labels alone do not establish a universal result.&lt;/p&gt;
&lt;p&gt;A normal background job attempting to read its controlling terminal is stopped by &lt;code&gt;SIGTTIN&lt;/code&gt;. Moving it to the background does not hand it an independent keyboard stream. Output is different: background writes are normally allowed, with additional behavior controlled by terminal settings such as TOSTOP. Background execution therefore does not imply complete separation from the terminal.&lt;/p&gt;
&lt;p&gt;Job specifications such as &lt;code&gt;%1&lt;/code&gt; belong to a particular shell&amp;rsquo;s job table; they are not global process identifiers. Another terminal or a noninteractive shell cannot be assumed to manage that same job using the same number. Pipelines and background commands also depend on whether job control is enabled in the actual shell invocation.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save the script as &lt;code&gt;jobcontrol.py&lt;/code&gt; and run &lt;code&gt;python3 jobcontrol.py&lt;/code&gt; on Linux with Bash and Python installed. The recorded run used Linux 6.12.76, Bash 5.2.15, and Python 3.11.2. It creates its own pseudo-terminal and interactive Bash, so it does not send control characters to the terminal from which you launch it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;pty&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;re&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;select&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;=~&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ECHO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ISIG&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ICANON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VINTR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x03&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VSUSP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x1a&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VEOF&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x04&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcsetattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCSANOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/bash&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bash&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--noprofile&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--norc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PATH&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/usr/bin:/bin&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;TERM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dumb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;LC_ALL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PS1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;XB-PROMPT&amp;gt; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;HISTFILE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/dev/null&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;XB-PROMPT&amp;gt; &amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;],[],[],&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PTY closed before prompt&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;XB-PROMPT&amp;gt; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;replace&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;terminal state did not change&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stat&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;) &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sleep 60 | cat&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x1a&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Stopped&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Stopped&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jobs -l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Ctrl-Z stops the foreground pipeline and returns the terminal to the interactive shell&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bg %1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fg %1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x03&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;RESULT:130&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;printf &amp;#39;RESULT:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;$?&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bg resumes without foreground ownership; fg restores ownership; Ctrl-C gives this pipeline status 130&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cat &amp;amp; printf &amp;#39;BG:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;$!&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;BG:(\d+)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;T&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Stopped (tty input)&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jobs -l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a background reader is stopped by terminal input rules, rather than receiving the shell keyboard stream&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fg %+&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;hello&lt;/span&gt;&lt;span class="se"&gt;\n\x04&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;hello&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;RESULT:0&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;printf &amp;#39;RESULT:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;$?&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;foregrounding that reader lets it read; EOF ends it normally; all jobs are reaped&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jobs -p&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;exit&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;waited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;waited&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WIFEXITED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;killpg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The fixture enables terminal signal processing and canonical input, explicitly assigns interrupt, suspend, and EOF characters, and disables input echo. It starts Bash without user startup files and uses a recognizable prompt. These choices make the experiment reproducible without relying on a user&amp;rsquo;s aliases, prompt commands, or customized terminal key bindings.&lt;/p&gt;
&lt;p&gt;Bash first runs &lt;code&gt;sleep 60 | cat&lt;/code&gt; in the foreground. The controller observes the terminal&amp;rsquo;s foreground PGID change from Bash to the pipeline. Sending the suspend character stops the job, produces a Stopped report, and returns foreground ownership to Bash. &lt;code&gt;jobs -l&lt;/code&gt; confirms that the shell still knows the stopped job.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bg %1&lt;/code&gt; resumes the pipeline while Bash retains the terminal. &lt;code&gt;fg %1&lt;/code&gt; then gives foreground ownership back to the pipeline. Sending the interrupt character ends this default-behavior pipeline, and Bash reports status 130. This is an observed Bash status for this command, not a promise that every application must exit on INT or return the same value.&lt;/p&gt;
&lt;p&gt;The script next launches a background &lt;code&gt;cat&lt;/code&gt;, records its PID, and waits for a stopped process state. Bash reports &lt;code&gt;Stopped (tty input)&lt;/code&gt;. This is a terminal-input restriction, not completion and not evidence that the reader consumed the shell&amp;rsquo;s next command. The shell remains the terminal&amp;rsquo;s foreground group throughout this background attempt.&lt;/p&gt;
&lt;p&gt;Foregrounding that reader lets it receive &lt;code&gt;hello&lt;/code&gt;. An EOF character at an empty canonical input line allows it to finish normally, after which Bash reports zero and its job table is empty. The controller then exits Bash and reaps it. Failure cleanup is limited to the isolated groups created by the experiment.&lt;/p&gt;
&lt;h2 id="choose-the-intended-lifetime-and-input-source"&gt;Choose the intended lifetime and input source&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;bg&lt;/code&gt; when a stopped job can proceed without interactive terminal input. Use &lt;code&gt;fg&lt;/code&gt; when it needs the terminal again. Redirecting input to a suitable file or pipe can let background work run independently of keyboard input, but it does not automatically detach the process from its session or define what happens when the login ends.&lt;/p&gt;
&lt;p&gt;Use a service manager for workloads whose lifetime must outlive an interactive session and whose restart, logging, and resource policies must be explicit. A shell job table is useful for current interactive work; it is not durable service state. Disowning a job or changing hangup handling also answers narrower questions than full service supervision.&lt;/p&gt;
&lt;p&gt;When a job seems frozen, inspect whether it is stopped, waiting for data, or still computing before sending termination signals. Correlate the job specification, PID, PGID, and terminal foreground group. This prevents confusing a legitimate terminal-input stop with a crashed process and helps restore the intended interaction without unnecessarily discarding the work.&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/Job-Control-Basics.html"&gt;Bash job-control basics&lt;/a&gt;, foreground groups, terminal input, suspension, and job states, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Job-Control-Builtins.html"&gt;Bash job-control builtins&lt;/a&gt;, jobs, bg, fg, and shell-local job specifications, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/termios.3.html"&gt;termios(3)&lt;/a&gt;, terminal control characters and signal generation, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/tcgetpgrp.3.html"&gt;tcgetpgrp(3)&lt;/a&gt;, reading and selecting the terminal foreground process group, checked 2026-09-12.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Environment variables are a per-process thing</title><link>https://www.xbcatm.com/posts/environment-variables-are-scoped-to-process/</link><pubDate>Sat, 25 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/environment-variables-are-scoped-to-process/</guid><description>Exported settings cross program boundaries as environment strings. Subshells, sourcing, and explicit launcher environments explain the important exceptions.</description><content:encoded>&lt;p&gt;An environment is a collection of name-value strings supplied to a program when it starts. A child normally receives the environment selected by its parent; changing that child&amp;rsquo;s copy does not rewrite the parent&amp;rsquo;s or a sibling&amp;rsquo;s copy. This is a useful configuration boundary, but it is not a promise that the command has no side effects. The program can still modify shared files, databases, or other resources using those settings.&lt;/p&gt;
&lt;h2 id="export-selects-values-for-another-program"&gt;Export selects values for another program&lt;/h2&gt;
&lt;p&gt;A shell variable and an environment entry are related but distinct. Assigning &lt;code&gt;DEMO_SETTING=local&lt;/code&gt; creates or updates a shell parameter. &lt;code&gt;export DEMO_SETTING&lt;/code&gt; marks it for inclusion when the shell launches external commands. A new Bash process normally cannot see an unexported shell variable, even though the parent can print it. Merely placing an assignment in a configuration file does not load that file into every shell.&lt;/p&gt;
&lt;p&gt;A value inherited through the environment is already marked for export by Bash. Changing its value retains that attribute, so a later grandchild can inherit the changed value without another export statement. &lt;code&gt;unset&lt;/code&gt; removes the variable; Bash&amp;rsquo;s &lt;code&gt;export -n&lt;/code&gt; removes its export attribute while retaining the shell value. The relevant question is which environment the immediate launcher supplied, not whether every ancestor repeated an export command.&lt;/p&gt;
&lt;h2 id="a-subshell-is-not-a-freshly-executed-shell"&gt;A subshell is not a freshly executed shell&lt;/h2&gt;
&lt;p&gt;Parenthesized command groups and command substitution run in a subshell environment copied from the current shell. They can see ordinary shell variables that were never exported. Their assignments still do not flow back into the outer shell. This differs from executing a separate shell program, which receives exported entries rather than a complete copy of the parent&amp;rsquo;s shell parameter table.&lt;/p&gt;
&lt;p&gt;Sourcing a file with &lt;code&gt;.&lt;/code&gt; runs its commands in the current shell. Its assignments can therefore change the caller directly; it is not an exception to process isolation because no separate process is returning those changes. A sourced file is executable shell code, not passive configuration. Only source a file whose contents you intend to execute in that context.&lt;/p&gt;
&lt;h2 id="per-command-settings-have-a-defined-lifetime"&gt;Per-command settings have a defined lifetime&lt;/h2&gt;
&lt;p&gt;A prefix such as &lt;code&gt;DEMO_SETTING=once external-command&lt;/code&gt; supplies a value for that invocation without permanently changing the surrounding shell&amp;rsquo;s value. This statement concerns an external command; assignment behavior around special builtins and functions has additional shell-specific rules. Do not generalize it into a claim that every command-looking construct creates an isolated process.&lt;/p&gt;
&lt;p&gt;At the operating system interface, &lt;code&gt;execve&lt;/code&gt; takes an explicit environment array and replaces the current process image. It does not itself create a new process. A launcher can preserve, filter, or replace the environment, which explains why privilege boundaries, service managers, and container configuration may produce values different from an interactive terminal. Process ancestry alone is insufficient to establish the actual settings delivered to an application.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Editing a terminal&amp;rsquo;s environment does not update an already running service. Likewise, changing an environment file has no effect until the relevant launcher rereads it and starts the process, unless the application provides its own reload mechanism. Check the configuration path used by that service and verify the resulting behavior in the process that actually handles requests.&lt;/p&gt;
&lt;p&gt;Treat missing and empty values separately, because applications may assign them different meanings. Environment strings cannot contain an embedded NUL, and platforms limit the combined argument and environment size, so they are unsuitable for arbitrary binary payloads or unbounded documents. Inspect only the specific nonsecret keys needed for diagnosis; dumping an entire environment can expose credentials inherited by unrelated tools.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this Bash example. It prints only a dedicated demonstration variable, uses no external configuration file, and leaves the surrounding terminal unchanged. The small here-document passed to &lt;code&gt;.&lt;/code&gt; contains the complete trusted configuration used for the sourcing test.&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;&lt;span class="nv"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&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;unset DEMO_SETTING
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;DEMO_SETTING=local
&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;shell=%s\n&amp;#39; &amp;#34;$DEMO_SETTING&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;bash --noprofile --norc -c &amp;#39;printf &amp;#34;unexported-child=%s\n&amp;#34; &amp;#34;${DEMO_SETTING-unset}&amp;#34;&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;(printf &amp;#39;subshell=%s\n&amp;#39; &amp;#34;$DEMO_SETTING&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;export DEMO_SETTING
&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 --noprofile --norc &amp;lt;&amp;lt;&amp;#39;CHILD&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;printf &amp;#39;exported-child=%s\n&amp;#39; &amp;#34;$DEMO_SETTING&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;DEMO_SETTING=child
&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 --noprofile --norc -c &amp;#39;printf &amp;#34;grandchild=%s\n&amp;#34; &amp;#34;$DEMO_SETTING&amp;#34;&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;CHILD
&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;parent-after-child=%s\n&amp;#39; &amp;#34;$DEMO_SETTING&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;DEMO_SETTING=once bash --noprofile --norc -c &amp;#39;printf &amp;#34;one-command=%s\n&amp;#34; &amp;#34;$DEMO_SETTING&amp;#34;&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;printf &amp;#39;parent-after-command=%s\n&amp;#39; &amp;#34;$DEMO_SETTING&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;. /dev/stdin &amp;lt;&amp;lt;&amp;#39;CONFIG&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;DEMO_SETTING=sourced
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CONFIG
&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;parent-after-source=%s\n&amp;#39; &amp;#34;$DEMO_SETTING&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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The unexported child prints &lt;code&gt;unset&lt;/code&gt;, while the parenthesized subshell sees &lt;code&gt;local&lt;/code&gt;. After export, the new child sees &lt;code&gt;local&lt;/code&gt;, changes its own copy, and passes &lt;code&gt;child&lt;/code&gt; to its grandchild. The outer shell remains &lt;code&gt;local&lt;/code&gt;. The one-command override prints &lt;code&gt;once&lt;/code&gt;, and the following parent check is still &lt;code&gt;local&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finally, sourcing changes that outer child shell&amp;rsquo;s value to &lt;code&gt;sourced&lt;/code&gt;. Together these observations distinguish shell parameters, exported entries, subshell copies, per-command overrides, and execution in the current shell. They establish variable scope, not the absence of application side effects or the correctness of a service&amp;rsquo;s deployment configuration.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Command-Execution-Environment"&gt;Bash execution environment&lt;/a&gt;, exported entries versus the state copied into subshells, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Environment"&gt;Bash environment&lt;/a&gt;, export attributes and per-command assignments, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/exec.html"&gt;POSIX exec functions&lt;/a&gt;, process-image replacement and explicit environment arrays, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Bash globbing expands before the command runs</title><link>https://www.xbcatm.com/posts/bash-globbing-expands-before-the-command-runs/</link><pubDate>Wed, 08 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/bash-globbing-expands-before-the-command-runs/</guid><description>Asterisks and brackets look like part of the command, but the shell expands them into filenames first and only then runs the result. Messing with that order breaks scripts.</description><content:encoded>&lt;p&gt;A wildcard in a Bash command usually becomes a list of pathnames before the target program starts. That makes &lt;code&gt;*.txt&lt;/code&gt; convenient, but it also makes the command depend on the current directory and shell options. The program receives arguments, without information saying which ones came from a wildcard. Decide whether you intend to pass matching files or a pattern for the program to interpret.&lt;/p&gt;
&lt;h2 id="match-first-then-preserve-the-arguments"&gt;Match first, then preserve the arguments&lt;/h2&gt;
&lt;p&gt;Bash performs word splitting before filename expansion. An unquoted &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, or bracket expression can therefore produce several arguments. A matching filename containing spaces remains one argument: Bash does not run another word-splitting pass over the filenames it just found. The common loss of spaces happens later when a loop expands its variable without quotes. Use &lt;code&gt;&amp;quot;$file&amp;quot;&lt;/code&gt; when passing each matched pathname onward.&lt;/p&gt;
&lt;p&gt;Quote a pattern when its recipient needs the pattern itself. For example, &lt;code&gt;find . -name '*.txt'&lt;/code&gt; gives the matching expression to find; leaving it unquoted lets Bash expand it against the current directory first. A quoted variable containing an asterisk also stays literal. An unquoted variable containing an asterisk can trigger pathname expansion, even though the star was not written directly in the command.&lt;/p&gt;
&lt;p&gt;A pattern operates on directory entries, not a promised collection of regular files. A directory named &lt;code&gt;archive.txt&lt;/code&gt; also matches &lt;code&gt;*.txt&lt;/code&gt;. Use a file-type check when that distinction matters. Ordinary stars do not cross slashes, and leading dots require explicit matching unless dotglob is enabled. Recursive traversal needs its own deliberate approach; plain &lt;code&gt;*&lt;/code&gt; does not search the whole tree.&lt;/p&gt;
&lt;p&gt;The environment can also change which names qualify. In Bash, setting GLOBIGNORE to a nonempty value filters matching names and enables dotglob as a side effect. A script inherited from an interactive session may therefore see hidden files unexpectedly. The fixture below clears that variable and selects its options explicitly so its assertions describe a reproducible configuration.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;By default, an unmatched pattern stays as literal text. A loop over &lt;code&gt;*.png&lt;/code&gt; can consequently run once with a nonexistent filename. With nullglob enabled, the pattern disappears instead. This avoids that literal but introduces another risk: a command receiving no file arguments may read standard input, or apply its own default target. Neither nounset nor pipefail checks that a glob found files.&lt;/p&gt;
&lt;p&gt;For optional inputs, collect matches in a Bash array with nullglob and test its element count before invoking the consumer. For required matches, failglob rejects an unmatched expansion before the command runs. Keep option changes inside a subshell when callers should retain their original settings. Do not assume a script&amp;rsquo;s behavior matches an interactive shell whose startup files changed those options.&lt;/p&gt;
&lt;p&gt;A filename beginning with a hyphen may be interpreted as an option by the receiving program. Expanding &lt;code&gt;./*.txt&lt;/code&gt; prefixes such names with a directory component; use the program&amp;rsquo;s documented &lt;code&gt;--&lt;/code&gt; separator where supported as well. Quoting preserves argument boundaries but does not change option parsing. Finally, enumeration and use are separate events: files can disappear between them, so a successful match is not proof that a later read will succeed.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run the following block with Bash and the usual temporary-directory tools. It creates an isolated directory, checks literal and expanded arguments, exercises both no-match policies, and removes only its own fixture. It does not require any preexisting text files.&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;export LC_ALL=C
&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 +f
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;unset GLOBIGNORE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -u nullglob failglob dotglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;work=$(mktemp -d)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;trap &amp;#39;rm -rf -- &amp;#34;$work&amp;#34;&amp;#39; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cd &amp;#34;$work&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;touch -- a.txt &amp;#39;two words.txt&amp;#39; .hidden.txt -option.txt
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;files=(./*.txt)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${#files[@]} -eq 3 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${files[2]} == &amp;#39;./two words.txt&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;printf &amp;#39;match &amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;${files[@]}&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;literal=(&amp;#39;*.txt&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;[[ ${literal[0]} == &amp;#39;*.txt&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;missing=(./*.missing)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${missing[0]} == &amp;#39;./*.missing&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;shopt -s nullglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;missing=(./*.missing)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${#missing[@]} -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;if (shopt -s failglob; printf &amp;#39;%s\n&amp;#39; ./*.missing) 2&amp;gt;/dev/null; then
&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;unexpected match\n&amp;#39; &amp;gt;&amp;amp;2
&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 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -s dotglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;files=(./*.txt)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${#files[@]} -eq 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;printf &amp;#39;PASS: spaces, literal, no-match policies, hidden file\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;Expect three initial match lines, with the spaced filename on one line, followed by the PASS message. The hidden file appears only in the final array after dotglob is enabled. The failing subshell is intentional and must not print an expanded filename. These assertions verify Bash expansion and argument preservation; they do not establish how an arbitrary downstream tool handles missing operands, directories, or options. Check that tool&amp;rsquo;s contract before replacing the harmless printf with a real operation.&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/Filename-Expansion.html"&gt;GNU Bash: Filename Expansion&lt;/a&gt;, matching rules and no-match options, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html"&gt;GNU Bash: Shell Expansions&lt;/a&gt;, ordering and changes to argument count, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Arrays.html"&gt;GNU Bash: Arrays&lt;/a&gt;, collecting matches and preserving elements, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A bare command the shell cannot find</title><link>https://www.xbcatm.com/posts/shell-command-not-found-path-search/</link><pubDate>Thu, 20 May 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-command-not-found-path-search/</guid><description>Command lookup depends on functions, builtins, cached paths, and PATH order. A slash bypasses the search; execution failures need separate diagnosis.</description><content:encoded>&lt;p&gt;“Command not found” identifies a lookup failure, but lookup does not always mean walking every directory in &lt;code&gt;PATH&lt;/code&gt;. In ordinary Bash, a command word without a slash can resolve to a function, builtin, cached executable path, or a newly searched executable. An alias may already have changed the command while the input was read. Inspect the shell that actually reported the failure before reinstalling a tool that may already exist.&lt;/p&gt;
&lt;h2 id="know-which-lookup-layer-answered"&gt;Know which lookup layer answered&lt;/h2&gt;
&lt;p&gt;After expansion, Bash checks functions and then builtins before searching for an external command. &lt;code&gt;type name&lt;/code&gt; describes the current resolution, and &lt;code&gt;type -a name&lt;/code&gt; exposes additional definitions or candidates. &lt;code&gt;command -v name&lt;/code&gt; is useful for testing availability in that shell, but its result need not be a filesystem path. It can identify a function or builtin, so do not blindly treat the output as a file to copy.&lt;/p&gt;
&lt;p&gt;For external commands, directories in &lt;code&gt;PATH&lt;/code&gt; are searched in order. An earlier executable can shadow a newer version later in the list. Bash also caches successful paths; &lt;code&gt;hash -t name&lt;/code&gt; inspects a cached entry and &lt;code&gt;hash -r&lt;/code&gt; clears the table. A changed file at an already cached location and a different path choice are separate issues. Starting a new terminal can hide this distinction by resetting shell state.&lt;/p&gt;
&lt;h2 id="a-slash-selects-a-path-directly"&gt;A slash selects a path directly&lt;/h2&gt;
&lt;p&gt;Any slash in the command word bypasses the external &lt;code&gt;PATH&lt;/code&gt; search. &lt;code&gt;./tool&lt;/code&gt; addresses a file in the current directory; &lt;code&gt;/opt/example/bin/tool&lt;/code&gt; addresses an absolute location. A bare &lt;code&gt;tool&lt;/code&gt; does not automatically search the current directory. Empty or relative entries in &lt;code&gt;PATH&lt;/code&gt; can make it do so, which allows the current working directory to change the selected executable.&lt;/p&gt;
&lt;p&gt;Use explicit, trusted directories when assembling a service&amp;rsquo;s environment. A working interactive terminal does not establish that cron, a service manager, or another user has the same path or startup files. Compare their actual environments and working directories. Changing a login configuration file will not update an already running process; apply the intended environment to the process that launches the command.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Bash normally reports &lt;code&gt;127&lt;/code&gt; when a command cannot be found and &lt;code&gt;126&lt;/code&gt; when a located command cannot be executed. These values guide investigation, but they are not a complete diagnosis: an existing script can fail because the interpreter named by its shebang is missing. Read the diagnostic and inspect the selected file&amp;rsquo;s permissions and interpreter instead of repeatedly appending directories.&lt;/p&gt;
&lt;p&gt;Do not “fix” lookup by adding writable project directories ahead of trusted tools or by using administrator privileges indiscriminately. First establish whether a function, an alias, or a cached location is responsible. &lt;code&gt;command name&lt;/code&gt; bypasses a same-named function in Bash; an explicit executable path bypasses path selection. Neither makes the selected program itself trustworthy or repairs its missing dependencies.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;This Bash example creates two disposable executables with the same name. Its temporary directories are the only additions to &lt;code&gt;PATH&lt;/code&gt;, and all changes stay inside the child shell. Cleanup names each file explicitly. No installed executable is modified.&lt;/p&gt;
&lt;p&gt;Setup uses &lt;code&gt;errexit&lt;/code&gt; to stop if fixture creation fails, then disables it before deliberately testing failures. On Bash 3.2.57, used to check this example, keeping that option enabled makes these execution-failure branches report &lt;code&gt;1&lt;/code&gt; instead of the ordinary &lt;code&gt;126&lt;/code&gt; and &lt;code&gt;127&lt;/code&gt;. Recording the shell version and options is therefore part of interpreting an unexpected status, not merely a reproducibility detail.&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 -eu
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_dir=$(mktemp -d)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;trap &amp;#39;rm -f &amp;#34;$demo_dir/one/demo-tool&amp;#34; &amp;#34;$demo_dir/two/demo-tool&amp;#34; &amp;#34;$demo_dir/blocked&amp;#34;; rmdir &amp;#34;$demo_dir/one&amp;#34; &amp;#34;$demo_dir/two&amp;#34; &amp;#34;$demo_dir&amp;#34;&amp;#39; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;mkdir &amp;#34;$demo_dir/one&amp;#34; &amp;#34;$demo_dir/two&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;#!/bin/sh\nprintf &amp;#34;one\\n&amp;#34;\n&amp;#39; &amp;gt; &amp;#34;$demo_dir/one/demo-tool&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;#!/bin/sh\nprintf &amp;#34;two\\n&amp;#34;\n&amp;#39; &amp;gt; &amp;#34;$demo_dir/two/demo-tool&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;chmod +x &amp;#34;$demo_dir/one/demo-tool&amp;#34; &amp;#34;$demo_dir/two/demo-tool&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;PATH=&amp;#34;$demo_dir/one:$demo_dir/two:$PATH&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;demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;hash -p &amp;#34;$demo_dir/two/demo-tool&amp;#34; demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;hash -r
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo-tool() { printf &amp;#39;function\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;demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;command demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;unset -f demo-tool
&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;$demo_dir/two/demo-tool&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;: &amp;gt; &amp;#34;$demo_dir/blocked&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;chmod 600 &amp;#34;$demo_dir/blocked&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;if &amp;#34;$demo_dir/blocked&amp;#34; 2&amp;gt;/dev/null; then
&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;unexpected execution\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;else
&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;not-executable=%s\n&amp;#39; &amp;#34;$?&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;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if &amp;#34;$demo_dir/missing&amp;#34; 2&amp;gt;/dev/null; then
&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;unexpected file\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;else
&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;missing=%s\n&amp;#39; &amp;#34;$?&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;fi
&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;The first three lines should be &lt;code&gt;one&lt;/code&gt;, &lt;code&gt;two&lt;/code&gt;, &lt;code&gt;one&lt;/code&gt;: normal search, an explicitly installed cache entry, and a fresh search after clearing it. Next come &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;one&lt;/code&gt;, and &lt;code&gt;two&lt;/code&gt;, distinguishing function dispatch, function bypass, and an explicit path. Finally, the nonexecutable file returns &lt;code&gt;126&lt;/code&gt; and the absent path returns &lt;code&gt;127&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This proves selection behavior with controlled files. It does not reproduce every operating system loader error or service environment. For a real incident, record the resolved command and its version in the failing environment, then verify the corrected invocation there. A successful lookup in an unrelated terminal is insufficient evidence that the original failure is repaired.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html"&gt;POSIX shell command search&lt;/a&gt;, command search rules and the not-found exit status, checked 2026-06-01.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Command-Search-and-Execution"&gt;Bash reference manual: command search and execution&lt;/a&gt;, function and builtin precedence plus cached executable paths, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>$@ and friends give a function its whole argument list</title><link>https://www.xbcatm.com/posts/shell-special-parameters-dollar-at/</link><pubDate>Fri, 16 Apr 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-special-parameters-dollar-at/</guid><description>Special parameters like $@, $#, and $* hand a shell construct its entire argument set, scope, and count without a loop over $1.</description><content:encoded>&lt;p&gt;A wrapper should pass the arguments it received without changing their boundaries. In Bash, &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; does that when used as command arguments: each positional parameter becomes a separate quoted word. This matters for empty values, spaces, newlines, and literal wildcard characters. A command that happens to work with two simple filenames can still be broken if its wrapper reconstructs the list as a single string.&lt;/p&gt;
&lt;h2 id="a-list-is-different-from-joined-text"&gt;A list is different from joined text&lt;/h2&gt;
&lt;p&gt;The positional parameters are the arguments of the current script or function. &lt;code&gt;$#&lt;/code&gt; reports their count, including arguments whose value is an empty string. &lt;code&gt;&amp;quot;$1&amp;quot;&lt;/code&gt; selects the first argument, while &lt;code&gt;&amp;quot;${10}&amp;quot;&lt;/code&gt; selects the tenth; braces distinguish that reference from the first argument followed by a zero. &lt;code&gt;$0&lt;/code&gt; identifies the invoked shell or script and is not an extra element of &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At a normal command boundary, &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; preserves each parameter as its own word. With no positional parameters, it produces no arguments. Quoted &lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; instead produces one string, joining parameter values with the first character of IFS. If IFS is unset, the join uses spaces; if it is empty, the join inserts no separator. The original spaces inside values remain, but the original boundaries cannot generally be recovered.&lt;/p&gt;
&lt;p&gt;Unquoted &lt;code&gt;$@&lt;/code&gt; is not an equivalent shorthand. Its values can undergo word splitting and filename expansion. A literal star supplied by the caller may suddenly match files in the wrapper&amp;rsquo;s directory. Adding quote characters to a joined string does not restore the lost structure. In Bash, save a list with &lt;code&gt;args=(&amp;quot;$@&amp;quot;)&lt;/code&gt; and forward it with &lt;code&gt;&amp;quot;${args[@]}&amp;quot;&lt;/code&gt; when it must survive later changes to the positional parameters.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A function receives its own positional parameters while it runs. Calling it with arguments replaces the visible list for that call, and returning restores the caller&amp;rsquo;s list. Using set or shift inside the function changes that function&amp;rsquo;s list. This does not mean every other shell setting is local to the function: changing IFS without a local declaration, for example, can affect the caller. Limit variable changes deliberately.&lt;/p&gt;
&lt;p&gt;Shift removes leading positional parameters and renumbers the remainder. Check that enough parameters remain before consuming an option and its value; shifting beyond the available count fails. A parser that only checks for a nonzero count can still mishandle an option requiring a second argument. Preserve the original list in an array if diagnostics or later forwarding require values already consumed by parsing.&lt;/p&gt;
&lt;p&gt;Quoting also cannot prevent the receiving command from interpreting an argument as an option. A filename equal to &lt;code&gt;--help&lt;/code&gt; remains one intact argument under &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt;, but a tool might treat it as a help request. Use the tool&amp;rsquo;s documented option terminator or a suitable pathname prefix when passing operands. The wrapper must decide where its own options end and where the downstream command&amp;rsquo;s options begin.&lt;/p&gt;
&lt;p&gt;For diagnostics, print the count and each argument separately. Joining everything with &lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; can make zero arguments, an empty argument, and arguments containing separators look misleadingly similar. Bash printf&amp;rsquo;s &lt;code&gt;%q&lt;/code&gt; is useful for showing otherwise invisible boundaries, but logging and forwarding serve different purposes. Forward the saved list itself; do not execute a reconstructed log line.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this complete Bash block. It verifies a four-element list containing an empty value, a spaced pathname, a literal star, and an option-like value. It then checks function scope, the difference between the two quoted expansions with no arguments, and an attempted shift beyond the list length. No files or external services are needed.&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;IFS=$&amp;#39; \t\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;set -- &amp;#39;&amp;#39; &amp;#39;report final.md&amp;#39; &amp;#39;*.log&amp;#39; &amp;#39;--help&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;saved=(&amp;#34;$@&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;verify() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $# -eq 4 &amp;amp;&amp;amp; $1 == &amp;#39;&amp;#39; &amp;amp;&amp;amp; $2 == &amp;#39;report final.md&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; [[ $3 == &amp;#39;*.log&amp;#39; &amp;amp;&amp;amp; $4 == &amp;#39;--help&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; printf &amp;#39;arg &amp;lt;%q&amp;gt;\n&amp;#39; &amp;#34;$@&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;verify &amp;#34;${saved[@]}&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;consume() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $# -eq 2 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $# -eq 1 &amp;amp;&amp;amp; $1 == child-two ]]
&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 -- replaced
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $1 == replaced ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;consume child-one child-two
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 4 &amp;amp;&amp;amp; $2 == &amp;#39;report final.md&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;joined=$(IFS=,; printf &amp;#39;%s&amp;#39; &amp;#34;$*&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;[[ $joined == &amp;#39;,report final.md,*.log,--help&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;count() { printf &amp;#39;%s&amp;#39; &amp;#34;$#&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 --
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(count &amp;#34;$@&amp;#34;) == 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;[[ $(count &amp;#34;$*&amp;#34;) == 1 ]]
&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 -- sole
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if shift 2; then
&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;unexpected shift success\n&amp;#39; &amp;gt;&amp;amp;2
&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 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == sole ]]
&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: forwarding, function scope, empty list, shift limit\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;Expect four escaped argument lines and the final PASS message. The first argument is explicitly empty, and the star remains literal. Returning from consume must leave the outer four-element list unchanged. A failed oversized shift must preserve the single remaining argument. These assertions test argument transport; a production wrapper still needs tests for its particular option grammar, required values, and downstream invocation.&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/Special-Parameters.html"&gt;GNU Bash: Special Parameters&lt;/a&gt;, positional-list expansions, joining, and counts, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Arrays.html"&gt;GNU Bash: Arrays&lt;/a&gt;, saving and forwarding separate elements, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html"&gt;GNU Bash: Word Splitting&lt;/a&gt;, unquoted expansions and empty arguments, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Exit status is a compact report of what went wrong</title><link>https://www.xbcatm.com/posts/shell-exit-status-reports-blur/</link><pubDate>Sat, 27 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-exit-status-reports-blur/</guid><description>Every command closes with a status: zero for success and any other number for failure, which is how a &amp;&amp; chain decides whether to keep going.</description><content:encoded>&lt;p&gt;An exit status is a small value that a shell uses to decide what runs next. Zero means success for shell control flow; a nonzero status takes the failure branch. That does not give every number a universal application meaning. A search with no matches, invalid input, and an unavailable server may deserve different handling even though all can produce nonzero results. Read the command&amp;rsquo;s contract before deciding what to retry or ignore.&lt;/p&gt;
&lt;h2 id="keep-the-status-attached-to-its-command"&gt;Keep the status attached to its command&lt;/h2&gt;
&lt;p&gt;Bash exposes the most recent foreground command&amp;rsquo;s status through &lt;code&gt;$?&lt;/code&gt;. Capture it before logging, testing another condition, or running cleanup. Those operations have statuses of their own and can replace the evidence you wanted. A function normally returns the status of its last executed command, so a successful final log message can accidentally turn an earlier failure into a successful function result.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;if command; then ... else ... fi&lt;/code&gt; when the command&amp;rsquo;s success determines the next action. In the else branch, save &lt;code&gt;$?&lt;/code&gt; immediately if the exact failure code matters. An explicit return or exit can then propagate that saved result after diagnostics. Avoid using &lt;code&gt;if ! command&lt;/code&gt; and expecting &lt;code&gt;$?&lt;/code&gt; inside the success branch to contain the original failure: the negation has already turned it into success.&lt;/p&gt;
&lt;p&gt;Bash uses status 127 when command lookup fails and 126 when it finds a command that cannot be executed. It represents fatal signal termination as 128 plus the signal number. These conventions help interpret shell reports, but an application can explicitly exit with the same number. A numerical status alone is therefore not proof of which internal event happened. Keep stderr and the identity of the failed operation alongside it.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A pipeline defaults to its last stage&amp;rsquo;s status. An upstream generator can fail while a downstream reader successfully consumes an empty or partial stream. With pipefail enabled, Bash selects the rightmost nonzero stage status, or zero if every stage succeeds. PIPESTATUS exposes each stage separately, but subsequent commands replace that array too. Capture the complete observation before running another command.&lt;/p&gt;
&lt;p&gt;Errexit, enabled by &lt;code&gt;set -e&lt;/code&gt;, has exceptions determined by syntax and calling context. Failures used as if conditions, certain commands in AND or OR lists, and other documented contexts do not automatically exit. A function invoked as a condition can continue after a failed command in its body. An ERR trap follows similar exclusions and is not a report of every possible failure. Neither mechanism proves a script is safe.&lt;/p&gt;
&lt;p&gt;Adding &lt;code&gt;|| true&lt;/code&gt; broadly discards useful distinctions. It may be appropriate when every failure from that exact operation is acceptable, but it can hide an unexpected permission error or an unavailable executable. Handle documented expected results specifically, and propagate unexpected ones. Likewise, &lt;code&gt;first &amp;amp;&amp;amp; second || recover&lt;/code&gt; runs recover when second fails too; it is not equivalent to an if statement that only tests first.&lt;/p&gt;
&lt;p&gt;Background launch has another boundary: successfully starting a job does not mean the job completed successfully. Save its identifier from &lt;code&gt;$!&lt;/code&gt; and inspect wait&amp;rsquo;s result for that job. If a script starts several jobs, retain each identifier and wait for each required result. Otherwise a final successful foreground command can mask work that failed after being launched.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;The following complete Bash experiment uses intentional failures and checks their exact propagation. It demonstrates immediate capture, negation, a function called in a conditional context, and a background job that fails after launch. It does not contact external services or require any files.&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;fail_seven() { return 7; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if fail_seven; then
&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 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; saved=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&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;saved status %s\n&amp;#39; &amp;#34;$saved&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;[[ $saved -eq 7 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $? -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;if ! fail_seven; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; negated=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $negated -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;continued=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;conditional_function() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; false
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; continued=yes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if conditional_function; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $continued == yes ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;else
&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 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&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 6) &amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;job_pid=$!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if wait &amp;#34;$job_pid&amp;#34;; then
&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 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; job_status=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $job_status -eq 6 ]]
&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: saved failure, negation, conditional context, wait\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;Expect a saved status of seven and the final PASS message. The negated status is zero, and the conditional function reaches its assignment despite errexit being set. Wait must report the child&amp;rsquo;s six. These assertions expose control-flow boundaries; they do not assign meanings to an arbitrary tool&amp;rsquo;s codes. When adopting the pattern, test both expected negative results and unexpected operational failures, and verify that the wrapper exits with the result its caller needs.&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/s/bash/manual/html_node/Exit-Status.html"&gt;GNU Bash: Exit Status&lt;/a&gt;, shell status conventions, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/The-Set-Builtin.html"&gt;GNU Bash: The Set Builtin&lt;/a&gt;, errexit exclusions, checked 2026-09-06.&lt;/li&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;, aggregate pipeline results, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A shell function reads its arguments like a script</title><link>https://www.xbcatm.com/posts/shell-functions-borrow-positional-parameters/</link><pubDate>Tue, 16 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-functions-borrow-positional-parameters/</guid><description>Inside a function, $1, $2 and friends refer to that function's own arguments, giving grouped reuse without global expectations.</description><content:encoded>&lt;p&gt;A shell function groups commands under a name and receives its own positional arguments. In an ordinary call, its body runs in the current shell rather than automatically starting a separate process. That makes functions useful for reusable operations, but it also means assignments, directory changes, and other shell state can affect the caller. A function needs an explicit contract for inputs, output, status, and any intentional side effects.&lt;/p&gt;
&lt;h2 id="arguments-belong-to-each-invocation"&gt;Arguments belong to each invocation&lt;/h2&gt;
&lt;p&gt;Calling &lt;code&gt;demo_func first second&lt;/code&gt; makes those two values available as &lt;code&gt;$1&lt;/code&gt; and &lt;code&gt;$2&lt;/code&gt; inside that invocation, with &lt;code&gt;$#&lt;/code&gt; equal to two. Another function call temporarily installs its own list. When it returns, the caller&amp;rsquo;s list is restored. Using &lt;code&gt;shift&lt;/code&gt; within the callee therefore does not consume the caller&amp;rsquo;s positional arguments. In Bash, &lt;code&gt;$0&lt;/code&gt; does not change to the function name.&lt;/p&gt;
&lt;p&gt;Forward the list with &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; so empty arguments, spaces, and wildcard characters retain their boundaries. Validate the argument count before accessing required positions, and distinguish an omitted argument from a deliberately empty value. A function has no parameter declaration that automatically enforces those rules.&lt;/p&gt;
&lt;h2 id="local-variables-have-dynamic-scope"&gt;Local variables have dynamic scope&lt;/h2&gt;
&lt;p&gt;Bash&amp;rsquo;s &lt;code&gt;local&lt;/code&gt; builtin creates a variable that shadows a name in an outer scope. When the function returns, that previous binding becomes visible again. Declare temporary values and loop counters locally when modifying the caller&amp;rsquo;s variable is not part of the interface. Portable shell code must not assume every interpreter implements Bash&amp;rsquo;s local-variable facilities.&lt;/p&gt;
&lt;p&gt;However, Bash uses dynamic scope: a function called from another function can see the caller&amp;rsquo;s local variables. If it assigns to such a name without declaring its own local, it can modify the caller&amp;rsquo;s local binding. The outer global remains protected, but the calling function&amp;rsquo;s value changes. Local variables are not lexically private objects inaccessible to all other functions.&lt;/p&gt;
&lt;p&gt;Local declarations also do not automatically restore the working directory, shell options, traps, or files modified by the body. Those effects need their own contract. Running a function inside ordinary command substitution instead gives it a subshell environment: its variable and directory changes do not propagate back, although file writes still can. Capturing output is not a general sandbox.&lt;/p&gt;
&lt;h2 id="status-and-data-use-different-channels"&gt;Status and data use different channels&lt;/h2&gt;
&lt;p&gt;A function&amp;rsquo;s status is the last executed command&amp;rsquo;s status unless an explicit &lt;code&gt;return&lt;/code&gt; changes it. Use standard output for the documented payload and standard error for diagnostics. &lt;code&gt;return 7&lt;/code&gt; reports a status to the caller; it does not return the text seven as data. &lt;code&gt;exit&lt;/code&gt; terminates the executing shell rather than merely returning from the function.&lt;/p&gt;
&lt;p&gt;Separate a local declaration from a command-substitution assignment when the producer&amp;rsquo;s status matters. In &lt;code&gt;local result=$(producer)&lt;/code&gt;, the observed status belongs to the declaration builtin and can hide the producer&amp;rsquo;s failure. A subsequent assignment without the declaration has the substitution&amp;rsquo;s status. Capture that result before logging or cleanup changes it.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;An unlocalized counter can corrupt a calling loop, including one using a local variable of its own. Tests should call helpers from another function as well as from top-level code. A global-only test misses the dynamic-scope interaction. An unexpected progress message on standard output similarly corrupts callers that capture a single promised value.&lt;/p&gt;
&lt;p&gt;Do not rely on &lt;code&gt;set -e&lt;/code&gt; to make a function fail predictably at every failed command. Its effect depends on calling context; a function used as an &lt;code&gt;if&lt;/code&gt; condition can continue after failures inside its body. Explicitly check operations whose failure must stop subsequent work, and propagate the required status after any diagnostics.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this complete block in Bash. It changes only variables and positional arguments inside a fresh shell. The worker intentionally modifies its caller&amp;rsquo;s local variable, while the second function demonstrates argument preservation and a nonzero return. The final pair contrasts combined declaration with separate assignment.&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;&lt;span class="nv"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&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;demo_value=global
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_worker() { demo_value=changed; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_scope() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local demo_value=inner
&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;local-before=%s\n&amp;#39; &amp;#34;$demo_value&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; demo_worker
&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;local-after=%s\n&amp;#39; &amp;#34;$demo_value&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_scope
&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;global-after=%s\n&amp;#39; &amp;#34;$demo_value&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;demo_args() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local demo_arg
&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;function-count=%s\n&amp;#39; &amp;#34;$#&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; for demo_arg in &amp;#34;$@&amp;#34;; do printf &amp;#39;argument=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$demo_arg&amp;#34;; done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift
&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;after-shift=%s\n&amp;#39; &amp;#34;$#&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; return 7
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&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 -- outer-one outer-two
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if demo_args &amp;#39;&amp;#39; &amp;#39;two words&amp;#39;; then
&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 99
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; demo_status=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&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;function-status=%s outer-count=%s outer-first=%s\n&amp;#39; &amp;#34;$demo_status&amp;#34; &amp;#34;$#&amp;#34; &amp;#34;$1&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;demo_masked() { local demo_result=$(return 9); }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_checked() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local demo_result
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; demo_result=$(return 9)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_masked
&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;combined-declaration=%s\n&amp;#39; &amp;#34;$?&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;if demo_checked; then exit 99; else printf &amp;#39;separate-assignment=%s\n&amp;#39; &amp;#34;$?&amp;#34;; fi
&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;Expect the local value to change from &lt;code&gt;inner&lt;/code&gt; to &lt;code&gt;changed&lt;/code&gt;, but the global value to remain &lt;code&gt;global&lt;/code&gt;. Both supplied arguments print separately, including the empty one. The function returns seven after shifting its list; the outer shell still has two arguments beginning with &lt;code&gt;outer-one&lt;/code&gt;. Finally, the combined declaration reports zero while the separate assignment reports nine. These results test the actual calling boundaries rather than assuming every variable called local is isolated.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Shell-Functions"&gt;Bash shell functions&lt;/a&gt;, function argument lists and dynamic variable scope, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Bash-Builtins"&gt;Bash-specific builtins&lt;/a&gt;, local declaration behavior and return status, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#The-Set-Builtin"&gt;Bash set builtin&lt;/a&gt;, errexit behavior when a function is used as a condition, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Command substitution puts one command's output into another line</title><link>https://www.xbcatm.com/posts/shell-command-substitution-embeds-output/</link><pubDate>Wed, 03 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-command-substitution-embeds-output/</guid><description>Wrapping a command in $(...) lets its stdout become arguments or words elsewhere, so scripts compose by borrowing another command's result at runtime.</description><content:encoded>&lt;p&gt;Command substitution captures a command&amp;rsquo;s standard output and inserts the resulting text into a surrounding expression. The familiar &lt;code&gt;$(command)&lt;/code&gt; form is useful for a timestamp, a computed label, or another small textual value. It is not a transparent channel for arbitrary bytes or a reliable way to transport a list of filenames. Its newline handling, execution environment, and exit status are part of the interface you need to understand.&lt;/p&gt;
&lt;h2 id="capture-text-then-decide-how-to-use-it"&gt;Capture text, then decide how to use it&lt;/h2&gt;
&lt;p&gt;For the standard substitution form discussed here, Bash executes the enclosed commands in a subshell environment. It captures standard output and removes all trailing newline characters. Newlines inside the output remain. Double quotes prevent later word splitting and filename expansion, but they do not undo the removal of trailing newlines. If the exact final bytes matter, use a file or a stream instead of a shell variable.&lt;/p&gt;
&lt;p&gt;A variable assignment such as &lt;code&gt;result=$(producer)&lt;/code&gt; stores the resulting text without splitting it into arguments. When using the value later, write &lt;code&gt;&amp;quot;$result&amp;quot;&lt;/code&gt; to preserve its boundary. Similarly, &lt;code&gt;consumer &amp;quot;$(producer)&amp;quot;&lt;/code&gt; supplies a single argument, even when the captured result is empty. Leaving the substitution unquoted can create several arguments or no arguments, and stars in the output may match filenames in the working directory.&lt;/p&gt;
&lt;p&gt;Standard error is separate from standard output unless you explicitly redirect it. A diagnostic can remain visible on the terminal while the captured value contains only the result. Redirecting both streams together may contaminate a machine-readable value with warning text. Design the producer&amp;rsquo;s output contract before deciding whether those streams belong together; suppressing every diagnostic can hide the reason a value is missing.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The subshell has its own variable changes and working directory. Assigning a variable or running cd inside the substitution does not update the caller&amp;rsquo;s corresponding state. Filesystem and network side effects still happen, however: creating a file inside the substitution creates a real file. Process isolation does not make an operation reversible or remove its external effects.&lt;/p&gt;
&lt;p&gt;Successful capture also says nothing about whether the producer succeeded. An assignment-only command gets the status of its last command substitution, so &lt;code&gt;if result=$(producer); then&lt;/code&gt; can check the result. In contrast, the status of &lt;code&gt;printf '%s' &amp;quot;$(producer)&amp;quot;&lt;/code&gt; normally describes printf. A producer can emit partial output and then fail; retain its status immediately and decide whether that output is usable. A later logging command overwrites the value represented by &lt;code&gt;$?&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Avoid relying on an implicit failure somewhere inside a multi-command substitution. Its final command can return success even after an earlier command failed. Explicitly test important operations in the producer and return the intended status. Likewise, in a function, separate a local declaration from the capturing assignment when you need to inspect the producer&amp;rsquo;s status; a declaration builtin can mask that status.&lt;/p&gt;
&lt;p&gt;Substitution also collects output before the surrounding command runs, so it is unsuitable for unbounded output or interactive streaming. Do not use an unquoted substitution over find output to construct an arbitrary pathname list: whitespace and wildcard characters lose their original meaning. Pass filenames through an interface that retains boundaries, such as find&amp;rsquo;s exec facility, or use a deliberately specified record protocol.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this complete block with Bash. It uses only shell builtins and asserts the output and status independently. No files or services need to be prepared. The producer with status seven is an intentional failure, and the test must still reach the final PASS line.&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;outer=parent
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;captured=$(outer=child; printf &amp;#39;first\nsecond\n\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;[[ $outer == parent ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $captured == $&amp;#39;first\nsecond&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;printf &amp;#39;captured &amp;lt;%q&amp;gt;\n&amp;#39; &amp;#34;$captured&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;if partial=$(printf &amp;#39;incomplete&amp;#39;; exit 7); then
&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;unexpected producer success\n&amp;#39; &amp;gt;&amp;amp;2
&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 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; status=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $status -eq 7 &amp;amp;&amp;amp; $partial == incomplete ]]
&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;producer status %s; partial &amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$status&amp;#34; &amp;#34;$partial&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;%s&amp;#39; &amp;#34;$(exit 9)&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;[[ $? -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;IFS=$&amp;#39; \t\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;set -- &amp;#34;$(printf &amp;#39;two words\n&amp;#39;)&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;two words&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 -- &amp;#34;$(printf &amp;#39;\n\n&amp;#39;)&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;&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;printf &amp;#39;PASS: capture, subshell scope, failure status, empty result\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;The escaped captured value should contain one internal newline and no trailing newline. The next line reports status seven alongside the partial text. The deliberately successful outer printf demonstrates why checking only the consumer&amp;rsquo;s status misses producer failure. These checks establish the behavior of the small synchronous producers shown here. A real producer still needs its own timeout, output-format validation, and explicit error contract where those requirements apply.&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/Command-Substitution.html"&gt;GNU Bash: Command Substitution&lt;/a&gt;, standard substitution scope and newline handling, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html"&gt;GNU Bash: Word Splitting&lt;/a&gt;, treatment of unquoted captured output, 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 status exposed by the question-mark parameter, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A here-string pipes text into a command's stdin</title><link>https://www.xbcatm.com/posts/shell-here-string-feeds-stdin/</link><pubDate>Mon, 15 Feb 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-here-string-feeds-stdin/</guid><description>A here-string like &lt;&lt;&lt; 'value' sends a single piece of text as stdin, turning many utilities that would read a file into one-line filters.</description><content:encoded>&lt;p&gt;A Bash here-string supplies the expanded value of one word to a command&amp;rsquo;s standard input, with a newline appended. It is convenient when a small textual value already exists in a variable and the receiving tool reads stdin. The syntax removes the need for a separate printing command, but it does not promise byte-for-byte identity with the variable or a particular implementation using memory, a pipe, or a file.&lt;/p&gt;
&lt;h2 id="expansion-happens-before-the-input-is-read"&gt;Expansion happens before the input is read&lt;/h2&gt;
&lt;p&gt;The word after the triple-less-than operator can contain parameter expansion, arithmetic expansion, or command substitution. It is therefore not necessarily a literal constant. Single quotes preserve literal source text, while double quotes permit the usual variable and command expansions. Text obtained by expanding a variable is not recursively executed as shell code merely because it contains a dollar sign and parentheses.&lt;/p&gt;
&lt;p&gt;In current Bash, a here-string&amp;rsquo;s word does not undergo word splitting or filename expansion. It is delivered as a single string. Quoting a variable in this position still makes the intended boundary easy to read, but do not confuse this redirection context with passing an unquoted variable as a normal command argument. The latter can split the value and expand wildcard characters into filenames.&lt;/p&gt;
&lt;p&gt;The appended newline is unconditional. A value without a final newline gains one; a value already ending in a newline gains another. An empty variable consequently produces a one-byte input containing a newline, rather than immediate end-of-file. This makes here-strings natural for many line-oriented tools and unsuitable when the receiving protocol requires an exact length or a specific final byte.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Read applies its own interpretation after the shell constructs the input. By default, its field splitting can remove surrounding IFS whitespace, and backslashes can act as escapes. For one line whose spaces and backslashes must be retained, use &lt;code&gt;IFS= read -r name&lt;/code&gt;. This protects the line&amp;rsquo;s content, but a single read still stops at the first newline. It does not capture an arbitrary multiline value into that variable.&lt;/p&gt;
&lt;p&gt;A redirected builtin or loop can run in the current shell, so its assignments remain visible. That is useful compared with a loop placed in a pipeline, where Bash normally runs the stage in a subshell. The input redirection itself does not create a subshell around the command. An explicit subshell or command substitution elsewhere in the expression still has its usual scope and effects.&lt;/p&gt;
&lt;p&gt;The absence of an explicit filename in the script does not establish how Bash stores the redirection internally. Avoid code that depends on stdin being seekable, on a fixed buffering capacity, or on there being no temporary backing storage. For large existing files, direct file redirection avoids first collecting the whole input in a shell variable. For exact textual bytes, a carefully formatted printf or a file can avoid the added newline.&lt;/p&gt;
&lt;p&gt;Here-strings are also a shell-language feature, not portable syntax for every interpreter called sh. A script using them should select an interpreter that supports them, and tests should invoke that interpreter explicitly. Conversely, changing an echo pipeline to a here-string can alter both newline behavior and variable scope. Compare the consumer&amp;rsquo;s observed bytes and state changes rather than treating the forms as interchangeable typography.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;This complete fixture needs Bash and Python 3. Python displays stdin as hexadecimal so appended newline bytes remain visible; capturing that hexadecimal output does not lose the input bytes being inspected. The remaining checks use read and a redirected loop to test whitespace preservation, literal text, and multiline consumption. No fixture files are required.&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;hex_input() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; python3 -c &amp;#39;import sys; print(sys.stdin.buffer.read().hex())&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(hex_input &amp;lt;&amp;lt;&amp;lt; X) == 580a ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(hex_input &amp;lt;&amp;lt;&amp;lt; &amp;#39;&amp;#39;) == 0a ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(hex_input &amp;lt;&amp;lt;&amp;lt; $&amp;#39;X\n&amp;#39;) == 580a0a ]]
&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;%s&amp;#39; X | hex_input) == 58 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;value=&amp;#39; a\b *.txt &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;IFS= read -r received &amp;lt;&amp;lt;&amp;lt; &amp;#34;$value&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;[[ $received == &amp;#34;$value&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;literal=&amp;#39;$(printf injected)&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;IFS= read -r received &amp;lt;&amp;lt;&amp;lt; &amp;#34;$literal&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;[[ $received == &amp;#39;$(printf injected)&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;value=$&amp;#39;one\ntwo&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;IFS= read -r first &amp;lt;&amp;lt;&amp;lt; &amp;#34;$value&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;[[ $first == one ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;lines=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;while IFS= read -r line; do
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; lines=$((lines + 1))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;done &amp;lt;&amp;lt;&amp;lt; &amp;#34;$value&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;[[ $lines -eq 2 ]]
&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: newline bytes, preserved text, single read, loop scope\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;Expect only the PASS line. The hexadecimal assertions distinguish one newline from two and compare a here-string with printf that adds none. The literal command-looking text must remain unchanged, and the loop count must survive in the outer shell. These results test the documented input behavior, not the internal storage mechanism. Before using the pattern with another reader, check how that reader handles empty lines, multiple records, and trailing whitespace of its own.&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/s/bash/manual/html_node/Redirections.html"&gt;GNU Bash: Redirections&lt;/a&gt;, here-string expansion and appended newline, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/Bash-Builtins.html"&gt;GNU Bash: Bash Builtins&lt;/a&gt;, read delimiters and raw-input mode, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html"&gt;GNU Bash: Command Substitution&lt;/a&gt;, captured output and trailing-newline removal, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>case runs one branch that matches a pattern</title><link>https://www.xbcatm.com/posts/shell-case-branches-match-patterns/</link><pubDate>Sat, 30 Jan 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-case-branches-match-patterns/</guid><description>The case statement compares a word against patterns in order and runs the first branch whose pattern matches, a tidy way to fan out by value.</description><content:encoded>&lt;p&gt;A case statement routes one value through an ordered list of shell patterns. It is useful for a command dispatcher, a small set of accepted answers, or filename-suffix classification. Each branch states the values it handles, and several patterns can share one body. The ordering and the default branch are part of the interface: they decide what happens when an input overlaps several categories or belongs to none.&lt;/p&gt;
&lt;h2 id="patterns-match-the-whole-value"&gt;Patterns match the whole value&lt;/h2&gt;
&lt;p&gt;Case uses shell patterns, not regular expressions. A star matches any string, a question mark matches one character, and brackets specify a character set or range. Thus &lt;code&gt;[0-9]*&lt;/code&gt; means a digit followed by anything; it does not mean an entire nonempty string of digits. To validate a decimal identifier without arithmetic interpretation, first reject an empty string and any string containing a non-digit.&lt;/p&gt;
&lt;p&gt;The alternatives in &lt;code&gt;start|up|run)&lt;/code&gt; share an action. A broad suffix pattern such as &lt;code&gt;*.log)&lt;/code&gt; can classify a string without opening a file or searching the directory. Unlike pathname expansion, this matching does not establish that the named resource exists. A slash or leading dot also does not have the special exclusion role it has during ordinary filename expansion. Classification and filesystem validation remain separate operations.&lt;/p&gt;
&lt;p&gt;The word being tested undergoes the documented expansions, but Bash does not word-split or glob it as ordinary command arguments. An empty unquoted variable in that position does not delete the syntactic word and cause a case syntax error. Quoting the tested value is still clear style. With nounset enabled, separately handle a missing positional parameter or validate the argument count; quotation alone does not make an unset parameter available.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;With the usual double-semicolon terminator, the first matching branch runs and case finishes. Put exact values before broader families and the catch-all star last. Otherwise a generic option branch can swallow a later exact status command. Syntax checking cannot establish that your ordering expresses the intended precedence; exercise an input matching both patterns and assert which action runs.&lt;/p&gt;
&lt;p&gt;If no branch matches, case returns zero without running an action. A missing default is therefore not automatically an error. A dispatcher that rejects unknown commands must explicitly report and return failure in its default branch. Conversely, an optional classification may deliberately allow no match. Decide that policy from the caller&amp;rsquo;s contract, rather than assuming case supplies input validation on its own.&lt;/p&gt;
&lt;p&gt;Quoting a pattern changes its meaning. A quoted &lt;code&gt;&amp;quot;*.log&amp;quot;&lt;/code&gt; matches the literal characters, whereas the unquoted pattern matches a suffix family. The same distinction matters when a variable supplies a pattern: decide whether it is a trusted matching expression or literal text. Do not accidentally grant wildcard behavior to an identifier that should be compared exactly.&lt;/p&gt;
&lt;p&gt;Bash options and versions introduce further boundaries. Nocasematch changes case sensitivity, while extended patterns need extglob enabled before the relevant code is parsed. Newer Bash also provides branch terminators that continue execution or resume matching; do not assume they behave like double semicolons or work in older shells. The experiment below deliberately uses ordinary patterns and double semicolons compatible with Bash 3.2.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this complete Bash block. Its dispatcher includes exact commands, grouped aliases, a wildcard family, an explicit empty-input error, and an unknown-input error. A second function checks decimal text without accepting a digit followed by letters. The assertions test results and error codes rather than merely looking for a successful script exit.&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;export LC_ALL=C
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -u nocasematch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;route() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; case &amp;#34;${1-}&amp;#34; in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; status|--status) printf &amp;#39;%s&amp;#39; status ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; start|up|run) printf &amp;#39;%s&amp;#39; start ;;
&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;#39;&amp;#39;) return 2 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; --*) return 3 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; *.log) printf &amp;#39;%s&amp;#39; log ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; *) return 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; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(route --status) == status ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(route up) == start ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(route &amp;#39;report final.log&amp;#39;) == log ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local expected=$1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; if route &amp;#34;$@&amp;#34;; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; return 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local actual=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $actual -eq $expected ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error 2 &amp;#39;&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;expect_error 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error 3 --unknown
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error 4 STATUS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;digits() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; case &amp;#34;${1-}&amp;#34; in
&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;#39;&amp;#39;|*[!0-9]*) return 1 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; *) return 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; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;digits 007
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if digits 7abc; then exit 1; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if digits &amp;#39;&amp;#39;; then exit 1; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;empty=&amp;#39;&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;matched=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;case $empty in &amp;#39;&amp;#39;) matched=yes ;; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $matched == yes ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;case unmatched in known) exit 1 ;; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $? -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;PASS: precedence, aliases, empty input, validation, no match\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;Expect the PASS line with no earlier output. The exact status option must bypass the generic option error, and uppercase STATUS must be rejected because case-insensitive matching is disabled. The final checks demonstrate that an empty expanded word is valid and that no match returns success. These tests cover routing decisions only; replace the harmless actions with real operations only after testing their arguments, side effects, and failure propagation separately.&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/Conditional-Constructs.html"&gt;GNU Bash: Conditional Constructs&lt;/a&gt;, case ordering, expansion, and return status, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/Pattern-Matching.html"&gt;GNU Bash: Pattern Matching&lt;/a&gt;, wildcard and extended-pattern semantics, checked 2026-09-06.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html"&gt;GNU Bash: Word Splitting&lt;/a&gt;, contexts that split expanded values, checked 2026-09-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell traps turn a signal into a cleanup step</title><link>https://www.xbcatm.com/posts/shell-signal-traps-run-on-the-way-out/</link><pubDate>Tue, 12 Jan 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/shell-signal-traps-run-on-the-way-out/</guid><description>Bash traps can route handled signals through shared cleanup, but exit status, child processes, and uncatchable termination still need explicit treatment.</description><content:encoded>&lt;p&gt;A trap lets a shell arrange an action for a signal or a shell event. It is useful for removing temporary files and coordinating shutdown, but it does not make interruption atomic or guarantee cleanup under every termination. In Bash, &lt;code&gt;trap&lt;/code&gt; is a builtin, and &lt;code&gt;EXIT&lt;/code&gt; is a shell event rather than an operating system signal. A reliable design names the exits it handles and preserves evidence of why execution stopped.&lt;/p&gt;
&lt;h2 id="separate-a-signal-handler-from-the-exit-handler"&gt;Separate a signal handler from the exit handler&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;trap 'cleanup' INT TERM&lt;/code&gt; replaces the default action for those signals. If the handler simply returns, the shell can continue executing; installing a handler does not inherently mean shutdown. One clear Bash pattern has the signal handlers explicitly exit, with a shared &lt;code&gt;EXIT&lt;/code&gt; handler doing cleanup. Normal completion and explicit &lt;code&gt;exit&lt;/code&gt; also reach that handler.&lt;/p&gt;
&lt;p&gt;Returning from a function or a sourced file is not the same as terminating the shell. A top-level &lt;code&gt;return&lt;/code&gt; in an ordinary executed script is not a general exit mechanism. Nor should a script assume that an unhandled fatal signal will always trigger &lt;code&gt;EXIT&lt;/code&gt; across shells. Handle the intended termination signals explicitly and test with the actual interpreter used by the service.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;trap - INT&lt;/code&gt; restores the default action; &lt;code&gt;trap '' INT&lt;/code&gt; ignores the signal. They are different policies. Noninteractive shells can also inherit ignored signals that they cannot reset or trap, so launcher behavior matters. Inspect registered actions with &lt;code&gt;trap -p&lt;/code&gt; when diagnosis requires confirming what was installed.&lt;/p&gt;
&lt;h2 id="preserve-the-reason-for-leaving"&gt;Preserve the reason for leaving&lt;/h2&gt;
&lt;p&gt;At entry to the exit handler, save &lt;code&gt;$?&lt;/code&gt; before another command overwrites it. Cleanup that ends with an unconditional &lt;code&gt;exit 1&lt;/code&gt; converts successful work into failure and hides distinct error codes. Exiting with the saved value preserves the initiating outcome. Cleanup failures still need visible diagnostics and a deliberate policy, especially if the main work succeeded.&lt;/p&gt;
&lt;p&gt;The example chooses 130 for handled INT and 143 for handled TERM, following Bash&amp;rsquo;s familiar signal-status convention. Those are explicit normal exits, not proof that the process was actually killed by a signal. A supervisor using the process API can distinguish the two. Here, disabling the exit trap and ignoring INT and TERM during the short cleanup prevents those handlers from reentering it.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;SIGKILL cannot be caught, and SIGSTOP cannot be caught or used as a cleanup opportunity. Power loss, crashes, and forced termination can leave resources behind. A trap is therefore not a replacement for crash-safe writes, transactional updates, or a separately defined recovery procedure. Restrict removal to paths the script owns; a partially initialized variable must never become a broad deletion target.&lt;/p&gt;
&lt;p&gt;Timing also matters. Bash can defer a trapped signal while waiting for a synchronous foreground command to finish. When it is waiting through the &lt;code&gt;wait&lt;/code&gt; builtin for an asynchronous command, a trapped signal instead interrupts that wait before the handler runs. Registering a trap does not establish an immediate shutdown deadline for arbitrary blocking work.&lt;/p&gt;
&lt;p&gt;Terminal Ctrl-C sends INT to the foreground process group, whereas signaling one PID targets that process. A child may therefore see the signal in one test but not the other. Scripts managing workers must define signal forwarding, waiting, and ownership of those workers. Killing unrelated processes or treating the shell&amp;rsquo;s exit as proof that every descendant stopped is not cleanup.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this with Python 3, Bash, and standard Unix utilities. Each case starts a separate Bash process that creates one file in a private temporary directory. Signals target only that new shell itself. Python owns the outer directory and removes the deliberately retained KILL artifact afterward.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;signal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;demo_file=$1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;cleanup() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; demo_status=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; trap - EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; trap &amp;#39;&amp;#39; INT TERM
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; rm -f -- &amp;#34;$demo_file&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; printf &amp;#39;cleanup=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;\n&amp;#39; &amp;#34;$demo_status&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; exit &amp;#34;$demo_status&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;trap cleanup EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;trap &amp;#39;exit 130&amp;#39; INT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;trap &amp;#39;exit 143&amp;#39; TERM
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;: &amp;gt; &amp;#34;$demo_file&amp;#34; || exit 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;case &amp;#34;$2&amp;#34; in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; normal) exit 7 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; *) kill &amp;#34;-$2&amp;#34; &amp;#34;$$&amp;#34; ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;exit 99
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash-trap-demo-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;normal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;TERM&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;INT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;KILL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--noprofile&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--norc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;trap-demo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cleanup=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;remains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;KILL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;KILL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: rc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cleaned=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; remains=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;remains&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Expect return codes 7, 143, and 130 with cleanup observed and no remaining file. KILL yields Python&amp;rsquo;s negative signal return code, normally &lt;code&gt;-9&lt;/code&gt;, with no cleanup output and a remaining file. An invoking shell would conventionally report 137 instead. The contrast verifies preserved status and the uncatchable boundary; it does not test worker forwarding, terminal process groups, or delayed traps around long-running commands.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Signals"&gt;Bash signals&lt;/a&gt;, signal handling while waiting and foreground process groups, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#trap"&gt;POSIX trap&lt;/a&gt;, exit events, inherited signal dispositions, and trap actions, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>