|
1 | | -# 功能实现计划:D2 持久化选项继承模型 |
| 1 | +# 功能实现计划:D2 共享选项继承模型(SharedOpts,≈ cobra PersistentFlags) |
2 | 2 |
|
3 | 3 | > 状态:**待实施**(建议独立里程碑,分阶段推进) |
4 | | -> 范围:`cmd.go`(持久选项存储/合并/分发)+ 新增 gflag 合并基元 + 可选的 `gcli.go`(gOpts per-App)。 |
| 4 | +> 范围:`cmd.go`(共享选项存储/合并/分发)+ 新增 gflag 合并基元 + 可选的 `gcli.go`(gOpts per-App)。 |
5 | 5 | > 依据:[../compare-with-others.zh-CN.md](../compare-with-others.zh-CN.md) 差距 5;[../TODO.md](../TODO.md) D2。 |
6 | 6 | > 对标:cobra 的 `PersistentFlags` / `LocalFlags` / `InheritedFlags` 三层模型。 |
7 | 7 |
|
|
14 | 14 | `bindingOpts(fs)`(`gcli.go:211`)绑定 `--help/-h`、`--version/-V`。App 复用它 |
15 | 15 | (`app.go:128` `app.opts = gOpts`,`app.go:203` 绑到 `app.fs`)。 |
16 | 16 | 2. **命令局部选项**:每个命令在 `Config` 里用 `c.BoolOpt/StrOpt/FromStruct/...` 绑到 `c.Flags`。 |
17 | | -- **缺中间层**:父命令定义、其所有子命令自动继承的「持久选项」不存在。多级命令里要让 |
| 17 | +- **缺中间层**:父命令定义、其所有子命令自动继承的「共享选项」不存在。多级命令里要让 |
18 | 18 | `git --git-dir <path> <subcmd> ...` 这种「父级选项对所有子命令生效」,只能在每个子命令重复声明。 |
19 | 19 | - **分发链路**(`cmd.go:409` `innerDispatch`):`parseOptions` → 找子命令 → |
20 | 20 | `sub.innerDispatch(args[1:])` 递归。解析在子命令名处停止(首个位置参数)。 |
|
31 | 31 | | 层 | 定义者 | 作用范围 | |
32 | 32 | |---|---|---| |
33 | 33 | | 全局(global) | App(现 `gOpts`) | 所有命令 | |
34 | | -| 持久(persistent) | 某命令 | 该命令**及其所有子孙命令** | |
| 34 | +| 共享(shared) | 某命令 | 该命令**及其所有子孙命令** | |
35 | 35 | | 局部(local) | 某命令 | 仅该命令 | |
36 | 36 |
|
37 | | -#### 动机:现状 vs 持久选项(直观对照) |
| 37 | +> API 命名采用 **`SharedOpts()`**(比 `PersistentFlags` 短、贴 gcli 的 `Opts` 词汇); |
| 38 | +> 下文「共享选项」≈ cobra 的 persistent flags。 |
| 39 | +
|
| 40 | +#### 动机:现状 vs 共享选项(直观对照) |
38 | 41 |
|
39 | 42 | 命令树 `app remote(父) → add(子)`,`--verbose` 定义在 `remote` 上。 |
40 | 43 |
|
41 | | -| 输入 | 现状(remote 局部选项) | 持久选项后 | |
| 44 | +| 输入 | 现状(remote 局部选项) | 共享选项后 | |
42 | 45 | |---|---|---| |
43 | 46 | | `app remote --verbose add` | ✅ 写在子命令名**之前**,由 remote 解析 | ✅ | |
44 | 47 | | `app remote add --verbose` | ❌ remote 解析到 `add` 即停,`--verbose` 交给 add;**add 不认识** → 报错 | ✅ add **继承**了 `--verbose`,能解析 | |
|
50 | 53 |
|
51 | 54 | > 它**只解决父 → 子方向**:不会让子命令自己的局部选项能写在子命令名之前。 |
52 | 55 |
|
53 | | -1. **D2.1**(核心):新增持久选项中间层,父命令的持久选项被子孙命令继承,且与 args 重排契合 |
| 56 | +1. **D2.1**(核心):新增共享选项中间层,父命令的共享选项被子孙命令继承,且与 args 重排契合 |
54 | 57 | (可写在叶子段任意位置)。 |
55 | 58 | 2. **D2.2**(可选、有风险):`gOpts` 由进程单例改为 per-App 实例,修多 App/并发共享。 |
56 | 59 | 3. **D2.3**:三层语义文档 + help 渲染(继承选项分组显示)+ 专项测试。 |
|
59 | 62 |
|
60 | 63 | ## 方案 |
61 | 64 |
|
62 | | -### D2.1 持久选项 tier(核心) |
| 65 | +### D2.1 共享选项 tier(核心) |
63 | 66 |
|
64 | 67 | #### API(复用现有全部绑定方法) |
65 | 68 |
|
66 | | -`Command.PersistentFlags() *gflag.Flags`:惰性创建并返回命令专属的持久选项持有器 `c.pFlags`。 |
| 69 | +`Command.SharedOpts() *gflag.Flags`:惰性创建并返回命令专属的共享选项持有器 `c.sharedFs`。 |
67 | 70 | 用户在它上面像平时一样绑定 —— 自动获得 `BoolOpt/StrOpt/IntOpt/DurationOpt/Opt[T]/FromStruct/...` |
68 | 71 | 全部能力: |
69 | 72 |
|
70 | 73 | ```go |
71 | 74 | var gitDir string |
72 | | -top.PersistentFlags().StrOpt(&gitDir, "git-dir", "", "", "the git work dir") |
73 | | -// 或结构体:top.PersistentFlags().FromStruct(&persistentOpts) |
| 75 | +top.SharedOpts().StrOpt(&gitDir, "git-dir", "", "", "the git work dir") |
| 76 | +// 或结构体:top.SharedOpts().FromStruct(&sharedOpts) |
74 | 77 | ``` |
75 | 78 |
|
76 | | -> 选 `PersistentFlags() *Flags`(对标 cobra `cmd.PersistentFlags()`)而非 `CliOpt.Persistent` 标记, |
77 | | -> 因为前者零成本复用所有绑定方法,且 local/persistent 清晰分离。`c.pFlags` 仅作**定义来源**, |
| 79 | +> 选 `SharedOpts() *Flags`(对标 cobra `cmd.PersistentFlags()`)而非 `CliOpt.Shared` 标记, |
| 80 | +> 因为前者零成本复用所有绑定方法,且 local/shared 清晰分离。`c.sharedFs` 仅作**定义来源**, |
78 | 81 | > 自身永不单独 Parse。 |
79 | 82 |
|
80 | 83 | #### 存储 |
81 | 84 |
|
82 | | -`Command` 增加字段 `pFlags *gflag.Flags`(lazy,`PersistentFlags()` 内首次创建)。 |
| 85 | +`Command` 增加字段 `sharedFs *gflag.Flags`(lazy,`SharedOpts()` 内首次创建)。 |
83 | 86 |
|
84 | 87 | #### 合并基元(gflag 新增) |
85 | 88 |
|
@@ -111,30 +114,30 @@ func (p *Parser) InheritOptsFrom(src *Parser) { |
111 | 114 | 在 `cmd.go` `parseOptions`(`:493`)中、`c.Parse(args)` 之前合并: |
112 | 115 |
|
113 | 116 | ```go |
114 | | -if !c.persistentMerged { |
115 | | - // 沿祖先链(含自身)收集持久选项, 从根到叶顺序合并进 c.Flags |
| 117 | +if !c.sharedMerged { |
| 118 | + // 沿祖先链(含自身)收集共享选项, 从根到叶顺序合并进 c.Flags |
116 | 119 | for _, anc := range c.ancestorsWithSelf() { // [root,...,parent,self] |
117 | | - if anc.pFlags != nil { |
118 | | - c.Flags.InheritOptsFrom(&anc.pFlags.Flags) |
| 120 | + if anc.sharedFs != nil { |
| 121 | + c.Flags.InheritOptsFrom(&anc.sharedFs.Flags) |
119 | 122 | } |
120 | 123 | } |
121 | | - c.persistentMerged = true |
| 124 | + c.sharedMerged = true |
122 | 125 | } |
123 | 126 | ``` |
124 | 127 |
|
125 | | -- 自身的 `pFlags` 也并入 `c.Flags` → 命令**自己**也能识别它定义的持久选项 |
| 128 | +- 自身的 `sharedFs` 也并入 `c.Flags` → 命令**自己**也能识别它定义的共享选项 |
126 | 129 | (写在子命令名之前的场景)。 |
127 | | -- 幂等 `persistentMerged bool`,避免重复注册。 |
128 | | -- **与 reorder 契合**:合并后持久选项已进入叶子的 flag set,`optMeta` 能识别其取值性, |
| 130 | +- 幂等 `sharedMerged bool`,避免重复注册。 |
| 131 | +- **与 reorder 契合**:合并后共享选项已进入叶子的 flag set,`optMeta` 能识别其取值性, |
129 | 132 | 写在叶子 arguments 之后也会被正确重排+解析。 |
130 | 133 |
|
131 | 134 | #### 多级解析走查 |
132 | 135 |
|
133 | | -`app top sub --git-dir /x arg`(`--git-dir` 是 top 的持久选项): |
| 136 | +`app top sub --git-dir /x arg`(`--git-dir` 是 top 的共享选项): |
134 | 137 | - app.fs 解析全局(重排关闭) → 停在 `top` → dispatch top `[sub,--git-dir,/x,arg]` |
135 | | -- top.parseOptions:合并 top.pFlags 进 top.Flags;top.Parse 停在 `sub` → dispatch sub `[--git-dir,/x,arg]` |
136 | | -- sub.parseOptions:合并祖先(top)的 pFlags 进 sub.Flags → sub.Parse 识别 `--git-dir`=/x,写回共享 ptr, |
137 | | - `arg` 余下 → doExecute。✅ 持久选项在叶子段任意位置可用。 |
| 138 | +- top.parseOptions:合并 top.sharedFs 进 top.Flags;top.Parse 停在 `sub` → dispatch sub `[--git-dir,/x,arg]` |
| 139 | +- sub.parseOptions:合并祖先(top)的 sharedFs 进 sub.Flags → sub.Parse 识别 `--git-dir`=/x,写回共享 ptr, |
| 140 | + `arg` 余下 → doExecute。✅ 共享选项在叶子段任意位置可用。 |
138 | 141 |
|
139 | 142 | #### help 渲染 |
140 | 143 |
|
@@ -162,29 +165,29 @@ if !c.persistentMerged { |
162 | 165 |
|
163 | 166 | ## 风险 / 兼容 |
164 | 167 |
|
165 | | -- **D2.1 增量、无破坏**:不碰现有 local/global 绑定路径;命令未用 `PersistentFlags()` 时 |
166 | | - `pFlags==nil`,合并是 no-op,行为完全不变。 |
| 168 | +- **D2.1 增量、无破坏**:不碰现有 local/global 绑定路径;命令未用 `SharedOpts()` 时 |
| 169 | + `sharedFs==nil`,合并是 no-op,行为完全不变。 |
167 | 170 | - **同名冲突**:局部同名选项优先(合并时 `HasOption` 跳过),语义明确且安全。 |
168 | | -- **Required 持久选项**:在「实际执行命令」处校验(合并后随 `validateAll` 生效),需在测试中固化。 |
| 171 | +- **Required 共享选项**:在「实际执行命令」处校验(合并后随 `validateAll` 生效),需在测试中固化。 |
169 | 172 | - **D2.2 有破坏风险**:改单例为 per-App 可能影响依赖 `gcli.GOpts()` 全局读取的代码; |
170 | 173 | 单独评估、独立提交、可暂缓。 |
171 | 174 |
|
172 | 175 | ## 测试(`github.com/gookit/goutil/x/assert`) |
173 | 176 |
|
174 | | -- **继承**:top 定义持久 `--git-dir`,`app top sub --git-dir /x` → sub 内读到 `/x`。 |
| 177 | +- **继承**:top 定义共享 `--git-dir`,`app top sub --git-dir /x` → sub 内读到 `/x`。 |
175 | 178 | - **任意位置**:`app top sub arg --git-dir /x`(重排)→ 同样生效,arg 仍为参数。 |
176 | | -- **多级继承**:三层 `app a b c`,a 的持久选项在 c 可用。 |
| 179 | +- **多级继承**:三层 `app a b c`,a 的共享选项在 c 可用(沿祖先链合并)。 |
177 | 180 | - **局部优先**:子命令定义同名局部选项时,覆盖继承(互不写串)。 |
178 | | -- **自身可用**:`app top --git-dir /x sub` → top 段即识别持久选项。 |
179 | | -- **Required 持久**:缺失时在执行命令处报错。 |
180 | | -- **未用持久**:现有用例全绿(pFlags==nil,零行为变化)。 |
| 181 | +- **自身可用**:`app top --git-dir /x sub` → top 段即识别共享选项。 |
| 182 | +- **Required 共享**:缺失时在执行命令处报错。 |
| 183 | +- **未用共享**:现有用例全绿(sharedFs==nil,零行为变化)。 |
181 | 184 | - **(D2.2)** 两个 App 实例的 verbose/strict 互不影响;并发 `-shuffle` 稳定。 |
182 | 185 |
|
183 | 186 | ## 提交拆分(按 R002) |
184 | 187 |
|
185 | 188 | 1. `feat(gflag): 增加 Parser.InheritOptsFrom 选项合并基元` |
186 | | -2. `feat(gcli): Command.PersistentFlags 持久选项定义 + 分发时合并(含幂等)` |
187 | | -3. `test(gcli): 持久选项继承/任意位置/局部优先/Required 用例` |
| 189 | +2. `feat(gcli): Command.SharedOpts 共享选项定义 + 分发时合并(含幂等)` |
| 190 | +3. `test(gcli): 共享选项继承/任意位置/局部优先/Required 用例` |
188 | 191 | 4. `docs: 三层选项模型说明(README/CHANGELOG) + struct/示例` |
189 | 192 | 5. (可选)`feat(gcli): help 渲染继承选项分组(Inherited Options)` |
190 | 193 | 6. (可选/独立评估)`refactor(gcli): gOpts 改 per-App 实例 + 多 App 测试` |
|
0 commit comments