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