<?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>Scripting · XB Field Notes</title><link>https://www.xbcatm.com/tags/scripting/</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/scripting/index.xml" rel="self" type="application/rss+xml"/><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>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>$@ 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></channel></rss>