hzDocs

从结构与 Tag 构建命令体系

BuildFrom Example

构造根命令以及整个命令体系可以采用如下的方案:

  • traditional stream calls (app.Cmd("verbose", "v").Action(onVerbose))
  • concise modes by [Create] and cmd/xxcmd.go
  • use [Create.BuildFrom] to build cmdsys from a struct value via [App.FromStruct], see example #example_Create_buildFromStructValue

Getting started from New or Create function.

从结构定义中提取信息并构建 RootCommand 及其子命令系统是从 v2.1.36 开始引入的。

在 v2.1.37 中,我们进一步拓展了此功能,使其能够被用于子命令。

这个特性类似于 kong 那样的构造风格,但较为简陋。不过它的能力并未受到限制,相反,你仍然可以通过 With()/Action 方法来进一步定制她。

./examples/tiny/struct/main.go
package main
 
import (
	"context"
	"os"
 
	"github.com/hedzr/cmdr/v2"
	logz "github.com/hedzr/logg/slog"
)
 
const (
	appName = "struct"
	desc    = `struct buidler version of tiny app.`
	version = cmdr.Version
	author  = `The Example Authors`
)
 
func main() {
 
func main() {
	var Root struct {
		Remove struct {
			Full struct {
				NoForce bool `desc:"DON'T Force removal."`
			} `desc:"remove full of files"`
 
			Force     bool `help:"Force removal."`
			Recursive bool `help:"Recursively remove files."`
 
			Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
		} `title:"remove" shorts:"rm" cmd:"" help:"Remove files."`
 
		List struct {
			Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
		} `title:"list" shorts:"ls" cmd:"" help:"List paths."`
	}
 
	app := cmdr.Create(appName, version, author, desc).
		// WithAdders(cmd.Commands...).
		BuildFrom(&Root)
 
	ctx := context.Background()
	if err := app.Run(ctx); err != nil {
		logz.ErrorContext(ctx, "Application Error:", "err", err) // stacktrace if in debug mode/build
		os.Exit(app.SuggestRetCode())
	} else if rc := app.SuggestRetCode(); rc != 0 {
		os.Exit(rc)
	}
}

一般地,可以使用如下的 Struct Tag 名字:

  • title, name:指定 Long title
  • shorts, short:逗号分隔的 Short Titles,其中第一个 title 被用于 Cmd/Flag.Short 字段,其它的被用于 ExtraShorts 字段
  • aliases, alias:指定 Long Alias titles
  • desc, help:指定 Desc 字段,用于帮助屏显示
  • group:指定 Group 字段
  • required:仅对 Flag 有效,指定 Flag.Required 属性。例如 required:"true"
  • env, envvars:仅对 Flag 有效,指定 Flag.EnvVars 属性。你可以指定逗号分隔的多个值,例如:env:"USER,USERPROFILE"
  • head-like, headLike:仅对 Flag 有效,指定 Flag.HeadLike 属性。你可以指定一个布尔量来使能 head-like 能力,例如 head-like:"true"
  • cmdr: 这个键包含特定能力。当前支持两个值:
    • cmdr:"-": 忽略该字段
    • cmdr:"positional": 如果该字段为 []string 类型,那么它将接受该 struct 所对应命令的 Positional Args 处理结果。
  • 其它名字被忽略,未来可能就此在兼容性上做更多工作

值得一提的是,从 v2.1.28 开始,我们支持结构变量的反向绑定,因此 cmdr 对命令行参数的处理结果均将回写到对应的结构变量中。后文 反向绑定 将会对此介绍。

运行效果如下:

$ go run -v -tags hzstudio,hzwork,vscode ./examples/tiny/struct
struct v2.1.36 ~ Copyright © 2025 by The Example Authors ~ All Rights Reserved.
 
Usage:
 
  $ struct  [Options...][files...]
 
Description:
 
  struct buidler version of tiny app.
 
Commands:
 
  rm,remove                                   Remove files.
  ls,list                                     List paths.
 
Global Flags:
 
  [Misc]
    -h, --help,--info,--usage                 Show this help screen (-?) [Env: HELP] (Default: false)
 
Type '-h'/'-?' or '--help' to get this help screen (273x25/46).
More: '-D'/'--debug', '-V'/'--version', '-#'/'--build-info', '--no-color'...

rm full 子命令的运行效果如下:

$ go run ./examples/tiny/struct rm full
struct v2.1.36 ~ Copyright © 2025 by The Example Authors ~ All Rights Reserved.
 
Usage:
 
  $ struct  remove full [Options...][files...]
 
Description:
 
  remove full of files
 
Flags:
 
  -no-force, --no-force                       DON'T Force removal. (Default: false)
 
Parent Flags:
(Cmd{'remove'}):
  -force, --force                             Force removal. (Default: false)
  -recursive, --recursive                     Recursively remove files. (Default: false)
  -path, --path                               Paths to remove. (Default: [])
 
Global Flags:
 
  [Misc]
    -h, --help,--info,--usage                 Show this help screen (-?) [Env: HELP] (Default: false)
 
Type '-h'/'-?' or '--help' to get this help screen (304x25/46).
More: '-D'/'--debug', '-V'/'--version', '-#'/'--build-info', '--no-color'...

使用 With 进行定制

在测试代码中我们也给出了增强的例子。

通过 Create().BuildFrom(R{}) 可以就下例构建出新的菜单结构。

type A struct {
	D
	F1 int
	F2 string
}
type B struct {
	F2 int
	F3 string
}
type C struct {
	F3 bool
	F4 string
}
type D struct {
	E
	FromNowOn F
	F3        bool
	F4        string
}
type E struct {
	F3 bool `title:"f3" shorts:"ff" alias:"f3ff" desc:"A flag for demo" required:"true"`
	F4 string
}
type F struct {
	F5 uint
	F6 byte
}
 
type R struct {
	b   bool // unexported values ignored
	Int int  `cmdr:"-"` // ignored
	A   `title:"a-cmd" shorts:"a,a1,a2" alias:"a1-cmd,a2-cmd" desc:"A command for demo" required:"true"`
	B
	C
	F1 int
	F2 string
}
 
func (A) With(cb cli.CommandBuilder) {
	// customize for A command, for instance: fb.ExtraShorts("ff")
	logz.Info(".   - A.With() invoked.", "cmdbuilder", cb)
}
func (A) F1With(fb cli.FlagBuilder) {
	// customize for A.F1 flag, for instance: fb.ExtraShorts("ff")
	logz.Info(".   - A.F1With() invoked.", "flgbuilder", fb)
}
 
func (s *F) Inc() {
	s.F5++
}

通过为 struct A 增加 With() 方法,你可以对其进一步进行定制,如果传统方案所做的那样。

增加 Action

在上一个例子中,可以为 E,F 子命令增加 OnAction,

// Action method will be called if end-user type subcmd for it (like `app a d e --f3`).
func (E) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	logz.Info(".   - E.Action() invoked.", "cmd", cmd, "args", args)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	return
}
 
// Action method will be called if end-user type subcmd for it (like `app a d f --f5=7`).
func (s F) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	(&s).Inc()
	logz.Info(".   - F.Action() invoked.", "cmd", cmd, "args", args, "F5", s.F5)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	return
}
 
func stringArrayToAnyArray(args []string) (ret []any) {
	for _, it := range args {
		ret = append(ret, it)
	}
	return
}

如此一来,子命令被执行时,Action 方法就将获得执行权。

你也可以为其他层次的子命令添加 Action,它们也能工作,但不被推荐,因为最终用户可能期待中间层次的命令自动显示帮助屏而不是执行某种行为。

指定默认值

在构建命令系统时,可以通过给结构变量指定初值的方式来给出 Flag 的默认值,

func TestStructBuilder_FromStruct(t *testing.T) {
	// New will initialize appS{} struct and make a new
	// rootCommand object into it.
	var w cli.Runner // an empty dummy runner for testing
	a := New(w).Info("demo-app", "0.3.1").Author("hedzr")
	app := a.(*appS)
	// logz.SetLevel(logz.DebugLevel)
 
	// FromStruct assumes creating a command system from RootCommand.Cmd
	// since a bracketed longTitle "(...)" passed.
	b := app.FromStruct(R{
		F2: "/tmp/value",
	})
	b.Build()
 
	assertEqual(t, int32(0), app.inCmd)
	assertEqual(t, int32(0), app.inFlg)
 
	root := app.root.Cmd.(*cli.CmdS)
	assertEqual(t, "/tmp/value", root.Flags()[1].DefaultValue())
 
  //...
}

用于子命令

基本用法

我们已经达成了目标:现在可以在子命令构造时直接引入 struct 定义的命令子系统了。

通过这种方法,有可能简化复杂命令子系统的代码书写问题。

你可以将该能力简单地用于简化代码书写。所以下面的示例中我们就地构造一个 root{} 并利用该结构变量的值完成子命令系统的构建工作。 随后 root{} 就被 Go 回收了。

这种方法适合大多数情况。你将通过典型的 cmdr.Store()/cmd.Store() 的方式访问命令行参数解析结果。并且不仅如此,在 Store 所提供的一致化界面下,你可以以统一的方式访问所有的配置数据集,这包括了命令行参数解析结果、环境变量、外部配置文件,远程配置中心等等。

cmd/multilevel.go
package cmd
 
import (
	"context"
 
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/cmdr/v2/pkg/logz"
)
 
type multiCmd struct{}
 
func (multiCmd) Add(app cli.App) {
	app.Cmd("multi", "m", "").
		Description("multi-level test and imported form struct").
		// Group("Test").
		TailPlaceHolders("[text1, text2, ...]").
		OnAction(soundex).
		FromStruct(root{}).
		With(func(b cli.CommandBuilder) {
			// b.FromStruct(&root{})
		})
}
 
type root struct {
	b   bool // unexported values ignored
	Int int  `cmdr:"-"` // ignored
	A   `title:"a-cmd" shorts:"a,a1,a2" alias:"a1-cmd,a2-cmd" desc:"A command for demo" required:"true"`
	B
	C
	F1 int
	F2 string
}
 
type A struct {
	D
	F1 int
	F2 string
}
type B struct {
	F2 int
	F3 string
}
type C struct {
	F3 bool
	F4 string
}
type D struct {
	E
	FromNowOn F
	F3        bool
	F4        string
}
type E struct {
	F3 bool `title:"f3" shorts:"ff" alias:"f3ff" desc:"A flag for demo" required:"true"`
	F4 string
}
type F struct {
	F5 uint
	F6 byte
}
 
// a --f1 1 --f2 str
// --a.f1 1 --a.f2 str
 
func (A) With(cb cli.CommandBuilder) {
	// customize for A command, for instance: fb.ExtraShorts("ff")
	logz.Info(".   - A.With() invoked.", "cmdbuilder", cb)
}
func (A) F1With(fb cli.FlagBuilder) {
	// customize for A.F1 flag, for instance: fb.ExtraShorts("ff")
	logz.Info(".   - A.F1With() invoked.", "flgbuilder", fb)
}
 
// Action method will be called if end-user type subcmd for it (like `app a d e --f3`).
func (E) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	logz.Info(".   - E.Action() invoked.", "cmd", cmd, "args", args)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	return
}
 
// Action method will be called if end-user type subcmd for it (like `app a d f --f5=7`).
func (s F) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	(&s).Inc()
	logz.Info(".   - F.Action() invoked.", "cmd", cmd, "args", args, "F5", s.F5)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	return
}
 
func (s *F) Inc() {
	s.F5++
}
 
func stringArrayToAnyArray(args []string) (ret []any) {
	for _, it := range args {
		ret = append(ret, it)
	}
	return
}

其运行效果类似于:

# go run ./cli/azt ~~tree
azt v1.0.0 ~ Copyright © 2025 by Azt Authors ~ All Rights Reserved.
azt                                           a good blueprint for you.
  m, multi                                    multi-level test and imported form struct
    a, a-cmd,a1-cmd,a2-cmd                    A command for demo
       = a1,a2
      d                                       (no desc)
        e                                     (no desc)
          * -ff, --f3,--f3ff                    A flag for demo (Default: false) REQUIRED
          -f4, --f4                           (no desc) (Default: )
        from-now-on                           (no desc)
          -f5, --f5                           (no desc) (Default: 0)
          -f6, --f6                           (no desc) (Default: 0)
        -f3, --f3                             (no desc) (Default: false)
        -f4, --f4                             (no desc) (Default: )
      -f1, --f1                               (no desc) (Default: 0)
      -f2, --f2                               (no desc) (Default: )
    b                                         (no desc)
      -f2, --f2                               (no desc) (Default: 0)
      -f3, --f3                               (no desc) (Default: )
    c                                         (no desc)
      -f3, --f3                               (no desc) (Default: false)
      -f4, --f4                               (no desc) (Default: )
    -f1, --f1                                 (no desc) (Default: 0)
    -f2, --f2                                 (no desc) (Default: )
  [Test]
    snd,soundex,sndx,sound                    soundex test
    wrong                                     a wrong command to return error for testing
  [Misc]
    -h, --help,--info,--usage                 Show this help screen (-?) [Env: HELP] (Default: false)
 
Matched flags:
- 1. tree (+1) Flg{'tree'} /TILDE/ | [owner: Cmd{''}] | final-value: true
 
ACTIONS:
- ShowTree

反向绑定用法

如果你习惯于使用其他的命令行参数处理软件包,例如 flag 等等,它们习惯于以各种方式将每一条命令行参数和一个变量相互绑定,如同 task := flag.String("task", "", "The task you want to add to your to-do list") 所做的那样,然后你通过 *task 来访问 --task=abc 的解析结果。

更多的其他第三方库,对此还有进一步的拓展。

这种思路的有利之处显而易见,直观。某些第三方库能够在通过 struct Tag 构造命令系统的同时将 struct 变量与解析结果相互绑定,于是像 TLS 参数包等等就可以以一个 struct 的方式被管理起来。

同样的能力,我们在 v2.1.38 中完成了对此的支持。

对于上一节的例子,你只需要简单地提供一个 struct 变量值的指针作为 FromStruct 的参数,就能获得该能力。

这意味着你将会持久化这个 root 结构的变量。

下面的例子是向你展示如何有效地管理它,如何通过 s.root 取得命令行参数的解析结果。这个示例片段可以被集成到任何 blueprint 示例程序的体系结构中。

cmd/multilevel.go
package cmd
 
import (
	"context"
	"fmt"
 
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/is"
	logz "github.com/hedzr/logg/slog"
)
 
var lastMultiCmd *multiCmd
 
type multiCmd struct {
	root root
}
 
func (s *multiCmd) Add(app cli.App) {
	lastMultiCmd = s
	if is.DebuggerAttached() {
		logz.SetLevel(logz.TraceLevel)
		// app.WithOpts(cli.WithArgs(os.Args[0], "~~tree"))
		logz.Trace(fmt.Sprintf("multiCmd.root.ptr = %p, .val = %+v\n    multiCmd.A.ptr = %p\n    multiCmd.A.D.ptr = %p\n    multiCmd.A.D.E.ptr = %p",
			&s.root, s.root,
			&s.root.A,
			&s.root.A.D,
			&s.root.A.E,
		))
		fmt.Printf("    >> A.Fa3 (positional): %p\n", &s.root.A.Fa3)
	}
 
	app.Cmd("multi", "mu", "").
		Description("multi-level test and imported form struct").
		// Group("Test").
		TailPlaceHolders("[text1, text2, ...]").
		FromStruct(&s.root).
		OnAction(s.Action).
		With(func(b cli.CommandBuilder) {
			// b.FromStruct(&root{})
		})
}
 
func (s *multiCmd) postAction(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	logz.Println(fmt.Sprintf("multiCmd.root: .ptr = %p, .val = %+v\n    multiCmd.A.ptr = %p\n    multiCmd.A.D.ptr = %p\n    multiCmd.A.D.E.ptr = %p",
		&s.root, s.root,
		&s.root.A,
		&s.root.A.D,
		&s.root.A.E,
	))
	logz.OK("postAction done", "s.root", s.root)
	_, _, _ = ctx, cmd, args
	return
}
 
func (s *multiCmd) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	logz.Println(".   - multiCmd.Action() invoked.", "cmd", cmd, "args", args)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	fmt.Printf("root: %+v\n", s.root)
	_ = s.postAction(ctx, cmd, args)
	return
}
 
type root struct {
	b   bool // unexported values ignored
	Int int  `cmdr:"-"` // ignored
	A   `title:"a-cmd" shorts:"a,a1,a2" alias:"a1-cmd,a2-cmd" desc:"A command for demo" required:"true"`
	B   `env:"B"`
	C
	F1 int
	F2 string
}
 
type A struct {
	D
	Fa1 int
	Fa2 string
	Fa3 []string `cmdr:"positional"`
}
type B struct {
	F2 int
	F3 string
}
type C struct {
	F3 bool
	F4 string
}
type D struct {
	E
	FromNowOn F
	F3        bool
	F4        string
}
type E struct {
	F3 bool `title:"f3" shorts:"ff" alias:"f3ff" desc:"A flag for demo" required:"true"`
	F4 string
}
type F struct {
	F5    uint
	F6    byte
	Files []string `cmdr:"positional"`
}
 
// a --f1 1 --f2 str
// --a.f1 1 --a.f2 str
 
func (A) With(cb cli.CommandBuilder) {
	// customize for A command, for instance: fb.ExtraShorts("ff")
	logz.Info(".   - A.With() invoked.", "cmdbuilder", cb)
}
func (A) F1With(fb cli.FlagBuilder) {
	// customize for A.F1 flag, for instance: fb.ExtraShorts("ff")
	logz.Info(".   - A.F1With() invoked.", "flgbuilder", fb)
}
 
// Action method will be called if end-user type subcmd for it (like `app a d e --f3`).
func (s E) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	logz.Info(".   - E.Action() invoked.", "cmd", cmd, "args", args)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	fmt.Printf("E: %+v\n", s)
	if lastMultiCmd != nil {
		fmt.Printf("D: %+v\n", lastMultiCmd.root.A.D)
		fmt.Printf("A: %+v\n", lastMultiCmd.root.A)
		fmt.Printf("A.Fa3 (positional): %p\n", &lastMultiCmd.root.A.Fa3)
		_ = lastMultiCmd.postAction(ctx, cmd, args)
	}
	return
}
 
// Action method will be called if end-user type subcmd for it (like `app a d f --f5=7`).
func (s F) Action(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
	(&s).Inc()
	logz.Info(".   - F.Action() invoked.", "cmd", cmd, "args", args, "F5", s.F5)
	_, err = cmd.App().DoBuiltinAction(ctx, cli.ActionDefault, stringArrayToAnyArray(args)...)
	fmt.Printf("F: %+v\n", s)
	if lastMultiCmd != nil {
		fmt.Printf("D: %+v\n", lastMultiCmd.root.A.D)
		fmt.Printf("A: %+v\n", lastMultiCmd.root.A)
		_ = lastMultiCmd.postAction(ctx, cmd, args)
	}
	return
}
 
func (s *F) Inc() {
	s.F5++
}
 
func stringArrayToAnyArray(args []string) (ret []any) {
	for _, it := range args {
		ret = append(ret, it)
	}
	return
}

代码块中的高亮行是显著有别于上一节例子的部分。

其运行效果类似于:

# go run ./cli/cmdr mu a --fa2=yes,man! -v -f2=ok -- jesus
...
 
Matched commands:
- 1. mu | Cmd{'multi'}
- 2. a | Cmd{'multi.a-cmd'}
 
Matched flags:
- 1. fa2 (+1) Flg{'multi.a-cmd.fa2'} // | [owner: Cmd{'multi.a-cmd'}] | final-value: yes,man!
- 2. v (+1) Flg{'verbose'} /short/ | [owner: Cmd{''}] | final-value: true
- 3. f2 (+1) Flg{'multi.f2'} /short/ | [owner: Cmd{'multi'}] | final-value: ok
 
Positional Args:
- 1. jesus
 
E: {F3:false F4:}
D: {E:{F3:false F4:} FromNowOn:{F5:0 F6:0 Files:[]} F3:false F4:}
A: {D:{E:{F3:false F4:} FromNowOn:{F5:0 F6:0 Files:[]} F3:false F4:} Fa1:0 Fa2:yes,man! Fa3:[jesus]}
A.Fa3 (positional): 0x10536bf60
11:24:56.500756+08:00| [ A ] multiCmd.root: .ptr = 0x10536bee0, .val = {b:false Int:0 A:{D:{E:{F3:false F4:} FromNowOn:{F5:0 F6:0 Files:[]} F3:false F4:} Fa1:0 Fa2:yes,man! Fa3:[jesus]} B:{F2:0 F3:} C:{F3:false F4:} F1:0 F2:ok} ./cli/cmdr/cmd/multilevel.go:43 cmd.(*multiCmd).postAction
        multiCmd.A.ptr = 0x10536bef0
        multiCmd.A.D.ptr = 0x10536bef0
        multiCmd.A.D.E.ptr = 0x10536bef0
 
...

考察该运行结果,Fa2:yes,man! Fa3:[jesus] 表示变量回写成功了,而且 positional-args 也记录了;另外,F2:ok 也代表着变量正确回写了。

上述的这些能力,主要目的在于提供更多的跨库的兼容性,以及针对特定类型的任务简化代码编写。

未来的计划

我们将来未来的几个版本迭代中,进一步地将 struct value 风格与 blueprint app 骨架结合起来,让你更简便快速地构建大型的命令体系结构,同时也不必失去精细控制能力。

同时,传统的构造方案并不受到影响。

:end:

How is this guide?

Edit on GitHub

Last updated on

On this page