Merge pull request 'move command to folder' (#13) from command into master
All checks were successful
Go Action / goaction (push) Successful in 36s

Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
2026-01-05 23:24:44 +03:00
24 changed files with 1268 additions and 614 deletions

57
app.go
View File

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

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"gitoa.ru/go-4devs/console" "gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
) )
//nolint:lll //nolint:lll
@@ -55,21 +56,15 @@ func ExampleNew_list() {
"--no-ansi", "--no-ansi",
} }
console.New(console.WithExit(func(int) {})). console.New(
console.WithExit(func(int) {}),
console.WithReplaceCommand,
).
Add( Add(
Command(), Command(),
&console.Command{ command.New("fdevs:console:arg", "Understanding how Console Arguments and Options Are Handled", Execute),
Name: "fdevs:console:arg", command.New("fdevs:console:hello", "example hello command", Execute),
Description: "Understanding how Console Arguments and Options Are Handled", command.New("app:start", "example command in other namespace", Execute),
},
&console.Command{
Name: "fdevs:console:hello",
Description: "example hello command",
},
&console.Command{
Name: "app:start",
Description: "example command in other namespace",
},
). ).
Execute(ctx) Execute(ctx)
// Output: // Output:

View File

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

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

View File

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

View File

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

100
help.go
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 "" return ""
} }
tpl := template.Must(template.New("help").Parse(cmd.Help))
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString("\n<comment>Help:</comment>") buf.WriteString("\n<comment>Help:</comment>")
_ = tpl.Execute(&buf, cmd) buf.WriteString(cmd.Help)
return buf.String() return buf.String()
} }

23
param/helper.go Normal file
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 package console
import ( import (
"errors"
"fmt" "fmt"
"regexp"
"sort"
"sync"
)
const ( "gitoa.ru/go-4devs/console/command"
CommandHelp = "help" "gitoa.ru/go-4devs/console/internal/registry"
CommandList = "list"
)
//nolint:gochecknoglobals
var (
commandsMu sync.RWMutex
commands = make(map[string]*Command)
findCommand = regexp.MustCompile("([^:]+|)")
) )
// MustRegister register command or panic if err. // MustRegister register command or panic if err.
func MustRegister(cmd *Command) { func MustRegister(cmd ...command.Command) {
err := Register(cmd) err := registry.Add(cmd...)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
// Register makes a command available execute in app. If Register is called twice with the same name or if driver is nil, return error. // Register makes a command available execute in app. If Register is called twice with the same name or if driver is nil, return error.
func Register(cmd *Command) error { func Register(cmd ...command.Command) error {
if cmd == nil { if err := registry.Add(cmd...); err != nil {
return ErrCommandNil return fmt.Errorf("%w", err)
} }
if _, err := Find(cmd.Name); !errors.Is(err, ErrNotFound) {
return fmt.Errorf("%w: command %s", ErrCommandDuplicate, cmd.Name)
}
register(cmd)
return nil return nil
} }
func register(cmd *Command) {
commandsMu.Lock()
defer commandsMu.Unlock()
if cmd != nil && cmd.Name != "" {
commands[cmd.Name] = cmd
}
}
// Commands returns a sorted list of the names of the registered commands.
func Commands() []string {
commandsMu.RLock()
defer commandsMu.RUnlock()
return commandNames()
}
func commandNames() []string {
names := make([]string, 0, len(commands))
for name := range commands {
names = append(names, name)
}
sort.Strings(names)
return names
}
// Find command by name, tries to find the best match if you give it an abbreviation of a name. // Find command by name, tries to find the best match if you give it an abbreviation of a name.
func Find(name string) (*Command, error) { func Find(name string) (command.Command, error) {
commandsMu.RLock() cmd, err := registry.Find(name)
defer commandsMu.RUnlock()
if cmd, ok := commands[name]; ok {
return cmd, nil
}
nameRegexp := findCommand.ReplaceAllStringFunc(name, func(in string) string {
return in + "[^:]*"
})
findCommands := make([]*Command, 0)
cmdRegexp, err := regexp.Compile("^" + nameRegexp + "$")
if err != nil { if err != nil {
return nil, fmt.Errorf("find by regexp:%w", err) return cmd, fmt.Errorf("%w", err)
} }
for name := range commands { return cmd, nil
if !commands[name].Hidden && cmdRegexp.MatchString(name) {
findCommands = append(findCommands, commands[name])
}
}
if len(findCommands) == 1 {
return findCommands[0], nil
}
if len(findCommands) > 1 {
names := make([]string, len(findCommands))
for i := range findCommands {
names[i] = findCommands[i].Name
}
return nil, AlternativesError{Alt: names, Err: ErrNotFound}
}
return nil, ErrNotFound
} }

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