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,
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 1The 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:
- Primary:
/etc/<app-name> - Secondary:
$HOME/.config/<app-name> - 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:
errors.New(format, args...)can format message or attach objects in various ways,- nested errors / errors container
var ec = errors.New(); ec.Attach(err) - auto collecting stacktrace info
- more enhanced apis
What is Next?
How is this guide?
Last updated on