拆解命令结构
粘贴一行命令后点击拆解。工具把第一个片段标记为 command,把双短横线开头的片段标记为 long flag,把短横线开头且长度不超过三字符的片段标记为 short flag / stdin,其他片段标记为 argument。
阅读示例
输入 codex exec --skip-git-repo-check -,输出会依次标记 codex 为命令、exec 为参数、--skip-git-repo-check 为长选项、单个短横线为短选项或标准输入标记。拆分使用空白字符,带引号的复杂 Shell 表达式不会被完整解析。具体参数作用应结合目标程序的帮助信息核对。
离线实测:看见程序真正收到的参数
前面的人工注解能帮助辨认选项归属,但命令文本并不是程序最终收到的参数列表。实际排错时,空值可能消失,变量里的空格可能拆成两项,通配符也可能先被 shell 换成当前目录里的文件名。下面把目标程序换成一个只打印参数的探针,让这些差异可以直接对照。
本实验只运行页内固定的 Bash 命令和 Python 标准库探针。它在自动清理的临时目录里创建 alpha.csv、two words.csv、sentinel.txt 三个无业务内容的文件,不调用 Codex、不联网、不读取真实仓库,也不执行读者粘贴的命令。不要把探针改成真实的上传、删除或部署程序来“试试看”。
先约定意图:--label 的值可以是空字符串;two words 必须作为一个值;*.csv 要原样交给目标程序,而不是让 shell 展开。这里的 --label 只是探针接收的示例文字,不是宣称任何产品存在此选项。若你的真实意图是枚举文件,则展开结果可能恰好正确,应另写自己的预期。
需要保留的参数列表:
["--label", "", "*.csv", "two words"]
下面分别测试空值、空格和通配符,最后合并验证恢复结果。
各场景精确输出见后文,不把不同命令的结果混为一组。完整脚本中每个场景启动一个新的 Bash;环境只传入所需字段,固定 C locale,关闭启动脚本,显式设定 IFS 和相关 glob 选项。因此结果不会借用当前终端里的 nullglob 或 BASH_ENV 设置。此处是在缩小实验变量,并非通用的恶意 shell 沙箱。
这里没有用 printf "%s\n" "$@" 的空白行数来推断参数数目:它在零参数时也可能打印空行。探针直接将 sys.argv[1:] 序列化为 JSON,所以零参数是 [],一个空参数是 [""]。两者长度不同,后续程序的行为也可能不同。
保存并运行完整参数探针
需要已经安装的 Bash 和 Python 3。本次实测是 Linux、Bash 5.2.37 与 Python 3.12.14;没有测试 zsh、PowerShell、cmd、其他 shell 或 macOS 内置 Bash。保存下面文件为 argv_lab.py,在空目录执行 python argv_lab.py。没有 Bash 时脚本会停止,不自动换成另一种 shell,也不安装软件。
"""Only executes the fixed commands below, inside a disposable fixture directory."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
EXPECTED = {
'empty_quoted': ['--label', '', 'tail'],
'empty_unquoted': ['--label', 'tail'],
'words_quoted': ['two words'],
'words_unquoted': ['two', 'words'],
'glob_quoted': ['*.csv'],
'glob_unquoted': ['alpha.csv', 'two words.csv'],
'no_match_default': ['*.missing'],
'no_match_nullglob': [],
'explicit_empty': [''],
'recovered': ['--label', '', '*.csv', 'two words'],
'direct_argv': ['--label', '', '*.csv', 'two words'],
}
COMMANDS = {
'empty_quoted': 'empty=""; probe --label "$empty" tail',
'empty_unquoted': 'empty=""; probe --label $empty tail',
'words_quoted': 'label="two words"; probe "$label"',
'words_unquoted': 'label="two words"; probe $label',
'glob_quoted': 'pattern="*.csv"; probe "$pattern"',
'glob_unquoted': 'pattern="*.csv"; probe $pattern',
'no_match_default': 'probe *.missing',
'no_match_nullglob': 'shopt -s nullglob; probe *.missing',
'explicit_empty': 'probe ""',
'recovered': 'empty=""; pattern="*.csv"; label="two words"; '
'probe --label "$empty" "$pattern" "$label"',
}
PREFIX = '''set +f
shopt -u nullglob failglob dotglob nocaseglob
unset GLOBIGNORE GLOBSORT
IFS=$' \\t\\n'
probe() { "$PYTHON" "$PROBE" "$@"; }
'''
def require(condition, message):
if not condition:
raise AssertionError(message)
def main():
require(sys.argv[1:] in ([], ['--mutate-empty'], ['--mutate-empty-expectation']), 'unsupported arguments')
bash = shutil.which('bash', path=os.defpath)
require(bash is not None, 'Bash is required; no alternate shell is used')
with tempfile.TemporaryDirectory(prefix='argv-lab-') as folder:
root = Path(folder)
probe_file = root / 'probe.py'
probe_file.write_text('import json, sys\nprint(json.dumps(sys.argv[1:]))\n',
encoding='utf-8')
for name in ('alpha.csv', 'two words.csv', 'sentinel.txt'):
(root / name).write_text('synthetic fixture\n', encoding='utf-8')
# Whitelist prevents BASH_ENV, imported functions, and shell options leaking in.
env = {'PATH': os.defpath, 'LC_ALL': 'C', 'PYTHON': sys.executable,
'PROBE': str(probe_file)}
def shell(command):
return subprocess.run([bash, '--noprofile', '--norc', '-c', PREFIX + command],
cwd=root, env=env, capture_output=True, text=True,
timeout=5, check=False)
actual = {}
commands = dict(COMMANDS)
expected = dict(EXPECTED)
if sys.argv[1:] == ['--mutate-empty-expectation']:
expected['no_match_nullglob'] = ['']
if sys.argv[1:] == ['--mutate-empty']:
commands['empty_quoted'] = COMMANDS['empty_unquoted']
for name, command in commands.items():
result = shell(command)
require(result.returncode == 0 and result.stderr == '',
f'{name}: unexpected process failure: {result.stderr}')
actual[name] = json.loads(result.stdout)
require(actual[name] == expected[name],
f'{name}: expected {expected[name]!r}, got {actual[name]!r}')
# A process argument list does not ask a shell to expand the literal values.
direct = subprocess.run([sys.executable, str(probe_file), '--label', '',
'*.csv', 'two words'], cwd=root, env=env,
capture_output=True, text=True, timeout=5, check=False)
require(direct.returncode == 0 and direct.stderr == '', 'direct_argv failed')
actual['direct_argv'] = json.loads(direct.stdout)
require(actual == expected, 'whole result differs from fixed oracle')
fail = shell('shopt -s failglob; probe *.missing')
require(fail.returncode != 0 and fail.stdout == '' and bool(fail.stderr),
'failglob must reject before the probe prints its argument array')
# Negative controls: bad versions must not satisfy the intended contract.
require(actual['empty_unquoted'] != EXPECTED['empty_quoted'],
'empty-argument negative control was not detected')
require(actual['words_unquoted'] != EXPECTED['words_quoted'],
'word-splitting negative control was not detected')
require(actual['glob_unquoted'] != EXPECTED['glob_quoted'],
'wildcard negative control was not detected')
require(actual['no_match_nullglob'] != actual['explicit_empty'],
'zero arguments confused with one empty argument')
require(sorted(p.name for p in root.iterdir()) ==
['alpha.csv', 'probe.py', 'sentinel.txt', 'two words.csv'],
'unexpected files in fixture')
print(json.dumps(actual, ensure_ascii=False, indent=2))
print('PASS: 11 fixed argv cases; failglob rejected; 4 negative controls')
print('Bash:', subprocess.check_output([bash, '--version'], env=env,
text=True).splitlines()[0])
print('Python:', sys.version.split()[0])
if __name__ == '__main__':
main()
EXPECTED 是手写的固定参数列表,不从被测输出生成。脚本先要求进程正常结束且 stderr 为空,再逐场景比较 JSON 数组。对于 failglob,单独要求非零退出、空 stdout 和非空诊断;不绑定随 Bash 版本可能变化的英文报错全文。
按固定输出定位,再验证修复
2026-10-14 实际离线运行的输出如下。临时目录名不参与期望值;数组保留空字符串及其位置,避免把“没有参数”和“一个空参数”显示成同一条空行。
{
"empty_quoted": [
"--label",
"",
"tail"
],
"empty_unquoted": [
"--label",
"tail"
],
"words_quoted": [
"two words"
],
"words_unquoted": [
"two",
"words"
],
"glob_quoted": [
"*.csv"
],
"glob_unquoted": [
"alpha.csv",
"two words.csv"
],
"no_match_default": [
"*.missing"
],
"no_match_nullglob": [],
"explicit_empty": [
""
],
"recovered": [
"--label",
"",
"*.csv",
"two words"
],
"direct_argv": [
"--label",
"",
"*.csv",
"two words"
]
}
PASS: 11 fixed argv cases; failglob rejected; 4 negative controls
Bash: GNU bash, version 5.2.37(1)-release (x86_64-pc-linux-gnu)
Python: 3.12.14
empty_unquoted 的空值消失后,tail 紧跟 --label;真实程序可能把它当选项值,也可能拒绝,具体取决于该程序。words_unquoted 把一个文本值拆成两项。glob_unquoted 则得到两个 CSV 文件名,其中 two words.csv 仍是一个参数;不能按打印出来的空格再次把它手工拆开。
未匹配的 *.missing 在本例默认选项下保留原样,启用 nullglob 后变成零个参数;显式的双引号空字符串仍是一个参数。启用 failglob 时则在展开阶段拒绝,探针没有输出参数数组。这三个分支说明:只看命令文本,甚至只看目录,都不足以推断实际参数。
恢复场景给每个变量加双引号,重新得到四项固定列表。direct_argv 用 Python 的进程参数列表传入同样四项,不让 shell 解释这些值;它仍不负责判断目标程序是否支持选项或是否应该执行。修复完成的证据是正确数组恢复,不是仅仅退出码为零。
让检查器故意失败一次
python argv_lab.py --mutate-empty这个内置负控只把 empty_quoted 场景替换为已写在脚本里的无引号版本,不接受任意代码。实测退出码为 1,最后一行如下;这次失败是预期结果。
AssertionError: empty_quoted: expected ['--label', '', 'tail'], got ['--label', 'tail']再执行不带参数的 python argv_lab.py,应恢复 PASS。正常运行还检查了空值丢失、单词拆分、文件名展开和零参数四组反例;如果删掉关键验证,不能再沿用本文的“检查通过”结论。
再故意写错“零参数”的预期
执行 python argv_lab.py --mutate-empty-expectation,只把 no_match_nullglob 的预期错写为一个空字符串,真实命令仍保持不变。程序应退出 1,最后一行为下面的诊断。这个反向变体证明检查器不会把 [] 和 [""] 混为一谈。随后去掉变体参数重跑,应恢复 PASS。
AssertionError: no_match_nullglob: expected [''], got []也可使用 python -O argv_lab.py,以及在两个变体命令的 python 后加 -O。检查用显式异常实现,不依赖会被优化模式移除的 assert;正常运行仍退出 0,两个变体仍退出 1。
把参数证据带回命令审阅
现在可以在审阅记录里分开保存三件事:脱敏后的命令文本、明确的 shell 与环境条件、目标程序实际收到的参数数组。预期是一个空值时,应检查数组里的空字符串,而不是数输出行;预期是一个路径时,应检查它是否完整占据一项。遇到差异,先回到引号、变量内容、目录与 glob 选项,再继续检查目标程序的帮助。
本实验没有修改本页拆解按钮或解析器,也没有浏览器复测新输入。既有按钮仍提供词项结构标注;页内实验是独立的终端练习。它没有证明目标命令安全、选项有效、账号可用或模型任务完成,也没有覆盖重定向、管道、命令替换和所有 shell 语法。
- GNU Bash:Word Splitting,核对未引用变量的分词与空参数保留规则。
- GNU Bash:Filename Expansion,核对文件名展开、无匹配与 nullglob、failglob 的区别。
- Python subprocess 参数说明,核对参数列表与默认不经 shell 的调用方式。
上述资料于 2026-10-14 查阅;官方规则与本机实测版本分别记录。所有文件名、选项文字与输入均为合成教学夹具,没有真实模型、产品 CLI 或生产目录的运行记录。
GNU 在线手册目前标示 Bash 5.3;本文运行的可执行文件报告 Bash 5.2.37(1)-release,不能把在线文档版本写成本机版本。Python 引用固定到 3.12 文档。复现包只包含教学正文、原创示意图、完整源码与正常输出;打开 HTML 无需联网,访问参考链接才需要网络。
常见问题
输入的命令会被执行吗?
不会。工具只输出结构标签,不能判断选项是否存在或命令是否能运行。