# 2026-05-14 — argparse 全局 flag 在 subparser 后会失效，必须前置 hoist

## 场景

写 `scripts/ops/gitea` CLI 时，spec 要求 `--json` / `--dry-run` 等全局 flag 可以放在命令行**任意位置**（R4 顺序无关）。但 argparse 的 subparser 模型默认**不**让顶层 flag 渗到 subcommand 之后。

具体翻车命令：

```bash
gitea pr 318 --json          # argparse: "unrecognized arguments: --json"
gitea label add 331 X --dry-run  # 同样失败
gitea --json pr 318          # 才能跑（不直观）
```

## 根因

argparse 的 `add_subparsers()` 给每个 subparser 独立的 namespace，**顶层定义的 `--json` 只在到达 subparser 前生效**。一旦 token 序列进入 `pr` subcommand 的解析器，它就不认识 `--json` 了。

这跟 click / typer / cobra 都不一样——后者的全局 flag 默认在任何位置可用。argparse 用户必须自己处理。

## 解法

**preprocess argv：识别已知全局 bool flag → 前置到队首再交给 argparse**。

```python
GLOBAL_BOOL_FLAGS = {"--json", "--no-color", "-n", "--dry-run", "-v", "-vv",
                    "--verbose"}

def _hoist_globals(argv: list[str]) -> list[str]:
    globals_found, rest = [], []
    for tok in argv:
        if tok in GLOBAL_BOOL_FLAGS:
            globals_found.append(tok)
        else:
            rest.append(tok)
    return globals_found + rest
```

调用：`argv = _hoist_globals(sys.argv[1:])` 然后再 `parser.parse_args(argv)`。

## 局限与边界

- 只能 hoist **bool flag**（无值）。带值 flag（`--foo VAL`）需要保持相邻，hoist 会拆散。本工具的全局 flag 都是 bool，所以够用。
- 如果将来要加带值全局 flag（如 `--token TOK`），需要扩展为成对 hoist（识别 `--token` 时把下一个 token 也搬过来）。
- 不要 hoist 未知 flag，否则会"吞掉"用户拼写错误的诊断。

## 替代方案（更重，未采用）

- 给每个 subparser `parents=[global_parser]` —— 解决但要在每个 subparser 上重复，维护负担大
- 切第三方库（click / typer）—— 违反 spec 第 10 节"argparse only" 约束
- 让用户记住"全局 flag 放前面" —— 违反 spec R4 顺序无关

## 应用范围

任何用 argparse + subparser 的本仓库 CLI 都该 hoist 全局 bool flag，否则用户体验会反复在这个坑上撞。可以考虑把 `_hoist_globals` 抽到 `_cli_helpers.py` 共享 module，下次写 CLI 直接 import。**第二个 CLI 出现时再做抽取，不提前抽**（YAGNI）。

## 引用

- spec：`docs/standards/15-cli-design-spec.md` R4
- 落地：`scripts/ops/gitea` `_hoist_globals` 函数
- Python argparse 文档没显式说明这个限制，要踩到才知道
