hzDocs

管理 app 配置

集成 `Store` 能够提供应用程序配置数据的管理能力

tiny2 Example

通过集成 hedzr/store1,可以无缝地添加应用程序配置数据的管理功能。

tiny1 示例程序中,实际上 cmdr 也隐含地使用了一个 DymmyStore,这个空包将会忽略对配置数据的访问请求。

通过集成 hedzr/cmdr-loaders 提供的插件,可以添加符合 GNU 文件夹规范的外部配置文件自动加载。

下面的示例完成了这些任务:

./examples/tiny2/main.go
package main
 
import (
	"context"
	"os"
 
	"github.com/hedzr/cmdr-tests/examples/common"
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/cmdr/v2/examples/cmd" // import jumpCmd
	logz "github.com/hedzr/logg/slog"
)
 
const (
	appName = "tiny2-app"
	desc    = ``
)
 
func main() {
	app := chain(common.PrepareApp(
		appName, desc,
	)(cmd.Commands...)) // import jumpCmd
 
	ctx := context.Background() // with cancel can be passed thru in your actions
	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)
	}
}
 
func chain(app cli.App) cli.App {
	return app
}

在 tiny2 示例程序中,我们加上了到 Store 的集成,并且创建了 jump to 这样的两级子命令。

hedzr/cmdr-loaders 提供了一个对 cmdr.Create() 更进一步的包装接口 loaders.Create(),它将会自动加载恰当位置的外部配置文件或者配置源。这个工具包的目的是整合对 TOML 和 JSON 格式的配置文件进行自动加载,且符合 GNU 以及 Unix-like 习惯的文件布局。

hedzr/cmdr-loaders 作用相同,除了增加了到更多配置文件格式(例如 YAML,HCL,等等)的支持,也带入了更多了到第三方库的依赖关系。

在此基础上,lite.PrepareApp()/loaders.PrepareApp() 是进一步的包装,目的在于为示例程序提供默认的 Commands 和 Flags 定义。

hedzr/cmdr-tests 延续上述思路,再一次对其包装提供一个 common.PrepareApp(),这样就能为 examples 示例程序们的编写提供支持。

它们的用途在于三点:

  1. 自动集成 store.Store
  2. 集成 loaders 以便加载环境变量和外部配置文件,
  3. 扁平化 app name,author 等基础信息。

总的来说,对于常规开发,我们推荐你优先使用 lite.Create() 来简化初始代码,其次的选择是使用 cmdr.Create() 或者 loaders.Create()

如果你愿意逐步完成初始化设定工作,那么使用 cmdr.New() 也是合适的。

手动集成 Store 以及 loaders

如果你想自行集成,下面的片段是一个做法:

import "github.com/hedzr/cmdr-loaders/lite"
 
	app = cmdr.New(
 		// use an option store explicitly, or a dummy store by default
 		cmdr.WithStore(store.New()),
 
		// import "github.com/hedzr/cmdr-loaders/local" to get in advanced external loading features
		cmdr.WithExternalLoaders(
			lite.NewConfigFileLoader(
				lite.WithAlternateWriteBack(false), // disable write-back the modified state into alternative config file
				lite.WithAlternateDotPrefix(false), // use '<app-name>.toml' instead of '.<app-name>.toml'
			),
			lite.NewEnvVarLoader(),
		),
	).
		Info("tiny-app", "0.3.1").
		Author("The Example Authors") // .Description(``).Header(``).Footer(``)

OnAction 为命令关联处理逻辑

通常来说,多级子命令的结构中,只有末级命令应该带有 OnAction —— 尽管我们允许你在中间层级也同样提供它。

如果你真的这么做了,它也并不会产生什么逻辑错误或者运行错误,只是对于终端用户来说稍微不那么友好,因为他可能并不会意识到 jump 也可以被 执行

另一个副作用是,在 Shell 执行 app jump 时,原本会自动显示帮助屏,但现在将会执行你所关联的 OnAction 代码了,所以你必须使用app jump -h 来查阅帮助屏。

cmd.Store() vs cmd.Set()

cmd.Store() vs cmd.Set()

除此而外,你也可以通过 app.Store()/app.Set() 读写你的数据。

cmd.Store() 返回一个完整 Store 的子集,也就是键值 app.cmd.jump.to 所下辖的子树。所以 cmd.Store().MustBool("full") 就可以直接取得 --full 的状态。

cmd.Set() 相当于调用 app.Set(),它获取整个 Store 对象。此时,你需要用 cmd.Set().MustBool("app.cmd.jump.to.full") 来取得 --full 的状态。另一个方法是,

set := cmd.Set().WithPrefix(cli.DottedPathToCommandOrFlag)
println(set.MustBool("full"))
assert.Equal(set, cmd.Store())
 
// for the subcmd cmd pointed to `jump.to`, you can use:
cs := cmd.Store()
assert.Equal(cs, cmd.Set().WithPrefix(cli.CommandsStoreKey, "jump.to"))
assert.Equal(cs, cmd.Set(cli.CommandsStoreKey, "jump.to"))
assert.Equal(cs, cmd.Set(cli.CommandsStoreKey, "jump", "to"))
assert.Equal(cs, cmdr.Set().WithPrefix(cli.CommandsStoreKey, "jump.to"))
assert.Equal(cs, cmdr.Store().WithPrefix("jump.to"))
assert.Equal(cs, cmdr.Store("jump.to"))
assert.Equal(cs, cmdr.Store("jump", "to"))

类似地,app.Store() 获得的是 "app.cmd" 子树,所以你也可以用 app.Store().MustBool("jump.to.full") 来问询。

当使用 GetXXX/MustXXX 来获取命令行标志所对应的选项值时,通常采用 cmd.Store()/app.Store() 以求简化调用语句。

app.cmd 前缀是为了在序列化 Option Store 为 YAML 或者其他外部格式时而特别建立的前缀,这样能够保证 Option Store 的序列化内容能够被恰当地包含到外部配置中心里(无论是 YAML,JSON,TOML,抑或是显式的微服务外部配置中心)。

值得注意的是,当你真正进行序列化时,app 前缀将被自动摘除,因为实际上这个顶级前缀是为了在内存中统领子树的目的,而在外部持久化时去掉它可以节省存储空间。而在编程操作时,app 通常也是对使用者不可见的,这是因为 cmd.Set() 返回的实际上就是带有 app 的子树。

对于那些不与命令行标志相挂钩的 Option Store 选项来说,它们通常被建立在 app 层级之下。

一个 Option Store 在序列化为 YAML 之后能够很好地展示出其层级关系。例如下面的配置文件将被用于载入到 Store 中,

# 隐含的公共前缀 `app` 在序列化到磁盘文件中时将被自动摘除。
logging:
  file: /var/log/app/stdout.log
server:
  port: 3000
  host: 0.0.0.0
  tls: {}
  domains: []
# 以下表项仅在内存中有效。这里列举它们只是为了让
# 你能够对完整的 Store 树有完整的认识。
cmd:
  debug: false # 对应于 `--debug`
  verbose: false # 对应于 `--verbose`

它显示了你的 app 应该如何组织自己的配置设定,又如何揉合了命令行参数的选项值到 Store 之中。

Build()With(cb)

app/b.Cmd("long","short",...)...Build() 是一种 Builder Pattern,末尾的 Build() 调用用于提交前面的一系列构建动作,就像 tiny1 示例程序所做的那样。 但同时,我们也提供 app/b.Flag("long","short",...)...With(cb) 风格,With(cb) 同样起到提交作用,所以这时候就不必在结尾加上 .Build() 调用了。

在 tiny2 中,借助于 With 回调代码块,我们为 to 子命令附着了 full 选项。 所以终端用户可以在命令行加上 --full 或者 -f 来使能这个选项。

类似地,在 jump 的 With 代码块中,可以添加它的下级子命令,当然也包括为其添加关联的选项。

OS 返回值

to 命令也展示了如何设置应用程序返回值。通过 app.SetSuggestRetCode(retCode) 可以设置一个错误码,并在 main() 中利用 os.Exit(app.SuggestRetCode()) 将其返回给操作系统。

Why so complex?

对于服务型应用程序来说,在退出程序之前往往需要进行占用资源清理(包括打开的文件、网络连接,以及运行中的 go routines 等等)。

所以在程序的中途执行 panic() 或者 os.Exit(##) 是不恰当的,这会导致资源的异常占用或者泄漏。

如此被推荐的最佳实践是,在需要的运行中途通过 app.SetSuggestRetCode(retCode) 标记一个退出码,然后逐级通过 return err 返回到 main() 函数,此时必要的清理代码都将正常执行,最后再调用 os.Exit(app.SuggestRetCode()) 向操作系统返回错误码。

Store 的操作

在相应的 OnAction 处理程序中,full 选项将导致在控制台打印 Store 的内部结构。

$ go run ./examples/tiny2 jump to --full
 
  app.cmd.                      <B>
    jump.to.full                <L> app.cmd.jump.to.full => true
    generate.                   <B>
      manual.                   <B>
        dir                     <L> app.cmd.generate.manual.dir =>
        type                    <L> app.cmd.generate.manual.type => 1
      doc.dir                   <L> app.cmd.generate.doc.dir =>
      shell.                    <B>
        dir                     <L> app.cmd.generate.shell.dir =>
        output                  <L> app.cmd.generate.shell.output =>
        auto                    <L> app.cmd.generate.shell.auto => true
        zsh                     <L> app.cmd.generate.shell.zsh => false
        bash                    <L> app.cmd.generate.shell.bash => false
        fi                      <B>
          sh                    <L> app.cmd.generate.shell.fish => false
          g                     <L> app.cmd.generate.shell.fig => false
        powershell              <L> app.cmd.generate.shell.powershell => false
        elvish                  <L> app.cmd.generate.shell.elvish => false
    strict-mode                 <L> app.cmd.strict-mode => false
    no-                         <B>
      env-overrides             <L> app.cmd.no-env-overrides => false
      color                     <L> app.cmd.no-color => false
    v                           <B>
      er                        <B>
        bose                    <L> app.cmd.verbose => false
        sion                    <L> app.cmd.version => false
          -sim                  <L> app.cmd.version-sim =>
      alue-type                 <L> app.cmd.value-type => false
    quiet                       <L> app.cmd.quiet => false
    debug                       <L> app.cmd.debug => false
      -output                   <L> app.cmd.debug-output =>
    env                         <L> app.cmd.env => false
    m                           <B>
      ore                       <L> app.cmd.more => false
      anual                     <L> app.cmd.manual => false
    raw                         <L> app.cmd.raw => false
    built-info                  <L> app.cmd.built-info => false
    help                        <L> app.cmd.help => false
    tree                        <L> app.cmd.tree => false
 
`jump to` is been invoked, and will return with code '1'.
exit status 1
 
$

在上面打印的 app 配置数据集树结构中,app.cmd 包含了 tiny2 的命令行选项集合的映射,以及这些选项的最终值。

../../headless/cmdr-guide-cmd-store-vs-cmd-set.mdx

app 子树

cmdr 管理的 Store 中,app 子树是自动创建的。

所有的命令行标志将被一一映射到 app.cmd 子树中,例如 jump to 命令的 full 选项,将被映射为 app.cmd.jump.to.full 键值。

所有的外部配置文件将被解析后挂载到 app 子树之下。 所以 app.toml 内容:

[general]
foo = "bar"

将被载入后挂载为 app.general.foo => "bar" 的键值对。

MustXXX()

检测一个选项参数在命令行中被指定过,可以使用 cmd.Store().MustXXX() 函数族。

MustXXX 函数族包含对很多数据类型的抽取。

例如,cmd.MustInt("option-long-name") 得到一个整数值, 而 cmd.MustDuration("option-long-name") 得到一个时间段值, 等等。

Learn More

本节中的示例程序是被推荐的编码风格,它已经集成了外部配置文件的加载能力。

但是相关的内容将在下一节中予以解说。

此外,下一节还提供一个较为冗长的逐步完成初始化的示例程序,用于展示使用 cmdr.New() 的方式自行完成集成工作。

Footnotes

  1. hedzr/store 提供应用程序配置数据集的操作能力,以层次化的键值对方式管理数据。

How is this guide?

Edit on GitHub

Last updated on

On this page