命令:重定向
Redirect To...
重定向某命令到另一目的地
。.SetRedirectTo(subcmd) 为根命令提供一个重定向目的地
从 v2.1.19 开始,.SetRedirectTo(subcmd) 不仅仅为根命令提供一个重定向目的地,也支持任何子命令的重定向。
重定向到另一命令 作用于根命令时,可以让命令行输入 app 等价于输入了 app a b c,这对于有的情形可能是有用的。
想象一下 pnpm 这样的包管理器命令,pnpm dev 实际上等价于 pnpm run dev 子命令,都是去运行 package.json 的 scripts 节里的 dev 条目所定义的命令行。
GAVEUP
cmdr.v2 有设想为 RedirectTo 加上 fallback: bool 的子特性。
但这一设想尚未定型,也没有纳入计划表。
Since v2.1.40
recursive 参数的作用在于进行子命令的递归转发。在非递归模式下,仅指定了 redirectTo 的命令自身会被转发到目的地;但在递归模式下,redirectTo 命令及其所有子命令都会被转发。
以 (root) -> server 的转发为例,在非递归模式下,用户输入的 app 相当于 app server 命令;但仅在递归模式下,app run 会被尝试转发为 app server run。
使能 recursive 参数的方法是 SetRedirectTo("server", true),第二个参数的用途即如此。
- 命令转发功能容易引发未能预料的问题,请谨慎使用。
- 使用大量的转发功能事实上代表着你的命令系统的设计存在结构性问题。
- 循环转发子命令可能导致死锁问题,尽管我们将会尝试自动避开他们。
定义
对于根命令
你需要调用到 RootCommand 的 *CmdS.SetRedirectTo(dottedCmdPath, recursive...bool) 接口。达到这一目的的途径可能有多种,下面是一个示例,
func main() {
app := cmdr.Create(appName, version, author, desc).With(func(app cli.App) {
// redirect root commands into "wrong" subcmd for testing.
//
// For a dad command such as "server" command, it
// would translate `app start|stop` -> `app server start|stop`.
app.WithRootCommand(func(root *cli.RootCommand) {
root.SetRedirectTo("wrong")
})
}).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/redirect-to-subcmd。
在范例中,WithAdders() 将会为 app 添加两套子命令:jump to 以及 wrong。这是标准的 examples/cmd/ 子包用于演示目的的基础设施。
而 With(func(app cli.App){...}) 闭包则提供一个流式调用中操作 app 对象的机会。
在这里,app.WithRootCommand(...) 可以用于操作 SetRedirectTo 接口函数。
现在,运行 app 的效果就和 app wrong 相同。
可以有多种途径达到目的
为了达到完成到 RootCommand.SetRedirectTo 的调用,可以有多种途径。例如下面这一种:
func prepareApp(commands ...cli.CmdAdder) cli.App {
return loaders.Create(
appName, version, author, desc,
).
// importing devmode package and run its init():
With(func(app cli.App) {
logz.Debug("in dev mode?", "mode", devmode.InDevelopmentMode())
app.RootBuilder(func(parent cli.CommandBuilder) {
parent.RedirectTo("new")
})
// app.WithRootCommand(func(root *cli.RootCommand) {
// root.SetRedirectTo("new")
// })
}).
WithBuilders(
// examples.AddHeadLikeFlagWithoutCmd, // add a `--line` option, feel free to remove it.
).
WithAdders(commands...).
Build()
}对于子命令(Since V2.1.19)
对于子命令的重定向,你可以使用 CommandBuilder.RedirectTo(dottedCmdPath) 接口。下面是一个示例:
package cmd
import (
"github.com/hedzr/cmdr/v2/cli"
logz "github.com/hedzr/logg/slog"
"github.com/hedzr/cmdr-cli/cli/cmdr/internal/core"
)
type initCmd struct{}
func (*initCmd) Add(app cli.App) {
coreTool := core.New()
logz.Verbose("initCmd")
app.Cmd("init", "i", "in").
Description("initial a cmdr-based app").
// Group("Test").
// TailPlaceHolders("[text1, text2, ...]").
Examples(`
$ {{.AppName}} init
create a new cmdr cli app with default name 'myapp'
$ {{.AppName}} init --output=/tmp/src.test myapp
create a new cmdr cli app with your name
`).
RedirectTo("new").
OnAction(coreTool.New).
With(func(b cli.CommandBuilder) {
})
}这个示例来自 cmdr-cli 工具的 init 子命令源代码。
同样地,使用 dottedCmdPath 可以重定向到多级子命令,例如 jump.to。
不恰当的定义
无效的 dottedCmdPath 并不会报错,但请勿依赖这一现状,cmdr 未来可能根据整体规划而调整这一策略,例如可能返回一个 well-known error 对象等等。
cmdr 在尝试解决重定向目的地时,简单地抛弃无效的 dottedCmdPath。
如果你定义了过于复杂的重定向路径,甚至导致了循环路径,cmdr 仍然应该工作良好并忽略问题并及时熔断。但这些情况对于终端用户仍然是不友好的,所以不宜滥用 cmdr 的容错能力。
一个例子是,当 rootCmd -> new 和 init -> new 同时存在时,就会隐式地形成了循环路径。这是因为 cmdr 在尝试为 init 命令匹配标志时,它首先会试图匹配 new 命令的标志,而当new 命令未能完成标志的匹配时,cmdr 将会向上匹配,即沿着 new 的所有父命令的拥有者链条反向地依次尝试完成标志的匹配,直到尝试在 rootCmd 上进行标志匹配,注意到 rootCmd -> new RedirectTo 的原因,又会重定向到 new 命令继续匹配。
这就导致了循环链条。
cmdr 将会追踪所有重定向规则,因此将会在最后一步从 rootCmd 推进到 new 的时候发生熔断,从而解决循环问题。
另一个简单的例子是,你尝试定义 new -> new 重定向规则。它将会完成,但显然是错误的。cmdr 通过追踪规则也同样解决该循环问题。
当前 cmdr 并不试图警告你编码构造了不恰当的规则和命令层次结构,这需要你自行规划和确认。
惯用法
由于 cmdr 在标志匹配时自动解决到重定向目标以及父级命令链条,所以全局内建标志 --help 可以在 app init --help 命令行的解析中正确地被找到。类似地,app init --out /tmp/src.test 能够正确地将 --out 解决为 属于 new 命令。
因此对于重定向命令本身(例如 init)来说,无需定义任何标志,它将自动引用重定向目标命令的标志集合来完成匹配。
多级命令
如果想要重定向到多级子命令,可以使用 dottedPath/dottedCmdPath。例如跳转到 jump to 子命令,可以使用 SetRedirectTo("jump.to") 来达成。
运行时
本文开始的示例程序的运行时效果如同这样:
$ go run ./examples/redirect-to-subcmd
the duration is: 0s
22:57:12.597014+08:00| [ERR] Application Error: err="[io: read/write on closed pipe | something's wrong | permission denied]" ./examples/redirect-to-subcmd/main.go:38 main.main
$返回的 Application Error 显示出根命令已经正确转向到了 “wrong” 子命令。
此时根命令默认功能就不再是显示帮助屏了,为了显示帮助屏,你需要显式地 -h/--help。
帮助屏中的效果
以下截图取自 cmdr-cli 工具(即将发布)。
Rootcmd

Subcmd

额外的话题
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?
最后更新于