命令:动态命令清单
loading dynamic commands at runtime
DynCommand
cmdr 支持动态地扫描和识别动态的子命令列表,它们在运行时刻被计算和列举。
实例:concise
示例程序 concise 在子命令 jump 上实现了动态加载外部脚本为子命令的功能。
这并不影响你按照常规方案为 jump 增加固定的子命令 to:
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)
}
}package cmd
import (
"github.com/hedzr/cmdr/v2/cli"
)
var Commands = []cli.CmdAdder{
jumpCmd{},
// wrongCmd{},
}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()
})
})
}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)
}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它们将会被在运行时动态载入为 jump 的子命令。
运行时
上面的示例程序的运行时效果如同这样:
$ 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%
$实现方法
从基本面上讲,实现动态命令加载主要是利用到 cmdr Cmd 构造器提供的
OnEvaluateSubCommands(dyncmd.OnEvalJumpSubCommands).接口。
你可以在 cmdr 需要列举、解析、运行动态命令的各种时刻,通过提供回调函数来返回你的命令清单。
如果你只想被回调一次而不是始终回调,那么可以使用 OnEvaluateSubCommandsOnce() 接口。
cmdr concise 以及 dyncmd 提供的是一个较为全面的实现方法,它构造了一个 liteCmd 对象用于包装诸如 cpu shell 脚本。
你可以仿照这种方法,也可以采用你自己的思路。
动态计算的标志?
确实也支持标志清单的动态计算。
请查阅 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?
最后更新于