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.
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
}外部脚本文件
cmdr 在 ci/ 中附加了一些短小的 Shell 脚本文件以便对 concise app 进行演示支持。
#!/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 0The 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 commandlineexec()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