move command to folder
All checks were successful
Go Action / goaction (pull_request) Successful in 45s

This commit is contained in:
2026-01-05 23:23:20 +03:00
parent 696032e1cc
commit e80e292830
24 changed files with 1268 additions and 614 deletions

57
app.go
View File

@@ -2,6 +2,7 @@ package console
import (
"context"
"fmt"
"os"
"gitoa.ru/go-4devs/config"
@@ -9,6 +10,10 @@ import (
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/command/help"
"gitoa.ru/go-4devs/console/command/list"
"gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
)
@@ -38,12 +43,20 @@ func WithExit(f func(int)) func(*App) {
}
}
func WithReplaceCommand(a *App) {
a.registry = registry.Set
}
// New creates and configure new console app.
func New(opts ...func(*App)) *App {
app := &App{
out: output.Stdout(),
exit: os.Exit,
in: chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(0):])), &memory.Default{}),
in: chain.New(
arg.New(arg.WithArgs(os.Args[resolveSkip(0):])),
&memory.Default{},
),
registry: registry.Add,
}
for _, opt := range opts {
@@ -55,26 +68,25 @@ func New(opts ...func(*App)) *App {
// App is collection of command and configure env.
type App struct {
cmds []*Command
out output.Output
in config.BindProvider
exit func(int)
registry func(...command.Command) error
out output.Output
in config.BindProvider
exit func(int)
}
// Add add or replace command.
func (a *App) Add(cmds ...*Command) *App {
a.cmds = append(a.cmds, cmds...)
func (a *App) Add(cmds ...command.Command) *App {
if err := a.registry(cmds...); err != nil {
a.printError(context.Background(), err)
a.exit(1)
}
return a
}
// Execute run the command by name and arguments.
func (a *App) Execute(ctx context.Context) {
for _, cmd := range a.cmds {
register(cmd)
}
cmd, err := a.find(ctx)
cmd, err := registry.Find(a.commandName())
if err != nil {
a.printError(ctx, err)
@@ -89,7 +101,7 @@ func (a *App) Execute(ctx context.Context) {
a.exec(ctx, cmd)
}
func (a *App) exec(ctx context.Context, cmd *Command) {
func (a *App) exec(ctx context.Context, cmd command.Command) {
err := Run(ctx, cmd, a.in, a.out)
if err != nil {
a.printError(ctx, err)
@@ -99,31 +111,30 @@ func (a *App) exec(ctx context.Context, cmd *Command) {
a.exit(0)
}
func (a *App) find(_ context.Context) (*Command, error) {
if len(os.Args) < 2 || os.Args[1][1] == '-' {
return Find(CommandList)
func (a *App) commandName() string {
name := list.Name
if len(os.Args) > 1 && len(os.Args[1]) > 1 && os.Args[1][1] != '-' {
name = os.Args[1]
}
name := os.Args[1]
return Find(name)
return name
}
func (a *App) list(ctx context.Context) error {
cmd, err := Find(CommandHelp)
cmd, err := registry.Find(help.Name)
if err != nil {
return err
return fmt.Errorf("%w", err)
}
arr := &memory.Map{}
arr.SetOption(value.New(CommandList), ArgumentCommandName)
arr.SetOption(value.New(list.Name), help.ArgumentCommandName)
in := chain.New(arr, a.in)
return Run(ctx, cmd, in, a.out)
}
func (a *App) printError(ctx context.Context, err error) {
ansi(ctx, a.in, a.out).Println(ctx, "<error>\n\n ", err, "\n</error>")
command.Ansi(ctx, a.in, a.out).Println(ctx, "<error>\n\n ", err, "\n</error>")
}
func resolveSkip(in int) int {

View File

@@ -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:

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/output"
)
@@ -52,6 +54,18 @@ func WithName(name string) Option {
}
}
func Wrap(cmd *Command) command.Command {
opts := make([]command.Option, 0)
if cmd.Hidden {
opts = append(opts, command.Hidden)
}
opts = append(opts, command.Configure(cmd.Init))
return command.New(cmd.Name, cmd.Description, cmd.Run, opts...)
}
// Deprecated: use command.New().
type Command struct {
// The name of the command.
Name string
@@ -101,7 +115,7 @@ func (c *Command) With(opts ...Option) *Command {
// Run run command with input and output.
func (c *Command) Run(ctx context.Context, in config.Provider, out output.Output) error {
if c.Execute == nil {
return fmt.Errorf("%w", ErrExecuteNil)
return fmt.Errorf("%w", errors.ErrExecuteNil)
}
if c.Handle != nil {

82
command/chain.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@@ -5,12 +5,10 @@ import (
"errors"
"fmt"
"strings"
"sync/atomic"
"testing"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
@@ -18,133 +16,59 @@ import (
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
cerr "gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/output"
)
//nolint:gochecknoinits
func init() {
console.MustRegister(Command().With(console.WithName("fdevs:console:test")))
console.MustRegister(Command().With(console.WithName("fdevs:console:arg")))
console.MustRegister(command.With(Command(), command.WithName("fdevs:console:test")))
console.MustRegister(command.With(Command(), command.WithName("fdevs:console:arg")))
}
func Command() *console.Command {
return &console.Command{
Name: "test:command",
Description: "test command",
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
var astr []string
if aerr := console.ReadValue(ctx, in, "string").Unmarshal(&astr); aerr != nil && !errors.Is(aerr, config.ErrNotFound) {
return fmt.Errorf("unmarshal string:%w", aerr)
}
out.Print(ctx,
"test argument:", console.ReadValue(ctx, in, "test_argument").String(), "\n",
"bool option:", console.ReadValue(ctx, in, "bool").Bool(), "\n",
"duration option with default:", console.ReadValue(ctx, in, "duration").Duration(), "\n",
"array string:[", strings.Join(astr, ","), "]\n",
"group string:", console.ReadValue(ctx, in, "group", "test", "string").String(), "\n",
"log http service:", console.ReadValue(ctx, in, "log", "http", "level").String(), "\n",
)
return nil
},
Configure: func(_ context.Context, def config.Definition) error {
def.
Add(
group.New("group", "group example",
option.Bool("bool", "bool"),
group.New("test", "test", option.String("string", "test group string", option.Default("group string default value"))),
),
group.New("log", "log",
proto.New("service", "service level",
option.String("level", "service level", option.Default("debug")),
),
),
arg.String("test_argument", "test argument"),
option.String("string", "array string", option.Slice),
option.Bool("bool", "test bool option"),
option.Duration("duration", "test duration with default", option.Default(value.New(time.Second))),
option.Time("hidden", "hidden time", option.Default(value.New(time.Second)), option.Hidden),
)
return nil
},
}
func Command() command.Command {
return command.New("test:command", "test command", Execute, command.Configure(Configure))
}
func TestChainPrepare(t *testing.T) {
t.Parallel()
var cnt int64
ctx := context.Background()
def := definition.New()
prepare := func(ctx context.Context, def config.Definition, n console.Configure) error {
atomic.AddInt64(&cnt, 1)
return n(ctx, def)
}
configure := func(context.Context, config.Definition) error {
return nil
func Execute(ctx context.Context, in config.Provider, out output.Output) error {
var astr []string
if aerr := console.ReadValue(ctx, in, "string").Unmarshal(&astr); aerr != nil && !errors.Is(aerr, config.ErrNotFound) {
return fmt.Errorf("unmarshal string:%w", aerr)
}
for i := range []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} {
prepares := make([]console.Prepare, i)
for p := range i {
prepares[p] = prepare
}
out.Print(ctx,
"test argument:", console.ReadValue(ctx, in, "test_argument").String(), "\n",
"bool option:", console.ReadValue(ctx, in, "bool").Bool(), "\n",
"duration option with default:", console.ReadValue(ctx, in, "duration").Duration(), "\n",
"array string:[", strings.Join(astr, ","), "]\n",
"group string:", console.ReadValue(ctx, in, "group", "test", "string").String(), "\n",
"log http service:", console.ReadValue(ctx, in, "log", "http", "level").String(), "\n",
)
cnt = 0
chain := console.ChainPrepare(prepares...)
err := chain(ctx, def, configure)
if err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int64(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
return nil
}
func TestChainHandle(t *testing.T) {
t.Parallel()
func Configure(_ context.Context, def config.Definition) error {
def.
Add(
group.New("group", "group example",
option.Bool("bool", "bool"),
group.New("test", "test", option.String("string", "test group string", option.Default("group string default value"))),
),
group.New("log", "log",
proto.New("service", "service level",
option.String("level", "service level", option.Default("debug")),
),
),
arg.String("test_argument", "test argument"),
option.String("string", "array string", option.Slice),
option.Bool("bool", "test bool option"),
option.Duration("duration", "test duration with default", option.Default(value.New(time.Second))),
option.Time("hidden", "hidden time", option.Default(value.New(time.Second)), option.Hidden),
)
var cnt int64
ctx := context.Background()
in := &memory.Map{}
out := output.Stdout()
handle := func(ctx context.Context, in config.Provider, out output.Output, next console.Action) error {
atomic.AddInt64(&cnt, 1)
return next(ctx, in, out)
}
action := func(context.Context, config.Provider, output.Output) error {
return nil
}
for i := range []int{0, 1, 2, 30, 40, 50} {
handles := make([]console.Handle, i)
for p := range i {
handles[p] = handle
}
cnt = 0
chain := console.ChainHandle(handles...)
err := chain(ctx, in, out, action)
if err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int64(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
return nil
}
func TestRunEmptyExecute(t *testing.T) {
@@ -158,7 +82,7 @@ func TestRunEmptyExecute(t *testing.T) {
out := output.Stdout()
err := empty.Run(ctx, in, out)
if !errors.Is(err, console.ErrExecuteNil) {
t.Fatalf("expected: %v, got: %v ", console.ErrExecuteNil, err)
if !errors.Is(err, cerr.ErrExecuteNil) {
t.Fatalf("expected: %v, got: %v ", cerr.ErrExecuteNil, err)
}
}

View File

@@ -3,55 +3,38 @@ package console
import (
"context"
"errors"
"fmt"
"log"
"math"
"os"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/command/help"
cerr "gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/verbosity"
)
const (
verboseTrace = 3
verboseDebug = 2
verboseInfo = 1
)
const (
OptionHelp = "help"
OptionVersion = "version"
OptionAnsi = "ansi"
OptionNoAnsi = "no-ansi"
OptionQuiet = "quiet"
OptionVerbose = "verbose"
)
const (
defaultOptionsPosition = math.MaxUint64 / 2
"gitoa.ru/go-4devs/console/param"
)
// Execute the current command with option.
func Execute(ctx context.Context, cmd *Command, opts ...func(*App)) {
func Execute(ctx context.Context, cmd command.Command, opts ...func(*App)) {
opts = append([]func(*App){WithSkipArgs(1)}, opts...)
New(opts...).exec(ctx, cmd)
}
// Run current command by input and output.
func Run(ctx context.Context, cmd *Command, in config.BindProvider, out output.Output) error {
func Run(ctx context.Context, cmd command.Command, in config.BindProvider, out output.Output) error {
def := definition.New()
err := cmd.Init(ctx, def)
err := cmd.Configure(ctx, def)
if err != nil {
return err
return fmt.Errorf("%w", err)
}
def.Add(Default()...)
command.Default(def)
berr := in.Bind(ctx, config.NewVars(def.Options()...))
if berr != nil {
@@ -60,85 +43,37 @@ func Run(ctx context.Context, cmd *Command, in config.BindProvider, out output.O
return showHelp(ctx, cmd, in, output.Ansi(out))
}
out = ansi(ctx, in, out)
out = command.Verbose(ctx, in, out)
out = verbose(ctx, in, out)
if ReadValue(ctx, in, OptionVersion).Bool() {
version := cmd.Version
if version == "" {
version = "unknown"
}
out.Println(ctx, "command <comment>", cmd.Name, "</comment> version: <info>", version, "</info>")
if command.IsShowVersion(ctx, in) {
out.Println(ctx, "command <comment>", cmd.Name(), "</comment> version: <info>", param.Version(cmd), "</info>")
return nil
}
if ReadValue(ctx, in, OptionHelp).Bool() {
if command.IsShowHelp(ctx, in) {
return showHelp(ctx, cmd, in, out)
}
return cmd.Run(ctx, in, out)
}
func ansi(ctx context.Context, in config.Provider, out output.Output) output.Output {
switch {
case ReadValue(ctx, in, OptionAnsi).Bool():
out = output.Ansi(out)
case ReadValue(ctx, in, OptionNoAnsi).Bool():
out = output.None(out)
case lookupEnv("NO_COLOR"):
out = output.None(out)
default:
out = output.Ansi(out)
if err := cmd.Execute(ctx, in, out); err != nil {
return fmt.Errorf("%w", err)
}
return out
return nil
}
func lookupEnv(name string) bool {
v, has := os.LookupEnv(name)
return has && v == "true"
}
func verbose(ctx context.Context, in config.Provider, out output.Output) output.Output {
switch {
case ReadValue(ctx, in, OptionQuiet).Bool():
out = output.Quiet()
default:
var verb []bool
_ = ReadValue(ctx, in, OptionVerbose).Unmarshal(&verb)
switch {
case len(verb) == verboseInfo:
out = output.Verbosity(out, verbosity.Info)
case len(verb) == verboseDebug:
out = output.Verbosity(out, verbosity.Debug)
case len(verb) >= verboseTrace:
out = output.Verbosity(out, verbosity.Trace)
default:
out = output.Verbosity(out, verbosity.Norm)
}
}
return out
}
func showHelp(ctx context.Context, cmd *Command, in config.Provider, out output.Output) error {
func showHelp(ctx context.Context, cmd command.Command, in config.Provider, out output.Output) error {
arr := &memory.Map{}
arr.SetOption(value.New(cmd.Name), ArgumentCommandName)
arr.SetOption(value.New(false), OptionHelp)
arr.SetOption(value.New(cmd.Name()), help.ArgumentCommandName)
arr.SetOption(value.New(false), command.OptionHelp)
if _, err := Find(cmd.Name); errors.Is(err, ErrNotFound) {
register(cmd)
if _, err := registry.Find(cmd.Name()); errors.Is(err, cerr.ErrNotFound) {
_ = registry.Add(cmd)
}
help, err := Find(CommandHelp)
help, err := registry.Find(help.Name)
if err != nil {
return err
return fmt.Errorf("%w", err)
}
w := chain.New(arr, in)
@@ -146,20 +81,6 @@ func showHelp(ctx context.Context, cmd *Command, in config.Provider, out output.
return Run(ctx, help, w, out)
}
// Default options and argument command.
func Default() []config.Option {
return []config.Option{
option.Bool(OptionNoAnsi, "Disable ANSI output", option.Position(defaultOptionsPosition)),
option.Bool(OptionAnsi, "Do not ask any interactive question", option.Position(defaultOptionsPosition)),
option.Bool(OptionVersion, "Display this application version", option.Short('V'), option.Position(defaultOptionsPosition)),
option.Bool(OptionHelp, "Display this help message", option.Short('h'), option.Position(defaultOptionsPosition)),
option.Bool(OptionVerbose,
"Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace",
option.Short('v'), option.Slice, option.Position(defaultOptionsPosition)),
option.Bool(OptionQuiet, "Do not output any message", option.Short('q'), option.Position(defaultOptionsPosition)),
}
}
func ReadValue(ctx context.Context, in config.Provider, path ...string) config.Value {
val, err := in.Value(ctx, path...)
if err != nil {

View File

@@ -1,4 +1,4 @@
package console
package errors //nolint:revive
import (
"errors"
@@ -7,10 +7,11 @@ import (
)
var (
ErrNotFound = errors.New("command not found")
ErrCommandNil = errors.New("console: Register command is nil")
ErrExecuteNil = errors.New("console: execute is nil")
ErrCommandDuplicate = errors.New("console: duplicate command")
ErrWrongType = errors.New("wrong type")
ErrNotFound = errors.New("not found")
ErrCommandNil = errors.New("command is nil")
ErrExecuteNil = errors.New("execute is nil")
ErrCommandDplicate = errors.New("duplicate command")
)
type AlternativesError struct {

100
help.go
View File

@@ -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
},
}
}

View 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
View File

@@ -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
}

View File

@@ -133,12 +133,10 @@ func txtHelp(cmd Command) string {
return ""
}
tpl := template.Must(template.New("help").Parse(cmd.Help))
var buf bytes.Buffer
buf.WriteString("\n<comment>Help:</comment>")
_ = tpl.Execute(&buf, cmd)
buf.WriteString(cmd.Help)
return buf.String()
}

23
param/helper.go Normal file
View 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
View 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
View 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,
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}