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