# 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)
}
1
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 不会被显示出来,但会影响到子命令/标志 的显示顺序。就像这样:

image-20200713221103126

我们可以看到 --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
		})
1
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/...
1
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)
	}
}
1
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") 等效的。

🔚