1 Commits

Author SHA1 Message Date
517895e319 update dump args
Some checks failed
Go Action / goaction (pull_request) Failing after 1m56s
2026-01-05 14:18:10 +03:00
34 changed files with 751 additions and 1426 deletions

57
app.go
View File

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

View File

@@ -5,7 +5,6 @@ import (
"os"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
)
//nolint:lll
@@ -56,15 +55,21 @@ func ExampleNew_list() {
"--no-ansi",
}
console.New(
console.WithExit(func(int) {}),
console.WithReplaceCommand,
).
console.New(console.WithExit(func(int) {})).
Add(
Command(),
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),
&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",
},
).
Execute(ctx)
// Output:

View File

@@ -5,8 +5,6 @@ import (
"fmt"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/output"
)
@@ -54,18 +52,6 @@ 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
@@ -115,7 +101,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", errors.ErrExecuteNil)
return fmt.Errorf("%w", ErrExecuteNil)
}
if c.Handle != nil {

View File

@@ -1,82 +0,0 @@
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)
}
}

View File

@@ -1,88 +0,0 @@
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)
}
}
}

View File

@@ -1,143 +0,0 @@
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
}

View File

@@ -1,137 +0,0 @@
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
}

View File

@@ -1,46 +0,0 @@
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)
}
}
}

View File

@@ -1,120 +0,0 @@
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
}

View File

@@ -1,136 +0,0 @@
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
}

View File

@@ -1,147 +0,0 @@
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
}

View File

@@ -1,20 +0,0 @@
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,10 +5,12 @@ 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"
@@ -16,59 +18,133 @@ import (
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
cerr "gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/output"
)
//nolint:gochecknoinits
func init() {
console.MustRegister(command.With(Command(), command.WithName("fdevs:console:test")))
console.MustRegister(command.With(Command(), command.WithName("fdevs:console:arg")))
console.MustRegister(Command().With(console.WithName("fdevs:console:test")))
console.MustRegister(Command().With(console.WithName("fdevs:console:arg")))
}
func Command() command.Command {
return command.New("test:command", "test command", Execute, command.Configure(Configure))
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 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)
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
}
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",
)
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
}
return nil
cnt = 0
chain := console.ChainPrepare(prepares...)
err := chain(ctx, def, configure)
if err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int64(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
}
func 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),
)
func TestChainHandle(t *testing.T) {
t.Parallel()
return nil
var cnt int64
ctx := context.Background()
in := &memory.Map{}
out := output.Stdout()
handle := func(ctx context.Context, in config.Provider, out output.Output, next console.Action) error {
atomic.AddInt64(&cnt, 1)
return next(ctx, in, out)
}
action := func(context.Context, config.Provider, output.Output) error {
return nil
}
for i := range []int{0, 1, 2, 30, 40, 50} {
handles := make([]console.Handle, i)
for p := range i {
handles[p] = handle
}
cnt = 0
chain := console.ChainHandle(handles...)
err := chain(ctx, in, out, action)
if err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int64(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
}
func TestRunEmptyExecute(t *testing.T) {
@@ -82,7 +158,7 @@ func TestRunEmptyExecute(t *testing.T) {
out := output.Stdout()
err := empty.Run(ctx, in, out)
if !errors.Is(err, cerr.ErrExecuteNil) {
t.Fatalf("expected: %v, got: %v ", cerr.ErrExecuteNil, err)
if !errors.Is(err, console.ErrExecuteNil) {
t.Fatalf("expected: %v, got: %v ", console.ErrExecuteNil, err)
}
}

View File

@@ -3,38 +3,55 @@ package console
import (
"context"
"errors"
"fmt"
"log"
"math"
"os"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/chain"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/command/help"
cerr "gitoa.ru/go-4devs/console/errors"
"gitoa.ru/go-4devs/console/internal/registry"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/param"
"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
)
// Execute the current command with option.
func Execute(ctx context.Context, cmd command.Command, opts ...func(*App)) {
func Execute(ctx context.Context, cmd *Command, opts ...func(*App)) {
opts = append([]func(*App){WithSkipArgs(1)}, opts...)
New(opts...).exec(ctx, cmd)
}
// Run current command by input and output.
func Run(ctx context.Context, cmd command.Command, in config.BindProvider, out output.Output) error {
func Run(ctx context.Context, cmd *Command, in config.BindProvider, out output.Output) error {
def := definition.New()
err := cmd.Configure(ctx, def)
err := cmd.Init(ctx, def)
if err != nil {
return fmt.Errorf("%w", err)
return err
}
command.Default(def)
def.Add(Default()...)
berr := in.Bind(ctx, config.NewVars(def.Options()...))
if berr != nil {
@@ -43,37 +60,85 @@ func Run(ctx context.Context, cmd command.Command, in config.BindProvider, out o
return showHelp(ctx, cmd, in, output.Ansi(out))
}
out = command.Verbose(ctx, in, out)
out = ansi(ctx, in, out)
if command.IsShowVersion(ctx, in) {
out.Println(ctx, "command <comment>", cmd.Name(), "</comment> version: <info>", param.Version(cmd), "</info>")
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>")
return nil
}
if command.IsShowHelp(ctx, in) {
if ReadValue(ctx, in, OptionHelp).Bool() {
return showHelp(ctx, cmd, in, out)
}
if err := cmd.Execute(ctx, in, out); err != nil {
return fmt.Errorf("%w", err)
}
return nil
return cmd.Run(ctx, in, out)
}
func showHelp(ctx context.Context, cmd command.Command, in config.Provider, out output.Output) error {
arr := &memory.Map{}
arr.SetOption(value.New(cmd.Name()), help.ArgumentCommandName)
arr.SetOption(value.New(false), command.OptionHelp)
if _, err := registry.Find(cmd.Name()); errors.Is(err, cerr.ErrNotFound) {
_ = registry.Add(cmd)
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)
}
help, err := registry.Find(help.Name)
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 {
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.SetOption(value.New(cmd.Name), ArgumentCommandName)
arr.SetOption(value.New(false), OptionHelp)
if _, err := Find(cmd.Name); errors.Is(err, ErrNotFound) {
register(cmd)
}
help, err := Find(CommandHelp)
if err != nil {
return fmt.Errorf("%w", err)
return err
}
w := chain.New(arr, in)
@@ -81,6 +146,20 @@ func showHelp(ctx context.Context, cmd command.Command, in config.Provider, 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 {
val, err := in.Value(ctx, path...)
if err != nil {

View File

@@ -1,4 +1,4 @@
package errors //nolint:revive
package console
import (
"errors"
@@ -7,11 +7,10 @@ import (
)
var (
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")
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")
)
type AlternativesError struct {

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"os"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/provider/chain"
@@ -23,7 +22,7 @@ func main() {
console.
New(console.WithInput(
chain.New(
arg.New(arg.WithArgs(os.Args)),
arg.New(arg.WithSkip(0)),
env.New(Namespace, AppName),
&memory.Default{},
),

View File

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

View File

@@ -1,6 +1,4 @@
gitoa.ru/go-4devs/config v0.0.8 h1:o4p8I9jWJMfiFVVKr50IqCGj1fF+8kmSPDkH0deRvn4=
gitoa.ru/go-4devs/config v0.0.8/go.mod h1:jHKqVafFVW400LC0M4i1ifPapiI9sqpX/QTh+VMadKw=
gitoa.ru/go-4devs/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/console v0.2.1-0.20260105202444-e2c6fc0a35a4 h1:zOk/59IvgUfCDL6ub6WX4hsqDK79FsZR0gf7s7t3fXM=
gitoa.ru/go-4devs/console v0.2.1-0.20260105202444-e2c6fc0a35a4/go.mod h1:PG/Zyj1dLh7eFlj9bgnV58+Ys6I/MTrS0q9W7oD7z4U=

View File

@@ -7,37 +7,32 @@ 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() 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
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
},
}
}

View File

@@ -5,47 +5,37 @@ import (
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
cparam "gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/param"
argument "gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/param"
)
func CreateUser(required bool) command.Command {
return command.New(
"app:create-user",
"Creates a new user.",
UserExecute,
command.Configure(UserConfigure(required)),
command.Help(func(param.HData) (string, error) {
return "This command allows you to create a user...", nil
}),
)
}
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 UserConfigure(required bool) func(_ context.Context, cfg config.Definition) error {
return func(_ context.Context, cfg config.Definition) error {
var opts []cparam.Option
if required {
opts = append(opts, option.Required)
}
cfg.
Add(
argument.String("username", "The username of the user.", option.Required),
argument.String("password", "User password", opts...),
)
cfg.
Add(
argument.String("username", "The username of the user.", option.Required),
argument.String("password", "User password", opts...),
)
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())
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,38 +7,33 @@ 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() 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
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
},
}
}

View File

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

View File

@@ -9,46 +9,45 @@ 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() command.Command {
return command.New("fdevs:command:long", "long command description", LongExecute, command.Configure(LongConfigure))
}
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 LongExecute(ctx context.Context, in config.Provider, out output.Output) error {
timeout := console.ReadValue(ctx, in, "timeout").Duration()
timer := time.NewTimer(timeout)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
out.Println(ctx, "ticker: <info>", t, "</info>")
case <-timer.C:
out.Println(ctx, "<error>stop timer</error>")
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
}
}
},
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),
))
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,16 +4,18 @@ import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/output"
)
func Namespace() command.Command {
return command.New("app:start", "example command in other namespace", NSExecute)
}
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")
func NSExecute(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "example command in other namespace")
return nil
return nil
},
}
}

2
go.sum
View File

@@ -1,7 +1,5 @@
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.8 h1:o4p8I9jWJMfiFVVKr50IqCGj1fF+8kmSPDkH0deRvn4=
gitoa.ru/go-4devs/config v0.0.8/go.mod h1:jHKqVafFVW400LC0M4i1ifPapiI9sqpX/QTh+VMadKw=
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 Normal file
View File

@@ -0,0 +1,100 @@
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

@@ -1,39 +0,0 @@
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 Normal file
View File

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

View File

@@ -1,23 +0,0 @@
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
}

View File

@@ -1,90 +0,0 @@
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)
}

View File

@@ -1,57 +0,0 @@
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,35 +1,114 @@
package console
import (
"errors"
"fmt"
"regexp"
"sort"
"sync"
)
"gitoa.ru/go-4devs/console/command"
"gitoa.ru/go-4devs/console/internal/registry"
const (
CommandHelp = "help"
CommandList = "list"
)
//nolint:gochecknoglobals
var (
commandsMu sync.RWMutex
commands = make(map[string]*Command)
findCommand = regexp.MustCompile("([^:]+|)")
)
// MustRegister register command or panic if err.
func MustRegister(cmd ...command.Command) {
err := registry.Add(cmd...)
func MustRegister(cmd *Command) {
err := Register(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.Command) error {
if err := registry.Add(cmd...); err != nil {
return fmt.Errorf("%w", err)
func Register(cmd *Command) error {
if cmd == nil {
return ErrCommandNil
}
if _, err := Find(cmd.Name); !errors.Is(err, ErrNotFound) {
return fmt.Errorf("%w: command %s", ErrCommandDuplicate, cmd.Name)
}
register(cmd)
return nil
}
// Find command by name, tries to find the best match if you give it an abbreviation of a name.
func Find(name string) (command.Command, error) {
cmd, err := registry.Find(name)
if err != nil {
return cmd, fmt.Errorf("%w", err)
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)
}
return cmd, nil
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 + "$")
if err != nil {
return nil, fmt.Errorf("find by regexp:%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
}

32
register_test.go Normal file
View File

@@ -0,0 +1,32 @@
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)
}
}
}