diff --git a/app.go b/app.go
index 99904b2..6b41af9 100644
--- a/app.go
+++ b/app.go
@@ -2,6 +2,7 @@ package console
import (
"context"
+ "fmt"
"os"
"gitoa.ru/go-4devs/config"
@@ -9,6 +10,10 @@ import (
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/command/help"
+ "gitoa.ru/go-4devs/console/command/list"
+ "gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
)
@@ -38,12 +43,20 @@ func WithExit(f func(int)) func(*App) {
}
}
+func WithReplaceCommand(a *App) {
+ a.registry = registry.Set
+}
+
// New creates and configure new console app.
func New(opts ...func(*App)) *App {
app := &App{
out: output.Stdout(),
exit: os.Exit,
- in: chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(0):])), &memory.Default{}),
+ in: chain.New(
+ arg.New(arg.WithArgs(os.Args[resolveSkip(0):])),
+ &memory.Default{},
+ ),
+ registry: registry.Add,
}
for _, opt := range opts {
@@ -55,26 +68,25 @@ func New(opts ...func(*App)) *App {
// App is collection of command and configure env.
type App struct {
- cmds []*Command
- out output.Output
- in config.BindProvider
- exit func(int)
+ registry func(...command.Command) error
+ out output.Output
+ in config.BindProvider
+ exit func(int)
}
// Add add or replace command.
-func (a *App) Add(cmds ...*Command) *App {
- a.cmds = append(a.cmds, cmds...)
+func (a *App) Add(cmds ...command.Command) *App {
+ if err := a.registry(cmds...); err != nil {
+ a.printError(context.Background(), err)
+ a.exit(1)
+ }
return a
}
// Execute run the command by name and arguments.
func (a *App) Execute(ctx context.Context) {
- for _, cmd := range a.cmds {
- register(cmd)
- }
-
- cmd, err := a.find(ctx)
+ cmd, err := registry.Find(a.commandName())
if err != nil {
a.printError(ctx, err)
@@ -89,7 +101,7 @@ func (a *App) Execute(ctx context.Context) {
a.exec(ctx, cmd)
}
-func (a *App) exec(ctx context.Context, cmd *Command) {
+func (a *App) exec(ctx context.Context, cmd command.Command) {
err := Run(ctx, cmd, a.in, a.out)
if err != nil {
a.printError(ctx, err)
@@ -99,31 +111,30 @@ func (a *App) exec(ctx context.Context, cmd *Command) {
a.exit(0)
}
-func (a *App) find(_ context.Context) (*Command, error) {
- if len(os.Args) < 2 || os.Args[1][1] == '-' {
- return Find(CommandList)
+func (a *App) commandName() string {
+ name := list.Name
+ if len(os.Args) > 1 && len(os.Args[1]) > 1 && os.Args[1][1] != '-' {
+ name = os.Args[1]
}
- name := os.Args[1]
-
- return Find(name)
+ return name
}
func (a *App) list(ctx context.Context) error {
- cmd, err := Find(CommandHelp)
+ cmd, err := registry.Find(help.Name)
if err != nil {
- return err
+ return fmt.Errorf("%w", err)
}
arr := &memory.Map{}
- arr.SetOption(value.New(CommandList), ArgumentCommandName)
+ arr.SetOption(value.New(list.Name), help.ArgumentCommandName)
in := chain.New(arr, a.in)
return Run(ctx, cmd, in, a.out)
}
func (a *App) printError(ctx context.Context, err error) {
- ansi(ctx, a.in, a.out).Println(ctx, "\n\n ", err, "\n")
+ command.Ansi(ctx, a.in, a.out).Println(ctx, "\n\n ", err, "\n")
}
func resolveSkip(in int) int {
diff --git a/app_test.go b/app_test.go
index 04f841b..e0f4792 100644
--- a/app_test.go
+++ b/app_test.go
@@ -5,6 +5,7 @@ import (
"os"
"gitoa.ru/go-4devs/console"
+ "gitoa.ru/go-4devs/console/command"
)
//nolint:lll
@@ -55,21 +56,15 @@ func ExampleNew_list() {
"--no-ansi",
}
- console.New(console.WithExit(func(int) {})).
+ console.New(
+ console.WithExit(func(int) {}),
+ console.WithReplaceCommand,
+ ).
Add(
Command(),
- &console.Command{
- Name: "fdevs:console:arg",
- Description: "Understanding how Console Arguments and Options Are Handled",
- },
- &console.Command{
- Name: "fdevs:console:hello",
- Description: "example hello command",
- },
- &console.Command{
- Name: "app:start",
- Description: "example command in other namespace",
- },
+ command.New("fdevs:console:arg", "Understanding how Console Arguments and Options Are Handled", Execute),
+ command.New("fdevs:console:hello", "example hello command", Execute),
+ command.New("app:start", "example command in other namespace", Execute),
).
Execute(ctx)
// Output:
diff --git a/command.go b/command.go
index 33b47b4..ffe344b 100644
--- a/command.go
+++ b/command.go
@@ -5,6 +5,8 @@ import (
"fmt"
"gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/output"
)
@@ -52,6 +54,18 @@ func WithName(name string) Option {
}
}
+func Wrap(cmd *Command) command.Command {
+ opts := make([]command.Option, 0)
+ if cmd.Hidden {
+ opts = append(opts, command.Hidden)
+ }
+
+ opts = append(opts, command.Configure(cmd.Init))
+
+ return command.New(cmd.Name, cmd.Description, cmd.Run, opts...)
+}
+
+// Deprecated: use command.New().
type Command struct {
// The name of the command.
Name string
@@ -101,7 +115,7 @@ func (c *Command) With(opts ...Option) *Command {
// Run run command with input and output.
func (c *Command) Run(ctx context.Context, in config.Provider, out output.Output) error {
if c.Execute == nil {
- return fmt.Errorf("%w", ErrExecuteNil)
+ return fmt.Errorf("%w", errors.ErrExecuteNil)
}
if c.Handle != nil {
diff --git a/command/chain.go b/command/chain.go
new file mode 100644
index 0000000..3926ffa
--- /dev/null
+++ b/command/chain.go
@@ -0,0 +1,82 @@
+package command
+
+import (
+ "context"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/console/output"
+)
+
+// ChainPrepare creates middleware for configures command.
+func ChainPrepare(prepare ...PrepareFn) PrepareFn {
+ num := len(prepare)
+ if num == 1 {
+ return prepare[0]
+ }
+
+ if num > 1 {
+ lastI := num - 1
+
+ return func(ctx context.Context, def config.Definition, next ConfigureFn) error {
+ var (
+ chainHandler func(context.Context, config.Definition) error
+ curI int
+ )
+
+ chainHandler = func(currentCtx context.Context, currentDef config.Definition) error {
+ if curI == lastI {
+ return next(currentCtx, currentDef)
+ }
+
+ curI++
+ err := prepare[curI](currentCtx, currentDef, chainHandler)
+ curI--
+
+ return err
+ }
+
+ return prepare[0](ctx, def, chainHandler)
+ }
+ }
+
+ return func(ctx context.Context, cfg config.Definition, next ConfigureFn) error {
+ return next(ctx, cfg)
+ }
+}
+
+// ChainHandle creates middleware for executes command.
+func ChainHandle(handlers ...HandleFn) HandleFn {
+ num := len(handlers)
+ if num == 1 {
+ return handlers[0]
+ }
+
+ if num > 1 {
+ lastI := num - 1
+
+ return func(ctx context.Context, in config.Provider, out output.Output, next ExecuteFn) error {
+ var (
+ chainHandler func(context.Context, config.Provider, output.Output) error
+ curI int
+ )
+
+ chainHandler = func(currentCtx context.Context, currentIn config.Provider, currentOut output.Output) error {
+ if curI == lastI {
+ return next(currentCtx, currentIn, currentOut)
+ }
+
+ curI++
+ err := handlers[curI](currentCtx, currentIn, currentOut, chainHandler)
+ curI--
+
+ return err
+ }
+
+ return handlers[0](ctx, in, out, chainHandler)
+ }
+ }
+
+ return func(ctx context.Context, in config.Provider, out output.Output, next ExecuteFn) error {
+ return next(ctx, in, out)
+ }
+}
diff --git a/command/chain_test.go b/command/chain_test.go
new file mode 100644
index 0000000..bf2e27d
--- /dev/null
+++ b/command/chain_test.go
@@ -0,0 +1,88 @@
+package command_test
+
+import (
+ "context"
+ "sync/atomic"
+ "testing"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/config/definition"
+ "gitoa.ru/go-4devs/config/provider/memory"
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/output"
+)
+
+func TestChainPrepare(t *testing.T) {
+ t.Parallel()
+
+ var cnt int64
+
+ ctx := context.Background()
+ def := definition.New()
+
+ prepare := func(ctx context.Context, def config.Definition, n command.ConfigureFn) error {
+ atomic.AddInt64(&cnt, 1)
+
+ return n(ctx, def)
+ }
+ configure := func(context.Context, config.Definition) error {
+ return nil
+ }
+
+ for i := range []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} {
+ prepares := make([]command.PrepareFn, i)
+ for p := range i {
+ prepares[p] = prepare
+ }
+
+ cnt = 0
+ chain := command.ChainPrepare(prepares...)
+
+ err := chain(ctx, def, configure)
+ if err != nil {
+ t.Errorf("expected nil err, got: %s", err)
+ }
+
+ if cnt != int64(i) {
+ t.Fatalf("expected: call prepare 1, got: %d ", cnt)
+ }
+ }
+}
+
+func TestChainHandle(t *testing.T) {
+ t.Parallel()
+
+ var cnt int64
+
+ ctx := context.Background()
+ in := &memory.Map{}
+ out := output.Stdout()
+
+ handle := func(ctx context.Context, in config.Provider, out output.Output, next command.ExecuteFn) error {
+ atomic.AddInt64(&cnt, 1)
+
+ return next(ctx, in, out)
+ }
+ action := func(context.Context, config.Provider, output.Output) error {
+ return nil
+ }
+
+ for i := range []int{0, 1, 2, 30, 40, 50} {
+ handles := make([]command.HandleFn, i)
+ for p := range i {
+ handles[p] = handle
+ }
+
+ cnt = 0
+ chain := command.ChainHandle(handles...)
+
+ err := chain(ctx, in, out, action)
+ if err != nil {
+ t.Errorf("expected nil err, got: %s", err)
+ }
+
+ if cnt != int64(i) {
+ t.Fatalf("expected: call prepare 1, got: %d ", cnt)
+ }
+ }
+}
diff --git a/command/command.go b/command/command.go
new file mode 100644
index 0000000..f656af0
--- /dev/null
+++ b/command/command.go
@@ -0,0 +1,143 @@
+package command
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/console/output"
+ "gitoa.ru/go-4devs/console/param"
+)
+
+type (
+ ExecuteFn func(ctx context.Context, input config.Provider, output output.Output) error
+ HandleFn func(ctx context.Context, in config.Provider, out output.Output, n ExecuteFn) error
+ ConfigureFn func(ctx context.Context, cfg config.Definition) error
+ PrepareFn func(ctx context.Context, cfg config.Definition, n ConfigureFn) error
+
+ Option func(*Command)
+)
+
+func Configure(fn ConfigureFn) Option {
+ return func(c *Command) {
+ c.configure = fn
+ }
+}
+
+func Version(in string) Option {
+ return func(c *Command) {
+ c.Params = param.WithVersion(in)(c.Params)
+ }
+}
+
+func Hidden(c *Command) {
+ c.Params = param.Hidden(c.Params)
+}
+
+func Help(fn param.HelpFn) Option {
+ return func(c *Command) {
+ c.Params = param.WithHelp(fn)(c.Params)
+ }
+}
+
+func WithName(name string) Option {
+ return func(c *Command) {
+ c.name = name
+ }
+}
+
+func Handle(fn HandleFn) Option {
+ return func(c *Command) {
+ handle := c.handle
+ c.handle = ChainHandle(fn, handle)
+ }
+}
+
+func Prepare(fn PrepareFn) Option {
+ return func(c *Command) {
+ prepare := c.prepare
+ c.prepare = ChainPrepare(fn, prepare)
+ }
+}
+
+func New(name, desc string, execute ExecuteFn, opts ...Option) Command {
+ cmd := Command{
+ name: name,
+ execute: execute,
+ configure: emptyConfigure,
+ handle: emptyHandle,
+ prepare: emptyPrepare,
+ Params: param.New(param.WithDescription(desc)),
+ }
+
+ for _, opt := range opts {
+ opt(&cmd)
+ }
+
+ return cmd
+}
+
+type Command struct {
+ param.Params
+
+ name string
+ execute ExecuteFn
+ configure ConfigureFn
+ prepare PrepareFn
+ handle HandleFn
+}
+
+func (c Command) Name() string {
+ return c.name
+}
+
+func (c Command) Execute(ctx context.Context, input config.Provider, output output.Output) error {
+ return c.handle(ctx, input, output, c.execute)
+}
+
+func (c Command) Configure(ctx context.Context, cfg config.Definition) error {
+ return c.prepare(ctx, cfg, c.configure)
+}
+
+func (c Command) IsZero() bool {
+ return c.name == "" ||
+ c.execute == nil ||
+ c.configure == nil ||
+ c.handle == nil ||
+ c.prepare == nil
+}
+
+func (c Command) String() string {
+ return fmt.Sprintf("command:%v, version:%v", c.Name(), param.Version(c))
+}
+
+func With(parent Command, opts ...Option) Command {
+ log.Print(parent.Name())
+ cmd := Command{
+ Params: parent.Params,
+ name: parent.Name(),
+ execute: parent.Execute,
+ configure: parent.Configure,
+ handle: emptyHandle,
+ prepare: emptyPrepare,
+ }
+
+ for _, opt := range opts {
+ opt(&cmd)
+ }
+
+ return cmd
+}
+
+func emptyPrepare(ctx context.Context, cfg config.Definition, n ConfigureFn) error {
+ return n(ctx, cfg)
+}
+
+func emptyHandle(ctx context.Context, in config.Provider, out output.Output, n ExecuteFn) error {
+ return n(ctx, in, out)
+}
+
+func emptyConfigure(context.Context, config.Definition) error {
+ return nil
+}
diff --git a/command/commands.go b/command/commands.go
new file mode 100644
index 0000000..81a7041
--- /dev/null
+++ b/command/commands.go
@@ -0,0 +1,137 @@
+package command
+
+import (
+ "fmt"
+ "regexp"
+ "sort"
+ "sync"
+
+ cerr "gitoa.ru/go-4devs/console/errors"
+ "gitoa.ru/go-4devs/console/param"
+)
+
+var findCommand = regexp.MustCompile("([^:]+|)")
+
+type Commands struct {
+ sync.RWMutex
+
+ cmds []Command
+ names map[string]int
+}
+
+func (c *Commands) Set(cmds ...Command) error {
+ c.Lock()
+ defer c.Unlock()
+
+ return c.set(cmds...)
+}
+
+func (c *Commands) Add(cmds ...Command) error {
+ c.Lock()
+ defer c.Unlock()
+
+ return c.add(cmds...)
+}
+
+func (c *Commands) Find(name string) (Command, error) {
+ c.Lock()
+ defer c.Unlock()
+
+ return c.find(name)
+}
+
+func (c *Commands) Names() []string {
+ c.Lock()
+ defer c.Unlock()
+
+ names := make([]string, 0, len(c.names))
+ for name := range c.names {
+ names = append(names, name)
+ }
+
+ sort.Strings(names)
+
+ return names
+}
+
+func (c *Commands) find(name string) (Command, error) {
+ if idx, ok := c.names[name]; ok {
+ return c.cmds[idx], nil
+ }
+
+ nameRegexp := findCommand.ReplaceAllStringFunc(name, func(in string) string {
+ return in + "[^:]*"
+ })
+
+ findCommands := make([]Command, 0, len(c.cmds))
+
+ cmdRegexp, err := regexp.Compile("^" + nameRegexp + "$")
+ if err != nil {
+ return Command{}, fmt.Errorf("find by regexp:%w", err)
+ }
+
+ for name, idx := range c.names {
+ if cmdRegexp.MatchString(name) && !param.IsHidden(c.cmds[idx]) {
+ findCommands = append(findCommands, c.cmds[idx])
+ }
+ }
+
+ if len(findCommands) == 1 {
+ return findCommands[0], nil
+ }
+
+ if len(findCommands) > 1 {
+ names := make([]string, len(findCommands))
+ for i := range findCommands {
+ names[i] = findCommands[i].Name()
+ }
+
+ return Command{}, cerr.AlternativesError{Alt: names, Err: cerr.ErrCommandDplicate}
+ }
+
+ return Command{}, fmt.Errorf("%w", cerr.ErrNotFound)
+}
+
+func (c *Commands) set(cmds ...Command) error {
+ if c.names == nil {
+ c.names = make(map[string]int, len(cmds))
+ }
+
+ for _, cmd := range cmds {
+ if cmd.IsZero() {
+ return fmt.Errorf("command:%w", cerr.ErrCommandNil)
+ }
+
+ if idx, ok := c.names[cmd.Name()]; ok {
+ c.cmds[idx] = cmd
+
+ continue
+ }
+
+ c.names[cmd.Name()] = len(c.cmds)
+ c.cmds = append(c.cmds, cmd)
+ }
+
+ return nil
+}
+
+func (c *Commands) add(cmds ...Command) error {
+ if c.names == nil {
+ c.names = make(map[string]int, len(cmds))
+ }
+
+ for _, cmd := range cmds {
+ if cmd.IsZero() {
+ return fmt.Errorf("command:%w", cerr.ErrCommandNil)
+ }
+
+ if _, ok := c.names[cmd.Name()]; ok {
+ return fmt.Errorf("command %s:%w", cmd.Name(), cerr.ErrCommandDplicate)
+ }
+
+ c.names[cmd.Name()] = len(c.cmds)
+ c.cmds = append(c.cmds, cmd)
+ }
+
+ return nil
+}
diff --git a/command/commands_test.go b/command/commands_test.go
new file mode 100644
index 0000000..63dcda8
--- /dev/null
+++ b/command/commands_test.go
@@ -0,0 +1,46 @@
+package command_test
+
+import (
+ "context"
+ "testing"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/output"
+)
+
+func testEmtyExecute(context.Context, config.Provider, output.Output) error {
+ return nil
+}
+
+func TestCommandsCommand(t *testing.T) {
+ t.Parallel()
+
+ cases := map[string]string{
+ "fdevs:console:test": "fdevs:console:test",
+ "fd:c:t": "fdevs:console:test",
+ "fd::t": "fdevs:console:test",
+ "f:c:t": "fdevs:console:test",
+ "f:c:a": "fdevs:console:arg",
+ }
+
+ var commands command.Commands
+
+ _ = commands.Add(
+ command.New("fdevs:console:test", "fdevs console test", testEmtyExecute),
+ command.New("fdevs:console:arg", "fdevs console arg", testEmtyExecute),
+ )
+
+ for name, ex := range cases {
+ res, err := commands.Find(name)
+ if err != nil {
+ t.Errorf("%v expect err, got:%s", name, err)
+
+ continue
+ }
+
+ if res.Name() != ex {
+ t.Errorf("%v expect: %s, got: %s", name, ex, res)
+ }
+ }
+}
diff --git a/command/default.go b/command/default.go
new file mode 100644
index 0000000..fbf8882
--- /dev/null
+++ b/command/default.go
@@ -0,0 +1,120 @@
+package command
+
+import (
+ "context"
+ "math"
+ "os"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/config/definition/option"
+ "gitoa.ru/go-4devs/config/value"
+ "gitoa.ru/go-4devs/console/output"
+ "gitoa.ru/go-4devs/console/output/verbosity"
+)
+
+const (
+ OptionHelp = "help"
+ OptionVersion = "version"
+ OptionAnsi = "ansi"
+ OptionNoAnsi = "no-ansi"
+ OptionQuiet = "quiet"
+ OptionVerbose = "verbose"
+)
+
+const (
+ verboseTrace = 3
+ verboseDebug = 2
+ verboseInfo = 1
+)
+
+const (
+ defaultOptionsPosition = math.MaxUint64 / 2
+)
+
+// Default options and argument command.
+func Default(def config.Definition) {
+ def.Add(
+ option.Bool(OptionNoAnsi, "Disable ANSI output", option.Position(defaultOptionsPosition)),
+ option.Bool(OptionAnsi, "Do not ask any interactive question", option.Position(defaultOptionsPosition)),
+ option.Bool(OptionVersion, "Display this application version", option.Short('V'), option.Position(defaultOptionsPosition)),
+ option.Bool(OptionHelp, "Display this help message", option.Short('h'), option.Position(defaultOptionsPosition)),
+ option.Bool(OptionVerbose,
+ "Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace",
+ option.Short('v'), option.Slice, option.Position(defaultOptionsPosition)),
+ option.Bool(OptionQuiet, "Do not output any message", option.Short('q'), option.Position(defaultOptionsPosition)),
+ )
+}
+
+func IsShowVersion(ctx context.Context, in config.Provider) bool {
+ v, err := in.Value(ctx, OptionVersion)
+ if err != nil {
+ return false
+ }
+
+ return v.Bool()
+}
+
+func IsShowHelp(ctx context.Context, in config.Provider) bool {
+ v, err := in.Value(ctx, OptionHelp)
+ if err != nil {
+ return false
+ }
+
+ return v.Bool()
+}
+
+func Ansi(ctx context.Context, in config.Provider, out output.Output) output.Output {
+ switch {
+ case ReadValue(ctx, in, OptionAnsi).Bool():
+ out = output.Ansi(out)
+ case ReadValue(ctx, in, OptionNoAnsi).Bool():
+ out = output.None(out)
+ case lookupEnv("NO_COLOR"):
+ out = output.None(out)
+ default:
+ out = output.Ansi(out)
+ }
+
+ return out
+}
+
+func lookupEnv(name string) bool {
+ v, has := os.LookupEnv(name)
+
+ return has && v == "true"
+}
+
+func Verbose(ctx context.Context, in config.Provider, out output.Output) output.Output {
+ out = Ansi(ctx, in, out)
+
+ switch {
+ case ReadValue(ctx, in, OptionQuiet).Bool():
+ out = output.Quiet()
+ default:
+ var verb []bool
+
+ _ = ReadValue(ctx, in, OptionVerbose).Unmarshal(&verb)
+
+ switch {
+ case len(verb) == verboseInfo:
+ out = output.Verbosity(out, verbosity.Info)
+ case len(verb) == verboseDebug:
+ out = output.Verbosity(out, verbosity.Debug)
+ case len(verb) >= verboseTrace:
+ out = output.Verbosity(out, verbosity.Trace)
+ default:
+ out = output.Verbosity(out, verbosity.Norm)
+ }
+ }
+
+ return out
+}
+
+func ReadValue(ctx context.Context, in config.Provider, path ...string) config.Value {
+ val, err := in.Value(ctx, path...)
+ if err != nil {
+ return value.EmptyValue()
+ }
+
+ return val
+}
diff --git a/command/help/command.go b/command/help/command.go
new file mode 100644
index 0000000..31b8c94
--- /dev/null
+++ b/command/help/command.go
@@ -0,0 +1,136 @@
+package help
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/config/definition"
+ "gitoa.ru/go-4devs/config/definition/option"
+ cparam "gitoa.ru/go-4devs/config/param"
+ "gitoa.ru/go-4devs/config/provider/arg"
+ "gitoa.ru/go-4devs/config/validator"
+ "gitoa.ru/go-4devs/config/value"
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/internal/registry"
+ "gitoa.ru/go-4devs/console/output"
+ "gitoa.ru/go-4devs/console/output/descriptor"
+ "gitoa.ru/go-4devs/console/param"
+)
+
+//nolint:gochecknoinits
+func init() {
+ err := registry.Add(Command())
+ if err != nil {
+ panic(err)
+ }
+}
+
+const (
+ ArgumentCommandName = "command_name"
+ OptionFormat = "format"
+ Name = "help"
+)
+
+func Command() command.Command {
+ return command.New(
+ Name,
+ "Displays help for a command",
+ Execute,
+ command.Configure(Configure),
+ command.Help(Help),
+ )
+}
+
+func Configure(_ context.Context, config config.Definition) error {
+ formats := descriptor.Descriptors()
+ config.
+ Add(
+ arg.String(ArgumentCommandName, "The command name", arg.Default(value.New("help"))),
+ option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
+ option.Required,
+ option.Default(value.New(formats[0])),
+ validator.Valid(
+ validator.NotBlank,
+ validator.Enum(formats...),
+ ),
+ ),
+ )
+
+ return nil
+}
+
+func Execute(ctx context.Context, in config.Provider, out output.Output) error {
+ var err error
+
+ cfg := read{Provider: in}
+ name := cfg.Value(ctx, ArgumentCommandName).String()
+ format := cfg.Value(ctx, OptionFormat).String()
+
+ des, err := descriptor.Find(format)
+ if err != nil {
+ return fmt.Errorf("find descriptor[%v]: %w", format, err)
+ }
+
+ cmd, err := registry.Find(name)
+ if err != nil {
+ return fmt.Errorf("find cmd: %w", err)
+ }
+
+ def := definition.New()
+ command.Default(def)
+
+ if err := cmd.Configure(ctx, def); err != nil {
+ return fmt.Errorf("init cmd: %w", err)
+ }
+
+ var bin string
+ if len(os.Args) > 0 {
+ bin = os.Args[0]
+ }
+
+ help, err := param.Help(cmd, param.HelpData(bin, cmd.Name()))
+ if err != nil {
+ return fmt.Errorf("create help:%w", err)
+ }
+
+ derr := des.Command(ctx, out, descriptor.Command{
+ Bin: bin,
+ Name: cmd.Name(),
+ Description: param.Description(cmd),
+ Help: help,
+ Options: def.With(cparam.New(descriptor.TxtStyle())),
+ })
+ if derr != nil {
+ return fmt.Errorf("descriptor help:%w", derr)
+ }
+
+ return nil
+}
+
+const tpl = `
+The %[2]s command displays help for a given command:
+ %[1]s %[2]s list
+You can also output the help in other formats by using the --format option:
+ %[1]s %[2]s --format=xml list
+To display the list of available commands, please use the list command.
+`
+
+func Help(data param.HData) (string, error) {
+ return fmt.Sprintf(tpl, data.Bin, data.Name), nil
+}
+
+type read struct {
+ config.Provider
+}
+
+func (r read) Value(ctx context.Context, key ...string) config.Value {
+ val, err := r.Provider.Value(ctx, key...)
+ if err != nil {
+ return value.Empty{Err: err}
+ }
+
+ return val
+}
diff --git a/command/list/command.go b/command/list/command.go
new file mode 100644
index 0000000..c1dce90
--- /dev/null
+++ b/command/list/command.go
@@ -0,0 +1,147 @@
+package list
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "gitoa.ru/go-4devs/config"
+ "gitoa.ru/go-4devs/config/definition"
+ "gitoa.ru/go-4devs/config/definition/option"
+ cparam "gitoa.ru/go-4devs/config/param"
+ "gitoa.ru/go-4devs/config/provider/arg"
+ "gitoa.ru/go-4devs/config/validator"
+ "gitoa.ru/go-4devs/config/value"
+ "gitoa.ru/go-4devs/console/command"
+ cerr "gitoa.ru/go-4devs/console/errors"
+ "gitoa.ru/go-4devs/console/internal/registry"
+ "gitoa.ru/go-4devs/console/output"
+ "gitoa.ru/go-4devs/console/output/descriptor"
+ "gitoa.ru/go-4devs/console/param"
+)
+
+//nolint:gochecknoinits
+func init() {
+ err := registry.Add(Command())
+ if err != nil {
+ panic(err)
+ }
+}
+
+const (
+ Name = "list"
+ ArgumentNamespace = "namespace"
+ OptionFormat = "format"
+ defaultLenNamespace = 2
+)
+
+func Command() command.Command {
+ return command.New(
+ Name,
+ "Lists commands",
+ Execite,
+ command.Configure(Configure),
+ command.Help(Help),
+ )
+}
+
+func Configure(_ context.Context, cfg config.Definition) error {
+ formats := descriptor.Descriptors()
+ cfg.
+ Add(
+ arg.String(ArgumentNamespace, "The namespace name"),
+ option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
+ option.Required,
+ option.Default(value.New(formats[0])),
+ validator.Valid(
+ validator.NotBlank,
+ validator.Enum(formats...),
+ ),
+ ),
+ )
+
+ return nil
+}
+
+//nolint:cyclop
+func Execite(ctx context.Context, in config.Provider, out output.Output) error {
+ opt := read{Provider: in}
+ ns := opt.Value(ctx, ArgumentNamespace).String()
+ format := opt.Value(ctx, OptionFormat).String()
+
+ des, err := descriptor.Find(format)
+ if err != nil {
+ return fmt.Errorf("find descriptor[%v]: %w", format, err)
+ }
+
+ def := definition.New()
+ command.Default(def)
+
+ cmds := registry.Commands()
+ commands := descriptor.Commands{
+ Namespace: ns,
+ Options: def.With(cparam.New(descriptor.TxtStyle())),
+ }
+ groups := make(map[string]*descriptor.NSCommand)
+ namespaces := make([]string, 0, len(cmds))
+ empty := descriptor.NSCommand{}
+
+ for _, name := range cmds {
+ if ns != "" && !strings.HasPrefix(name, ns+":") {
+ continue
+ }
+
+ cmd, _ := registry.Find(name)
+ if param.IsHidden(cmd) {
+ continue
+ }
+
+ gn := strings.SplitN(name, ":", defaultLenNamespace)
+ if len(gn) != defaultLenNamespace {
+ empty.Append(cmd.Name(), param.Description(cmd))
+
+ continue
+ }
+
+ if _, ok := groups[gn[0]]; !ok {
+ groups[gn[0]] = &descriptor.NSCommand{
+ Name: gn[0],
+ }
+
+ namespaces = append(namespaces, gn[0])
+ }
+
+ groups[gn[0]].Append(name, param.Description(cmd))
+ }
+
+ if len(empty.Commands) > 0 {
+ commands.Commands = append(commands.Commands, empty)
+ }
+
+ for _, name := range namespaces {
+ commands.Commands = append(commands.Commands, *groups[name])
+ }
+
+ if ns != "" && len(commands.Commands) == 0 {
+ return fmt.Errorf("%w: namespace %s", cerr.ErrNotFound, ns)
+ }
+
+ if err := des.Commands(ctx, out, commands); err != nil {
+ return fmt.Errorf("descriptor:%w", err)
+ }
+
+ return nil
+}
+
+type read struct {
+ config.Provider
+}
+
+func (r read) Value(ctx context.Context, key ...string) config.Value {
+ val, err := r.Provider.Value(ctx, key...)
+ if err != nil {
+ return value.Empty{Err: err}
+ }
+
+ return val
+}
diff --git a/command/list/help.go b/command/list/help.go
new file mode 100644
index 0000000..816ddbd
--- /dev/null
+++ b/command/list/help.go
@@ -0,0 +1,20 @@
+package list
+
+import (
+ "fmt"
+
+ "gitoa.ru/go-4devs/console/param"
+)
+
+const tpl = `
+The %[2]s command lists all commands:
+ %[1]s %[2]s
+You can also display the commands for a specific namespace:
+ %[1]s %[2]s test
+You can also output the information in other formats by using the --format option:
+ %[1]s %[2]s --format=xml
+`
+
+func Help(data param.HData) (string, error) {
+ return fmt.Sprintf(tpl, data.Bin, data.Name), nil
+}
diff --git a/command_test.go b/command_test.go
index 5cb9f8f..ead79bd 100644
--- a/command_test.go
+++ b/command_test.go
@@ -5,12 +5,10 @@ import (
"errors"
"fmt"
"strings"
- "sync/atomic"
"testing"
"time"
"gitoa.ru/go-4devs/config"
- "gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
@@ -18,133 +16,59 @@ import (
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console"
+ "gitoa.ru/go-4devs/console/command"
+ cerr "gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/output"
)
//nolint:gochecknoinits
func init() {
- console.MustRegister(Command().With(console.WithName("fdevs:console:test")))
- console.MustRegister(Command().With(console.WithName("fdevs:console:arg")))
+ console.MustRegister(command.With(Command(), command.WithName("fdevs:console:test")))
+ console.MustRegister(command.With(Command(), command.WithName("fdevs:console:arg")))
}
-func Command() *console.Command {
- return &console.Command{
- Name: "test:command",
- Description: "test command",
- Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
- var astr []string
- if aerr := console.ReadValue(ctx, in, "string").Unmarshal(&astr); aerr != nil && !errors.Is(aerr, config.ErrNotFound) {
- return fmt.Errorf("unmarshal string:%w", aerr)
- }
-
- out.Print(ctx,
- "test argument:", console.ReadValue(ctx, in, "test_argument").String(), "\n",
- "bool option:", console.ReadValue(ctx, in, "bool").Bool(), "\n",
- "duration option with default:", console.ReadValue(ctx, in, "duration").Duration(), "\n",
- "array string:[", strings.Join(astr, ","), "]\n",
- "group string:", console.ReadValue(ctx, in, "group", "test", "string").String(), "\n",
- "log http service:", console.ReadValue(ctx, in, "log", "http", "level").String(), "\n",
- )
-
- return nil
- },
- Configure: func(_ context.Context, def config.Definition) error {
- def.
- Add(
- group.New("group", "group example",
- option.Bool("bool", "bool"),
- group.New("test", "test", option.String("string", "test group string", option.Default("group string default value"))),
- ),
- group.New("log", "log",
- proto.New("service", "service level",
- option.String("level", "service level", option.Default("debug")),
- ),
- ),
- arg.String("test_argument", "test argument"),
- option.String("string", "array string", option.Slice),
- option.Bool("bool", "test bool option"),
- option.Duration("duration", "test duration with default", option.Default(value.New(time.Second))),
- option.Time("hidden", "hidden time", option.Default(value.New(time.Second)), option.Hidden),
- )
-
- return nil
- },
- }
+func Command() command.Command {
+ return command.New("test:command", "test command", Execute, command.Configure(Configure))
}
-func TestChainPrepare(t *testing.T) {
- t.Parallel()
-
- var cnt int64
-
- ctx := context.Background()
- def := definition.New()
-
- prepare := func(ctx context.Context, def config.Definition, n console.Configure) error {
- atomic.AddInt64(&cnt, 1)
-
- return n(ctx, def)
- }
- configure := func(context.Context, config.Definition) error {
- return nil
+func Execute(ctx context.Context, in config.Provider, out output.Output) error {
+ var astr []string
+ if aerr := console.ReadValue(ctx, in, "string").Unmarshal(&astr); aerr != nil && !errors.Is(aerr, config.ErrNotFound) {
+ return fmt.Errorf("unmarshal string:%w", aerr)
}
- for i := range []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} {
- prepares := make([]console.Prepare, i)
- for p := range i {
- prepares[p] = prepare
- }
+ out.Print(ctx,
+ "test argument:", console.ReadValue(ctx, in, "test_argument").String(), "\n",
+ "bool option:", console.ReadValue(ctx, in, "bool").Bool(), "\n",
+ "duration option with default:", console.ReadValue(ctx, in, "duration").Duration(), "\n",
+ "array string:[", strings.Join(astr, ","), "]\n",
+ "group string:", console.ReadValue(ctx, in, "group", "test", "string").String(), "\n",
+ "log http service:", console.ReadValue(ctx, in, "log", "http", "level").String(), "\n",
+ )
- cnt = 0
- chain := console.ChainPrepare(prepares...)
-
- err := chain(ctx, def, configure)
- if err != nil {
- t.Errorf("expected nil err, got: %s", err)
- }
-
- if cnt != int64(i) {
- t.Fatalf("expected: call prepare 1, got: %d ", cnt)
- }
- }
+ return nil
}
-func TestChainHandle(t *testing.T) {
- t.Parallel()
+func Configure(_ context.Context, def config.Definition) error {
+ def.
+ Add(
+ group.New("group", "group example",
+ option.Bool("bool", "bool"),
+ group.New("test", "test", option.String("string", "test group string", option.Default("group string default value"))),
+ ),
+ group.New("log", "log",
+ proto.New("service", "service level",
+ option.String("level", "service level", option.Default("debug")),
+ ),
+ ),
+ arg.String("test_argument", "test argument"),
+ option.String("string", "array string", option.Slice),
+ option.Bool("bool", "test bool option"),
+ option.Duration("duration", "test duration with default", option.Default(value.New(time.Second))),
+ option.Time("hidden", "hidden time", option.Default(value.New(time.Second)), option.Hidden),
+ )
- var cnt int64
-
- ctx := context.Background()
- in := &memory.Map{}
- out := output.Stdout()
-
- handle := func(ctx context.Context, in config.Provider, out output.Output, next console.Action) error {
- atomic.AddInt64(&cnt, 1)
-
- return next(ctx, in, out)
- }
- action := func(context.Context, config.Provider, output.Output) error {
- return nil
- }
-
- for i := range []int{0, 1, 2, 30, 40, 50} {
- handles := make([]console.Handle, i)
- for p := range i {
- handles[p] = handle
- }
-
- cnt = 0
- chain := console.ChainHandle(handles...)
-
- err := chain(ctx, in, out, action)
- if err != nil {
- t.Errorf("expected nil err, got: %s", err)
- }
-
- if cnt != int64(i) {
- t.Fatalf("expected: call prepare 1, got: %d ", cnt)
- }
- }
+ return nil
}
func TestRunEmptyExecute(t *testing.T) {
@@ -158,7 +82,7 @@ func TestRunEmptyExecute(t *testing.T) {
out := output.Stdout()
err := empty.Run(ctx, in, out)
- if !errors.Is(err, console.ErrExecuteNil) {
- t.Fatalf("expected: %v, got: %v ", console.ErrExecuteNil, err)
+ if !errors.Is(err, cerr.ErrExecuteNil) {
+ t.Fatalf("expected: %v, got: %v ", cerr.ErrExecuteNil, err)
}
}
diff --git a/console.go b/console.go
index 224e7eb..5892142 100644
--- a/console.go
+++ b/console.go
@@ -3,55 +3,38 @@ package console
import (
"context"
"errors"
+ "fmt"
"log"
- "math"
- "os"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
- "gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/command/help"
+ cerr "gitoa.ru/go-4devs/console/errors"
+ "gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
- "gitoa.ru/go-4devs/console/output/verbosity"
-)
-
-const (
- verboseTrace = 3
- verboseDebug = 2
- verboseInfo = 1
-)
-
-const (
- OptionHelp = "help"
- OptionVersion = "version"
- OptionAnsi = "ansi"
- OptionNoAnsi = "no-ansi"
- OptionQuiet = "quiet"
- OptionVerbose = "verbose"
-)
-
-const (
- defaultOptionsPosition = math.MaxUint64 / 2
+ "gitoa.ru/go-4devs/console/param"
)
// Execute the current command with option.
-func Execute(ctx context.Context, cmd *Command, opts ...func(*App)) {
+func Execute(ctx context.Context, cmd command.Command, opts ...func(*App)) {
opts = append([]func(*App){WithSkipArgs(1)}, opts...)
New(opts...).exec(ctx, cmd)
}
// Run current command by input and output.
-func Run(ctx context.Context, cmd *Command, in config.BindProvider, out output.Output) error {
+func Run(ctx context.Context, cmd command.Command, in config.BindProvider, out output.Output) error {
def := definition.New()
- err := cmd.Init(ctx, def)
+ err := cmd.Configure(ctx, def)
if err != nil {
- return err
+ return fmt.Errorf("%w", err)
}
- def.Add(Default()...)
+ command.Default(def)
berr := in.Bind(ctx, config.NewVars(def.Options()...))
if berr != nil {
@@ -60,85 +43,37 @@ func Run(ctx context.Context, cmd *Command, in config.BindProvider, out output.O
return showHelp(ctx, cmd, in, output.Ansi(out))
}
- out = ansi(ctx, in, out)
+ out = command.Verbose(ctx, in, out)
- out = verbose(ctx, in, out)
-
- if ReadValue(ctx, in, OptionVersion).Bool() {
- version := cmd.Version
- if version == "" {
- version = "unknown"
- }
-
- out.Println(ctx, "command ", cmd.Name, " version: ", version, "")
+ if command.IsShowVersion(ctx, in) {
+ out.Println(ctx, "command ", cmd.Name(), " version: ", param.Version(cmd), "")
return nil
}
- if ReadValue(ctx, in, OptionHelp).Bool() {
+ if command.IsShowHelp(ctx, in) {
return showHelp(ctx, cmd, in, out)
}
- return cmd.Run(ctx, in, out)
-}
-
-func ansi(ctx context.Context, in config.Provider, out output.Output) output.Output {
- switch {
- case ReadValue(ctx, in, OptionAnsi).Bool():
- out = output.Ansi(out)
- case ReadValue(ctx, in, OptionNoAnsi).Bool():
- out = output.None(out)
- case lookupEnv("NO_COLOR"):
- out = output.None(out)
- default:
- out = output.Ansi(out)
+ if err := cmd.Execute(ctx, in, out); err != nil {
+ return fmt.Errorf("%w", err)
}
- return out
+ return nil
}
-func lookupEnv(name string) bool {
- v, has := os.LookupEnv(name)
-
- return has && v == "true"
-}
-
-func verbose(ctx context.Context, in config.Provider, out output.Output) output.Output {
- switch {
- case ReadValue(ctx, in, OptionQuiet).Bool():
- out = output.Quiet()
- default:
- var verb []bool
-
- _ = ReadValue(ctx, in, OptionVerbose).Unmarshal(&verb)
-
- switch {
- case len(verb) == verboseInfo:
- out = output.Verbosity(out, verbosity.Info)
- case len(verb) == verboseDebug:
- out = output.Verbosity(out, verbosity.Debug)
- case len(verb) >= verboseTrace:
- out = output.Verbosity(out, verbosity.Trace)
- default:
- out = output.Verbosity(out, verbosity.Norm)
- }
- }
-
- return out
-}
-
-func showHelp(ctx context.Context, cmd *Command, in config.Provider, out output.Output) error {
+func showHelp(ctx context.Context, cmd command.Command, in config.Provider, out output.Output) error {
arr := &memory.Map{}
- arr.SetOption(value.New(cmd.Name), ArgumentCommandName)
- arr.SetOption(value.New(false), OptionHelp)
+ arr.SetOption(value.New(cmd.Name()), help.ArgumentCommandName)
+ arr.SetOption(value.New(false), command.OptionHelp)
- if _, err := Find(cmd.Name); errors.Is(err, ErrNotFound) {
- register(cmd)
+ if _, err := registry.Find(cmd.Name()); errors.Is(err, cerr.ErrNotFound) {
+ _ = registry.Add(cmd)
}
- help, err := Find(CommandHelp)
+ help, err := registry.Find(help.Name)
if err != nil {
- return err
+ return fmt.Errorf("%w", err)
}
w := chain.New(arr, in)
@@ -146,20 +81,6 @@ func showHelp(ctx context.Context, cmd *Command, in config.Provider, out output.
return Run(ctx, help, w, out)
}
-// Default options and argument command.
-func Default() []config.Option {
- return []config.Option{
- option.Bool(OptionNoAnsi, "Disable ANSI output", option.Position(defaultOptionsPosition)),
- option.Bool(OptionAnsi, "Do not ask any interactive question", option.Position(defaultOptionsPosition)),
- option.Bool(OptionVersion, "Display this application version", option.Short('V'), option.Position(defaultOptionsPosition)),
- option.Bool(OptionHelp, "Display this help message", option.Short('h'), option.Position(defaultOptionsPosition)),
- option.Bool(OptionVerbose,
- "Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace",
- option.Short('v'), option.Slice, option.Position(defaultOptionsPosition)),
- option.Bool(OptionQuiet, "Do not output any message", option.Short('q'), option.Position(defaultOptionsPosition)),
- }
-}
-
func ReadValue(ctx context.Context, in config.Provider, path ...string) config.Value {
val, err := in.Value(ctx, path...)
if err != nil {
diff --git a/error.go b/errors/errors.go
similarity index 57%
rename from error.go
rename to errors/errors.go
index 6e29f34..13a3132 100644
--- a/error.go
+++ b/errors/errors.go
@@ -1,4 +1,4 @@
-package console
+package errors //nolint:revive
import (
"errors"
@@ -7,10 +7,11 @@ import (
)
var (
- ErrNotFound = errors.New("command not found")
- ErrCommandNil = errors.New("console: Register command is nil")
- ErrExecuteNil = errors.New("console: execute is nil")
- ErrCommandDuplicate = errors.New("console: duplicate command")
+ ErrWrongType = errors.New("wrong type")
+ ErrNotFound = errors.New("not found")
+ ErrCommandNil = errors.New("command is nil")
+ ErrExecuteNil = errors.New("execute is nil")
+ ErrCommandDplicate = errors.New("duplicate command")
)
type AlternativesError struct {
diff --git a/help.go b/help.go
deleted file mode 100644
index 172874a..0000000
--- a/help.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package console
-
-import (
- "context"
- "fmt"
- "os"
- "strings"
-
- "gitoa.ru/go-4devs/config"
- "gitoa.ru/go-4devs/config/definition"
- "gitoa.ru/go-4devs/config/definition/option"
- "gitoa.ru/go-4devs/config/param"
- "gitoa.ru/go-4devs/config/provider/arg"
- "gitoa.ru/go-4devs/config/validator"
- "gitoa.ru/go-4devs/config/value"
- "gitoa.ru/go-4devs/console/output"
- "gitoa.ru/go-4devs/console/output/descriptor"
-)
-
-//nolint:gochecknoinits
-func init() {
- MustRegister(help())
-}
-
-const (
- ArgumentCommandName = "command_name"
- OptionFormat = "format"
-)
-
-func help() *Command {
- return &Command{
- Name: CommandHelp,
- Description: `Displays help for a command`,
- Help: `
-The {{ .Name }} command displays help for a given command:
- {{ .Bin }} {{ .Name }} list
-You can also output the help in other formats by using the --format option:
- {{ .Bin }} {{ .Name }} --format=xml list
-To display the list of available commands, please use the list command.
-`,
- Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
- var err error
-
- name := ReadValue(ctx, in, ArgumentCommandName).String()
- format := ReadValue(ctx, in, OptionFormat).String()
-
- des, err := descriptor.Find(format)
- if err != nil {
- return fmt.Errorf("find descriptor[%v]: %w", format, err)
- }
-
- cmd, err := Find(name)
- if err != nil {
- return fmt.Errorf("find cmd: %w", err)
- }
-
- def := definition.New()
- def.Add(Default()...)
-
- if err := cmd.Init(ctx, def); err != nil {
- return fmt.Errorf("init cmd: %w", err)
- }
-
- var bin string
- if len(os.Args) > 0 {
- bin = os.Args[0]
- }
-
- derr := des.Command(ctx, out, descriptor.Command{
- Bin: bin,
- Name: cmd.Name,
- Description: cmd.Description,
- Help: cmd.Help,
- Options: def.With(param.New(descriptor.TxtStyle())),
- })
- if derr != nil {
- return fmt.Errorf("descriptor help:%w", derr)
- }
-
- return nil
- },
- Configure: func(_ context.Context, config config.Definition) error {
- formats := descriptor.Descriptors()
- config.
- Add(
- arg.String(ArgumentCommandName, "The command name", arg.Default(value.New("help"))),
- option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
- option.Required,
- option.Default(value.New(formats[0])),
- validator.Valid(
- validator.NotBlank,
- validator.Enum(formats...),
- ),
- ),
- )
-
- return nil
- },
- }
-}
diff --git a/internal/registry/command.go b/internal/registry/command.go
new file mode 100644
index 0000000..7ebabae
--- /dev/null
+++ b/internal/registry/command.go
@@ -0,0 +1,39 @@
+package registry
+
+import (
+ "fmt"
+
+ "gitoa.ru/go-4devs/console/command"
+)
+
+//nolint:gochecknoglobals
+var commands = command.Commands{}
+
+func Find(name string) (command.Command, error) {
+ prov, err := commands.Find(name)
+ if err != nil {
+ return prov, fmt.Errorf("%w", err)
+ }
+
+ return prov, nil
+}
+
+func Commands() []string {
+ return commands.Names()
+}
+
+func Add(cmds ...command.Command) error {
+ if err := commands.Add(cmds...); err != nil {
+ return fmt.Errorf("add:%w", err)
+ }
+
+ return nil
+}
+
+func Set(cmds ...command.Command) error {
+ if err := commands.Set(cmds...); err != nil {
+ return fmt.Errorf("set:%w", err)
+ }
+
+ return nil
+}
diff --git a/list.go b/list.go
deleted file mode 100644
index 8a6b6ac..0000000
--- a/list.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package console
-
-import (
- "context"
- "fmt"
- "strings"
-
- "gitoa.ru/go-4devs/config"
- "gitoa.ru/go-4devs/config/definition"
- "gitoa.ru/go-4devs/config/definition/option"
- "gitoa.ru/go-4devs/config/param"
- "gitoa.ru/go-4devs/config/provider/arg"
- "gitoa.ru/go-4devs/config/validator"
- "gitoa.ru/go-4devs/config/value"
- "gitoa.ru/go-4devs/console/output"
- "gitoa.ru/go-4devs/console/output/descriptor"
-)
-
-const defaultLenNamespace = 2
-
-//nolint:gochecknoinits
-func init() {
- MustRegister(list())
-}
-
-const (
- ArgumentNamespace = "namespace"
-)
-
-func list() *Command {
- return &Command{
- Name: CommandList,
- Description: "Lists commands",
- Help: `
-The {{ .Name }} command lists all commands:
- {{ .Bin }} {{ .Name }}
-You can also display the commands for a specific namespace:
- {{ .Bin }} {{ .Name }} test
-You can also output the information in other formats by using the --format option:
- {{ .Bin }} {{ .Name }} --format=xml
-`,
- Execute: executeList,
- Configure: func(_ context.Context, cfg config.Definition) error {
- formats := descriptor.Descriptors()
- cfg.
- Add(
- arg.String(ArgumentNamespace, "The namespace name"),
- option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
- option.Required,
- option.Default(value.New(formats[0])),
- validator.Valid(
- validator.NotBlank,
- validator.Enum(formats...),
- ),
- ),
- )
-
- return nil
- },
- }
-}
-
-//nolint:cyclop
-func executeList(ctx context.Context, in config.Provider, out output.Output) error {
- ns := ReadValue(ctx, in, ArgumentNamespace).String()
- format := ReadValue(ctx, in, OptionFormat).String()
-
- des, err := descriptor.Find(format)
- if err != nil {
- return fmt.Errorf("find descriptor[%v]: %w", format, err)
- }
-
- cmds := Commands()
- commands := descriptor.Commands{
- Namespace: ns,
- Options: definition.New(Default()...).With(param.New(descriptor.TxtStyle())),
- }
- groups := make(map[string]*descriptor.NSCommand)
- namespaces := make([]string, 0, len(cmds))
- empty := descriptor.NSCommand{}
-
- for _, name := range cmds {
- if ns != "" && !strings.HasPrefix(name, ns+":") {
- continue
- }
-
- cmd, _ := Find(name)
- if cmd.Hidden {
- continue
- }
-
- gn := strings.SplitN(name, ":", defaultLenNamespace)
- if len(gn) != defaultLenNamespace {
- empty.Append(cmd.Name, cmd.Description)
-
- continue
- }
-
- if _, ok := groups[gn[0]]; !ok {
- groups[gn[0]] = &descriptor.NSCommand{
- Name: gn[0],
- }
-
- namespaces = append(namespaces, gn[0])
- }
-
- groups[gn[0]].Append(name, cmd.Description)
- }
-
- if len(empty.Commands) > 0 {
- commands.Commands = append(commands.Commands, empty)
- }
-
- for _, name := range namespaces {
- commands.Commands = append(commands.Commands, *groups[name])
- }
-
- if ns != "" && len(commands.Commands) == 0 {
- return fmt.Errorf("%w: namespace %s", ErrNotFound, ns)
- }
-
- if err := des.Commands(ctx, out, commands); err != nil {
- return fmt.Errorf("descriptor:%w", err)
- }
-
- return nil
-}
diff --git a/output/descriptor/txt.go b/output/descriptor/txt.go
index c6b4dc4..acde1ac 100644
--- a/output/descriptor/txt.go
+++ b/output/descriptor/txt.go
@@ -133,12 +133,10 @@ func txtHelp(cmd Command) string {
return ""
}
- tpl := template.Must(template.New("help").Parse(cmd.Help))
-
var buf bytes.Buffer
buf.WriteString("\nHelp:")
- _ = tpl.Execute(&buf, cmd)
+ buf.WriteString(cmd.Help)
return buf.String()
}
diff --git a/param/helper.go b/param/helper.go
new file mode 100644
index 0000000..d9ef017
--- /dev/null
+++ b/param/helper.go
@@ -0,0 +1,23 @@
+package param
+
+func Bool(in Params, key any) (bool, bool) {
+ data, ok := in.Param(key)
+ if !ok {
+ return false, false
+ }
+
+ res, ok := data.(bool)
+
+ return res, ok
+}
+
+func String(in Params, key any) (string, bool) {
+ data, ok := in.Param(key)
+ if !ok {
+ return "", false
+ }
+
+ res, ok := data.(string)
+
+ return res, ok
+}
diff --git a/param/keys.go b/param/keys.go
new file mode 100644
index 0000000..231a848
--- /dev/null
+++ b/param/keys.go
@@ -0,0 +1,90 @@
+package param
+
+import (
+ "fmt"
+
+ cerr "gitoa.ru/go-4devs/console/errors"
+)
+
+type key uint8
+
+const (
+ paramHidden key = iota + 1
+ paramDescription
+ paramVerssion
+ paramHelp
+)
+
+const (
+ defaultVersion = "undefined"
+)
+
+func IsHidden(in Params) bool {
+ data, ok := Bool(in, paramHidden)
+
+ return ok && data
+}
+
+func Hidden(in Params) Params {
+ return in.With(paramHidden, true)
+}
+
+func Description(in Params) string {
+ data, _ := String(in, paramDescription)
+
+ return data
+}
+
+func WithDescription(desc string) Option {
+ return func(p Params) Params {
+ return p.With(paramDescription, desc)
+ }
+}
+
+func Version(in Params) string {
+ if data, ok := String(in, paramVerssion); ok {
+ return data
+ }
+
+ return defaultVersion
+}
+
+func WithVersion(in string) Option {
+ return func(p Params) Params {
+ return p.With(paramVerssion, in)
+ }
+}
+
+func HelpData(bin, name string) HData {
+ return HData{
+ Bin: bin,
+ Name: name,
+ }
+}
+
+type HData struct {
+ Bin string
+ Name string
+}
+
+type HelpFn func(data HData) (string, error)
+
+func WithHelp(fn HelpFn) Option {
+ return func(p Params) Params {
+ return p.With(paramHelp, fn)
+ }
+}
+
+func Help(in Params, data HData) (string, error) {
+ fn, ok := in.Param(paramHelp)
+ if !ok {
+ return "", nil
+ }
+
+ hfn, fok := fn.(HelpFn)
+ if !fok {
+ return "", fmt.Errorf("%w: expect:%T, got:%T", cerr.ErrWrongType, (HelpFn)(nil), fn)
+ }
+
+ return hfn(data)
+}
diff --git a/param/params.go b/param/params.go
new file mode 100644
index 0000000..8038bc5
--- /dev/null
+++ b/param/params.go
@@ -0,0 +1,57 @@
+package param
+
+//nolint:gochecknoglobals
+var eparam = empty{}
+
+func New(opts ...Option) Params {
+ var param Params
+
+ param = eparam
+ for _, opt := range opts {
+ param = opt(param)
+ }
+
+ return param
+}
+
+type Params interface {
+ Param(key any) (any, bool)
+ With(key, val any) Params
+}
+
+type Option func(Params) Params
+
+type empty struct{}
+
+func (e empty) Param(any) (any, bool) {
+ return nil, false
+}
+
+func (e empty) With(key, val any) Params {
+ return data{
+ parent: e,
+ key: key,
+ val: val,
+ }
+}
+
+type data struct {
+ parent Params
+ key, val any
+}
+
+func (d data) Param(key any) (any, bool) {
+ if d.key == key {
+ return d.val, true
+ }
+
+ return d.parent.Param(key)
+}
+
+func (d data) With(key, val any) Params {
+ return data{
+ parent: d,
+ key: key,
+ val: val,
+ }
+}
diff --git a/register.go b/register.go
index caf6b0b..15a0187 100644
--- a/register.go
+++ b/register.go
@@ -1,114 +1,35 @@
package console
import (
- "errors"
"fmt"
- "regexp"
- "sort"
- "sync"
-)
-const (
- CommandHelp = "help"
- CommandList = "list"
-)
-
-//nolint:gochecknoglobals
-var (
- commandsMu sync.RWMutex
- commands = make(map[string]*Command)
- findCommand = regexp.MustCompile("([^:]+|)")
+ "gitoa.ru/go-4devs/console/command"
+ "gitoa.ru/go-4devs/console/internal/registry"
)
// MustRegister register command or panic if err.
-func MustRegister(cmd *Command) {
- err := Register(cmd)
+func MustRegister(cmd ...command.Command) {
+ err := registry.Add(cmd...)
if err != nil {
panic(err)
}
}
// Register makes a command available execute in app. If Register is called twice with the same name or if driver is nil, return error.
-func Register(cmd *Command) error {
- if cmd == nil {
- return ErrCommandNil
+func Register(cmd ...command.Command) error {
+ if err := registry.Add(cmd...); err != nil {
+ return fmt.Errorf("%w", err)
}
- if _, err := Find(cmd.Name); !errors.Is(err, ErrNotFound) {
- return fmt.Errorf("%w: command %s", ErrCommandDuplicate, cmd.Name)
- }
-
- register(cmd)
-
return nil
}
-func register(cmd *Command) {
- commandsMu.Lock()
- defer commandsMu.Unlock()
-
- if cmd != nil && cmd.Name != "" {
- commands[cmd.Name] = cmd
- }
-}
-
-// Commands returns a sorted list of the names of the registered commands.
-func Commands() []string {
- commandsMu.RLock()
- defer commandsMu.RUnlock()
-
- return commandNames()
-}
-
-func commandNames() []string {
- names := make([]string, 0, len(commands))
- for name := range commands {
- names = append(names, name)
- }
-
- sort.Strings(names)
-
- return names
-}
-
// Find command by name, tries to find the best match if you give it an abbreviation of a name.
-func Find(name string) (*Command, error) {
- commandsMu.RLock()
- defer commandsMu.RUnlock()
-
- if cmd, ok := commands[name]; ok {
- return cmd, nil
- }
-
- nameRegexp := findCommand.ReplaceAllStringFunc(name, func(in string) string {
- return in + "[^:]*"
- })
-
- findCommands := make([]*Command, 0)
-
- cmdRegexp, err := regexp.Compile("^" + nameRegexp + "$")
+func Find(name string) (command.Command, error) {
+ cmd, err := registry.Find(name)
if err != nil {
- return nil, fmt.Errorf("find by regexp:%w", err)
+ return cmd, fmt.Errorf("%w", err)
}
- for name := range commands {
- if !commands[name].Hidden && cmdRegexp.MatchString(name) {
- findCommands = append(findCommands, commands[name])
- }
- }
-
- if len(findCommands) == 1 {
- return findCommands[0], nil
- }
-
- if len(findCommands) > 1 {
- names := make([]string, len(findCommands))
- for i := range findCommands {
- names[i] = findCommands[i].Name
- }
-
- return nil, AlternativesError{Alt: names, Err: ErrNotFound}
- }
-
- return nil, ErrNotFound
+ return cmd, nil
}
diff --git a/register_test.go b/register_test.go
deleted file mode 100644
index 6176b8e..0000000
--- a/register_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package console_test
-
-import (
- "testing"
-
- "gitoa.ru/go-4devs/console"
-)
-
-func TestFind(t *testing.T) {
- t.Parallel()
-
- cases := map[string]string{
- "fdevs:console:test": "fdevs:console:test",
- "fd:c:t": "fdevs:console:test",
- "fd::t": "fdevs:console:test",
- "f:c:t": "fdevs:console:test",
- "f:c:a": "fdevs:console:arg",
- }
-
- for name, ex := range cases {
- res, err := console.Find(name)
- if err != nil {
- t.Errorf("%v expect err, got:%s", name, err)
-
- continue
- }
-
- if res.Name != ex {
- t.Errorf("%v expect: %s, got: %s", name, ex, res)
- }
- }
-}