Compare commits
7 Commits
5577600f95
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1382bea590 | |||
| 797c938628 | |||
| 0d661986d1 | |||
| e2c6fc0a35 | |||
| e80e292830 | |||
| 696032e1cc | |||
| 60d879caf2 |
68
app.go
68
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"
|
||||
)
|
||||
|
||||
@@ -28,7 +33,10 @@ func WithInput(in config.BindProvider) func(*App) {
|
||||
|
||||
// WithSkipArgs sets how many arguments are passed. For example, you don't need to pass the name of a single command.
|
||||
func WithSkipArgs(l int) func(*App) {
|
||||
return WithInput(chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(l):])), &memory.Default{}))
|
||||
return WithInput(chain.New(
|
||||
arg.New(arg.WithArgs(os.Args[ResolveSkip(l):])),
|
||||
&memory.Default{}),
|
||||
)
|
||||
}
|
||||
|
||||
// WithExit sets exit callback by default os.Exit.
|
||||
@@ -38,12 +46,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 +71,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 +104,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,34 +114,37 @@ 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>")
|
||||
printErr(ctx, a.in, a.out, err)
|
||||
}
|
||||
|
||||
func resolveSkip(in int) int {
|
||||
func printErr(ctx context.Context, in config.Provider, out output.Output, err error) {
|
||||
command.Ansi(ctx, in, out).Printf(ctx, "<error>\n\n %v\n</error>\n", err)
|
||||
}
|
||||
|
||||
func ResolveSkip(in int) int {
|
||||
res := 2
|
||||
|
||||
switch {
|
||||
|
||||
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/errs"
|
||||
"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", errs.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
153
command/command.go
Normal file
153
command/command.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
"gitoa.ru/go-4devs/console/setting"
|
||||
)
|
||||
|
||||
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.Setting = setting.WithVersion(in)(c.Setting)
|
||||
}
|
||||
}
|
||||
|
||||
func Hidden(c *Command) {
|
||||
c.Setting = setting.Hidden(c.Setting)
|
||||
}
|
||||
|
||||
func Help(fn setting.HelpFn) Option {
|
||||
return func(c *Command) {
|
||||
c.Setting = setting.WithHelp(fn)(c.Setting)
|
||||
}
|
||||
}
|
||||
|
||||
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 Usage(fn setting.UsageFn) Option {
|
||||
return func(c *Command) {
|
||||
c.Setting = setting.WithUsage(fn)(c.Setting)
|
||||
}
|
||||
}
|
||||
|
||||
func EmptyUsage(cmd *Command) {
|
||||
cmd.Setting = setting.WithUsage(func(setting.UData) (string, error) {
|
||||
return "", nil
|
||||
})(cmd.Setting)
|
||||
}
|
||||
|
||||
func New(name, desc string, execute ExecuteFn, opts ...Option) Command {
|
||||
cmd := Command{
|
||||
name: name,
|
||||
execute: execute,
|
||||
configure: emptyConfigure,
|
||||
handle: emptyHandle,
|
||||
prepare: emptyPrepare,
|
||||
Setting: setting.New(setting.WithDescription(desc)),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&cmd)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
setting.Setting
|
||||
|
||||
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(), setting.Version(c))
|
||||
}
|
||||
|
||||
func With(parent Command, opts ...Option) Command {
|
||||
cmd := Command{
|
||||
Setting: parent.Setting,
|
||||
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"
|
||||
|
||||
"gitoa.ru/go-4devs/console/errs"
|
||||
"gitoa.ru/go-4devs/console/setting"
|
||||
)
|
||||
|
||||
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) && !setting.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{}, errs.AlternativesError{Alt: names, Err: errs.ErrCommandDplicate}
|
||||
}
|
||||
|
||||
return Command{}, fmt.Errorf("%w", errs.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", errs.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", errs.ErrCommandNil)
|
||||
}
|
||||
|
||||
if _, ok := c.names[cmd.Name()]; ok {
|
||||
return fmt.Errorf("command %s:%w", cmd.Name(), errs.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
|
||||
}
|
||||
67
command/dump/reference.go
Normal file
67
command/dump/reference.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package dump
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/config/definition"
|
||||
"gitoa.ru/go-4devs/config/definition/option"
|
||||
"gitoa.ru/go-4devs/config/provider/arg"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/errs"
|
||||
"gitoa.ru/go-4devs/console/internal/registry"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
//go:generate go tool config config:generate
|
||||
|
||||
const NameRefernce = "config:dump-reference"
|
||||
|
||||
func Command() command.Command {
|
||||
return command.New(NameRefernce, "dump reference by command", RExecute, command.Configure(RConfigure))
|
||||
}
|
||||
|
||||
func RExecute(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
provs, ok := in.(config.Providers)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: expect %T got %T", errs.ErrWrongType, (config.Providers)(nil), in)
|
||||
}
|
||||
|
||||
cfg := NewRConfigureConfig(in)
|
||||
|
||||
cmd, err := registry.Find(cfg.CommandName(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmd:%w", err)
|
||||
}
|
||||
|
||||
def := definition.New()
|
||||
if err := cmd.Configure(ctx, def); err != nil {
|
||||
return fmt.Errorf("configure:%w", err)
|
||||
}
|
||||
|
||||
prov, err := provs.Provider(cfg.Format(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("prov:%w", errs.AlternativesError{Alt: provs.Names(), Err: err})
|
||||
}
|
||||
|
||||
bind, ok := prov.(config.DumpProvider)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: expect config.DunpProvider got %T", errs.ErrWrongType, prov)
|
||||
}
|
||||
|
||||
if err := bind.DumpReference(ctx, out, def); err != nil {
|
||||
return fmt.Errorf("dump:%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RConfigure(_ context.Context, def config.Definition) error {
|
||||
def.Add(
|
||||
arg.String("command-name", "command name", option.Required),
|
||||
option.String("format", "format", option.Default(arg.Name)),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
89
command/dump/reference_config.go
Normal file
89
command/dump/reference_config.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
|
||||
package dump
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gitoa.ru/go-4devs/config"
|
||||
)
|
||||
|
||||
func WithRConfigureConfigHandle(fn func(context.Context, error)) func(*RConfigureConfig) {
|
||||
return func(ci *RConfigureConfig) {
|
||||
ci.handle = fn
|
||||
}
|
||||
}
|
||||
|
||||
func NewRConfigureConfig(prov config.Provider, opts ...func(*RConfigureConfig)) RConfigureConfig {
|
||||
i := RConfigureConfig{
|
||||
Provider: prov,
|
||||
handle: func(_ context.Context, err error) {
|
||||
fmt.Printf("RConfigureConfig:%v", err)
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&i)
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
type RConfigureConfig struct {
|
||||
config.Provider
|
||||
handle func(context.Context, error)
|
||||
}
|
||||
|
||||
// readCommandName command name.
|
||||
func (i RConfigureConfig) readCommandName(ctx context.Context) (v string, e error) {
|
||||
val, err := i.Value(ctx, "command-name")
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("read [%v]:%w", []string{"command-name"}, err)
|
||||
|
||||
}
|
||||
|
||||
return val.ParseString()
|
||||
|
||||
}
|
||||
|
||||
// ReadCommandName command name.
|
||||
func (i RConfigureConfig) ReadCommandName(ctx context.Context) (string, error) {
|
||||
return i.readCommandName(ctx)
|
||||
}
|
||||
|
||||
// CommandName command name.
|
||||
func (i RConfigureConfig) CommandName(ctx context.Context) string {
|
||||
val, err := i.readCommandName(ctx)
|
||||
if err != nil {
|
||||
i.handle(ctx, err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// readFormat format.
|
||||
func (i RConfigureConfig) readFormat(ctx context.Context) (v string, e error) {
|
||||
val, err := i.Value(ctx, "format")
|
||||
if err != nil {
|
||||
i.handle(ctx, err)
|
||||
|
||||
return "arg", nil
|
||||
}
|
||||
|
||||
return val.ParseString()
|
||||
|
||||
}
|
||||
|
||||
// ReadFormat format.
|
||||
func (i RConfigureConfig) ReadFormat(ctx context.Context) (string, error) {
|
||||
return i.readFormat(ctx)
|
||||
}
|
||||
|
||||
// Format format.
|
||||
func (i RConfigureConfig) Format(ctx context.Context) string {
|
||||
val, err := i.readFormat(ctx)
|
||||
if err != nil {
|
||||
i.handle(ctx, err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
152
command/help/command.go
Normal file
152
command/help/command.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package help
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/command"
|
||||
"gitoa.ru/go-4devs/console/errs"
|
||||
"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/setting"
|
||||
)
|
||||
|
||||
//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 := setting.Help(cmd, setting.HelpData(bin, cmd.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create help:%w", err)
|
||||
}
|
||||
|
||||
hasUsage := true
|
||||
|
||||
usage, err := setting.Usage(cmd, setting.UsageData(cmd.Name(), def))
|
||||
if err != nil {
|
||||
if !errors.Is(err, errs.ErrNotFound) {
|
||||
return fmt.Errorf("create usage:%w", err)
|
||||
}
|
||||
|
||||
hasUsage = false
|
||||
}
|
||||
|
||||
derr := des.Command(ctx, out, descriptor.Command{
|
||||
Bin: bin,
|
||||
Name: cmd.Name(),
|
||||
Description: setting.Description(cmd),
|
||||
Help: help,
|
||||
Usage: func() (string, bool) {
|
||||
return usage, hasUsage
|
||||
},
|
||||
Options: def.With(param.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 setting.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"
|
||||
"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/errs"
|
||||
"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/setting"
|
||||
)
|
||||
|
||||
//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(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, _ := registry.Find(name)
|
||||
if setting.IsHidden(cmd) {
|
||||
continue
|
||||
}
|
||||
|
||||
gn := strings.SplitN(name, ":", defaultLenNamespace)
|
||||
if len(gn) != defaultLenNamespace {
|
||||
empty.Append(cmd.Name(), setting.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, setting.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", errs.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/setting"
|
||||
)
|
||||
|
||||
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 setting.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"
|
||||
"gitoa.ru/go-4devs/console/errs"
|
||||
"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, errs.ErrExecuteNil) {
|
||||
t.Fatalf("expected: %v, got: %v ", errs.ErrExecuteNil, err)
|
||||
}
|
||||
}
|
||||
|
||||
134
console.go
134
console.go
@@ -3,142 +3,76 @@ package console
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
"gitoa.ru/go-4devs/console/errs"
|
||||
"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/setting"
|
||||
)
|
||||
|
||||
// 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)
|
||||
New(opts...).exec(ctx, command.With(cmd, command.EmptyUsage))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Print(berr)
|
||||
printErr(ctx, in, out, berr)
|
||||
|
||||
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>", setting.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, errs.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 +80,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 errs
|
||||
|
||||
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 {
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"gitoa.ru/go-4devs/config/provider/arg"
|
||||
"gitoa.ru/go-4devs/config/provider/chain"
|
||||
@@ -22,7 +23,7 @@ func main() {
|
||||
console.
|
||||
New(console.WithInput(
|
||||
chain.New(
|
||||
arg.New(arg.WithSkip(0)),
|
||||
arg.New(arg.WithArgs(os.Args)),
|
||||
env.New(Namespace, AppName),
|
||||
&memory.Default{},
|
||||
),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module gitoa.ru/go-4devs/console/example
|
||||
|
||||
go 1.23
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
gitoa.ru/go-4devs/config v0.0.7
|
||||
gitoa.ru/go-4devs/console v0.2.0
|
||||
gitoa.ru/go-4devs/config v0.0.8
|
||||
gitoa.ru/go-4devs/console v0.2.1-0.20260105202444-e2c6fc0a35a4
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
gitoa.ru/go-4devs/config v0.0.7 h1:8q6axRNLgXE5dYQd8Jbh9j+STqevbibVyvwrtsuHpZk=
|
||||
gitoa.ru/go-4devs/config v0.0.7/go.mod h1:UINWnObZA0nLiJro+TtavUBBvN0cSt17aRHOk20pP74=
|
||||
gitoa.ru/go-4devs/config v0.0.8 h1:o4p8I9jWJMfiFVVKr50IqCGj1fF+8kmSPDkH0deRvn4=
|
||||
gitoa.ru/go-4devs/config v0.0.8/go.mod h1:jHKqVafFVW400LC0M4i1ifPapiI9sqpX/QTh+VMadKw=
|
||||
gitoa.ru/go-4devs/console v0.2.0 h1:6lsbArs99GA8vGdnwNDThZNKjFNctNtTlSCUjhgwIpU=
|
||||
gitoa.ru/go-4devs/console v0.2.0/go.mod h1:xi4Svw7T+lylckAQiJQS/2qwDwF4YbIanlhcbQrBAiI=
|
||||
gitoa.ru/go-4devs/console v0.2.1-0.20260105202444-e2c6fc0a35a4 h1:zOk/59IvgUfCDL6ub6WX4hsqDK79FsZR0gf7s7t3fXM=
|
||||
gitoa.ru/go-4devs/console v0.2.1-0.20260105202444-e2c6fc0a35a4/go.mod h1:PG/Zyj1dLh7eFlj9bgnV58+Ys6I/MTrS0q9W7oD7z4U=
|
||||
|
||||
@@ -7,32 +7,37 @@ import (
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/config/definition/option"
|
||||
"gitoa.ru/go-4devs/console"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
func Args() *console.Command {
|
||||
return &console.Command{
|
||||
Name: "fdevs:console:arg",
|
||||
Description: "Understanding how Console Arguments and Options Are Handled",
|
||||
Configure: func(_ context.Context, def config.Definition) error {
|
||||
def.Add(
|
||||
option.Bool("foo", "foo option", option.Short('f')),
|
||||
option.String("bar", "required bar option", option.Required, option.Short('b')),
|
||||
option.String("cat", "cat option", option.Short('c')),
|
||||
option.Time("time", "time example"),
|
||||
option.Time("hidden", "hidden time example", option.Hidden),
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
out.Println(ctx, "foo: <info>", console.ReadValue(ctx, in, "foo").Bool(), "</info>")
|
||||
out.Println(ctx, "bar: <info>", console.ReadValue(ctx, in, "bar").String(), "</info>")
|
||||
out.Println(ctx, "cat: <info>", console.ReadValue(ctx, in, "cat").String(), "</info>")
|
||||
out.Println(ctx, "time: <info>", console.ReadValue(ctx, in, "time").Time().Format(time.RFC3339), "</info>")
|
||||
out.Println(ctx, "hidden: <info>", console.ReadValue(ctx, in, "hidden").Time().Format(time.RFC3339), "</info>")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
func Args() command.Command {
|
||||
return command.New(
|
||||
"fdevs:console:arg",
|
||||
"Understanding how Console Arguments and Options Are Handled",
|
||||
ArgExecute,
|
||||
command.Configure(ArgConfigure),
|
||||
)
|
||||
}
|
||||
|
||||
func ArgConfigure(_ context.Context, def config.Definition) error {
|
||||
def.Add(
|
||||
option.Bool("foo", "foo option", option.Short('f')),
|
||||
option.String("bar", "required bar option", option.Required, option.Short('b')),
|
||||
option.String("cat", "cat option", option.Short('c')),
|
||||
option.Time("time", "time example"),
|
||||
option.Time("hidden", "hidden time example", option.Hidden),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ArgExecute(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
out.Println(ctx, "foo: <info>", console.ReadValue(ctx, in, "foo").Bool(), "</info>")
|
||||
out.Println(ctx, "bar: <info>", console.ReadValue(ctx, in, "bar").String(), "</info>")
|
||||
out.Println(ctx, "cat: <info>", console.ReadValue(ctx, in, "cat").String(), "</info>")
|
||||
out.Println(ctx, "time: <info>", console.ReadValue(ctx, in, "time").Time().Format(time.RFC3339), "</info>")
|
||||
out.Println(ctx, "hidden: <info>", console.ReadValue(ctx, in, "hidden").Time().Format(time.RFC3339), "</info>")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,37 +5,47 @@ import (
|
||||
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/config/definition/option"
|
||||
"gitoa.ru/go-4devs/config/param"
|
||||
cparam "gitoa.ru/go-4devs/config/param"
|
||||
argument "gitoa.ru/go-4devs/config/provider/arg"
|
||||
"gitoa.ru/go-4devs/console"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
"gitoa.ru/go-4devs/console/param"
|
||||
)
|
||||
|
||||
func CreateUser(required bool) *console.Command {
|
||||
return &console.Command{
|
||||
Name: "app:create-user",
|
||||
Description: "Creates a new user.",
|
||||
Help: "This command allows you to create a user...",
|
||||
Configure: func(_ context.Context, cfg config.Definition) error {
|
||||
var opts []param.Option
|
||||
if required {
|
||||
opts = append(opts, option.Required)
|
||||
}
|
||||
func CreateUser(required bool) command.Command {
|
||||
return command.New(
|
||||
"app:create-user",
|
||||
"Creates a new user.",
|
||||
UserExecute,
|
||||
command.Configure(UserConfigure(required)),
|
||||
command.Help(func(param.HData) (string, error) {
|
||||
return "This command allows you to create a user...", nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
cfg.
|
||||
Add(
|
||||
argument.String("username", "The username of the user.", option.Required),
|
||||
argument.String("password", "User password", opts...),
|
||||
)
|
||||
func UserConfigure(required bool) func(_ context.Context, cfg config.Definition) error {
|
||||
return func(_ context.Context, cfg config.Definition) error {
|
||||
var opts []cparam.Option
|
||||
if required {
|
||||
opts = append(opts, option.Required)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
// outputs a message followed by a "\n"
|
||||
out.Println(ctx, "User Creator")
|
||||
out.Println(ctx, "Username: ", console.ReadValue(ctx, in, "username").String())
|
||||
cfg.
|
||||
Add(
|
||||
argument.String("username", "The username of the user.", option.Required),
|
||||
argument.String("password", "User password", opts...),
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UserExecute(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
// outputs a message followed by a "\n"
|
||||
out.Println(ctx, "User Creator")
|
||||
out.Println(ctx, "Username: ", console.ReadValue(ctx, in, "username").String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,33 +7,38 @@ import (
|
||||
"gitoa.ru/go-4devs/config/definition/option"
|
||||
"gitoa.ru/go-4devs/config/provider/arg"
|
||||
"gitoa.ru/go-4devs/console"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
func Hello() *console.Command {
|
||||
return &console.Command{
|
||||
Name: "fdevs:console:hello",
|
||||
Description: "example hello command",
|
||||
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
name := console.ReadValue(ctx, in, "name").String()
|
||||
out.Println(ctx, "<error>Hello</error> <info>", name, "</info>")
|
||||
|
||||
out.Info(ctx, "same trace info\n")
|
||||
out.Debug(ctx, "have some question?\n")
|
||||
out.Trace(ctx, "this message shows with -vvv\n")
|
||||
|
||||
pass := console.ReadValue(ctx, in, "pass").String()
|
||||
out.Println(ctx, "hidden option pass <info>", pass, "</info>")
|
||||
|
||||
return nil
|
||||
},
|
||||
Configure: func(_ context.Context, def config.Definition) error {
|
||||
def.Add(
|
||||
arg.String("name", "Same name", arg.Default("World")),
|
||||
option.String("pass", "password", option.Hidden),
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
func Hello() command.Command {
|
||||
return command.New(
|
||||
"fdevs:console:hello",
|
||||
"example hello command",
|
||||
HelloExecute,
|
||||
command.Configure(HelloConfigure),
|
||||
)
|
||||
}
|
||||
|
||||
func HelloExecute(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
name := console.ReadValue(ctx, in, "name").String()
|
||||
out.Println(ctx, "<error>Hello</error> <info>", name, "</info>")
|
||||
|
||||
out.Info(ctx, "same trace info\n")
|
||||
out.Debug(ctx, "have some question?\n")
|
||||
out.Trace(ctx, "this message shows with -vvv\n")
|
||||
|
||||
pass := console.ReadValue(ctx, in, "pass").String()
|
||||
out.Println(ctx, "hidden option pass <info>", pass, "</info>")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HelloConfigure(_ context.Context, def config.Definition) error {
|
||||
def.Add(
|
||||
arg.String("name", "Same name", arg.Default("World")),
|
||||
option.String("pass", "password", option.Hidden),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,19 +4,16 @@ import (
|
||||
"context"
|
||||
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/console"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
func Hidden() *console.Command {
|
||||
return &console.Command{
|
||||
Name: "fdevs:console:hidden",
|
||||
Description: "hidden command exmale",
|
||||
Hidden: true,
|
||||
Execute: func(ctx context.Context, _ config.Provider, out output.Output) error {
|
||||
out.Println(ctx, "<info> call hidden command</info>")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
func Hidden() command.Command {
|
||||
return command.New("fdevs:console:hidden", "hidden command exmale", HiddenExecute, command.Hidden)
|
||||
}
|
||||
|
||||
func HiddenExecute(ctx context.Context, _ config.Provider, out output.Output) error {
|
||||
out.Println(ctx, "<info> call hidden command</info>")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,45 +9,46 @@ import (
|
||||
"gitoa.ru/go-4devs/config/validator"
|
||||
"gitoa.ru/go-4devs/config/value"
|
||||
"gitoa.ru/go-4devs/console"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
const defaultTimeout = time.Second * 30
|
||||
|
||||
// Long example of a command that takes a long time to run.
|
||||
func Long() *console.Command {
|
||||
return &console.Command{
|
||||
Name: "fdevs:command:long",
|
||||
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
timeout := console.ReadValue(ctx, in, "timeout").Duration()
|
||||
timer := time.NewTimer(timeout)
|
||||
func Long() command.Command {
|
||||
return command.New("fdevs:command:long", "long command description", LongExecute, command.Configure(LongConfigure))
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
func LongExecute(ctx context.Context, in config.Provider, out output.Output) error {
|
||||
timeout := console.ReadValue(ctx, in, "timeout").Duration()
|
||||
timer := time.NewTimer(timeout)
|
||||
|
||||
for {
|
||||
select {
|
||||
case t := <-ticker.C:
|
||||
out.Println(ctx, "ticker: <info>", t, "</info>")
|
||||
case <-timer.C:
|
||||
out.Println(ctx, "<error>stop timer</error>")
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
out.Println(ctx, "<info>cancel context</info>")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
Configure: func(_ context.Context, def config.Definition) error {
|
||||
def.Add(option.Duration("timeout", "set duration run command",
|
||||
option.Default(value.New(defaultTimeout)),
|
||||
option.Short('t'),
|
||||
validator.Valid(validator.NotBlank),
|
||||
))
|
||||
for {
|
||||
select {
|
||||
case t := <-ticker.C:
|
||||
out.Println(ctx, "ticker: <info>", t, "</info>")
|
||||
case <-timer.C:
|
||||
out.Println(ctx, "<error>stop timer</error>")
|
||||
|
||||
return nil
|
||||
},
|
||||
case <-ctx.Done():
|
||||
out.Println(ctx, "<info>cancel context</info>")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LongConfigure(_ context.Context, def config.Definition) error {
|
||||
def.Add(option.Duration("timeout", "set duration run command",
|
||||
option.Default(value.New(defaultTimeout)),
|
||||
option.Short('t'),
|
||||
validator.Valid(validator.NotBlank),
|
||||
))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,18 +4,16 @@ import (
|
||||
"context"
|
||||
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/console"
|
||||
"gitoa.ru/go-4devs/console/command"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
)
|
||||
|
||||
func Namespace() *console.Command {
|
||||
return &console.Command{
|
||||
Name: "app:start",
|
||||
Description: "example command in other namespace",
|
||||
Execute: func(ctx context.Context, _ config.Provider, out output.Output) error {
|
||||
out.Println(ctx, "example command in other namespace")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
func Namespace() command.Command {
|
||||
return command.New("app:start", "example command in other namespace", NSExecute)
|
||||
}
|
||||
|
||||
func NSExecute(ctx context.Context, _ config.Provider, out output.Output) error {
|
||||
out.Println(ctx, "example command in other namespace")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
9
go.mod
9
go.mod
@@ -2,9 +2,7 @@ module gitoa.ru/go-4devs/console
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require gitoa.ru/go-4devs/config v0.0.8
|
||||
|
||||
replace gitoa.ru/go-4devs/config => ../config
|
||||
require gitoa.ru/go-4devs/config v0.0.10
|
||||
|
||||
require (
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
@@ -12,4 +10,7 @@ require (
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
)
|
||||
|
||||
tool golang.org/x/tools/cmd/stringer
|
||||
tool (
|
||||
gitoa.ru/go-4devs/config/cmd/config
|
||||
golang.org/x/tools/cmd/stringer
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,5 +1,9 @@
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
gitoa.ru/go-4devs/config v0.0.9 h1:Z43kM6k6ocC2YIRQkELq5zWO9LO8fl1l14RyqjLC4I4=
|
||||
gitoa.ru/go-4devs/config v0.0.9/go.mod h1:cLW1+4E4uM4Pw+z4RuKEKbO1Lz6UTs2b2fTPyeEgTx8=
|
||||
gitoa.ru/go-4devs/config v0.0.10 h1:NSagD0voj77/IGqRGsbR0DZmDvFcxbx+oRoWQnLnSy4=
|
||||
gitoa.ru/go-4devs/config v0.0.10/go.mod h1:cLW1+4E4uM4Pw+z4RuKEKbO1Lz6UTs2b2fTPyeEgTx8=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
|
||||
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
|
||||
}
|
||||
@@ -25,6 +25,7 @@ type Command struct {
|
||||
Bin string
|
||||
Name string
|
||||
Description string
|
||||
Usage func() (string, bool)
|
||||
Help string
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"gitoa.ru/go-4devs/config/param"
|
||||
"gitoa.ru/go-4devs/config/provider/arg"
|
||||
"gitoa.ru/go-4devs/console/output"
|
||||
"gitoa.ru/go-4devs/console/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,9 +23,9 @@ const (
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
txtFunc = template.FuncMap{
|
||||
"synopsis": txtSynopsis,
|
||||
"definition": txtDefinition,
|
||||
"help": txtHelp,
|
||||
"usage": txtUsage,
|
||||
"commands": txtCommands,
|
||||
}
|
||||
|
||||
@@ -34,11 +35,9 @@ var (
|
||||
{{- if .Description -}}
|
||||
<comment>Description:</comment>
|
||||
{{ .Description }}
|
||||
|
||||
{{ end -}}
|
||||
<comment>Usage:</comment>
|
||||
{{ .Name }} {{ synopsis .Options }}
|
||||
{{ definition .Options }}
|
||||
{{- usage . }}
|
||||
{{- definition .Options }}
|
||||
{{- help . }}
|
||||
`))
|
||||
|
||||
@@ -128,17 +127,32 @@ func txtCommands(cmds []NSCommand) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func txtUsage(cmd Command) string {
|
||||
if cmd.Usage == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, has := cmd.Usage()
|
||||
if has && data == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if data == "" {
|
||||
data = defaultUsage(setting.UsageData(cmd.Name, cmd.Options))
|
||||
}
|
||||
|
||||
return "\n<comment>Usage:</comment>\n " + data + "\n"
|
||||
}
|
||||
|
||||
func txtHelp(cmd Command) string {
|
||||
if cmd.Help == "" {
|
||||
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()
|
||||
}
|
||||
@@ -154,10 +168,12 @@ func txtDefinition(options config.Options) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func txtSynopsis(options config.Options) string {
|
||||
def := arg.NewViews(options, nil)
|
||||
func defaultUsage(data setting.UData) string {
|
||||
def := arg.NewViews(data.Options, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(data.Name)
|
||||
buf.WriteString(" ")
|
||||
|
||||
if len(def.Options()) > 0 {
|
||||
buf.WriteString("[options] ")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
setting/helper.go
Normal file
23
setting/helper.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package setting
|
||||
|
||||
func Bool(in Setting, key any) (bool, bool) {
|
||||
data, ok := in.Param(key)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
|
||||
res, ok := data.(bool)
|
||||
|
||||
return res, ok
|
||||
}
|
||||
|
||||
func String(in Setting, key any) (string, bool) {
|
||||
data, ok := in.Param(key)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
res, ok := data.(string)
|
||||
|
||||
return res, ok
|
||||
}
|
||||
127
setting/keys.go
Normal file
127
setting/keys.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitoa.ru/go-4devs/config"
|
||||
"gitoa.ru/go-4devs/console/errs"
|
||||
)
|
||||
|
||||
type key uint8
|
||||
|
||||
const (
|
||||
paramHidden key = iota + 1
|
||||
paramDescription
|
||||
paramVerssion
|
||||
paramHelp
|
||||
paramUsage
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVersion = "undefined"
|
||||
)
|
||||
|
||||
func IsHidden(in Setting) bool {
|
||||
data, ok := Bool(in, paramHidden)
|
||||
|
||||
return ok && data
|
||||
}
|
||||
|
||||
func Hidden(in Setting) Setting {
|
||||
return in.With(paramHidden, true)
|
||||
}
|
||||
|
||||
func Description(in Setting) string {
|
||||
data, _ := String(in, paramDescription)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func WithDescription(desc string) Option {
|
||||
return func(p Setting) Setting {
|
||||
return p.With(paramDescription, desc)
|
||||
}
|
||||
}
|
||||
|
||||
func Version(in Setting) string {
|
||||
if data, ok := String(in, paramVerssion); ok {
|
||||
return data
|
||||
}
|
||||
|
||||
return defaultVersion
|
||||
}
|
||||
|
||||
func WithVersion(in string) Option {
|
||||
return func(p Setting) Setting {
|
||||
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 Setting) Setting {
|
||||
return p.With(paramHelp, fn)
|
||||
}
|
||||
}
|
||||
|
||||
func Help(in Setting, data HData) (string, error) {
|
||||
fn, ok := in.Param(paramHelp)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
hfn, fok := fn.(HelpFn)
|
||||
if !fok {
|
||||
return "", fmt.Errorf("%w: expect:func(data HData) (string, error), got:%T", errs.ErrWrongType, fn)
|
||||
}
|
||||
|
||||
return hfn(data)
|
||||
}
|
||||
|
||||
func UsageData(name string, opts config.Options) UData {
|
||||
return UData{
|
||||
Options: opts,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
type UData struct {
|
||||
config.Options
|
||||
|
||||
Name string
|
||||
}
|
||||
|
||||
type UsageFn func(data UData) (string, error)
|
||||
|
||||
func WithUsage(fn UsageFn) Option {
|
||||
return func(p Setting) Setting {
|
||||
return p.With(paramUsage, fn)
|
||||
}
|
||||
}
|
||||
|
||||
func Usage(in Setting, data UData) (string, error) {
|
||||
fn, ok := in.Param(paramUsage)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w", errs.ErrNotFound)
|
||||
}
|
||||
|
||||
ufn, ok := fn.(UsageFn)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: expect: func(data Udata) (string, error), got:%T", errs.ErrWrongType, fn)
|
||||
}
|
||||
|
||||
return ufn(data)
|
||||
}
|
||||
57
setting/setting.go
Normal file
57
setting/setting.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package setting
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var eparam = empty{}
|
||||
|
||||
func New(opts ...Option) Setting {
|
||||
var param Setting
|
||||
|
||||
param = eparam
|
||||
for _, opt := range opts {
|
||||
param = opt(param)
|
||||
}
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
type Setting interface {
|
||||
Param(key any) (any, bool)
|
||||
With(key, val any) Setting
|
||||
}
|
||||
|
||||
type Option func(Setting) Setting
|
||||
|
||||
type empty struct{}
|
||||
|
||||
func (e empty) Param(any) (any, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e empty) With(key, val any) Setting {
|
||||
return data{
|
||||
parent: e,
|
||||
key: key,
|
||||
val: val,
|
||||
}
|
||||
}
|
||||
|
||||
type data struct {
|
||||
parent Setting
|
||||
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) Setting {
|
||||
return data{
|
||||
parent: d,
|
||||
key: key,
|
||||
val: val,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user