<?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>Bash · XB Field Notes</title><link>https://www.xbcatm.com/tags/bash/</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/bash/index.xml" rel="self" type="application/rss+xml"/><item><title>Bash history is your searchable command log</title><link>https://www.xbcatm.com/posts/linux-bash-history-session/</link><pubDate>Mon, 28 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/linux-bash-history-session/</guid><description>Bash history supports command recall, but session memory, disk persistence, filters, and explicit imports determine what remains searchable.</description><content:encoded>&lt;p&gt;A useful command history saves retyping, but it is not a complete record of everything executed. Bash normally enables history in interactive shells, and settings can exclude entries or limit retention. The searchable list in a running shell and the history file on disk are separate states. That distinction explains why a command may be visible in one terminal, absent in another, and lost when an unwritten session ends unexpectedly.&lt;/p&gt;
&lt;h2 id="session-memory-differs-from-the-file"&gt;Session memory differs from the file&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HISTSIZE&lt;/code&gt; limits the in-memory history entries; &lt;code&gt;HISTFILESIZE&lt;/code&gt; limits the saved file&amp;rsquo;s lines. Multiline commands and timestamp records mean that line counts and command counts are not interchangeable. &lt;code&gt;HISTFILE&lt;/code&gt; selects the file, normally the user&amp;rsquo;s Bash history file. An unset or empty value prevents ordinary saving, and an unwritable location can leave apparently healthy in-memory recall with no persistent record.&lt;/p&gt;
&lt;p&gt;Bash records command text before parameter expansion but after history expansion, subject to &lt;code&gt;HISTCONTROL&lt;/code&gt; and &lt;code&gt;HISTIGNORE&lt;/code&gt;. A saved line containing a variable therefore does not necessarily reveal the value used during the original execution. Current directories, aliases, functions, environment values, and external files can all change before a command is recalled. History preserves useful input, not a reproducible execution environment or a trustworthy audit trail.&lt;/p&gt;
&lt;h2 id="appending-does-not-refresh-other-terminals"&gt;Appending does not refresh other terminals&lt;/h2&gt;
&lt;p&gt;On normal exit, Bash saves history; &lt;code&gt;histappend&lt;/code&gt; selects appending rather than overwriting the file. Without earlier writes, abnormal termination can lose all unsaved entries, not merely the final command. &lt;code&gt;history -a&lt;/code&gt; appends newly entered lines that this session has not already appended. Saving before exit reduces that loss window, but does not promise durable storage through every system failure.&lt;/p&gt;
&lt;p&gt;Another running shell has its own list. It needs an import such as &lt;code&gt;history -n&lt;/code&gt; to read newly appended lines; enabling &lt;code&gt;histappend&lt;/code&gt; alone does not make another terminal&amp;rsquo;s searches update live. &lt;code&gt;history -r&lt;/code&gt; reads the entire file into the existing list and can introduce duplicates. &lt;code&gt;history -w&lt;/code&gt; replaces the file with the current list and can discard other sessions&amp;rsquo; records. These operations should not be treated as interchangeable synchronization commands.&lt;/p&gt;
&lt;p&gt;If append and import operations are integrated into &lt;code&gt;PROMPT_COMMAND&lt;/code&gt;, preserve existing prompt hooks and any use of the preceding command&amp;rsquo;s status. Replacing a configured hook wholesale can break other shell behavior. Concurrent sessions also do not create a reliable global execution order merely by sharing a file. Keep separate operational logs when ordering, exit status, or accountability matters.&lt;/p&gt;
&lt;h2 id="search-inspect-then-execute"&gt;Search, inspect, then execute&lt;/h2&gt;
&lt;p&gt;In the usual Emacs-style Readline bindings, Ctrl-R searches backward incrementally, and pressing it again finds older matches. Use an editing key to leave the search and inspect the selected line before running it; Ctrl-G cancels the search. Enter accepts the command. Ctrl-J is also a newline and must not be described as a harmless preview key. Bindings can differ in another editing mode or a customized terminal.&lt;/p&gt;
&lt;p&gt;Inspect the hostname, directory, options, and intended target whenever reusing an old command. A command that was correct last month may address a different environment today. Prefer a distinctive search fragment and review the whole line, rather than trusting that the first matching command is the right operation.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Missing commands can result from filtering, retention limits, a different history file, failed saving, or an unrefreshed session. Check those causes separately. &lt;code&gt;ignoredups&lt;/code&gt; excludes consecutive duplicates, not every duplicate in the entire list. &lt;code&gt;ignoreboth&lt;/code&gt; combines that behavior with &lt;code&gt;ignorespace&lt;/code&gt;; it does not produce a globally unique command archive.&lt;/p&gt;
&lt;p&gt;A leading space excludes an entry only when the relevant setting is active. It does not remove terminal recordings, process arguments, or logs written elsewhere, so do not put credentials directly into command lines and rely on history filtering for secrecy. Diagnostic examples should use synthetic entries and a temporary history file, rather than printing someone&amp;rsquo;s real recent commands.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Run this with Python 3, Bash, and standard Unix utilities. The environment points Bash to a temporary file before it starts. Only synthetic entries are inspected. The brace group avoids recording the demonstration&amp;rsquo;s own control commands after history is enabled.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;set -eu
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;set -o history
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -c
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -s &amp;#39;demo original&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -w &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;first-file:\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;cat &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -c
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -s &amp;#39;demo replacement&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -w &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;replacement-file:\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;cat &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -r &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;memory-after-reread:\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history | sed &amp;#39;s/^[[:space:]]*[0-9][0-9]*[[:space:]]*//&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;unset HISTFILE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;}&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash-history-demo-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;history_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;history&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;HISTSIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;100&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HISTFILESIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;100&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTCONTROL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTIGNORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HISTTIMEFORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--noprofile&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--norc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first file contains &lt;code&gt;demo original&lt;/code&gt;; after replacement it contains only &lt;code&gt;demo replacement&lt;/code&gt;. Reading that file into a list already containing the replacement produces two identical entries in memory. This verifies overwrite and import semantics without executing the stored strings. It does not claim to test keyboard bindings or crash durability, which require their own observations.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Bash-History-Facilities"&gt;Bash history facilities&lt;/a&gt;, session lists and saving to the history file, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Bash-History-Builtins"&gt;Bash history builtins&lt;/a&gt;, append and replacement versus importing entries, checked 2026-09-07.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Commands-For-History"&gt;Readline history commands&lt;/a&gt;, search and command-acceptance bindings, checked 2026-09-07.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>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></channel></rss>