15 Commits

Author SHA1 Message Date
b07ffef760 Merge pull request 'update command dump-reference' (#18) from reference into master
All checks were successful
Go Action / goaction (push) Successful in 44s
Reviewed-on: #18
2026-02-17 18:30:03 +03:00
80fa32fa5f update command dump-reference
All checks were successful
Go Action / goaction (pull_request) Successful in 2m44s
2026-02-17 18:26:16 +03:00
6b69bf3361 Merge pull request 'update readme' (#17) from command into master
All checks were successful
Go Action / goaction (push) Successful in 41s
Reviewed-on: #17
2026-01-06 17:31:45 +03:00
a4b92d112c update readme
All checks were successful
Go Action / goaction (pull_request) Successful in 38s
2026-01-06 17:28:59 +03:00
fb778795b0 Merge pull request 'command' (#16) from command into master
All checks were successful
Go Action / goaction (push) Successful in 35s
Reviewed-on: #16
2026-01-06 16:49:22 +03:00
85edd5acec update help for list and help command
All checks were successful
Go Action / goaction (pull_request) Successful in 37s
2026-01-06 16:45:56 +03:00
7b4dc88d8b update example 2026-01-06 16:45:39 +03:00
8f8623ebe7 Merge pull request 'update commands' (#15) from command into master
All checks were successful
Go Action / goaction (push) Successful in 32s
Reviewed-on: #15
2026-01-06 16:25:25 +03:00
1382bea590 update commands
All checks were successful
Go Action / goaction (pull_request) Successful in 2m26s
2026-01-06 16:19:45 +03:00
797c938628 Merge pull request 'update example' (#14) from command into master
All checks were successful
Go Action / goaction (push) Successful in 36s
Reviewed-on: #14
2026-01-05 23:53:31 +03:00
0d661986d1 update example
All checks were successful
Go Action / goaction (pull_request) Successful in 56s
2026-01-05 23:51:55 +03:00
e2c6fc0a35 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
2026-01-05 23:24:44 +03:00
e80e292830 move command to folder
All checks were successful
Go Action / goaction (pull_request) Successful in 45s
2026-01-05 23:23:20 +03:00
696032e1cc Merge pull request 'update dump args' (#12) from arg into master
All checks were successful
Go Action / goaction (push) Successful in 33s
Reviewed-on: #12
2026-01-05 14:23:28 +03:00
60d879caf2 update dump args
All checks were successful
Go Action / goaction (pull_request) Successful in 2m3s
2026-01-05 14:21:00 +03:00
40 changed files with 1754 additions and 821 deletions

116
README.md
View File

@@ -15,15 +15,16 @@ package command
import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/config"
)
func CreateUser() *console.Command {
return &console.Command{
Name: "app:create-user",
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
func CreateUser() command.Command {
return command.New(
"app:create-user",
"create user",
func(ctx context.Context, in config.Provider, out output.Output) error {
return nil
},
}
@@ -32,23 +33,36 @@ func CreateUser() *console.Command {
## Configure command
```go
func CreateUser() *console.Command {
return &console.Command{
//...
Description: "Creates a new user.",
Help: "This command allows you to create a user...",
func CreateUser() command.Command {
return command.New(
"app:create-user",
"Creates a new user.",
Execute,
command.Help( "This command allows you to create a user..."),
}
}
func Execute(ctx context.Context, in config.Provider, out output.Output) error{
return nil
}
```
## Add arguments
```go
func CreateUser(required bool) *console.Command {
return &console.Command{
//....
Configure: func(ctx context.Context, cfg config.Definition) error {
func CreateUser(required bool) command.Command {
return command.New(
"name",
"description",
Execute,
command.Configure(Configure(required)),
)
}
}
func Configure(required bool) func(ctx context.Context, cfg config.Definition) error {
return func (ctx context.Context, cfg config.Definition) error{
var opts []func(*arg.Option)
if required {
opts = append(opts, arg.Required)
@@ -58,7 +72,6 @@ func CreateUser(required bool) *console.Command {
)
return nil
},
}
}
```
@@ -96,20 +109,23 @@ run command `bin/console app:create-user``
The Execute field has access to the output stream to write messages to the console:
```go
func CreateUser(required bool) *console.Command {
return &console.Command{
// ....
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
// outputs a message followed by a "\n"
out.Println(ctx, "User Creator")
out.Println(ctx, "Whoa!")
func CreateUser(required bool) command.Command {
return command.New(
"app:user:create",
"create user",
Execute,
)
}
// outputs a message without adding a "\n" at the end of the line
out.Print(ctx, "You are about to ", "create a user.")
func Execute(ctx context.Context, in config.Provider, out output.Output) error {
// outputs a message followed by a "\n"
out.Println(ctx, "User Creator")
out.Println(ctx, "Whoa!")
return nil
},
}
// outputs a message without adding a "\n" at the end of the line
out.Print(ctx, "You are about to ", "create a user.")
return nil
}
```
@@ -127,29 +143,31 @@ You are about to create a user.
Use input options or arguments to pass information to the command:
```go
func CreateUser(required bool) *console.Command {
return &console.Command{
Configure: func(ctx context.Context, cfg config.Definition) error {
var opts []func(*input.Argument)
if required {
opts = append(opts, argument.Required)
}
cfg.Add(
arg.String("username", "The username of the user.", arg.Required),
arg.String("password", "User password", opts...),
)
func CreateUser() command.Command {
return command.New(
"app:user:create",
"create user",
Execute,
command.Configure(Configure),
)
}
return nil
},
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
// outputs a message followed by a "\n"
username, _ := in.Value(ctx, "username")
out.Println(ctx, "User Creator")
out.Println(ctx, "Username: ", username.String())
func Configure(ctx context.Context, cfg config.Definition) error {
cfg.Add(
arg.String("username", "The username of the user.", arg.Required),
arg.String("password", "User password"),
)
return nil
},
}
return nil
}
func Execute(ctx context.Context, in config.Provider, out output.Output) error {
// outputs a message followed by a "\n"
username, _ := in.Value(ctx, "username")
out.Println(ctx, "User Creator")
out.Println(ctx, "Username: ", username.String())
return nil
}
```
@@ -180,7 +198,7 @@ import (
func TestCreateUser(t *testing.T) {
ctx := context.Background()
in := memory.Map{}
in.Set("andrey","username")
in.SetOption("andrey","username")
buf := bytes.Buffer{}
out := output.Buffer(&buf)

68
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"
)
@@ -28,7 +33,10 @@ func WithInput(in config.BindProvider) func(*App) {
// WithSkipArgs sets how many arguments are passed. For example, you don't need to pass the name of a single command.
func WithSkipArgs(l int) func(*App) {
return WithInput(chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(l):])), &memory.Default{}))
return WithInput(chain.New(
arg.New(arg.WithArgs(os.Args[ResolveSkip(l):])),
&memory.Default{}),
)
}
// WithExit sets exit callback by default os.Exit.
@@ -38,12 +46,20 @@ func WithExit(f func(int)) func(*App) {
}
}
func WithReplaceCommand(a *App) {
a.registry = registry.Set
}
// New creates and configure new console app.
func New(opts ...func(*App)) *App {
app := &App{
out: output.Stdout(),
exit: os.Exit,
in: chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(0):])), &memory.Default{}),
in: chain.New(
arg.New(arg.WithArgs(os.Args[ResolveSkip(0):])),
&memory.Default{},
),
registry: registry.Add,
}
for _, opt := range opts {
@@ -55,26 +71,25 @@ func New(opts ...func(*App)) *App {
// App is collection of command and configure env.
type App struct {
cmds []*Command
out output.Output
in config.BindProvider
exit func(int)
registry func(...command.Command) error
out output.Output
in config.BindProvider
exit func(int)
}
// Add add or replace command.
func (a *App) Add(cmds ...*Command) *App {
a.cmds = append(a.cmds, cmds...)
func (a *App) Add(cmds ...command.Command) *App {
if err := a.registry(cmds...); err != nil {
a.printError(context.Background(), err)
a.exit(1)
}
return a
}
// Execute run the command by name and arguments.
func (a *App) Execute(ctx context.Context) {
for _, cmd := range a.cmds {
register(cmd)
}
cmd, err := a.find(ctx)
cmd, err := registry.Find(a.commandName())
if err != nil {
a.printError(ctx, err)
@@ -89,7 +104,7 @@ func (a *App) Execute(ctx context.Context) {
a.exec(ctx, cmd)
}
func (a *App) exec(ctx context.Context, cmd *Command) {
func (a *App) exec(ctx context.Context, cmd command.Command) {
err := Run(ctx, cmd, a.in, a.out)
if err != nil {
a.printError(ctx, err)
@@ -99,34 +114,37 @@ func (a *App) exec(ctx context.Context, cmd *Command) {
a.exit(0)
}
func (a *App) find(_ context.Context) (*Command, error) {
if len(os.Args) < 2 || os.Args[1][1] == '-' {
return Find(CommandList)
func (a *App) commandName() string {
name := list.Name
if len(os.Args) > 1 && len(os.Args[1]) > 1 && os.Args[1][1] != '-' {
name = os.Args[1]
}
name := os.Args[1]
return Find(name)
return name
}
func (a *App) list(ctx context.Context) error {
cmd, err := Find(CommandHelp)
cmd, err := registry.Find(help.Name)
if err != nil {
return err
return fmt.Errorf("%w", err)
}
arr := &memory.Map{}
arr.SetOption(value.New(CommandList), ArgumentCommandName)
arr.SetOption(value.New(list.Name), help.ArgumentCommandName)
in := chain.New(arr, a.in)
return Run(ctx, cmd, in, a.out)
}
func (a *App) printError(ctx context.Context, err error) {
ansi(ctx, a.in, a.out).Println(ctx, "<error>\n\n ", err, "\n</error>")
printErr(ctx, a.in, a.out, err)
}
func resolveSkip(in int) int {
func printErr(ctx context.Context, in config.Provider, out output.Output, err error) {
command.Ansi(ctx, in, out).Printf(ctx, "<error>\n\n %v\n</error>\n", err)
}
func ResolveSkip(in int) int {
res := 2
switch {

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

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

153
command/command.go Normal file
View File

@@ -0,0 +1,153 @@
package command
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/setting"
)
type (
ExecuteFn func(ctx context.Context, input config.Provider, output output.Output) error
HandleFn func(ctx context.Context, in config.Provider, out output.Output, n ExecuteFn) error
ConfigureFn func(ctx context.Context, cfg config.Definition) error
PrepareFn func(ctx context.Context, cfg config.Definition, n ConfigureFn) error
Option func(*Command)
)
func Configure(fn ConfigureFn) Option {
return func(c *Command) {
c.configure = fn
}
}
func Version(in string) Option {
return func(c *Command) {
c.Setting = setting.WithVersion(in)(c.Setting)
}
}
func Hidden(c *Command) {
c.Setting = setting.Hidden(c.Setting)
}
func Help(fn setting.HelpFn) Option {
return func(c *Command) {
c.Setting = setting.WithHelp(fn)(c.Setting)
}
}
func WithName(name string) Option {
return func(c *Command) {
c.name = name
}
}
func Handle(fn HandleFn) Option {
return func(c *Command) {
handle := c.handle
c.handle = ChainHandle(fn, handle)
}
}
func Prepare(fn PrepareFn) Option {
return func(c *Command) {
prepare := c.prepare
c.prepare = ChainPrepare(fn, prepare)
}
}
func Usage(fn setting.UsageFn) Option {
return func(c *Command) {
c.Setting = setting.WithUsage(fn)(c.Setting)
}
}
func EmptyUsage(cmd *Command) {
cmd.Setting = setting.WithUsage(func(setting.UData) (string, error) {
return "", nil
})(cmd.Setting)
}
func New(name, desc string, execute ExecuteFn, opts ...Option) Command {
cmd := Command{
name: name,
execute: execute,
configure: emptyConfigure,
handle: emptyHandle,
prepare: emptyPrepare,
Setting: setting.New(setting.WithDescription(desc)),
}
for _, opt := range opts {
opt(&cmd)
}
return cmd
}
type Command struct {
setting.Setting
name string
execute ExecuteFn
configure ConfigureFn
prepare PrepareFn
handle HandleFn
}
func (c Command) Name() string {
return c.name
}
func (c Command) Execute(ctx context.Context, input config.Provider, output output.Output) error {
return c.handle(ctx, input, output, c.execute)
}
func (c Command) Configure(ctx context.Context, cfg config.Definition) error {
return c.prepare(ctx, cfg, c.configure)
}
func (c Command) IsZero() bool {
return c.name == "" ||
c.execute == nil ||
c.configure == nil ||
c.handle == nil ||
c.prepare == nil
}
func (c Command) String() string {
return fmt.Sprintf("command:%v, version:%v", c.Name(), setting.Version(c))
}
func With(parent Command, opts ...Option) Command {
cmd := Command{
Setting: parent.Setting,
name: parent.Name(),
execute: parent.Execute,
configure: parent.Configure,
handle: emptyHandle,
prepare: emptyPrepare,
}
for _, opt := range opts {
opt(&cmd)
}
return cmd
}
func emptyPrepare(ctx context.Context, cfg config.Definition, n ConfigureFn) error {
return n(ctx, cfg)
}
func emptyHandle(ctx context.Context, in config.Provider, out output.Output, n ExecuteFn) error {
return n(ctx, in, out)
}
func emptyConfigure(context.Context, config.Definition) error {
return nil
}

137
command/commands.go Normal file
View File

@@ -0,0 +1,137 @@
package command
import (
"fmt"
"regexp"
"sort"
"sync"
"gitoa.ru/go-4devs/console/errs"
"gitoa.ru/go-4devs/console/setting"
)
var findCommand = regexp.MustCompile("([^:]+|)")
type Commands struct {
sync.RWMutex
cmds []Command
names map[string]int
}
func (c *Commands) Set(cmds ...Command) error {
c.Lock()
defer c.Unlock()
return c.set(cmds...)
}
func (c *Commands) Add(cmds ...Command) error {
c.Lock()
defer c.Unlock()
return c.add(cmds...)
}
func (c *Commands) Find(name string) (Command, error) {
c.Lock()
defer c.Unlock()
return c.find(name)
}
func (c *Commands) Names() []string {
c.Lock()
defer c.Unlock()
names := make([]string, 0, len(c.names))
for name := range c.names {
names = append(names, name)
}
sort.Strings(names)
return names
}
func (c *Commands) find(name string) (Command, error) {
if idx, ok := c.names[name]; ok {
return c.cmds[idx], nil
}
nameRegexp := findCommand.ReplaceAllStringFunc(name, func(in string) string {
return in + "[^:]*"
})
findCommands := make([]Command, 0, len(c.cmds))
cmdRegexp, err := regexp.Compile("^" + nameRegexp + "$")
if err != nil {
return Command{}, fmt.Errorf("find by regexp:%w", err)
}
for name, idx := range c.names {
if cmdRegexp.MatchString(name) && !setting.IsHidden(c.cmds[idx]) {
findCommands = append(findCommands, c.cmds[idx])
}
}
if len(findCommands) == 1 {
return findCommands[0], nil
}
if len(findCommands) > 1 {
names := make([]string, len(findCommands))
for i := range findCommands {
names[i] = findCommands[i].Name()
}
return Command{}, errs.AlternativesError{Alt: names, Err: errs.ErrCommandDplicate}
}
return Command{}, fmt.Errorf("%w", errs.ErrNotFound)
}
func (c *Commands) set(cmds ...Command) error {
if c.names == nil {
c.names = make(map[string]int, len(cmds))
}
for _, cmd := range cmds {
if cmd.IsZero() {
return fmt.Errorf("command:%w", errs.ErrCommandNil)
}
if idx, ok := c.names[cmd.Name()]; ok {
c.cmds[idx] = cmd
continue
}
c.names[cmd.Name()] = len(c.cmds)
c.cmds = append(c.cmds, cmd)
}
return nil
}
func (c *Commands) add(cmds ...Command) error {
if c.names == nil {
c.names = make(map[string]int, len(cmds))
}
for _, cmd := range cmds {
if cmd.IsZero() {
return fmt.Errorf("command:%w", errs.ErrCommandNil)
}
if _, ok := c.names[cmd.Name()]; ok {
return fmt.Errorf("command %s:%w", cmd.Name(), errs.ErrCommandDplicate)
}
c.names[cmd.Name()] = len(c.cmds)
c.cmds = append(c.cmds, cmd)
}
return nil
}

46
command/commands_test.go Normal file
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
}

67
command/dump/reference.go Normal file
View File

@@ -0,0 +1,67 @@
package dump
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/errs"
"gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
)
//go:generate go tool config config:generate
const NameRefernce = "config:dump-reference"
func Command() command.Command {
return command.New(NameRefernce, "dump reference by command", RExecute, command.Configure(RConfigure))
}
func RExecute(ctx context.Context, in config.Provider, out output.Output) error {
provs, ok := in.(config.Providers)
if !ok {
return fmt.Errorf("%w: expect %T got %T", errs.ErrWrongType, (config.Providers)(nil), in)
}
cfg := NewRConfigureConfig(in)
cmd, err := registry.Find(cfg.CommandName(ctx))
if err != nil {
return fmt.Errorf("cmd:%w", err)
}
def := definition.New()
if err := cmd.Configure(ctx, def); err != nil {
return fmt.Errorf("configure:%w", err)
}
prov, err := provs.ByName(cfg.Format(ctx))
if err != nil {
return fmt.Errorf("prov:%w", errs.AlternativesError{Alt: provs.Names(), Err: err})
}
bind, ok := prov.(config.DumpProvider)
if !ok {
return fmt.Errorf("%w: expect config.DunpProvider got %T", errs.ErrWrongType, prov)
}
if err := bind.DumpReference(ctx, out, def); err != nil {
return fmt.Errorf("dump:%w", err)
}
return nil
}
func RConfigure(_ context.Context, def config.Definition) error {
def.Add(
arg.String("command-name", "command name", option.Required),
option.String("format", "format", option.Default(arg.Name)),
)
return nil
}

View File

@@ -0,0 +1,89 @@
// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
package dump
import (
"context"
"fmt"
"gitoa.ru/go-4devs/config"
)
func WithRConfigureConfigHandle(fn func(context.Context, error)) func(*RConfigureConfig) {
return func(ci *RConfigureConfig) {
ci.handle = fn
}
}
func NewRConfigureConfig(prov config.Provider, opts ...func(*RConfigureConfig)) RConfigureConfig {
i := RConfigureConfig{
Provider: prov,
handle: func(_ context.Context, err error) {
fmt.Printf("RConfigureConfig:%v", err)
},
}
for _, opt := range opts {
opt(&i)
}
return i
}
type RConfigureConfig struct {
config.Provider
handle func(context.Context, error)
}
// readCommandName command name.
func (i RConfigureConfig) readCommandName(ctx context.Context) (v string, e error) {
val, err := i.Value(ctx, "command-name")
if err != nil {
return v, fmt.Errorf("read [%v]:%w", []string{"command-name"}, err)
}
return val.ParseString()
}
// ReadCommandName command name.
func (i RConfigureConfig) ReadCommandName(ctx context.Context) (string, error) {
return i.readCommandName(ctx)
}
// CommandName command name.
func (i RConfigureConfig) CommandName(ctx context.Context) string {
val, err := i.readCommandName(ctx)
if err != nil {
i.handle(ctx, err)
}
return val
}
// readFormat format.
func (i RConfigureConfig) readFormat(ctx context.Context) (v string, e error) {
val, err := i.Value(ctx, "format")
if err != nil {
i.handle(ctx, err)
return "arg", nil
}
return val.ParseString()
}
// ReadFormat format.
func (i RConfigureConfig) ReadFormat(ctx context.Context) (string, error) {
return i.readFormat(ctx)
}
// Format format.
func (i RConfigureConfig) Format(ctx context.Context) string {
val, err := i.readFormat(ctx)
if err != nil {
i.handle(ctx, err)
}
return val
}

151
command/help/command.go Normal file
View File

@@ -0,0 +1,151 @@
package help
import (
"context"
"errors"
"fmt"
"os"
"strings"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/errs"
"gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/descriptor"
"gitoa.ru/go-4devs/console/setting"
)
//nolint:gochecknoinits
func init() {
err := registry.Add(Command())
if err != nil {
panic(err)
}
}
const (
ArgumentCommandName = "command_name"
OptionFormat = "format"
Name = "help"
)
func Command() command.Command {
return command.New(
Name,
"Displays help for a command",
Execute,
command.Configure(Configure),
command.Help(Help),
)
}
func Configure(_ context.Context, config config.Definition) error {
formats := descriptor.Descriptors()
config.
Add(
arg.String(ArgumentCommandName, "The command name", arg.Default(value.New("help"))),
option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
option.Required,
option.Default(value.New(formats[0])),
validator.Valid(
validator.NotBlank,
validator.Enum(formats...),
),
),
)
return nil
}
func Execute(ctx context.Context, in config.Provider, out output.Output) error {
var err error
cfg := read{Provider: in}
name := cfg.Value(ctx, ArgumentCommandName).String()
format := cfg.Value(ctx, OptionFormat).String()
des, err := descriptor.Find(format)
if err != nil {
return fmt.Errorf("find descriptor[%v]: %w", format, err)
}
cmd, err := registry.Find(name)
if err != nil {
return fmt.Errorf("find cmd: %w", err)
}
def := definition.New()
command.Default(def)
if err := cmd.Configure(ctx, def); err != nil {
return fmt.Errorf("init cmd: %w", err)
}
var bin string
if len(os.Args) > 0 {
bin = os.Args[0]
}
help, err := setting.Help(cmd, setting.HelpData(bin, cmd.Name()))
if err != nil {
return fmt.Errorf("create help:%w", err)
}
hasUsage := true
usage, err := setting.Usage(cmd, setting.UsageData(cmd.Name(), def))
if err != nil {
if !errors.Is(err, errs.ErrNotFound) {
return fmt.Errorf("create usage:%w", err)
}
hasUsage = false
}
derr := des.Command(ctx, out, descriptor.Command{
Bin: bin,
Name: cmd.Name(),
Description: setting.Description(cmd),
Help: help,
Usage: func() (string, bool) {
return usage, hasUsage
},
Options: def.With(param.New(descriptor.TxtStyle())),
})
if derr != nil {
return fmt.Errorf("descriptor help:%w", derr)
}
return nil
}
const tpl = `The <info>%[2]s</info> command displays help for a given command:
<info>%[1]s %[2]s list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>%[1]s %[2]s --format=xml list</info>
To display the list of available commands, please use the <info>list</info> command.
`
func Help(data setting.HData) (string, error) {
return fmt.Sprintf(tpl, data.Bin, data.Name), nil
}
type read struct {
config.Provider
}
func (r read) Value(ctx context.Context, key ...string) config.Value {
val, err := r.Provider.Value(ctx, key...)
if err != nil {
return value.Empty{Err: err}
}
return val
}

147
command/list/command.go Normal file
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"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/errs"
"gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/descriptor"
"gitoa.ru/go-4devs/console/setting"
)
//nolint:gochecknoinits
func init() {
err := registry.Add(Command())
if err != nil {
panic(err)
}
}
const (
Name = "list"
ArgumentNamespace = "namespace"
OptionFormat = "format"
defaultLenNamespace = 2
)
func Command() command.Command {
return command.New(
Name,
"Lists commands",
Execite,
command.Configure(Configure),
command.Help(Help),
)
}
func Configure(_ context.Context, cfg config.Definition) error {
formats := descriptor.Descriptors()
cfg.
Add(
arg.String(ArgumentNamespace, "The namespace name"),
option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
option.Required,
option.Default(value.New(formats[0])),
validator.Valid(
validator.NotBlank,
validator.Enum(formats...),
),
),
)
return nil
}
//nolint:cyclop
func Execite(ctx context.Context, in config.Provider, out output.Output) error {
opt := read{Provider: in}
ns := opt.Value(ctx, ArgumentNamespace).String()
format := opt.Value(ctx, OptionFormat).String()
des, err := descriptor.Find(format)
if err != nil {
return fmt.Errorf("find descriptor[%v]: %w", format, err)
}
def := definition.New()
command.Default(def)
cmds := registry.Commands()
commands := descriptor.Commands{
Namespace: ns,
Options: def.With(param.New(descriptor.TxtStyle())),
}
groups := make(map[string]*descriptor.NSCommand)
namespaces := make([]string, 0, len(cmds))
empty := descriptor.NSCommand{}
for _, name := range cmds {
if ns != "" && !strings.HasPrefix(name, ns+":") {
continue
}
cmd, _ := registry.Find(name)
if setting.IsHidden(cmd) {
continue
}
gn := strings.SplitN(name, ":", defaultLenNamespace)
if len(gn) != defaultLenNamespace {
empty.Append(cmd.Name(), setting.Description(cmd))
continue
}
if _, ok := groups[gn[0]]; !ok {
groups[gn[0]] = &descriptor.NSCommand{
Name: gn[0],
}
namespaces = append(namespaces, gn[0])
}
groups[gn[0]].Append(name, setting.Description(cmd))
}
if len(empty.Commands) > 0 {
commands.Commands = append(commands.Commands, empty)
}
for _, name := range namespaces {
commands.Commands = append(commands.Commands, *groups[name])
}
if ns != "" && len(commands.Commands) == 0 {
return fmt.Errorf("%w: namespace %s", errs.ErrNotFound, ns)
}
if err := des.Commands(ctx, out, commands); err != nil {
return fmt.Errorf("descriptor:%w", err)
}
return nil
}
type read struct {
config.Provider
}
func (r read) Value(ctx context.Context, key ...string) config.Value {
val, err := r.Provider.Value(ctx, key...)
if err != nil {
return value.Empty{Err: err}
}
return val
}

18
command/list/help.go Normal file
View File

@@ -0,0 +1,18 @@
package list
import (
"fmt"
"gitoa.ru/go-4devs/console/setting"
)
const tpl = `The <info>%[2]s</info> command lists all commands:
<info>%[1]s %[2]s</info>
You can also display the commands for a specific namespace:
<info>%[1]s %[2]s test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>%[1]s %[2]s --format=xml</info>`
func Help(data setting.HData) (string, error) {
return fmt.Sprintf(tpl, data.Bin, data.Name), nil
}

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

View File

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

View File

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

View File

@@ -2,12 +2,14 @@ package main
import (
"context"
"os"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/config/provider/env"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command/dump"
"gitoa.ru/go-4devs/console/example/pkg/command"
)
@@ -22,12 +24,13 @@ func main() {
console.
New(console.WithInput(
chain.New(
arg.New(arg.WithSkip(0)),
arg.New(arg.WithArgs(os.Args[console.ResolveSkip(0):])),
env.New(Namespace, AppName),
&memory.Default{},
),
)).
Add(
dump.Command(),
command.Long(),
command.Args(),
).

View File

@@ -4,6 +4,7 @@ import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command/dump"
"gitoa.ru/go-4devs/console/example/pkg/command"
)
@@ -11,6 +12,7 @@ func main() {
console.
New().
Add(
dump.Command(),
command.Hello(),
command.Args(),
command.Hidden(),

View File

@@ -1,10 +1,10 @@
module gitoa.ru/go-4devs/console/example
go 1.23
go 1.24.0
toolchain go1.24.1
require (
gitoa.ru/go-4devs/config v0.0.7
gitoa.ru/go-4devs/console v0.2.0
gitoa.ru/go-4devs/config v0.0.10
gitoa.ru/go-4devs/console v0.4.0
)

View File

@@ -1,4 +1,4 @@
gitoa.ru/go-4devs/config v0.0.7 h1:8q6axRNLgXE5dYQd8Jbh9j+STqevbibVyvwrtsuHpZk=
gitoa.ru/go-4devs/config v0.0.7/go.mod h1:UINWnObZA0nLiJro+TtavUBBvN0cSt17aRHOk20pP74=
gitoa.ru/go-4devs/console v0.2.0 h1:6lsbArs99GA8vGdnwNDThZNKjFNctNtTlSCUjhgwIpU=
gitoa.ru/go-4devs/console v0.2.0/go.mod h1:xi4Svw7T+lylckAQiJQS/2qwDwF4YbIanlhcbQrBAiI=
gitoa.ru/go-4devs/config v0.0.10 h1:NSagD0voj77/IGqRGsbR0DZmDvFcxbx+oRoWQnLnSy4=
gitoa.ru/go-4devs/config v0.0.10/go.mod h1:cLW1+4E4uM4Pw+z4RuKEKbO1Lz6UTs2b2fTPyeEgTx8=
gitoa.ru/go-4devs/console v0.4.0 h1:3N4VMWsHsaP32nWHxALxjPhp/MhYx1hXZdDl3xGJQ3k=
gitoa.ru/go-4devs/console v0.4.0/go.mod h1:+J69iA4KQfC2H1FxItwL4pLkb3oW8g466qUSo0jWYjo=

View File

@@ -7,32 +7,37 @@ import (
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
)
func Args() *console.Command {
return &console.Command{
Name: "fdevs:console:arg",
Description: "Understanding how Console Arguments and Options Are Handled",
Configure: func(_ context.Context, def config.Definition) error {
def.Add(
option.Bool("foo", "foo option", option.Short('f')),
option.String("bar", "required bar option", option.Required, option.Short('b')),
option.String("cat", "cat option", option.Short('c')),
option.Time("time", "time example"),
option.Time("hidden", "hidden time example", option.Hidden),
)
return nil
},
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
out.Println(ctx, "foo: <info>", console.ReadValue(ctx, in, "foo").Bool(), "</info>")
out.Println(ctx, "bar: <info>", console.ReadValue(ctx, in, "bar").String(), "</info>")
out.Println(ctx, "cat: <info>", console.ReadValue(ctx, in, "cat").String(), "</info>")
out.Println(ctx, "time: <info>", console.ReadValue(ctx, in, "time").Time().Format(time.RFC3339), "</info>")
out.Println(ctx, "hidden: <info>", console.ReadValue(ctx, in, "hidden").Time().Format(time.RFC3339), "</info>")
return nil
},
}
func Args() command.Command {
return command.New(
"fdevs:console:arg",
"Understanding how Console Arguments and Options Are Handled",
ArgExecute,
command.Configure(ArgConfigure),
)
}
func ArgConfigure(_ context.Context, def config.Definition) error {
def.Add(
option.Bool("foo", "foo option", option.Short('f')),
option.String("bar", "required bar option", option.Required, option.Short('b')),
option.String("cat", "cat option", option.Short('c')),
option.Time("time", "time example"),
option.Time("hidden", "hidden time example", option.Hidden),
)
return nil
}
func ArgExecute(ctx context.Context, in config.Provider, out output.Output) error {
out.Println(ctx, "foo: <info>", console.ReadValue(ctx, in, "foo").Bool(), "</info>")
out.Println(ctx, "bar: <info>", console.ReadValue(ctx, in, "bar").String(), "</info>")
out.Println(ctx, "cat: <info>", console.ReadValue(ctx, in, "cat").String(), "</info>")
out.Println(ctx, "time: <info>", console.ReadValue(ctx, in, "time").Time().Format(time.RFC3339), "</info>")
out.Println(ctx, "hidden: <info>", console.ReadValue(ctx, in, "hidden").Time().Format(time.RFC3339), "</info>")
return nil
}

View File

@@ -8,34 +8,44 @@ import (
"gitoa.ru/go-4devs/config/param"
argument "gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/setting"
)
func CreateUser(required bool) *console.Command {
return &console.Command{
Name: "app:create-user",
Description: "Creates a new user.",
Help: "This command allows you to create a user...",
Configure: func(_ context.Context, cfg config.Definition) error {
var opts []param.Option
if required {
opts = append(opts, option.Required)
}
func CreateUser(required bool) command.Command {
return command.New(
"app:create-user",
"Creates a new user.",
UserExecute,
command.Configure(UserConfigure(required)),
command.Help(func(setting.HData) (string, error) {
return "This command allows you to create a user...", nil
}),
)
}
cfg.
Add(
argument.String("username", "The username of the user.", option.Required),
argument.String("password", "User password", opts...),
)
func UserConfigure(required bool) func(_ context.Context, cfg config.Definition) error {
return func(_ context.Context, cfg config.Definition) error {
var opts []param.Option
if required {
opts = append(opts, option.Required)
}
return nil
},
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
// outputs a message followed by a "\n"
out.Println(ctx, "User Creator")
out.Println(ctx, "Username: ", console.ReadValue(ctx, in, "username").String())
cfg.
Add(
argument.String("username", "The username of the user.", option.Required),
argument.String("password", "User password", opts...),
)
return nil
},
return nil
}
}
func UserExecute(ctx context.Context, in config.Provider, out output.Output) error {
// outputs a message followed by a "\n"
out.Println(ctx, "User Creator")
out.Println(ctx, "Username: ", console.ReadValue(ctx, in, "username").String())
return nil
}

View File

@@ -7,33 +7,38 @@ import (
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
)
func Hello() *console.Command {
return &console.Command{
Name: "fdevs:console:hello",
Description: "example hello command",
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
name := console.ReadValue(ctx, in, "name").String()
out.Println(ctx, "<error>Hello</error> <info>", name, "</info>")
out.Info(ctx, "same trace info\n")
out.Debug(ctx, "have some question?\n")
out.Trace(ctx, "this message shows with -vvv\n")
pass := console.ReadValue(ctx, in, "pass").String()
out.Println(ctx, "hidden option pass <info>", pass, "</info>")
return nil
},
Configure: func(_ context.Context, def config.Definition) error {
def.Add(
arg.String("name", "Same name", arg.Default("World")),
option.String("pass", "password", option.Hidden),
)
return nil
},
}
func Hello() command.Command {
return command.New(
"fdevs:console:hello",
"example hello command",
HelloExecute,
command.Configure(HelloConfigure),
)
}
func HelloExecute(ctx context.Context, in config.Provider, out output.Output) error {
name := console.ReadValue(ctx, in, "name").String()
out.Println(ctx, "<error>Hello</error> <info>", name, "</info>")
out.Info(ctx, "same trace info\n")
out.Debug(ctx, "have some question?\n")
out.Trace(ctx, "this message shows with -vvv\n")
pass := console.ReadValue(ctx, in, "pass").String()
out.Println(ctx, "hidden option pass <info>", pass, "</info>")
return nil
}
func HelloConfigure(_ context.Context, def config.Definition) error {
def.Add(
arg.String("name", "Same name", arg.Default("World")),
option.String("pass", "password", option.Hidden),
)
return nil
}

View File

@@ -4,19 +4,16 @@ import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
)
func Hidden() *console.Command {
return &console.Command{
Name: "fdevs:console:hidden",
Description: "hidden command exmale",
Hidden: true,
Execute: func(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "<info> call hidden command</info>")
return nil
},
}
func Hidden() command.Command {
return command.New("fdevs:console:hidden", "hidden command exmale", HiddenExecute, command.Hidden)
}
func HiddenExecute(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "<info> call hidden command</info>")
return nil
}

View File

@@ -9,45 +9,46 @@ import (
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
)
const defaultTimeout = time.Second * 30
// Long example of a command that takes a long time to run.
func Long() *console.Command {
return &console.Command{
Name: "fdevs:command:long",
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
timeout := console.ReadValue(ctx, in, "timeout").Duration()
timer := time.NewTimer(timeout)
func Long() command.Command {
return command.New("fdevs:command:long", "long command description", LongExecute, command.Configure(LongConfigure))
}
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
func LongExecute(ctx context.Context, in config.Provider, out output.Output) error {
timeout := console.ReadValue(ctx, in, "timeout").Duration()
timer := time.NewTimer(timeout)
for {
select {
case t := <-ticker.C:
out.Println(ctx, "ticker: <info>", t, "</info>")
case <-timer.C:
out.Println(ctx, "<error>stop timer</error>")
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
return nil
case <-ctx.Done():
out.Println(ctx, "<info>cancel context</info>")
return nil
}
}
},
Configure: func(_ context.Context, def config.Definition) error {
def.Add(option.Duration("timeout", "set duration run command",
option.Default(value.New(defaultTimeout)),
option.Short('t'),
validator.Valid(validator.NotBlank),
))
for {
select {
case t := <-ticker.C:
out.Println(ctx, "ticker: <info>", t, "</info>")
case <-timer.C:
out.Println(ctx, "<error>stop timer</error>")
return nil
},
case <-ctx.Done():
out.Println(ctx, "<info>cancel context</info>")
return nil
}
}
}
func LongConfigure(_ context.Context, def config.Definition) error {
def.Add(option.Duration("timeout", "set duration run command",
option.Default(value.New(defaultTimeout)),
option.Short('t'),
validator.Valid(validator.NotBlank),
))
return nil
}

View File

@@ -4,18 +4,16 @@ import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
)
func Namespace() *console.Command {
return &console.Command{
Name: "app:start",
Description: "example command in other namespace",
Execute: func(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "example command in other namespace")
return nil
},
}
func Namespace() command.Command {
return command.New("app:start", "example command in other namespace", NSExecute)
}
func NSExecute(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "example command in other namespace")
return nil
}

9
go.mod
View File

@@ -2,9 +2,7 @@ module gitoa.ru/go-4devs/console
go 1.24.0
require gitoa.ru/go-4devs/config v0.0.8
replace gitoa.ru/go-4devs/config => ../config
require gitoa.ru/go-4devs/config v0.0.11
require (
golang.org/x/mod v0.31.0 // indirect
@@ -12,4 +10,7 @@ require (
golang.org/x/tools v0.40.0 // indirect
)
tool golang.org/x/tools/cmd/stringer
tool (
gitoa.ru/go-4devs/config/cmd/config
golang.org/x/tools/cmd/stringer
)

2
go.sum
View File

@@ -1,5 +1,7 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
gitoa.ru/go-4devs/config v0.0.11 h1:lPqWT2ppRybIUqqUDEUFfTZd3vl4+E5QkDFhhDyH2OE=
gitoa.ru/go-4devs/config v0.0.11/go.mod h1:cLW1+4E4uM4Pw+z4RuKEKbO1Lz6UTs2b2fTPyeEgTx8=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=

100
help.go
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

@@ -25,6 +25,7 @@ type Command struct {
Bin string
Name string
Description string
Usage func() (string, bool)
Help string
}

View File

@@ -12,6 +12,7 @@ import (
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/setting"
)
const (
@@ -22,25 +23,24 @@ const (
//nolint:gochecknoglobals
var (
txtFunc = template.FuncMap{
"synopsis": txtSynopsis,
"definition": txtDefinition,
"help": txtHelp,
"usage": txtUsage,
"commands": txtCommands,
}
txtHelpTemplate = template.Must(template.New("txt_template").
txtHelpTemplate = template.Must(
template.New("txt_template").
Funcs(txtFunc).
Parse(`
{{- if .Description -}}
<comment>Description:</comment>
{{ .Description }}
{{ end -}}
<comment>Usage:</comment>
{{ .Name }} {{ synopsis .Options }}
{{ definition .Options }}
{{- help . }}
`))
{{- usage . }}
{{- definition .Options }}
{{- help . }}`),
)
txtListTemplate = template.Must(template.New("txt_list").
Funcs(txtFunc).
@@ -128,19 +128,29 @@ func txtCommands(cmds []NSCommand) string {
return buf.String()
}
func txtUsage(cmd Command) string {
if cmd.Usage == nil {
return ""
}
data, has := cmd.Usage()
if has && data == "" {
return ""
}
if data == "" {
data = defaultUsage(setting.UsageData(cmd.Name, cmd.Options))
}
return "\n<comment>Usage:</comment>\n " + data + "\n"
}
func txtHelp(cmd Command) string {
if cmd.Help == "" {
return ""
}
tpl := template.Must(template.New("help").Parse(cmd.Help))
var buf bytes.Buffer
buf.WriteString("\n<comment>Help:</comment>")
_ = tpl.Execute(&buf, cmd)
return buf.String()
return "\n<comment>Help:</comment>\n" + cmd.Help + "\n"
}
func txtDefinition(options config.Options) string {
@@ -154,10 +164,12 @@ func txtDefinition(options config.Options) string {
return buf.String()
}
func txtSynopsis(options config.Options) string {
def := arg.NewViews(options, nil)
func defaultUsage(data setting.UData) string {
def := arg.NewViews(data.Options, nil)
var buf bytes.Buffer
buf.WriteString(data.Name)
buf.WriteString(" ")
if len(def.Options()) > 0 {
buf.WriteString("[options] ")

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

23
setting/helper.go Normal file
View File

@@ -0,0 +1,23 @@
package setting
func Bool(in Setting, key any) (bool, bool) {
data, ok := in.Param(key)
if !ok {
return false, false
}
res, ok := data.(bool)
return res, ok
}
func String(in Setting, key any) (string, bool) {
data, ok := in.Param(key)
if !ok {
return "", false
}
res, ok := data.(string)
return res, ok
}

127
setting/keys.go Normal file
View File

@@ -0,0 +1,127 @@
package setting
import (
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/errs"
)
type key uint8
const (
paramHidden key = iota + 1
paramDescription
paramVerssion
paramHelp
paramUsage
)
const (
defaultVersion = "undefined"
)
func IsHidden(in Setting) bool {
data, ok := Bool(in, paramHidden)
return ok && data
}
func Hidden(in Setting) Setting {
return in.With(paramHidden, true)
}
func Description(in Setting) string {
data, _ := String(in, paramDescription)
return data
}
func WithDescription(desc string) Option {
return func(p Setting) Setting {
return p.With(paramDescription, desc)
}
}
func Version(in Setting) string {
if data, ok := String(in, paramVerssion); ok {
return data
}
return defaultVersion
}
func WithVersion(in string) Option {
return func(p Setting) Setting {
return p.With(paramVerssion, in)
}
}
func HelpData(bin, name string) HData {
return HData{
Bin: bin,
Name: name,
}
}
type HData struct {
Bin string
Name string
}
type HelpFn func(data HData) (string, error)
func WithHelp(fn HelpFn) Option {
return func(p Setting) Setting {
return p.With(paramHelp, fn)
}
}
func Help(in Setting, data HData) (string, error) {
fn, ok := in.Param(paramHelp)
if !ok {
return "", nil
}
hfn, fok := fn.(HelpFn)
if !fok {
return "", fmt.Errorf("%w: expect:func(data HData) (string, error), got:%T", errs.ErrWrongType, fn)
}
return hfn(data)
}
func UsageData(name string, opts config.Options) UData {
return UData{
Options: opts,
Name: name,
}
}
type UData struct {
config.Options
Name string
}
type UsageFn func(data UData) (string, error)
func WithUsage(fn UsageFn) Option {
return func(p Setting) Setting {
return p.With(paramUsage, fn)
}
}
func Usage(in Setting, data UData) (string, error) {
fn, ok := in.Param(paramUsage)
if !ok {
return "", fmt.Errorf("%w", errs.ErrNotFound)
}
ufn, ok := fn.(UsageFn)
if !ok {
return "", fmt.Errorf("%w: expect: func(data Udata) (string, error), got:%T", errs.ErrWrongType, fn)
}
return ufn(data)
}

57
setting/setting.go Normal file
View File

@@ -0,0 +1,57 @@
package setting
//nolint:gochecknoglobals
var eparam = empty{}
func New(opts ...Option) Setting {
var param Setting
param = eparam
for _, opt := range opts {
param = opt(param)
}
return param
}
type Setting interface {
Param(key any) (any, bool)
With(key, val any) Setting
}
type Option func(Setting) Setting
type empty struct{}
func (e empty) Param(any) (any, bool) {
return nil, false
}
func (e empty) With(key, val any) Setting {
return data{
parent: e,
key: key,
val: val,
}
}
type data struct {
parent Setting
key, val any
}
func (d data) Param(key any) (any, bool) {
if d.key == key {
return d.val, true
}
return d.parent.Param(key)
}
func (d data) With(key, val any) Setting {
return data{
parent: d,
key: key,
val: val,
}
}