From e80e2928305028eb90c8e5d9aeef5b7712520b87 Mon Sep 17 00:00:00 2001 From: andrey Date: Mon, 5 Jan 2026 23:23:20 +0300 Subject: [PATCH] move command to folder --- app.go | 57 +++++++------ app_test.go | 21 ++--- command.go | 16 +++- command/chain.go | 82 ++++++++++++++++++ command/chain_test.go | 88 ++++++++++++++++++++ command/command.go | 143 ++++++++++++++++++++++++++++++++ command/commands.go | 137 ++++++++++++++++++++++++++++++ command/commands_test.go | 46 +++++++++++ command/default.go | 120 +++++++++++++++++++++++++++ command/help/command.go | 136 ++++++++++++++++++++++++++++++ command/list/command.go | 147 +++++++++++++++++++++++++++++++++ command/list/help.go | 20 +++++ command_test.go | 156 +++++++++-------------------------- console.go | 129 ++++++----------------------- error.go => errors/errors.go | 11 +-- help.go | 100 ---------------------- internal/registry/command.go | 39 +++++++++ list.go | 127 ---------------------------- output/descriptor/txt.go | 4 +- param/helper.go | 23 ++++++ param/keys.go | 90 ++++++++++++++++++++ param/params.go | 57 +++++++++++++ register.go | 101 +++-------------------- register_test.go | 32 ------- 24 files changed, 1268 insertions(+), 614 deletions(-) create mode 100644 command/chain.go create mode 100644 command/chain_test.go create mode 100644 command/command.go create mode 100644 command/commands.go create mode 100644 command/commands_test.go create mode 100644 command/default.go create mode 100644 command/help/command.go create mode 100644 command/list/command.go create mode 100644 command/list/help.go rename error.go => errors/errors.go (57%) delete mode 100644 help.go create mode 100644 internal/registry/command.go delete mode 100644 list.go create mode 100644 param/helper.go create mode 100644 param/keys.go create mode 100644 param/params.go delete mode 100644 register_test.go 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) - } - } -}