hzDocs

With External Loaders

tiny3 Example

In this section, we introduce how to enable auto-loading app settings from external souces (file) by using hedzr/cmdr-loaders.

The sample app is,

./examples/tiny3/main.go
package main
 
import (
	"context"
	"io"
	"os"
 
	"gopkg.in/hedzr/errors.v3"
 
	"github.com/hedzr/cmdr-loaders/lite"
	"github.com/hedzr/cmdr/v2"
	"github.com/hedzr/cmdr/v2/cli"
	"github.com/hedzr/cmdr/v2/pkg/dir"
	logz "github.com/hedzr/logg/slog"
	"github.com/hedzr/store"
)
 
func main() {
	ctx := context.Background() // with cancel can be passed thru in your actions
	app := prepareApp(
		// 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(),
		),
 
		cmdr.WithTasksBeforeRun(func(ctx context.Context, cmd cli.Cmd, runner cli.Runner, extras ...any) (err error) {
			logz.DebugContext(ctx, "command running...", "cmd", cmd, "runner", runner, "extras", extras)
			return
		}),
 
		// true for debug in developing time, it'll disable onAction on each Cmd.
		// for productive mode, comment this line.
		// The envvars FORCE_DEFAULT_ACTION & FORCE_RUN can override this.
		// cmdr.WithForceDefaultAction(true),
 
		// cmdr.WithAutoEnvBindings(true),
	)
	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 prepareApp(opts ...cli.Opt) (app cli.App) {
	app = cmdr.New(opts...).
		Info("tiny3-app", "0.3.1").
		Author("The Example Authors") // .Description(``).Header(``).Footer(``)
 
	// another way to disable `cmdr.WithForceDefaultAction(true)` is using
	// env-var FORCE_RUN=1 (builtin already).
	app.Flg("no-default").
		Description("disable force default action").
		// Group(cli.UnsortedGroup).
		OnMatched(func(f *cli.Flag, position int, hitState *cli.MatchState) (err error) {
			if b, ok := hitState.Value.(bool); ok {
				// disable/enable the final state about 'force default action'
				f.Set().Set("app.force-default-action", b)
			}
			return
		}).
		Build()
 
	app.Cmd("jump").
		Description("jump command").
		Examples(`jump example`). // {{.AppName}}, {{.AppVersion}}, {{.DadCommands}}, {{.Commands}}, ...
		Deprecated(`v1.1.0`).
		// Group(cli.UnsortedGroup).
		Hidden(false).
		// Both With(cb) and Build() to end a building sequence
		With(func(b cli.CommandBuilder) {
			b.Cmd("to").
				Description("to command").
				Examples(``).
				Deprecated(`v0.1.1`).
				OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
					// cmd.Set() == cmdr.Set(), cmd.Store() == cmdr.Store(cmd.GetDottedPath())
					_, _ = cmd.Set().Set("tiny3.working", dir.GetCurrentDir())
					println()
					println("dir:", cmd.Set().WithPrefix("tiny3").MustString("working"))
 
					cs := cmd.Set().WithPrefix(cli.CommandsStoreKey, "jump.to")
					if cs.MustBool("full") {
						println()
						println(cmd.Set().Dump())
					}
					cs2 := cmd.Store()
					if cs2.MustBool("full") != cs.MustBool("full") {
						logz.Panic("a bug found")
					}
					app.SetSuggestRetCode(1) // ret code must be in 0-255
					return                   // handling command action here
				}).
				With(func(b cli.CommandBuilder) {
					b.Flg("full", "f").
						Default(false).
						Description("full command").
						Build()
				})
		})
 
	app.Flg("dry-run", "n").
		Default(false).
		Description("run all but without committing").
		Build()
 
	app.Flg("wet-run", "w").
		Default(false).
		Description("run all but with committing").
		Build() // no matter even if you're adding the duplicated one.
 
	app.Cmd("wrong").
		Description("a wrong command to return error for testing").
		// cmdline `FORCE_RUN=1 go run ./tiny wrong -d 8s` to verify this command to see the returned application error.
		OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
			dur := cmd.Store().MustDuration("duration")
			println("the duration is:", dur.String())
 
			ec := errors.New()
			defer ec.Defer(&err) // store the collected errors in native err and return it
			ec.Attach(io.ErrClosedPipe, errors.New("something's wrong"), os.ErrPermission)
			// see the application error by running `go run ./tiny/tiny/main.go wrong`.
			return
		}).
		With(func(b cli.CommandBuilder) {
			b.Flg("duration", "d").
				Default("5s").
				Description("a duration var").
				Build()
		})
	return
}

There is a sample settings file named as tiny3-app.toml within the root folder of hedzr/cmdr-tests repo. It contains:

[app.tiny3]
foo = "bar"

The settings will be loaded in running tiny3 app. To ensure it, we could invoke tiny3 --full to display the whold data tree:

$ go run ./examples/tiny3 jump to --full
 
~work/godev/cmdr.v2/cmdr.tests
 
  app.                          <B>
    cmd.                        <B>
      jump.to.full              <L> app.cmd.jump.to.full => true
      w                         <B>
        rong.duration           <L> app.cmd.wrong.duration => 5s
        et-run                  <L> app.cmd.wet-run => false
      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
      no-                       <B>
        default                 <L> app.cmd.no-default => <nil>
        env-overrides           <L> app.cmd.no-env-overrides => false
        color                   <L> app.cmd.no-color => false
      d                         <B>
        ry-run                  <L> app.cmd.dry-run => false
        ebug                    <L> app.cmd.debug => false
          -output               <L> app.cmd.debug-output =>
      strict-mode               <L> app.cmd.strict-mode => 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
      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
      config                    <L> app.cmd.config =>
    tiny3.                      <B>
      foo                       <L> app.tiny3.foo => bar
      working                   <L> app.tiny3.working => ~work/godev/cmdr.v2/cmdr.tests
 
exit status 1

The notable point is the highlight line, which shows the value of app.tiny3.foo was loaded properly.

Searching the settings folders

hedzr/cmdr-loaders provides a file loader to compliant with GNU Folder Spec, UNIX Filesystem Conventions (wiki) and Filesystem Hierarchy Standard (wiki).

Therefore, cmdr and cmdr-loaders will auto-search the app-settings folders from:

  1. Primary: /etc/<app-name>
  2. Secondary: $HOME/.config/<app-name>
  3. Alternative(s): . and <exeutable-dir>

to look for whether a <app-name>.toml file exists or not.

If it's found (such as tiny3-app.toml in this demo), load it into memory and merge the parsed settings into cmdr.App. Then, for Primary or Secondary setting source, check conf.d directory within the contains folder, and load all files as settings.

In loading, hedzr/cmdr-loaders could parse the file format by its extension name. Some suffixes are:

  • .toml
  • .yaml, .yml
  • .json
  • .hjson
  • .hcl
  • .nestedtext, .txt, .conf

It's possible to mix all them up.

Write-Back the Changeset

For Alternative setting source, Write-Back machnism is supported: once the app terminating, the modified subset of app settings in memory will be written into Alternative file.

By default, only a <app-name>.{toml,yaml,...} in the current directory is Alternative setting source, thus it enables Write-Back automatically.

NOTE:
The file in <exeutable-dir>, another Alternative setting source, doesn't allow write-back.

Error Trace

The subcommand wrong gives a sample to show you how to return an error to top main() function.

And in DebugMode (enabled by cmdline --debug, checked by is.DebugMode()), our main() will print the stacktrace of the error. Here is it:

$ go run ./examples/tiny3 wrong --debug
...
22:21:25.753233+08:00| c/[cmdr][1] [ERR] Application Error:                   err="[io: read/write on closed pipe | something's wrong | permission denied]" examples/blueprint/main.go:59 main.main
       error: [io: read/write on closed pipe | something's wrong | permission denied]
   file/line: examples/cmd/wrong.go:26
    function: github.com/hedzr/cmdr/v2/examples/cmd.wrongCmd.Add.func1
 
...stacktrace ignored here...
 
$ go run ./examples/tiny3 wrong
...
22:21:25.753233+08:00| c/[cmdr][1] [ERR] Application Error:                   err="[io: read/write on closed pipe | something's wrong | permission denied]" examples/blueprint/main.go:59 main.main
 
$

While a source raised an error without stacktrace info, likewise, the above log will miss them. A fast and better solution is wrapping the err with hedzr/errors library. For a instance, the following codes show you how to wrap the error returned by go-git/v5, which is a simple stringerror object.

import "gopkg.in/hedzr/errors.v3"
 
		if repo, err = git.PlainOpen(localDir); err != nil {
      err = errors.New().WithErrors(err)
			return
		}

hedzr/errors.v3 is compliant with both of go1.11 - latest in theory. You can migrate from go1.13 errors to it smoothly.

The benefis of using hedzr/errors.v3 are:

  1. errors.New(format, args...) can format message or attach objects in various ways,
  2. nested errors / errors container var ec = errors.New(); ec.Attach(err)
  3. auto collecting stacktrace info
  4. more enhanced apis

How is this guide?

Edit on GitHub

Last updated on

On this page