A preview release for the frozen stable API - v2.1.x series
[cmdr-docs-dev](https://github.com/hedzr/cmdr-docs-dev)
in composing.
cmdr
is a POSIX-compliant, command-line argument parser library with Golang.
Since v2, our license moved to Apache 2.0.
Since v2.1, the minimal toolchain move to go1.23.7. A preview version released at v2.1.0, the stable API has been freezing.
There are many dirty codes in the cmdr.v1 which cannot be refactored as well. It prompted we reimplment a new one as v2.
The passing winter, we did rewrite the cmdr.v2 to keep it clean and absorbed in parsing and dispatching.
Some abilities were removed and relayouted to new modules.
That's why the Option Store
has been split as a standalone module hedzr/store1.
A faster and colorful slog-like logger has been implemented freshly as hedzr/logg2.
hedzr/evendeep3 provides a deep fully-functional object copy tool. It helps to deep copy some internal objects easily. It is also ready for you.
hedzr/is4 is an environment detecting framework with many out-of-the-box detectors, such as is.InTesting
and is.InDebugging
.
Anyway, the whole supply chain painted:
graph BT
hzis(hedzr/is)-->hzlogg(hedzr/logg/slog)
hzis-->hzdiff(hedzr/evendeep)
hzlogg-->hzdiff
hzerrors(gopkg.in/hedzr/errors.v3)-->hzdiff
hzerrors-->hzstore(hedzr/store)
hzis-->hzstore(hedzr/store)
hzlogg-->hzstore(hedzr/store)
hzdiff-->hzstore(hedzr/store)
hzlogg-->cmdr(hedzr/cmdr/v2)
hzis-->cmdr
hzlogg-->cmdr
hzdiff-->cmdr
hzstore-->cmdr
v2 is in earlier state but the baseline is stable:
-
Basic command-line arguments parser like POSIX getopt and go stdlib flag.
-
Short flag, single character or a string here to support golang CLI style
- Compact flags if possible. Also the sticking value will be parsed. For example:
-c1b23zv
=-c 1 -b 23 -z -v
- Hit info:
-v -v -v
=-v
(hitCount == 3, hitTitle == 'v') - Optimized for slice:
-a 1,2,3 -a 4 -a 5,6
=> []int{1,2,3,4,5,6} - Value can be sticked or not. Valid forms:
-c1
,-c 1
,-c=1
and quoted:-c"1"
,-c'1'
,-c="1"
,-c='1'
, etc. - ...
- Compact flags if possible. Also the sticking value will be parsed. For example:
-
Long flags and aliases
-
Eventual subcommands: an
OnAction
handler can be attached. -
Eventual subcommands and flags: PreActions, PostAction, OnMatching, OnMatched, ...,
-
Auto bind to environment variables, For instance: command line
HELP=1 app
=app --help
. -
Builtin commands and flags:
--help
,-h
--version
,-V
--verbose
.-v
- ...
-
Help Screen: auto generate and print
-
Smart suggestions when wrong cmd or flag parsed. Jaro-winkler distance is used.
-
-
Loosely parse subcmds and flags:
- Subcommands and flags can be input in any order
- Lookup a flag along with subcommands tree for resolving the duplicated flags
-
Can integrate with hedzr/store1
- High-performance in-memory KV store for hierarchical data.
- Extract data to user-spec type with auto-converting
- Loadable external sources: environ, config files, consul, etcd, etc..
- extensible codecs and providers for loading from data sources
-
Three kinds of config files are searched and loaded via
loaders.NewConfigFileLoader()
:- Primary: main config, shipped with installable package.
- Secondary: 2ndry config. Wrapped by reseller(s).
- Alternative: user's local config, writeable. The runtime changeset will be written back to this file while app stopping.
-
TODO
- Shell autocompletion
- ...
More minor details need to be evaluated and reimplemented if it's still meaningful in v2.
Since v2.0.3, loaders had been splitted as a standalone repo so that we can keep cmdr v2 smaller and independer. See the relevant subproject cmdr-loaders5.
The full-featured tests and examples are moved into cmdr-tests.
v2.1 is a stable version:
- v2.1.3 or v2.2.0 PLANNED: the final stable version for v2.
- v2.1.x: preview version for the new APIs.
- Full list: CHANGELOG
A simple cli-app can be:
package main
import (
"context"
"io"
"os"
"github.com/hedzr/cmdr/v2"
"github.com/hedzr/cmdr/v2/cli"
"github.com/hedzr/cmdr/v2/examples/cmd"
"github.com/hedzr/cmdr/v2/examples/dyncmd"
"github.com/hedzr/cmdr/v2/pkg/dir"
"gopkg.in/hedzr/errors.v3"
logz "github.com/hedzr/logg/slog"
)
const (
appName = "lite-app"
desc = `lite-app version of tiny app.`
version = cmdr.Version
author = `The Example Authors`
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app := cmdr.Create(appName, version, author, desc).
WithAdders(cmd.Commands...).
Build()
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)
}
}
var Commands = []cli.CmdAdder{
jumpCmd{},
wrongCmd{},
invokeCmd{},
presetCmd{},
}
type jumpCmd struct{}
func (jumpCmd) Add(app cli.App) {
app.Cmd("jump").
Description("jump command").
Examples(`jump example`). // {{.AppName}}, {{.AppVersion}}, {{.DadCommands}}, {{.Commands}}, ...
Deprecated(`v1.1.0`).
Group("Test").
// Group(cli.UnsortedGroup).
// Hidden(false).
OnEvaluateSubCommands(dyncmd.OnEvalJumpSubCommands).
OnEvaluateSubCommandsFromConfig().
// 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.Store(), cmd.Store() == cmdr.Store()
cmd.Set().Set("tiny3.working", dir.GetCurrentDir())
println()
println("dir:", cmd.Set().WithPrefix("tiny3").MustString("working"))
cs := cmdr.Store().WithPrefix("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()
})
})
}
type wrongCmd struct{}
func (wrongCmd) Add(app cli.App) {
app.Cmd("wrong").
Description("a wrong command to return error for testing").
Group("Test").
// 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
}).
Build()
}
type invokeCmd struct{}
func (invokeCmd) Add(app cli.App) {
app.Cmd("invoke").Description(`test invoke feature`).
With(func(b cli.CommandBuilder) {
b.Cmd("shell").Description(`invoke shell cmd`).InvokeShell(`ls -la`).UseShell("/bin/bash").OnAction(nil).Build()
b.Cmd("proc").Description(`invoke gui program`).InvokeProc(`say "hello, world!"`).OnAction(nil).Build()
})
}
type presetCmd struct{}
func (presetCmd) Add(app cli.App) {
app.Cmd("preset", "p").
Description("preset command to inject into user input").
With(func(b cli.CommandBuilder) {
b.Flg("preset", "p").
Default(false).
Description("preset arg").
Build()
b.Cmd("cmd", "c").Description("inject `-pv` into user input cmdline").
PresetCmdLines(`-pv`).
OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
_, err = app.DoBuiltinAction(ctx, cli.ActionDefault)
return
}).Build()
})
}
More examples please go to cmdr-tests/examples.
Thanks to JetBrains for donating product licenses to help develop cmdr
Since v2, our license moved to Apache 2.0.
The v1 keeps under MIT itself.
Footnotes
-
hedzr/store
is a high-performance configure management library ↩ ↩2 -
hedzr/logg
provides a slog like and colorful logging library ↩ -
hedzr/evendeep
offers a customizable deepcopy tool to you. There are also deepequal, deepdiff tools in it. ↩ -
hedzr/is
is a basic environ detectors library ↩ -
hedzr/cmdr-loaders
provides an external config file loaders with GNU File Standard supports. ↩