<?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>参数 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E5%8F%82%E6%95%B0/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E5%8F%82%E6%95%B0/index.xml" rel="self" type="application/rss+xml"/><item><title>Shell 位置参数是一份有编号的清单</title><link>https://www.xbcatm.com/zh/posts/shell-positional-arguments-slot-the-cli/</link><pubDate>Mon, 29 Nov 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-positional-arguments-slot-the-cli/</guid><description>在脚本里,传入的参数会以 $1、$2……依次暴露,再有 $@ 和 $# 表示整份清单,因此顺序与个数都是一目了然的。</description><content:encoded>&lt;p&gt;Shell 脚本接收的是有顺序的参数列表，不是需要重新按空格拆分的一整条命令字符串。位置参数通过 &lt;code&gt;$1&lt;/code&gt;、&lt;code&gt;$2&lt;/code&gt; 等编号暴露这份列表，&lt;code&gt;$#&lt;/code&gt; 表示数量。固定且简单的接口可以直接使用位置，带可选设置或开关的接口则适合明确解析。无论采用哪种形式，都需要规定允许几个参数、是否允许空值，以及输入错误时怎样失败。&lt;/p&gt;
&lt;h2 id="解释含义之前先保住列表边界"&gt;解释含义之前，先保住列表边界&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;$0&lt;/code&gt; 表示脚本名称等特殊身份，不属于位置参数，也不计入 &lt;code&gt;$#&lt;/code&gt;。使用 &lt;code&gt;bash -c&lt;/code&gt; 时，命令字符串之后的第一个参数会成为 &lt;code&gt;$0&lt;/code&gt;，包装脚本若误以为它是 &lt;code&gt;$1&lt;/code&gt;，就会看起来少收到一个参数。超过第九个位置必须使用花括号：&lt;code&gt;${10}&lt;/code&gt; 是第十个参数，&lt;code&gt;$10&lt;/code&gt; 却是第一个参数再接一个字面数字零，两种写法不能互换。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 会为每个传入参数保留一个词，包括空字符串和包含空格的值。未引用的展开可能再次分词并展开通配符，&lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; 则会把列表连成一个字符串。转发或遍历时应使用带引号的完整列表。&lt;code&gt;set --&lt;/code&gt; 替换当前的位置参数，&lt;code&gt;shift&lt;/code&gt; 删除前面的若干项；这些操作在对应脚本或函数作用域中生效，不会反向改写外部调用者原来的参数列表。&lt;/p&gt;
&lt;p&gt;空参数仍然占据一个位置。检查 &lt;code&gt;$#&lt;/code&gt; 可以区分完全没有提供参数，与明确传入 &lt;code&gt;''&lt;/code&gt; 两种情况。&lt;code&gt;${1:?message}&lt;/code&gt; 同时拒绝缺失和空值，只有接口要求非空时才合适。应先验证数量，再读取必需的位置，最后判断内容是否合法，避免在参数本来不存在时就触发与接口说明无关的展开错误。&lt;/p&gt;
&lt;h2 id="getopts-检查语法业务规则检查含义"&gt;getopts 检查语法，业务规则检查含义&lt;/h2&gt;
&lt;p&gt;可移植的 &lt;code&gt;getopts&lt;/code&gt; 处理短选项。选项字符后面的冒号表示该选项需要一个值，但不会自动提供 &lt;code&gt;--user&lt;/code&gt; 这类长选项支持。选项说明开头的冒号表示采用安静错误报告：未知选项产生问号，缺少值产生冒号，相关选项字符放在 &lt;code&gt;OPTARG&lt;/code&gt; 中。脚本必须明确处理这些分支，不能因为解析循环继续运行就误以为输入合法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OPTIND&lt;/code&gt; 记录解析进度。在同一 Shell 中开始解析另一份参数之前，应把它重置为一；解析完成后，使用 &lt;code&gt;shift &amp;quot;$((OPTIND - 1))&amp;quot;&lt;/code&gt; 让剩余操作数成为当前列表。&lt;code&gt;--&lt;/code&gt; 结束选项处理，因此以连字符开头的文件名也能被当作操作数。普通的非选项同样会结束扫描，不能假定写在文件名后面的选项会被自动搬到前面。&lt;/p&gt;
&lt;p&gt;解析器拿到一个值，不等于它的业务含义有效。调用者显式提供空字符串，可以满足“这里有一个选项参数”的语法要求，却不满足“名称必须非空”的接口规则。默认值也只应用于省略的可选设置，不应掩盖错误输入。诊断应写到标准错误，并在执行实际工作前返回约定的非零状态，让调用方能够可靠识别参数错误。&lt;/p&gt;
&lt;p&gt;可复用的 Bash 解析函数应当同时局部化 OPTARG 与 OPTIND。getopts 在处理选项时会更新或取消 OPTARG；即使每次正确重置索引，未局部化的 OPTARG 仍可能破坏外层解析器状态。下面让两者都局部化，并确认调用者预先设置的哨兵值在全部调用之后仍然保留。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;常见问题之一是正常输入能跑，多余的操作数却被悄悄丢弃。另一种是忘记重置解析索引，导致函数第一次调用正确，后续调用跳过部分选项。未引用转发也可能先改变参数数量，产生类似症状。应分别检查接收到的列表与解析状态，而不是不断补几个移位操作，直到某一次特定调用碰巧成功。&lt;/p&gt;
&lt;p&gt;下面的示例明确规定一个小接口：可选的名称选项、可选的详细输出开关，以及恰好一个输入操作数。名称必须非空。函数使用 Bash 的局部变量声明，因此完整示例依赖 Bash，虽然其中的选项解析器本身是可移植的。面向其他 Shell 的脚本，应采用目标 Shell 支持的作用域规则，而不能把局部声明也当成统一标准。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;在 Bash 中运行完整代码块。开头检查第十个参数的引用方式，以及空参数是否被保留。随后测试一次合法调用，再分别测试缺值、未知选项、空名称和多余操作数。演示函数只打印解析结果，不修改文件，也不启动外部程序，所以可以直接观察参数边界，而不把其他业务副作用混进验证。&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 OPTARG 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;OPTARG=caller-sentinel
&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;[[ $OPTARG == caller-sentinel ]] || 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;printf &amp;#39;caller OPTARG preserved\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;第一行应分别得到 &lt;code&gt;one0&lt;/code&gt; 与 &lt;code&gt;ten&lt;/code&gt;，之后参数数量为二，打印出空字符串和带空格的第二项。合法调用保留 &lt;code&gt;Jane Doe&lt;/code&gt; 和 &lt;code&gt;-input file&lt;/code&gt; 的完整内容。四类错误输入都应返回状态二。最后一次调用重新使用默认名称、关闭详细开关，并接收 &lt;code&gt;second&lt;/code&gt;，证明重复解析从新的状态开始，而不是沿用前一次的索引或选项值。&lt;/p&gt;
&lt;p&gt;这些观察验证了接口边界，不只是证明存在一段用法提示。扩展真实命令时，应补充新选项的内容规则，同时维持已有的引用、空值和结束选项标记语义。参数能够被读到，是解析的开始；只有合法输入被接受、非法输入被明确拒绝，接口约定才真正成立。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&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 位置参数&lt;/a&gt;，编号参数及函数作用域规则，已核验 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;，选项语法错误与解析状态重置，已核验 2026-09-07。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>