Merge pull request 'move command to folder' (#13) from command into master
All checks were successful
Go Action / goaction (push) Successful in 36s
All checks were successful
Go Action / goaction (push) Successful in 36s
Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
51
app.go
51
app.go
@@ -2,6 +2,7 @@ package console
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
@@ -9,6 +10,10 @@ import (
|
|||||||
"gitoa.ru/go-4devs/config/provider/chain"
|
"gitoa.ru/go-4devs/config/provider/chain"
|
||||||
"gitoa.ru/go-4devs/config/provider/memory"
|
"gitoa.ru/go-4devs/config/provider/memory"
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"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"
|
"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.
|
// New creates and configure new console app.
|
||||||
func New(opts ...func(*App)) *App {
|
func New(opts ...func(*App)) *App {
|
||||||
app := &App{
|
app := &App{
|
||||||
out: output.Stdout(),
|
out: output.Stdout(),
|
||||||
exit: os.Exit,
|
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 {
|
for _, opt := range opts {
|
||||||
@@ -55,26 +68,25 @@ func New(opts ...func(*App)) *App {
|
|||||||
|
|
||||||
// App is collection of command and configure env.
|
// App is collection of command and configure env.
|
||||||
type App struct {
|
type App struct {
|
||||||
cmds []*Command
|
registry func(...command.Command) error
|
||||||
out output.Output
|
out output.Output
|
||||||
in config.BindProvider
|
in config.BindProvider
|
||||||
exit func(int)
|
exit func(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add add or replace command.
|
// Add add or replace command.
|
||||||
func (a *App) Add(cmds ...*Command) *App {
|
func (a *App) Add(cmds ...command.Command) *App {
|
||||||
a.cmds = append(a.cmds, cmds...)
|
if err := a.registry(cmds...); err != nil {
|
||||||
|
a.printError(context.Background(), err)
|
||||||
|
a.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute run the command by name and arguments.
|
// Execute run the command by name and arguments.
|
||||||
func (a *App) Execute(ctx context.Context) {
|
func (a *App) Execute(ctx context.Context) {
|
||||||
for _, cmd := range a.cmds {
|
cmd, err := registry.Find(a.commandName())
|
||||||
register(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, err := a.find(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.printError(ctx, err)
|
a.printError(ctx, err)
|
||||||
|
|
||||||
@@ -89,7 +101,7 @@ func (a *App) Execute(ctx context.Context) {
|
|||||||
a.exec(ctx, cmd)
|
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)
|
err := Run(ctx, cmd, a.in, a.out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.printError(ctx, err)
|
a.printError(ctx, err)
|
||||||
@@ -99,31 +111,30 @@ func (a *App) exec(ctx context.Context, cmd *Command) {
|
|||||||
a.exit(0)
|
a.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) find(_ context.Context) (*Command, error) {
|
func (a *App) commandName() string {
|
||||||
if len(os.Args) < 2 || os.Args[1][1] == '-' {
|
name := list.Name
|
||||||
return Find(CommandList)
|
if len(os.Args) > 1 && len(os.Args[1]) > 1 && os.Args[1][1] != '-' {
|
||||||
|
name = os.Args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
name := os.Args[1]
|
return name
|
||||||
|
|
||||||
return Find(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) list(ctx context.Context) error {
|
func (a *App) list(ctx context.Context) error {
|
||||||
cmd, err := Find(CommandHelp)
|
cmd, err := registry.Find(help.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
arr := &memory.Map{}
|
arr := &memory.Map{}
|
||||||
arr.SetOption(value.New(CommandList), ArgumentCommandName)
|
arr.SetOption(value.New(list.Name), help.ArgumentCommandName)
|
||||||
in := chain.New(arr, a.in)
|
in := chain.New(arr, a.in)
|
||||||
|
|
||||||
return Run(ctx, cmd, in, a.out)
|
return Run(ctx, cmd, in, a.out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) printError(ctx context.Context, err error) {
|
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 {
|
func resolveSkip(in int) int {
|
||||||
|
|||||||
21
app_test.go
21
app_test.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/console"
|
"gitoa.ru/go-4devs/console"
|
||||||
|
"gitoa.ru/go-4devs/console/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
@@ -55,21 +56,15 @@ func ExampleNew_list() {
|
|||||||
"--no-ansi",
|
"--no-ansi",
|
||||||
}
|
}
|
||||||
|
|
||||||
console.New(console.WithExit(func(int) {})).
|
console.New(
|
||||||
|
console.WithExit(func(int) {}),
|
||||||
|
console.WithReplaceCommand,
|
||||||
|
).
|
||||||
Add(
|
Add(
|
||||||
Command(),
|
Command(),
|
||||||
&console.Command{
|
command.New("fdevs:console:arg", "Understanding how Console Arguments and Options Are Handled", Execute),
|
||||||
Name: "fdevs:console:arg",
|
command.New("fdevs:console:hello", "example hello command", Execute),
|
||||||
Description: "Understanding how Console Arguments and Options Are Handled",
|
command.New("app:start", "example command in other namespace", Execute),
|
||||||
},
|
|
||||||
&console.Command{
|
|
||||||
Name: "fdevs:console:hello",
|
|
||||||
Description: "example hello command",
|
|
||||||
},
|
|
||||||
&console.Command{
|
|
||||||
Name: "app:start",
|
|
||||||
Description: "example command in other namespace",
|
|
||||||
},
|
|
||||||
).
|
).
|
||||||
Execute(ctx)
|
Execute(ctx)
|
||||||
// Output:
|
// Output:
|
||||||
|
|||||||
16
command.go
16
command.go
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
|
"gitoa.ru/go-4devs/console/command"
|
||||||
|
"gitoa.ru/go-4devs/console/errors"
|
||||||
"gitoa.ru/go-4devs/console/output"
|
"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 {
|
type Command struct {
|
||||||
// The name of the command.
|
// The name of the command.
|
||||||
Name string
|
Name string
|
||||||
@@ -101,7 +115,7 @@ func (c *Command) With(opts ...Option) *Command {
|
|||||||
// Run run command with input and output.
|
// Run run command with input and output.
|
||||||
func (c *Command) Run(ctx context.Context, in config.Provider, out output.Output) error {
|
func (c *Command) Run(ctx context.Context, in config.Provider, out output.Output) error {
|
||||||
if c.Execute == nil {
|
if c.Execute == nil {
|
||||||
return fmt.Errorf("%w", ErrExecuteNil)
|
return fmt.Errorf("%w", errors.ErrExecuteNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Handle != nil {
|
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
|
||||||
|
}
|
||||||
104
command_test.go
104
command_test.go
@@ -5,12 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/definition"
|
|
||||||
"gitoa.ru/go-4devs/config/definition/group"
|
"gitoa.ru/go-4devs/config/definition/group"
|
||||||
"gitoa.ru/go-4devs/config/definition/option"
|
"gitoa.ru/go-4devs/config/definition/option"
|
||||||
"gitoa.ru/go-4devs/config/definition/proto"
|
"gitoa.ru/go-4devs/config/definition/proto"
|
||||||
@@ -18,20 +16,22 @@ import (
|
|||||||
"gitoa.ru/go-4devs/config/provider/memory"
|
"gitoa.ru/go-4devs/config/provider/memory"
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"gitoa.ru/go-4devs/config/value"
|
||||||
"gitoa.ru/go-4devs/console"
|
"gitoa.ru/go-4devs/console"
|
||||||
|
"gitoa.ru/go-4devs/console/command"
|
||||||
|
cerr "gitoa.ru/go-4devs/console/errors"
|
||||||
"gitoa.ru/go-4devs/console/output"
|
"gitoa.ru/go-4devs/console/output"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoinits
|
//nolint:gochecknoinits
|
||||||
func init() {
|
func init() {
|
||||||
console.MustRegister(Command().With(console.WithName("fdevs:console:test")))
|
console.MustRegister(command.With(Command(), command.WithName("fdevs:console:test")))
|
||||||
console.MustRegister(Command().With(console.WithName("fdevs:console:arg")))
|
console.MustRegister(command.With(Command(), command.WithName("fdevs:console:arg")))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Command() *console.Command {
|
func Command() command.Command {
|
||||||
return &console.Command{
|
return command.New("test:command", "test command", Execute, command.Configure(Configure))
|
||||||
Name: "test:command",
|
}
|
||||||
Description: "test command",
|
|
||||||
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
|
func Execute(ctx context.Context, in config.Provider, out output.Output) error {
|
||||||
var astr []string
|
var astr []string
|
||||||
if aerr := console.ReadValue(ctx, in, "string").Unmarshal(&astr); aerr != nil && !errors.Is(aerr, config.ErrNotFound) {
|
if aerr := console.ReadValue(ctx, in, "string").Unmarshal(&astr); aerr != nil && !errors.Is(aerr, config.ErrNotFound) {
|
||||||
return fmt.Errorf("unmarshal string:%w", aerr)
|
return fmt.Errorf("unmarshal string:%w", aerr)
|
||||||
@@ -47,8 +47,9 @@ func Command() *console.Command {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
}
|
||||||
Configure: func(_ context.Context, def config.Definition) error {
|
|
||||||
|
func Configure(_ context.Context, def config.Definition) error {
|
||||||
def.
|
def.
|
||||||
Add(
|
Add(
|
||||||
group.New("group", "group example",
|
group.New("group", "group example",
|
||||||
@@ -68,83 +69,6 @@ func Command() *console.Command {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunEmptyExecute(t *testing.T) {
|
func TestRunEmptyExecute(t *testing.T) {
|
||||||
@@ -158,7 +82,7 @@ func TestRunEmptyExecute(t *testing.T) {
|
|||||||
out := output.Stdout()
|
out := output.Stdout()
|
||||||
|
|
||||||
err := empty.Run(ctx, in, out)
|
err := empty.Run(ctx, in, out)
|
||||||
if !errors.Is(err, console.ErrExecuteNil) {
|
if !errors.Is(err, cerr.ErrExecuteNil) {
|
||||||
t.Fatalf("expected: %v, got: %v ", console.ErrExecuteNil, err)
|
t.Fatalf("expected: %v, got: %v ", cerr.ErrExecuteNil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
console.go
129
console.go
@@ -3,55 +3,38 @@ package console
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gitoa.ru/go-4devs/config"
|
"gitoa.ru/go-4devs/config"
|
||||||
"gitoa.ru/go-4devs/config/definition"
|
"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/chain"
|
||||||
"gitoa.ru/go-4devs/config/provider/memory"
|
"gitoa.ru/go-4devs/config/provider/memory"
|
||||||
"gitoa.ru/go-4devs/config/value"
|
"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"
|
||||||
"gitoa.ru/go-4devs/console/output/verbosity"
|
"gitoa.ru/go-4devs/console/param"
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Execute the current command with option.
|
// 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...)
|
opts = append([]func(*App){WithSkipArgs(1)}, opts...)
|
||||||
New(opts...).exec(ctx, cmd)
|
New(opts...).exec(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run current command by input and output.
|
// 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()
|
def := definition.New()
|
||||||
|
|
||||||
err := cmd.Init(ctx, def)
|
err := cmd.Configure(ctx, def)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
def.Add(Default()...)
|
command.Default(def)
|
||||||
|
|
||||||
berr := in.Bind(ctx, config.NewVars(def.Options()...))
|
berr := in.Bind(ctx, config.NewVars(def.Options()...))
|
||||||
if berr != nil {
|
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))
|
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 command.IsShowVersion(ctx, in) {
|
||||||
|
out.Println(ctx, "command <comment>", cmd.Name(), "</comment> version: <info>", param.Version(cmd), "</info>")
|
||||||
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>")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ReadValue(ctx, in, OptionHelp).Bool() {
|
if command.IsShowHelp(ctx, in) {
|
||||||
return showHelp(ctx, cmd, in, out)
|
return showHelp(ctx, cmd, in, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd.Run(ctx, in, out)
|
if err := cmd.Execute(ctx, in, out); err != nil {
|
||||||
}
|
return fmt.Errorf("%w", err)
|
||||||
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupEnv(name string) bool {
|
func showHelp(ctx context.Context, cmd command.Command, in config.Provider, out output.Output) error {
|
||||||
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 {
|
|
||||||
arr := &memory.Map{}
|
arr := &memory.Map{}
|
||||||
arr.SetOption(value.New(cmd.Name), ArgumentCommandName)
|
arr.SetOption(value.New(cmd.Name()), help.ArgumentCommandName)
|
||||||
arr.SetOption(value.New(false), OptionHelp)
|
arr.SetOption(value.New(false), command.OptionHelp)
|
||||||
|
|
||||||
if _, err := Find(cmd.Name); errors.Is(err, ErrNotFound) {
|
if _, err := registry.Find(cmd.Name()); errors.Is(err, cerr.ErrNotFound) {
|
||||||
register(cmd)
|
_ = registry.Add(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
help, err := Find(CommandHelp)
|
help, err := registry.Find(help.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := chain.New(arr, in)
|
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)
|
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 {
|
func ReadValue(ctx context.Context, in config.Provider, path ...string) config.Value {
|
||||||
val, err := in.Value(ctx, path...)
|
val, err := in.Value(ctx, path...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package console
|
package errors //nolint:revive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -7,10 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = errors.New("command not found")
|
ErrWrongType = errors.New("wrong type")
|
||||||
ErrCommandNil = errors.New("console: Register command is nil")
|
ErrNotFound = errors.New("not found")
|
||||||
ErrExecuteNil = errors.New("console: execute is nil")
|
ErrCommandNil = errors.New("command is nil")
|
||||||
ErrCommandDuplicate = errors.New("console: duplicate command")
|
ErrExecuteNil = errors.New("execute is nil")
|
||||||
|
ErrCommandDplicate = errors.New("duplicate command")
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlternativesError struct {
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl := template.Must(template.New("help").Parse(cmd.Help))
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
buf.WriteString("\n<comment>Help:</comment>")
|
buf.WriteString("\n<comment>Help:</comment>")
|
||||||
_ = tpl.Execute(&buf, cmd)
|
buf.WriteString(cmd.Help)
|
||||||
|
|
||||||
return buf.String()
|
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
|
package console
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"gitoa.ru/go-4devs/console/command"
|
||||||
CommandHelp = "help"
|
"gitoa.ru/go-4devs/console/internal/registry"
|
||||||
CommandList = "list"
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
|
||||||
var (
|
|
||||||
commandsMu sync.RWMutex
|
|
||||||
commands = make(map[string]*Command)
|
|
||||||
findCommand = regexp.MustCompile("([^:]+|)")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MustRegister register command or panic if err.
|
// MustRegister register command or panic if err.
|
||||||
func MustRegister(cmd *Command) {
|
func MustRegister(cmd ...command.Command) {
|
||||||
err := Register(cmd)
|
err := registry.Add(cmd...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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.
|
// 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 {
|
func Register(cmd ...command.Command) error {
|
||||||
if cmd == nil {
|
if err := registry.Add(cmd...); err != nil {
|
||||||
return ErrCommandNil
|
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
|
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.
|
// 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) {
|
func Find(name string) (command.Command, error) {
|
||||||
commandsMu.RLock()
|
cmd, err := registry.Find(name)
|
||||||
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 + "$")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("find by regexp:%w", err)
|
return cmd, fmt.Errorf("%w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for name := range commands {
|
return cmd, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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