hzDocs

From struct-value and Tag

BuildFrom Example

You can build command system by kinds of forms:

  • 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.

Building command hierarchy from struct (and value and Tag) is a new feature since cmdr.v2 v2.1.36.

And more, from v2.1.27, FromStruct can be applied on subcommand building.

Again, since v2.1.38, passing the pointer of the struct variable into FromStruct will make binding-to-variable(s) feature available.

This feature is quite like kong, but a little bit rough. Nonetheless, its abilities are still fully by supplying With() and Action() methods to a stuct.

./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)
	}
}

Commonly these keys are useful:

  • title, name: Long title field
  • shorts, short: comma-separated Short Titles. First of them will be used for Flag.Short field, else for ExtraShorts
  • aliases, alias: Long Alias titles
  • desc, help: Desc field for displaying in help screen
  • group: Group field for help screen
  • required: Avaliable for Flag, the Flag.Required field
  • env, envvars: Avaliable for Flag, the Flag.EnvVars field. comma-separated, just like env:"USER,USERPROFILE"
  • head-like, headLike: Avaliable for Flag, the Flag.HeadLike field。For example, head-like:"true"
  • cmdr: some abilities like:
    • cmdr:"-": the field should be ignored
    • cmdr:"positional": when the field has type []string, it'll receive the positional args from parsing.
  • the others will be ignored

The notable thing is, since v2.1.28, binding-to-var/field is supported. See the section Bind to Variable or Field later.

Run the app like this,

$ 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'...

The help screen of rm full subcommand is:

$ 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'...

Using With() method

Defining each structs is useful to controlling them.

The following structs can be passing into cmdr by 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++
}

Here the With() methods allow you customize a command or flag with traditional way.

Using Action() method

In above example, we could add Action() method to E and F to give the invoking OnAction callback.

// 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
}

So, there it is.

Specifying default value

To specify default value to a flag, you could setup them in struct-value.

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())
 
  //...
}

For Subcmd

Basic Usage

We reached a new mailstone: now you can import children command system into anywhere from a given struct value and tags.

This way can simplify coding work sometimes.

In most cases, we just need a more shorter and rapider approach to code the initial jobs. So here is a sample code to reflect-read struct type and tags from root{} and build subcommands for a subcmd multi. After Add(), the root{} will be recycled by Go GC.

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
}

Running it and get these outputs:

# 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

Binding to Variable or Field

You could rightfully build command-line args parser with others library, like flag, or some 3rd-party libraries. All of them are used to bind a flag onto a variable (or its pointer), as task := flag.String("task", "", "The task you want to add to your to-do list") have been doing. And you would get the parsed result by *task.

More of the others libs did some further expansions on it.

The same thing had been done at cmdr.v2 v2.1.38 release.

As a control of the above sample code, you could just supply a pointer of struct value to FromStruct to get bindings of all of struct fields. It's observed that you must persist the value somewhere so that you can access the result in it later.

The below sample shows how we did these. It can also be integrated to any blueprint like's app.

cmd/multilevel.go
package cmd
 
import (
	"context"
	"fmt"
 
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/is"
	logz "github.com/hedzr/logg/slog"
)
 
type multiCmd struct {
	root root
}
 
func set(app cli.App, s *multiCmd) {
	app.Store().Set("multi-cmd", s)
}
 
func get(app cli.App) (s *multiCmd) {
	if ptr, found := app.Store().Get("multi-cmd"); found && ptr != nil {
		if ss, ok := ptr.(*multiCmd); ok {
			s = ss
		}
	}
	return
}
 
func (s *multiCmd) Add(app cli.App) {
	set(app, s)
 
	// just for debugging, removing this 'if' branch is safe.
	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)
	}
 
	// add 'multi' subcmd, and add more subcmds and flags from `root` struct
	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, anyArrayToAnyArray(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, anyArrayToAnyArray(args)...)
	fmt.Printf("E: %+v\n", s)
	if lastMultiCmd := get(cmd.App()); 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, anyArrayToAnyArray(args)...)
	fmt.Printf("F: %+v\n", s)
	if lastMultiCmd := get(cmd.App()); 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 anyArrayToAnyArray[T any](args []T) (ret []any) {
	ret = make([]any, 0, len(args))
	for _, it := range args {
		ret = append(ret, it)
	}
	return
}

The result is,

# 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
 
...

Reviewing the result outputs, Fa2:yes,man! Fa3:[jesus] proved the write-back of --fa2 is successful. And also positional-args are record into Fa3. At another side, F2:ok means the write-back of -f2=ok is ok.

Conslusion

In this article, the above abilities of binding to struct field, can be used to speed up your coding.

The future

We could iterate this feature in recent versions to integrated with blueprint app.

:end:

How is this guide?

Edit on GitHub

Last updated on

On this page