hzDocs
hzDocs
文章 / 文档hedzr.com首页

cmdr series

介绍 cmdr

Guide

你的首个 CLI app更适合工程实践的版本
循序渐进
基本概念
命令命令:执行外部程序命令:预设参数命令:重定向命令:动态命令清单命令:在配置文件中定义别名清单命令:事件响应函数标志标志:必须项标志:可翻转组标志:枚举值标志:`Head -1` 风格标志:调用外部工具获得输入标志:自动否定标志:加号 `+` 前缀标志:事件响应函数解析结果内建命令和标志帮助子系统可共享共存的 app 实例辨析顶级函数WithOptsBackstage
如何……
Auto-close the ClosersRead config into structUsing is DetectorsUsing Store

References

What's New
Packages

Others

Examples
Blueprint
产品发布
产品发布之前
介绍 cmdr-cxx

Guide

cmdr supports

Intro

Guide

More features

References

Others

evendeep(-go)

Guide

Usagesdeepcopydeepdiffdeepequal
logg/slog(-go)

Guide

Guide

others

Components
trie-cxx

Guide

Guide

links

On Github

命令:动态命令清单

loading dynamic commands at runtime

DynCommand

cmdr 支持动态地扫描和识别动态的子命令列表,它们在运行时刻被计算和列举。

实例:concise

示例程序 concise 在子命令 jump 上实现了动态加载外部脚本为子命令的功能。

这并不影响你按照常规方案为 jump 增加固定的子命令 to:

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

它们将会被在运行时动态载入为 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?

Components

Components

On Github

How is this guide?

最后更新于

目录

DynCommand
实例:concise
外部脚本文件
运行时
实现方法
动态计算的标志?
额外的话题