# Subcommands
# 简单 CLI
一个简单的 CLI 应用程序,例如 wget
,并不需要定义子命令。所以这样的应用程序只需要一个 RootCommand
就够了:
func buildRootCmd() (rootCmd *cmdr.RootCommand) {
root := cmdr.Root("wget-cover", wgetVersion).
Header(`GNU Wget 1.20, a non-interactive network retriever.
Usage: wget [OPTION]... [URL]...
Mandatory arguments to long options are mandatory for short options too.`).
Description("", "").
Action(func(cmd *cmdr.Command, args []string) (err error) {
return
})
rootCmd = root.RootCommand()
setStartupFlags(root)
setLoggerFlags(root)
// ...
return
}
func setStartupFlags(root cmdr.OptCmd) {
cmdr.NewBool().
Titles("version", "V").
Description("display the version of Wget and exit").
Group(cStartup).
Action(func(cmd *cmdr.Command, args []string) (err error) {
cmd.PrintVersion()
return cmdr.ErrShouldBeStopException
}).
AttachTo(root)
cmdr.NewBool().
Titles("help", "h").
Description("print this help screen").
Group(cStartup).
Action(func(cmd *cmdr.Command, args []string) (err error) {
// cmd.PrintHelp(false)
return
}).
AttachTo(root)
cmdr.NewBool().
Titles("background", "b", "bg").
Description("go to background after startup").
Group(cStartup).
AttachTo(root)
cmdr.NewString().
Titles("execute", "e").
Description("execute a `.wgetrc'-style command").
Placeholder("COMMAND").
Group(cStartup).
AttachTo(root)
}
func setLoggerFlags(root cmdr.OptCmd) {
cmdr.NewString().
Titles("append-output", "a").
Name("011.append-output").
Description("append messages to FILE").
Placeholder("FILE").
Group(cLogging).
AttachTo(root)
cmdr.NewString().
Titles("output-file", "o").
Name("001.output-file").
Description("log messages to FILE").
Placeholder("FILE").
Group(cLogging).
AttachTo(root)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
可以看到,通过 cmdr.Root()
以及级联的 Description()
,Action
等函数调用我们定义了一个 *cmdr.RootCommand
对象。
Action 函数将会提供一个响应函数。当 wget-cover
被运行时,这个响应函数将会获得控制权。值得注意的是,一旦定义了 Action 函数,那么应用程序运行时就不会自动显示帮助屏了,除非你使用 wget-cover --help
(或者 -h
, -?
等)等方式(这也是 POSIX 兼容的方案)。
# 关于 PreAction 和 PostAction
对于 RootCommand 来说,除了能够定义应用程序级别的信息,例如 appName,version,banner 等等之外,需要注意的就是它的 PreAction 和 PostAction 函数了。在 RootCommand 的 Action 函数被调用之前和之后,这两个函数会分别获得执行控制权。
它们的特殊之处在于,对于子命令的 Action 函数被执行的情况,除了子命令的 PreAction 和 PostAction 会有机会被执行之外,RootCommand 的 PreAction 和 PostAction 也会被执行。所以它们隐含着全局的前置和后置调用的含义。
# 关于排序
子命令/标志 都是可以排序的。
在默认时,子命令/标志 按照它们的 Long/Full 标题字符串的 ASCII 字母数字顺序被显示在帮助屏中(所以添加顺序不重要)。但通过 Name() 在指定一个子命令/标志 的内部名称的同时,还可以添加一个排序用的前缀。如同 Name("011.append-output")
这样。
这个前缀 011
不会被显示出来,但会影响到子命令/标志 的显示顺序。就像这样:
我们可以看到 --output-file
和 --append-output
的顺序被定制过了。
排序用的前缀,可以是英文字母和阿拉伯数字的组合,它们将被依照 ASCII 字符串比较大小的方式来影响到对应条目的顺序。像 zzzz.
,zzz9
.,1234.
等都是有效的前缀。
前缀和常规名称之间以句点分隔。
同样的策略,还被应用在 Group() 上,从而可以影响到 子命令分组、标志分组的顺序。
# 带有子命令的 CLI
我们可以很容易地定义出像 docker 那样的多级命令结构。从理论上说,着没有任何限制,你可以无限地定义多级子命令。
这是通过 command.NewSubCommand()
的方式来实现的。从 rootCmd 开始,我们可以这样定义:
root := cmdr.Root(appName, cmdr_examples.Version).
Copyright(copyright, "hedzr").
Description(desc, longDesc).
Examples(examples)
rootCmd = root.RootCommand()
root.NewSubCommand("soundex", "snd", "sndx", "sound").
Description("soundex test").
Group("Test").
TailPlaceholder("[text1, text2, ...]").
Action(func(cmd *cmdr.Command, args []string) (err error) {
for ix, s := range args {
fmt.Printf("%5d. %s => %s\n", ix, s, cmdr.Soundex(s))
}
return
})
pa := root.NewSubCommand("panic-test", "pa").
Description("test panic inside cmdr actions", "").
Group("Test")
val := 9
zeroVal := zero
dz := pa.NewSubCommand("division-by-zero", "dz").
Description("").
Group("Test").
Action(func(cmd *cmdr.Command, args []string) (err error) {
fmt.Println(val / zeroVal)
return
})
cmdr.NewBool(false).
Titles("enable-abc", "abc").
AttachTo(dz)
pa.NewSubCommand("panic", "pa").
Description("").
Group("Test").
Action(func(cmd *cmdr.Command, args []string) (err error) {
panic(9)
return
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
上面的代码建立了这样的子命令结构:
❯ go run ./examples/simple/ ~~tree
ROOT
snd, soundex, sndx, sound - soundex test
pa, panic-test - test panic inside cmdr actions
dz, division-by-zero -
pa, panic -
g, generate, gen - generators for this app.
s, shell, sh - generate the bash/zsh auto-completion script or install it.
m, manual, man - generate linux man page.
d, doc, markdown, pdf, docx, tex - generate a markdown document, or: pdf/TeX/...
2
3
4
5
6
7
8
9
10
注意 generate
及其子命令时内置的,你也可以通过 cmdr.WithBuiltinCommands()
来禁止几组内置命令集合被自动组合到你的应用程序中去:
func main() {
if err := cmdr.Exec(buildRootCmd(),
// To disable internal commands and flags, uncomment the following codes
cmdr.WithBuiltinCommands(false, false, false, false, true),
); err != nil {
log.Fatalf("Error: %v", err)
}
}
2
3
4
5
6
7
8
前面我们已经提到过,在子命令的 Action 函数被执行的情况,除了子命令的 PreAction 和 PostAction 会有机会被执行之外,RootCommand 的 PreAction 和 PostAction 也会被执行。
所以 RootCommand 的 PreAction 和 PostAction 实际上隐含着全局的前置和后置调用的含义。
# 关于标题
子命令的 Short 短标题、别名 Alias 标题,和长 Long/Full 标题具有相同的含义,所以你可以在命令行中混用它们。
# 关于长标题和 keyPath
但以后我们会看到,子命令以及标志的长标题非常重要,它不能为空,并且会在 cmdr 内部被用作索引关键字来标识一个子命令对象。
对于 division-by-zero
这条子命令来说,它的 keyPath 为 app.panic-test.division-by-zero
,其中 app
是内置的前缀(OptionPrefix),可以被 cmdr.WithOptionPrefix()
所修改。
对于 division-by-zero
专属的标志 enable-abc
来说,其 keyPath 为 app.panic-test.division-by-zero.enable-abc
,也就是所属子命令的 keyPath 加上标志自身的长标题以构成。标志的 keyPath 被用在 cmdr.GetXXX(
) 命令族中以访问该标志的具体值。
例如:cmdr.GetBoolR("panic-test.division-by-zero.enable-abc")
可以取得 enable-abc
的值。如果你在命令行中使用了 --enable-abc
,那么GetBoolR会为其返回 true。
带有 R 后缀的 GetXXX 命令要求你提供的 keyPath 不必包含所谓的 OptionPrefix,所以
cmdr.GetBoolR("panic-test.division-by-zero.enable-abc")
实际上是等效于cmdr.GetBool("app.panic-test.division-by-zero.enable-abc")
的。
带有 P 后缀的 GetXXX 命令要求你分为两个部分来提供 keyPath,所以
cmdr.GetBoolRP("panic-test.division-by-zero“, "enable-abc")
是和cmdr.GetBool("app.panic-test.division-by-zero.enable-abc")
等效的。
🔚