Skip to content

Commit 77f2391

Browse files
inhereclaude
andcommitted
feat(gcli): Command.SharedOpts 共享选项定义 + 分发时合并(含幂等)
新增 Command.SharedOpts() 惰性返回共享选项持有器(对标 cobra PersistentFlags), parseOptions 在 Parse 前沿祖先链(含自身)从根到叶把共享选项合并进 c.Flags(幂等)。 共享选项的 Required 校验延后到实际执行命令(doExecute)统一处理: 因共享选项可写在叶子 命令段, 中间命令解析时尚未见到取值, 不能在那时误报必填; 局部同名选项优先且互不写串。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 323219f commit 77f2391

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

cmd.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ type Command struct {
129129
standalone bool
130130
// global option binding on standalone. deny error on repeat run.
131131
gOptBounded bool
132+
133+
// sharedFs holds the command's shared options (≈ cobra PersistentFlags).
134+
// 共享选项的定义来源: 本命令及其所有子孙命令都会继承这些选项。lazy 创建于 SharedOpts()。
135+
sharedFs *gflag.Flags
136+
// sharedMerged marks shared options(self + ancestors) have been merged into c.Flags.
137+
// 幂等标记, 保证分发时只合并一次共享选项。
138+
sharedMerged bool
139+
// localOptNames snapshots the command's own local option names before shared merge.
140+
// 合并共享选项前的本地选项名快照, 用于区分局部定义与继承副本(保留局部 Required、跳过被局部覆盖的共享必填校验)。
141+
localOptNames map[string]bool
132142
}
133143

134144
// NewCommand create a new command instance.
@@ -182,6 +192,19 @@ func (c *Command) Use(handlers ...RunnerFunc) *Command {
182192
// AttachTo attach the command to CLI application
183193
func (c *Command) AttachTo(app *App) { app.AddCommand(c) }
184194

195+
// SharedOpts 返回命令专属的共享选项持有器(惰性创建), 对标 cobra 的 PersistentFlags()。
196+
//
197+
// 在它上面像普通选项一样绑定(BoolOpt/StrOpt/Opt[T]/FromStruct/...), 这些选项会被本命令
198+
// 及其所有子孙命令继承: 父命令定义、子命令也能解析, 且父子读写同一个变量(共享 flag.Value)。
199+
//
200+
// 注意: sharedFs 仅作为「定义来源」, 自身永不单独 Parse; 分发时由 parseOptions 合并进 c.Flags。
201+
func (c *Command) SharedOpts() *gflag.Flags {
202+
if c.sharedFs == nil {
203+
c.sharedFs = gflag.New(c.Name)
204+
}
205+
return c.sharedFs
206+
}
207+
185208
// Disable set cmd is disabled
186209
func (c *Command) Disable() { c.disabled = true }
187210

@@ -508,6 +531,10 @@ func (c *Command) parseOptions(args []string) (ss []string, err error) {
508531
// args reorder(默认开启)在子命令名处停止, 确保多级命令时只重排最终执行命令的 args。
509532
c.Flags.SetReorderStop(c.isReorderStopName)
510533

534+
// 合并共享选项: 沿祖先链(含自身)从根到叶把共享选项并入 c.Flags, 使其在本命令段可解析。
535+
// 幂等: sharedMerged 保证只合并一次; 合并后写在叶子段任意位置(配合 reorder)也能被识别。
536+
c.mergeSharedOpts()
537+
511538
Debugf("cmd: %s - will parse options from args: %v", c.Name, args)
512539

513540
// parse options, don't contains command name.
@@ -520,6 +547,81 @@ func (c *Command) parseOptions(args []string) (ss []string, err error) {
520547
return c.RawArgs(), nil
521548
}
522549

550+
// mergeSharedOpts 沿祖先链(含自身)从根到叶把各级的共享选项合并进 c.Flags。
551+
//
552+
// 顺序为 [root,...,parent,self], 这样祖先(更靠近根)的共享选项先注册; 同名时 InheritOptsFrom
553+
// 内部以「已存在则跳过」保证局部/更近的定义优先。幂等: sharedMerged 仅合并一次。
554+
//
555+
// 关于 Required: 共享选项可能写在叶子命令段, 而本命令(若是中间祖先)解析时尚未见到该值,
556+
// 因此合并进 c.Flags 的副本会清除 Required, 避免在「会继续向子命令分发的命令」上误报必填。
557+
// 真正的 Required 校验延后到实际执行命令时由 validateSharedRequired 统一处理。
558+
func (c *Command) mergeSharedOpts() {
559+
if c.sharedMerged {
560+
return
561+
}
562+
c.sharedMerged = true
563+
564+
// 沿 parent 链收集 [self, parent, ..., root]
565+
var chain []*Command
566+
for cur := c; cur != nil; cur = cur.parent {
567+
chain = append(chain, cur)
568+
}
569+
570+
// 合并前快照: c.Flags 此刻已有的选项均为命令自身的局部定义, 它们的 Required 应保留。
571+
c.localOptNames = make(map[string]bool, len(c.Flags.Opts()))
572+
for name := range c.Flags.Opts() {
573+
c.localOptNames[name] = true
574+
}
575+
576+
// 反向遍历(从根到叶)合并, 使祖先共享选项先注册、局部/更近定义优先
577+
for i := len(chain) - 1; i >= 0; i-- {
578+
anc := chain[i]
579+
if anc.sharedFs != nil {
580+
c.Flags.InheritOptsFrom(anc.sharedFs)
581+
}
582+
}
583+
584+
// 清除「新继承进来」的共享选项副本的 Required: 共享值可能写在叶子命令段, 中间命令解析时
585+
// 还看不到取值, 不能在那时误报必填。共享选项的 Required 延后到执行命令时由
586+
// validateSharedRequired 统一校验。局部已定义的同名选项(快照中)保持其 Required 不变。
587+
for name, opt := range c.Flags.Opts() {
588+
if !c.localOptNames[name] {
589+
opt.Required = false
590+
}
591+
}
592+
}
593+
594+
// validateSharedRequired 在实际执行命令前, 沿祖先链(含自身)校验所有 Required 共享选项是否已赋值。
595+
//
596+
// 共享选项的 Required 校验延后到此处统一处理: 因为共享选项可写在叶子命令段, 中间命令解析时
597+
// 还看不到取值, 不能在那时报必填。到达执行命令时, 共享值已写回同一 ptr, 此处即可正确判定。
598+
// 若执行命令定义了同名局部选项(共享被跳过继承), 则由局部自身的校验负责, 这里跳过。
599+
func (c *Command) validateSharedRequired() error {
600+
for cur := c; cur != nil; cur = cur.parent {
601+
if cur.sharedFs == nil {
602+
continue
603+
}
604+
for name, opt := range cur.sharedFs.Opts() {
605+
if !opt.Required {
606+
continue
607+
}
608+
// 执行命令有同名局部选项 → 共享被局部覆盖, 由局部 validateAll 负责, 此处跳过
609+
if c.localOptNames[name] {
610+
continue
611+
}
612+
613+
val := ""
614+
if fItem := opt.Flag(); fItem != nil {
615+
val = fItem.Value.String()
616+
}
617+
if val == "" {
618+
return fmt.Errorf("option '%s' is required", name)
619+
}
620+
}
621+
}
622+
return nil
623+
}
624+
523625
// prepare: before execute the command
524626
func (c *Command) prepare(_ []string) (status int, err error) {
525627
return
@@ -534,6 +636,13 @@ func (p panicErr) Error() string { return fmt.Sprint(p.val) }
534636

535637
// do execute the command
536638
func (c *Command) doExecute(args []string) (err error) {
639+
// 共享 Required 选项的延后校验: 到达实际执行命令时统一检查祖先链(含自身)的必填共享选项
640+
if err = c.validateSharedRequired(); err != nil {
641+
c.Fire(gevent.OnCmdRunError, map[string]any{"cmd": c.Name, "err": err})
642+
Logf(VerbError, "command '%s' shared required option err: <red>%s</>", c.Name, err.Error())
643+
return err
644+
}
645+
537646
// collect and binding named argument
538647
Debugf("cmd: %s - collect and binding named arguments", c.Name)
539648
if err := c.ParseArgs(args); err != nil {

0 commit comments

Comments
 (0)