move command to folder
All checks were successful
Go Action / goaction (pull_request) Successful in 45s
All checks were successful
Go Action / goaction (pull_request) Successful in 45s
This commit is contained in:
57
app.go
57
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, "<error>\n\n ", err, "\n</error>")
|
||||
command.Ansi(ctx, a.in, a.out).Println(ctx, "<error>\n\n ", err, "\n</error>")
|
||||
}
|
||||
|
||||
func resolveSkip(in int) int {
|
||||
|
||||
21
app_test.go
21
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:
|
||||
|
||||
16
command.go
16
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 {
|
||||
|
||||
82
command/chain.go
Normal file
82
command/chain.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
88
command/chain_test.go
Normal file
88
command/chain_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
143
command/command.go
Normal file
143
command/command.go
Normal file
@@ -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
|
||||
}
|
||||
137
command/commands.go
Normal file
137
command/commands.go
Normal file
@@ -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
|
||||
}
|
||||
46
command/commands_test.go
Normal file
46
command/commands_test.go
Normal file
@@ -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 <nil> err, got:%s", name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if res.Name() != ex {
|
||||
t.Errorf("%v expect: %s, got: %s", name, ex, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
command/default.go
Normal file
120
command/default.go
Normal file
@@ -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
|
||||
}
|
||||
136
command/help/command.go
Normal file
136
command/help/command.go
Normal file
@@ -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 <info>%[2]s</info> command displays help for a given command:
|
||||
<info>%[1]s %[2]s list</info>
|
||||
You can also output the help in other formats by using the <comment>--format</comment> option:
|
||||
<info>%[1]s %[2]s --format=xml list</info>
|
||||
To display the list of available commands, please use the <info>list</info> 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
|
||||
}
|
||||
147
command/list/command.go
Normal file
147
command/list/command.go
Normal file
@@ -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
|
||||
}
|
||||
20
command/list/help.go
Normal file
20
command/list/help.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitoa.ru/go-4devs/console/param"
|
||||
)
|
||||
|
||||
const tpl = `
|
||||
The <info>%[2]s</info> command lists all commands:
|
||||
<info>%[1]s %[2]s</info>
|
||||
You can also display the commands for a specific namespace:
|
||||
<info>%[1]s %[2]s test</info>
|
||||
You can also output the information in other formats by using the <comment>--format</comment> option:
|
||||
<info>%[1]s %[2]s --format=xml</info>
|
||||
`
|
||||
|
||||
func Help(data param.HData) (string, error) {
|
||||
return fmt.Sprintf(tpl, data.Bin, data.Name), nil
|
||||
}
|
||||
156
command_test.go
156
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)
|
||||
}
|
||||
}
|
||||
|
||||
129
console.go
129
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 <comment>", cmd.Name, "</comment> version: <info>", version, "</info>")
|
||||
if command.IsShowVersion(ctx, in) {
|
||||
out.Println(ctx, "command <comment>", cmd.Name(), "</comment> version: <info>", param.Version(cmd), "</info>")
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
100
help.go
100
help.go
@@ -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 <info>{{ .Name }}</info> command displays help for a given command:
|
||||
<info>{{ .Bin }} {{ .Name }} list</info>
|
||||
You can also output the help in other formats by using the <comment>--format</comment> option:
|
||||
<info>{{ .Bin }} {{ .Name }} --format=xml list</info>
|
||||
To display the list of available commands, please use the <info>list</info> 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
|
||||
},
|
||||
}
|
||||
}
|
||||
39
internal/registry/command.go
Normal file
39
internal/registry/command.go
Normal file
@@ -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
|
||||
}
|
||||
127
list.go
127
list.go
@@ -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 <info>{{ .Name }}</info> command lists all commands:
|
||||
<info>{{ .Bin }} {{ .Name }}</info>
|
||||
You can also display the commands for a specific namespace:
|
||||
<info>{{ .Bin }} {{ .Name }} test</info>
|
||||
You can also output the information in other formats by using the <comment>--format</comment> option:
|
||||
<info>{{ .Bin }} {{ .Name }} --format=xml</info>
|
||||
`,
|
||||
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
|
||||
}
|
||||
@@ -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("\n<comment>Help:</comment>")
|
||||
_ = tpl.Execute(&buf, cmd)
|
||||
buf.WriteString(cmd.Help)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
23
param/helper.go
Normal file
23
param/helper.go
Normal file
@@ -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
|
||||
}
|
||||
90
param/keys.go
Normal file
90
param/keys.go
Normal file
@@ -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)
|
||||
}
|
||||
57
param/params.go
Normal file
57
param/params.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
101
register.go
101
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
|
||||
}
|
||||
|
||||
@@ -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 <nil> err, got:%s", name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if res.Name != ex {
|
||||
t.Errorf("%v expect: %s, got: %s", name, ex, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user