hzDocs
hzDocs
Articles / Postshedzr.comIntroduction

Guide

Your First CLI AppConcise Version
Step by step
Concepts
CommandCommand: Invoke programCommand: Presetting ArgsCommand: RedirectToCommand: DynCommandCommand: Aliases from ConfigCommand: Event HandlersFlagFlag: RequiredFlag: Toggle GroupFlag: Valid ArgsFlag: `Head -1` styleFlag: External EditorFlag: NegatableFlag: Leading Plus Sign `+`Flag: Event Handlers解析结果Builtin Commands & Flags帮助子系统Shared App辨析Package level functionsWithOptsBackstage
Howto ...
Auto-close the ClosersRead config into structUsing is DetectorsUsing Store

References

What's New
Packages

Others

Examples
Blueprint
产品发布
产品发布之前
Concepts

Command: DynCommand

loading dynamic commands at runtime

Lists dynamic commands at runtime

cmdr allows scanning and collecting subcmds at runtime.

Sample in concise

The example app concise demostrates dyn-cmd in codes of cmd jump.

At same time, jump still adds its normal subcmd to programatically.

./examples/tiny1/main.go
package main

import (
	"context"
	"os"

	"github.com/hedzr/cmdr/v2"
	"github.com/hedzr/cmdr/v2/examples/cmd"
	"github.com/hedzr/cmdr/v2/pkg/logz"
)

const (
	appName = "concise"
	desc    = `concise version of tiny app.`
	version = cmdr.Version
	author  = `The Example Authors`
)

func main() {
	app := cmdr.Create(appName, version, author, desc).
		WithAdders(cmd.Commands...).
		Build()

	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)
	}
}
./examples/cmd/all.go
package cmd

import (
	"github.com/hedzr/cmdr/v2/cli"
)

var Commands = []cli.CmdAdder{
	jumpCmd{},
	// wrongCmd{},
}
./examples/cmd/all.go
package cmd

import (
	"context"

	"github.com/hedzr/cmdr/v2"
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/cmdr/v2/examples/dyncmd"
	"github.com/hedzr/cmdr/v2/pkg/dir"
	logz "github.com/hedzr/logg/slog"
)

type jumpCmd struct{}

func (jumpCmd) Add(app cli.App) {
	app.Cmd("jump").
		Description("jump command").
		Examples(`jump example`). // {{.AppName}}, {{.AppVersion}}, {{.DadCommands}}, {{.Commands}}, ...
		Deprecated(`v1.1.0`).
		Group("Test").
		// Group(cli.UnsortedGroup).
		// Hidden(false).
		OnEvaluateSubCommands(dyncmd.OnEvalJumpSubCommands).
		// Both With(cb) and Build() to end a building sequence
		With(func(b cli.CommandBuilder) {
			b.Cmd("to").
				Description("to command").
				Examples(``).
				Deprecated(`v0.1.1`).
				OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
					// cmd.Set() == cmdr.Set(), cmd.Store() == cmdr.Store(cmd.GetDottedPath())
					_, _ = cmd.Set().Set("tiny3.working", dir.GetCurrentDir())
					println()
					println("dir:", cmd.Set().WithPrefix("tiny3").MustString("working"))

					cs := cmd.Set().WithPrefix(cli.CommandsStoreKey, "jump.to")
					if cs.MustBool("full") {
						println()
						println(cmd.Set().Dump())
					}
					cs2 := cmd.Store()
					if cs2.MustBool("full") != cs.MustBool("full") {
						logz.Panic("a bug found")
					}
					app.SetSuggestRetCode(1) // ret code must be in 0-255
					return                   // handling command action here
				}).
				With(func(b cli.CommandBuilder) {
					b.Flg("full", "f").
						Default(false).
						Description("full command").
						Build()
				})
		})
}
./examples/dyncmd/dyncmd.go
package dyncmd

import (
	"context"

	"github.com/hedzr/cmdr/v2/cli"
)

// OnEvalJumpSubCommands querys shell scripts in EXT directory
// (typically it is `/usr/local/lib/<app-name>/ext/`) and build
// as subcommands dynamically.
//
// In this demo app, we looks up `./ci/pkg/usr.local.lib.large-app/ext`
// with hard-code.
//
// EXT directory: see the [cmdr.UsrLibDir()] for its location.
func OnEvalJumpSubCommands(ctx context.Context, c cli.Cmd) (it cli.EvalIterator, err error) {
	return onEvalJumpSubCommands(ctx, c)
}
./examples/dyncmd/litecmd.go
package dyncmd

import (
	"context"
	"os"
	"path"

	"github.com/hedzr/cmdr/v2"
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/cmdr/v2/pkg/dir"
	"github.com/hedzr/is/exec"
	"github.com/hedzr/is/term/color"
	"github.com/hedzr/store"
)

// onEvalJumpSubCommands querys shell scripts in EXT directory
// (typically it is `/usr/local/lib/<app-name>/ext/`) and build
// as subcommands dynamically.
//
// In this demo app, we looks up `./ci/pkg/usr.local.lib.large-app/ext`
// with hard-code.
//
// EXT directory: see the [cmdr.UsrLibDir()] for its location.
func onEvalJumpSubCommands(ctx context.Context, c cli.Cmd) (it cli.EvalIterator, err error) {
	files := make(map[string]*liteCmdS)
	pos := 0
	var keys []string

	baseDir := cmdr.UsrLibDir()
	if dir.FileExists(baseDir) {
		baseDir = path.Join(baseDir, "ext")
	} else {
		baseDir = path.Join("ci", "pkg", "usr.local.lib", c.App().Name(), "ext")
	}
	if !dir.FileExists(baseDir) {
		return
	}

	err = dir.ForFile(baseDir, func(depth int, dirName string, fi os.DirEntry) (stop bool, err error) {
		if fi.Name()[0] == '.' {
			return
		}
		key := path.Join(dirName, fi.Name())
		files[key] = &liteCmdS{dirName: dirName, fi: fi, depth: depth, owner: c}
		keys = append(keys, key)
		return
	})

	it = func(context.Context) (bo cli.Cmd, hasNext bool, err error) {
		if pos < len(keys) {
			key := keys[pos]
			bo = files[key]
			pos++
			hasNext = pos < len(keys)
		}
		return
	}
	return
}

type liteCmdS struct {
	dirName string
	fi      os.DirEntry
	depth   int
	owner   cli.Cmd
	group   string

	hitTitle string
	hitTimes int
}

var _ cli.Cmd = (*liteCmdS)(nil)

// var _ cli.CmdPriv = (*liteCmdS)(nil)

func (s *liteCmdS) name() string { return s.fi.Name() }

func (s *liteCmdS) String() string { return path.Join(s.dirName, s.name()) }

func (s *liteCmdS) GetDottedPath() string        { return cli.DottedPath(s) }
func (s *liteCmdS) GetTitleName() string         { return s.name() }
func (s *liteCmdS) GetTitleNamesArray() []string { return []string{s.name()} }
func (s *liteCmdS) GetTitleNames() string        { return s.name() }

func (s *liteCmdS) App() cli.App       { return nil }
func (s *liteCmdS) Set() store.Store   { return s.Root().App().Store() }
func (s *liteCmdS) Store() store.Store { return cmdr.Store() }

func (s *liteCmdS) OwnerIsValid() bool {
	if s.OwnerIsNotNil() {
		if cx, ok := s.owner.(*liteCmdS); ok {
			return cx != s
		}
	}
	return false
}
func (s *liteCmdS) OwnerIsNil() bool                    { return s.owner == nil }
func (s *liteCmdS) OwnerIsNotNil() bool                 { return s.owner != nil }
func (s *liteCmdS) OwnerCmd() cli.Cmd                   { return s.owner }
func (s *liteCmdS) SetOwnerCmd(c cli.Cmd)               { s.owner = c }
func (s *liteCmdS) Root() *cli.RootCommand              { return s.owner.Root() }
func (s *liteCmdS) SetRoot(*cli.RootCommand)            {}
func (s *liteCmdS) OwnerOrParent() cli.BacktraceableMin { return s.owner.(cli.Backtraceable) }

func (s *liteCmdS) Name() string             { return s.name() }
func (s *liteCmdS) SetName(string)           {}
func (s *liteCmdS) ShortName() string        { return s.name() }
func (s *liteCmdS) ShortNames() []string     { return []string{s.name()} }
func (s *liteCmdS) AliasNames() []string     { return nil }
func (s *liteCmdS) Desc() string             { return s.String() }
func (s *liteCmdS) DescLong() string         { return "" }
func (s *liteCmdS) SetDesc(desc string)      {}
func (s *liteCmdS) Examples() string         { return "" }
func (s *liteCmdS) TailPlaceHolder() string  { return "" }
func (s *liteCmdS) GetCommandTitles() string { return s.name() }

func (s *liteCmdS) GroupTitle() string { return cmdr.RemoveOrderedPrefix(s.SafeGroup()) }
func (s *liteCmdS) GroupHelpTitle() string {
	tmp := s.SafeGroup()
	if tmp == cli.UnsortedGroup {
		return ""
	}
	return cmdr.RemoveOrderedPrefix(tmp)
}
func (s *liteCmdS) SafeGroup() string {
	if s.group == "" {
		return cli.UnsortedGroup
	}
	return s.group
}
func (s *liteCmdS) AllGroupKeys(chooseFlag, sort bool) []string { return nil }
func (s *liteCmdS) Hidden() bool                                { return false }
func (s *liteCmdS) VendorHidden() bool                          { return false }
func (s *liteCmdS) Deprecated() string                          { return "" }
func (s *liteCmdS) DeprecatedHelpString(trans func(ss string, clr color.Color) string, clr, clrDefault color.Color) (hs, plain string) {
	return
}

func (s *liteCmdS) CountOfCommands() int                               { return 0 }
func (s *liteCmdS) CommandsInGroup(groupTitle string) (list []cli.Cmd) { return nil }
func (s *liteCmdS) FlagsInGroup(groupTitle string) (list []*cli.Flag)  { return nil }
func (s *liteCmdS) SubCommands() []*cli.CmdS                           { return nil }
func (s *liteCmdS) Flags() []*cli.Flag                                 { return nil }

func (s *liteCmdS) HeadLikeFlag() *cli.Flag   { return nil }
func (s *liteCmdS) SetHeadLikeFlag(*cli.Flag) {}

func (s *liteCmdS) SetHitTitle(title string) {
	s.hitTitle = title
	s.hitTimes++
}
func (s *liteCmdS) HitTitle() string { return s.hitTitle }
func (s *liteCmdS) HitTimes() int    { return s.hitTimes }

func (s *liteCmdS) RedirectTo() (dottedPath string) { return }
func (s *liteCmdS) SetRedirectTo(dottedPath string) {}

func (s *liteCmdS) PresetCmdLines() []string         { return nil }
func (s *liteCmdS) InvokeProc() string               { return "" }
func (s *liteCmdS) InvokeShell() string              { return "" }
func (s *liteCmdS) Shell() string                    { return "" }
func (c *liteCmdS) SetPresetCmdLines(args ...string) {}
func (c *liteCmdS) SetInvokeProc(str string)         {}
func (c *liteCmdS) SetInvokeShell(str string)        {}
func (c *liteCmdS) SetShell(str string)              {}

func (s *liteCmdS) CanInvoke() bool {
	return s.fi.Type().IsRegular()
}

func (s *liteCmdS) Invoke(ctx context.Context, args []string) (err error) {
	fullPath := path.Join(s.dirName, s.name())
	err = exec.Run("sh", "-c", fullPath)
	return
}

func (s *liteCmdS) OnEvalSubcommands() cli.OnEvaluateSubCommands {
	return nil
}
func (s *liteCmdS) OnEvalSubcommandsOnce() cli.OnEvaluateSubCommands {
	return nil
}
func (s *liteCmdS) OnEvalSubcommandsOnceInvoked() bool {
	return false
}
func (s *liteCmdS) OnEvalSubcommandsOnceCache() []cli.Cmd {
	return nil
}
func (s *liteCmdS) OnEvalSubcommandsOnceSetCache(list []cli.Cmd) {
}

func (c *liteCmdS) IsDynamicCommandsLoading() bool { return false }
func (c *liteCmdS) IsDynamicFlagsLoading() bool    { return false }

func (s *liteCmdS) OnEvalFlags() cli.OnEvaluateFlags {
	return nil
}
func (s *liteCmdS) OnEvalFlagsOnce() cli.OnEvaluateFlags {
	return nil
}
func (s *liteCmdS) OnEvalFlagsOnceInvoked() bool {
	return false
}
func (s *liteCmdS) OnEvalFlagsOnceCache() []*cli.Flag {
	return nil
}
func (s *liteCmdS) OnEvalFlagsOnceSetCache(list []*cli.Flag) {
}

func (s *liteCmdS) findSubCommandIn(ctx context.Context, cc cli.Cmd, children []cli.Cmd, longName string, wide bool) (res cli.Cmd) {
	return
}
func (s *liteCmdS) findFlagIn(ctx context.Context, cc cli.Cmd, children []cli.Cmd, longName string, wide bool) (res *cli.Flag) {
	return
}
func (s *liteCmdS) findFlagBackwardsIn(ctx context.Context, cc cli.Cmd, children []cli.Cmd, longName string) (res *cli.Flag) {
	return
}
func (s *liteCmdS) partialMatchFlag(context.Context, string, bool, bool, map[string]*cli.Flag) (matched, remains string, ff *cli.Flag, err error) {
	return
}

func (s *liteCmdS) Match(ctx context.Context, title string) (short bool, cc cli.Cmd) {
	return
}
func (s *liteCmdS) TryOnMatched(position int, hitState *cli.MatchState) (handled bool, err error) {
	return
}
func (s *liteCmdS) MatchFlag(ctx context.Context, vp *cli.FlagValuePkg) (ff *cli.Flag, err error) { //nolint:revive
	return
}

func (s *liteCmdS) FindSubCommand(ctx context.Context, longName string, wide bool) (res cli.Cmd) {
	return
}
func (s *liteCmdS) FindFlagBackwards(ctx context.Context, longName string) (res *cli.Flag) {
	return
}
func (c *liteCmdS) SubCmdBy(longName string) (res cli.Cmd) { return }
func (c *liteCmdS) FlagBy(longName string) (res *cli.Flag) { return }
func (s *liteCmdS) ForeachFlags(context.Context, func(f *cli.Flag) (stop bool)) (stop bool) {
	return
}
func (s *liteCmdS) Walk(ctx context.Context, cb cli.WalkCB) {
	return
}
func (s *liteCmdS) WalkGrouped(ctx context.Context, cb cli.WalkGroupedCB) {
	return
}
func (s *liteCmdS) WalkBackwardsCtx(ctx context.Context, cb cli.WalkBackwardsCB, pc *cli.WalkBackwardsCtx) {
	return
}
func (s *liteCmdS) WalkEverything(ctx context.Context, cb cli.WalkEverythingCB) {
}
func (s *liteCmdS) WalkFast(ctx context.Context, cb cli.WalkFastCB) (stop bool) { return }

func (s *liteCmdS) DottedPathToCommandOrFlag(dottedPath string) (cc cli.Backtraceable, ff *cli.Flag) {
	return
}
all.go
jump.go
dyncmd.go
litecmd.go
main.go

外部脚本文件

cmdr 在 ci/ 中附加了一些短小的 Shell 脚本文件以便对 concise app 进行演示支持。

cpu
disk
memory
#!/usr/bin/env bash
[ -f ../bash.config ] && . ../bash.config || { [ -f /usr/local/bin/bash.config ] && . /usr/local/bin/bash.config || :; }
is_darwin()      { [[ $OSTYPE == *darwin* ]]; }
is_darwin && {
  ps -A -o %cpu | awk '{s+=$1} END {print s "%"}'
} || {
  top -b -n2 -p 1 | fgrep "Cpu(s)" | tail -1 | awk -F'id,' -v prefix="$prefix" '{ split($1, vs, ","); v=vs[length(vs)]; sub("%", "", v); printf "%s%.1f%%\n", prefix, 100 - v }'
}
exit 0
#!/usr/bin/env bash
[ -f ../bash.config ] && . ../bash.config || { [ -f /usr/local/bin/bash.config ] && . /usr/local/bin/bash.config || :; }
df -hal
:
#!/usr/bin/env bash
[ -f ../bash.config ] && . ../bash.config || { [ -f /usr/local/bin/bash.config ] && . /usr/local/bin/bash.config || :; }
is_darwin()      { [[ $OSTYPE == *darwin* ]]; }
is_darwin && {
  vm_stat | perl -ne '/page size of (\d+)/ and $size=$1; /Pages\s+([^:]+)[^\d]+(\d+)/ and printf("%-16s % 16.2f Mi\n", "$1:", $2 * $size / 1048576);'
} || {
  free
}
exit 0

The shell scripts, cpu, memory and disk in the directory ./ci/usr.local.lib/concise/ext/ will be added as subcmd of jump.

The special directory ./ci/usr.local.lib/concise/ext/ is for development. For the product mode, cmdr will locate the system folder at /usr/local/lib/concise/ext/.

Run

The result of the example app is,

$ go run ./examples/tiny/concise jump
concise v2.1.1 ~ Copyright © 2025 by The Example Authors ~ All Rights Reserved.

Usage:

  $ concise jump [Options...][files...]

Description:

  jump command

Examples:

  jump example

Commands:
  to                                          to command [Since: v0.1.1]
  cpu                                         ci/pkg/usr.local.lib/concise/ext/cpu
  disk                                        ci/pkg/usr.local.lib/concise/ext/disk
  memory                                      ci/pkg/usr.local.lib/concise/ext/memory

Global Flags:
  [Misc]
    -h, --help,--info,--usage                 Show this help screen (-?) [Env: HELP] (Default: false)

Type '-h'/'-?' or '--help' to get command help screen.
More: '-D'/'--debug', '-V'/'--version', '-#'/'--build-info', '--no-color'...
$ go run ./examples/tiny/concise jump cpu
100.5%
$

The dyncmds have been listed in help screen, and they will be launched properly.

Backstages

The backstage of these dyncmds is in litecmd.go. At cmdr preparing time, it loads the dyncmds from certain a target folder, make them invokeable (by InvokeShell(...)). For the detail, see also function onEvalJumpSubCommands.

Basically, loading dyncmds needs specifying the callback to OnEvaluateSubCommands(cb),

OnEvaluateSubCommands(dyncmd.OnEvalJumpSubCommands).

cmdr will request the callback handler at these time:

  • preprocess() at bootstrap time
  • printing help screen
  • parse() parsing the commandline
  • exec() invoking the parsed result
  • ...

An expcetion, the callback handler will be ignored in ~~tree listing.

Another case is OnEvaluateSubCommandsOnce(), which will be called back just once.

DynFlags ?

cmdr supports calculating dynflags indeed.

Please looking for app.Flg().OnEvaluateFlags() / OnEvaluateFlagsOnce().

额外的话题

Howto run a subcmd directly from root

Command

Command: Invokes

Command: Preset Args

Command: Redirect To

Command: Dynamic Cmd

Command: Dynamic Cmd From Config

Command: Event Handlers

What is Next?

How is this guide?

Last updated on

Command: RedirectTo

Redirect To...

Command: Aliases from Config

loading alias commands from a config file

On this page

Lists dynamic commands at runtime
Sample in concise
外部脚本文件
Run
Backstages
DynFlags ?
额外的话题