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(),它将会自动加载恰当位置的外部配置文件或者配置源。

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

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

它们的用途在于三点:

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

总的来说,对于常规开发,我们推荐你使用 cmdr.Create()loaders.Create() 这两个接口。

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

手动集成 Store 以及 loaders

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

import "github.com/hedzr/cmdr-loaders/local"
 
	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(
			local.NewConfigFileLoader(
				local.WithAlternateWriteBack(false), // disable write-back the modified state into alternative config file
				local.WithAlternateDotPrefix(false), // use '<app-name>.toml' instead of '.<app-name>.toml'
			),
			local.NewEnvVarLoader(),
		),
	).
		Info("tiny-app", "0.3.1").
		Author("The Example Authors") // .Description(``).Header(``).Footer(``)

OnAction 为命令关联处理逻辑

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

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

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

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 的命令行选项集合的映射,以及这些选项的最终值。

cmd.Store() vs cmd.Set()

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

OnAction 回调函数中,cmd.Store() 返回一个完整 Store 的相对于当前命令的子集。对于 jump to 子命令,键值 app.cmd.jump.to 所下辖的子树将被返回。所以 cmd.Store().MustBool("full") 就可以直接取得 --full 的状态。

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

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

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