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 开始引入的。

这个特性类似于 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 字段,用于帮助屏显示
  • required:仅对 Flag 有效,指定 Flag.Required 属性
  • 其它名字被忽略,未来可能就此在兼容性上做更多工作

运行效果如下:

$ 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 value 风格与 blueprint app 骨架结合起来,让你更简便快速地构建大型的命令体系结构,同时也不必失去精细控制能力。

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

:end:

How is this guide?

Edit on GitHub

Last updated on

On this page