17 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
e48a9b29d8 Merge pull request 'update go tools' (#11) from tools into master
All checks were successful
Go Action / goaction (push) Successful in 20s
Reviewed-on: #11
2025-12-27 21:42:54 +03:00
71f774aa5a update go tools
All checks were successful
Go Action / goaction (pull_request) Successful in 52s
2025-12-27 21:41:14 +03:00
c6a6300edf Merge pull request 'update readme' (#10) from readme into master
All checks were successful
Go Action / goaction (push) Successful in 18s
Reviewed-on: #10
2025-12-27 20:16:31 +03:00
c9b4f4cfd0 update readme
All checks were successful
Go Action / goaction (pull_request) Successful in 18s
2025-12-27 20:15:09 +03:00
d2ef3f7d0a update example (#8)
All checks were successful
Go Action / goaction (push) Successful in 20s
Reviewed-on: #8
2025-12-27 20:01:51 +03:00
3b32bb2759 input (#7)
All checks were successful
Go Action / goaction (push) Successful in 55s
Reviewed-on: #7
2025-12-27 19:41:06 +03:00
ad5cf18535 Merge pull request 'check empty execute' (#6) from execute into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #6
2022-09-24 12:20:33 +03:00
andrey1s
1151e7c3ad check empty execute
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-24 12:16:23 +03:00
44d8837dbc Merge pull request 'add example hidden option' (#5) from example into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #5
2022-09-18 23:14:22 +03:00
andrey1s
0b6a6ee99b add example hidden option
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-18 23:11:24 +03:00
7771ff495d Merge pull request 'add hidden option' (#4) from hidden into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #4
2022-09-18 22:36:09 +03:00
andrey1s
662cbdb510 add hidden option
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-18 22:34:16 +03:00
65a754363f add config example (#3)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: andrey1s <andrey@4devs.pro>
Reviewed-on: #3
Co-authored-by: andrey <andrey@4devs.io>
Co-committed-by: andrey <andrey@4devs.io>
2022-09-18 22:12:29 +03:00
ef4d5d126a Merge pull request 'add input config pkg' (#2) from config into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #2
2022-09-18 21:56:37 +03:00
andrey1s
4ddc526abf add input config pkg
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
continuous-integration/drone/pr Build is passing
2022-09-18 21:43:18 +03:00
4fdeb73e8a add value vithh error (#1)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Co-authored-by: andrey1s <andrey@4devs.pro>
Reviewed-on: #1
Co-authored-by: andrey <andrey@4devs.io>
Co-committed-by: andrey <andrey@4devs.io>
2022-09-18 21:37:25 +03:00
79 changed files with 834 additions and 2581 deletions

View File

@@ -1,24 +0,0 @@
kind: pipeline
name: default
steps:
- name: golangci-lint
image: golangci/golangci-lint:v1.26
volumes:
- name: deps
path: /go/src/mod
commands:
- golangci-lint run --timeout 5m
- name: test
image: golang
volumes:
- name: deps
path: /go/src/mod
commands:
- go test ./...
volumes:
- name: deps
temp: {}

View File

@@ -0,0 +1,34 @@
name: Go Action
on:
push:
branches:
- master
pull_request:
paths:
- 'example/**'
jobs:
goaction:
runs-on: ubuntu-latest # Use a Gitea Actions runner label
steps:
- name: Check out repository code
uses: actions/checkout@v4 # Action to clone the repo
- name: Set up Go
uses: actions/setup-go@v5 # Action to install a specific Go version
with:
go-version: '1.25.5' # Specify your required Go version
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 # Use the golangci-lint action
with:
version: v2.7.2 # Specify the linter version
# Optional: additional arguments
args: --verbose
working-directory: example
- name: Run go test
run: go test ./...
working-directory: example

View File

@@ -0,0 +1,32 @@
name: Go Action
on:
push:
branches:
- master
pull_request:
paths-ignore:
- 'example/**'
jobs:
goaction:
runs-on: ubuntu-latest # Use a Gitea Actions runner label
steps:
- name: Check out repository code
uses: actions/checkout@v4 # Action to clone the repo
- name: Set up Go
uses: actions/setup-go@v5 # Action to install a specific Go version
with:
go-version: '1.25.5' # Specify your required Go version
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8 # Use the golangci-lint action
with:
version: v2.7.2 # Specify the linter version
# Optional: additional arguments
args: --verbose
- name: Run go test
run: go test ./...

View File

@@ -1,4 +1,16 @@
linters-settings:
version: "2"
linters:
default: all
disable:
- exhaustruct
- gomoddirectives
- ireturn
- wsl
- noinlineerr
- depguard
settings:
funcorder:
constructor: false
dupl:
threshold: 100
funlen:
@@ -9,28 +21,44 @@ linters-settings:
min-occurrences: 2
gocyclo:
min-complexity: 15
golint:
min-confidence: 0
gomnd:
settings:
mnd:
# don't include the "operation" and "assign"
checks: argument,case,condition,return
govet:
check-shadowing: true
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
linters:
enable-all: true
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- gomnd
locale: US
staticcheck:
checks:
- all
- -SA1030
varnamelen:
min-name-length: 2
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- mnd
- varnamelen
path: _test\.go
- linters:
- dupl
path: input/variable
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -1,7 +1,6 @@
# Console
[![Build Status](https://drone.gitoa.ru/api/badges/go-4devs/console/status.svg)](https://drone.gitoa.ru/go-4devs/console)
![Build Status](https://gitoa.ru/go-4devs/console/actions/workflows/goaction.yml/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/gitoa.ru/go-4devs/console)](https://goreportcard.com/report/gitoa.ru/go-4devs/console)
[![GoDoc](https://godoc.org/gitoa.ru/go-4devs/console?status.svg)](http://godoc.org/gitoa.ru/go-4devs/console)
@@ -17,14 +16,14 @@ import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/config"
)
func CreateUser() *console.Command {
return &console.Command{
Name: "app:create-user",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
return nil
},
}
@@ -49,12 +48,14 @@ func CreateUser() *console.Command {
func CreateUser(required bool) *console.Command {
return &console.Command{
//....
Configure: func(ctx context.Context, cfg *input.Definition) error {
var opts []func(*input.Argument)
Configure: func(ctx context.Context, cfg config.Definition) error {
var opts []func(*arg.Option)
if required {
opts = append(opts, argument.Required)
opts = append(opts, arg.Required)
}
cfg.SetArgument("password", "User password", opts...)
cfg.Add(
arg.String("password", "User password", opts...)
)
return nil
},
@@ -98,7 +99,7 @@ The Execute field has access to the output stream to write messages to the conso
func CreateUser(required bool) *console.Command {
return &console.Command{
// ....
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
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!")
@@ -128,21 +129,23 @@ 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 *input.Definition) error {
Configure: func(ctx context.Context, cfg config.Definition) error {
var opts []func(*input.Argument)
if required {
opts = append(opts, argument.Required)
}
cfg.
SetArgument("username", "The username of the user.", argument.Required).
SetArgument("password", "User password", opts...)
cfg.Add(
arg.String("username", "The username of the user.", arg.Required),
arg.String("password", "User password", opts...),
)
return nil
},
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
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: ", in.Argument(ctx, "username").String())
out.Println(ctx, "Username: ", username.String())
return nil
},
@@ -170,14 +173,14 @@ import (
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
"gitoa.ru/go-4devs/console/input/array"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/console/output"
)
func TestCreateUser(t *testing.T) {
ctx := context.Background()
in := input.Array{}
in.SetArgument("username","andrey")
in := memory.Map{}
in.Set("andrey","username")
buf := bytes.Buffer{}
out := output.Buffer(&buf)

77
app.go
View File

@@ -4,8 +4,11 @@ import (
"context"
"os"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/provider/arg"
"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/output"
)
@@ -17,7 +20,7 @@ func WithOutput(out output.Output) func(*App) {
}
// WithInput sets input, by default creates inpur by os.Args.
func WithInput(in input.Input) func(*App) {
func WithInput(in config.BindProvider) func(*App) {
return func(a *App) {
a.in = in
}
@@ -25,9 +28,7 @@ func WithInput(in input.Input) 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 func(a *App) {
a.skipArgv = l
}
return WithInput(chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(l):])), &memory.Default{}))
}
// WithExit sets exit callback by default os.Exit.
@@ -39,44 +40,25 @@ func WithExit(f func(int)) func(*App) {
// New creates and configure new console app.
func New(opts ...func(*App)) *App {
a := &App{
app := &App{
out: output.Stdout(),
exit: os.Exit,
in: chain.New(arg.New(arg.WithArgs(os.Args[resolveSkip(0):])), &memory.Default{}),
}
for _, opt := range opts {
opt(a)
opt(app)
}
if a.in == nil {
skip := 2
switch {
case a.skipArgv > 0 && len(os.Args) > a.skipArgv:
skip = a.skipArgv
case a.skipArgv > 0:
skip = len(os.Args)
case len(os.Args) == 1:
skip = 1
case len(os.Args) > 1 && os.Args[1][0] == '-':
skip = 1
}
a.in = &input.Argv{
Args: os.Args[skip:],
}
}
return a
return app
}
// App is collection of command and configure env.
type App struct {
cmds []*Command
out output.Output
in input.Input
skipArgv int
exit func(int)
cmds []*Command
out output.Output
in config.BindProvider
exit func(int)
}
// Add add or replace command.
@@ -96,7 +78,8 @@ func (a *App) Execute(ctx context.Context) {
if err != nil {
a.printError(ctx, err)
if err := a.list(ctx); err != nil {
err := a.list(ctx)
if err != nil {
a.printError(ctx, err)
}
@@ -107,7 +90,8 @@ func (a *App) Execute(ctx context.Context) {
}
func (a *App) exec(ctx context.Context, cmd *Command) {
if err := Run(ctx, cmd, a.in, a.out); err != nil {
err := Run(ctx, cmd, a.in, a.out)
if err != nil {
a.printError(ctx, err)
a.exit(1)
}
@@ -131,9 +115,9 @@ func (a *App) list(ctx context.Context) error {
return err
}
arr := &input.Array{}
arr.SetArgument("command_name", value.New(CommandList))
in := input.Chain(arr, a.in)
arr := &memory.Map{}
arr.SetOption(value.New(CommandList), ArgumentCommandName)
in := chain.New(arr, a.in)
return Run(ctx, cmd, in, a.out)
}
@@ -141,3 +125,20 @@ func (a *App) list(ctx context.Context) error {
func (a *App) printError(ctx context.Context, err error) {
ansi(ctx, a.in, a.out).Println(ctx, "<error>\n\n ", err, "\n</error>")
}
func resolveSkip(in int) int {
res := 2
switch {
case in > 0 && len(os.Args) > in:
res = in
case in > 0:
res = len(os.Args)
case len(os.Args) == 1:
res = 1
case len(os.Args) > 1 && os.Args[1][0] == '-':
res = 1
}
return res
}

View File

@@ -5,10 +5,9 @@ import (
"os"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
)
//nolint: lll
//nolint:lll
func ExampleNew_help() {
ctx := context.Background()
os.Args = []string{
@@ -31,18 +30,21 @@ func ExampleNew_help() {
// test:command [options] [--] [<test_argument>]
//
// Arguments:
// test_argument test argument
// test_argument test argument
//
// Options:
// --duration[=DURATION] test duration with default [default: 1s]
// --bool test bool option
// --string[=STRING] array string (multiple values allowed)
// -q, --quiet Do not output any message
// -v, --verbose Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace (multiple values allowed)
// -h, --help Display this help message
// -V, --version Display this application version
// --ansi Do not ask any interactive question
// --no-ansi Disable ANSI output
// --string[=STRING] array string (multiple values allowed)
// --group-bool bool
// --group-test-string[=GROUP-TEST-STRING] test group string [default:group string default value]
// --log-{service}-level[=LOG-{SERVICE}-LEVEL] service level [default:debug]
// --bool test bool option
// --duration[=DURATION] test duration with default
// --ansi Do not ask any interactive question
// -V, --version Display this application version
// -h, --help Display this help message
// -v, --verbose Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace (multiple values allowed)
// -q, --quiet Do not output any message
// --no-ansi Disable ANSI output
}
func ExampleNew_list() {
@@ -56,9 +58,18 @@ func ExampleNew_list() {
console.New(console.WithExit(func(int) {})).
Add(
Command(),
command.Hello(),
command.Args(),
command.Namespace(),
&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:
@@ -66,12 +77,12 @@ func ExampleNew_list() {
// command [options] [arguments]
//
// Options:
// -q, --quiet Do not output any message
// -v, --verbose Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace (multiple values allowed)
// -h, --help Display this help message
// -V, --version Display this application version
// --ansi Do not ask any interactive question
// --no-ansi Disable ANSI output
// --ansi Do not ask any interactive question
// -V, --version Display this application version
// -h, --help Display this help message
// -v, --verbose Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace (multiple values allowed)
// -q, --quiet Do not output any message
//
// Available commands:
// help Displays help for a command

View File

@@ -4,51 +4,51 @@ import (
"context"
"fmt"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/output"
)
type (
Action func(ctx context.Context, input input.Input, output output.Output) error
Handle func(ctx context.Context, in input.Input, out output.Output, n Action) error
Configure func(ctx context.Context, cfg *input.Definition) error
Prepare func(ctx context.Context, cfg *input.Definition, n Configure) error
Action func(ctx context.Context, input config.Provider, output output.Output) error
Handle func(ctx context.Context, in config.Provider, out output.Output, n Action) error
Configure func(ctx context.Context, cfg config.Definition) error
Prepare func(ctx context.Context, cfg config.Definition, n Configure) error
Option func(*Command)
)
// WithPrepare append middleware for configuration command.
func WithPrepare(p ...Prepare) Option {
func WithPrepare(prepares ...Prepare) Option {
return func(c *Command) {
if c.Prepare != nil {
p = append([]Prepare{c.Prepare}, p...)
prepares = append([]Prepare{c.Prepare}, prepares...)
}
c.Prepare = ChainPrepare(p...)
c.Prepare = ChainPrepare(prepares...)
}
}
// WithHandle append middleware for executed command.
func WithHandle(h ...Handle) Option {
func WithHandle(handles ...Handle) Option {
return func(c *Command) {
if c.Handle != nil {
h = append([]Handle{c.Handle}, h...)
handles = append([]Handle{c.Handle}, handles...)
}
c.Handle = ChainHandle(h...)
c.Handle = ChainHandle(handles...)
}
}
// WithHidden sets hidden command.
func WithHidden(v bool) Option {
func WithHidden(hidden bool) Option {
return func(c *Command) {
c.Hidden = v
c.Hidden = hidden
}
}
// WithName sets name command.
func WithName(n string) Option {
func WithName(name string) Option {
return func(c *Command) {
c.Name = n
c.Name = name
}
}
@@ -99,7 +99,11 @@ func (c *Command) With(opts ...Option) *Command {
}
// Run run command with input and output.
func (c *Command) Run(ctx context.Context, in input.Input, out output.Output) error {
func (c *Command) Run(ctx context.Context, in config.Provider, out output.Output) error {
if c.Execute == nil {
return fmt.Errorf("%w", ErrExecuteNil)
}
if c.Handle != nil {
return c.Handle(ctx, in, out, c.Execute)
}
@@ -108,12 +112,12 @@ func (c *Command) Run(ctx context.Context, in input.Input, out output.Output) er
}
// Init configures command.
func (c *Command) Init(ctx context.Context, cfg *input.Definition) error {
func (c *Command) Init(ctx context.Context, cfg config.Definition) error {
switch {
case c.Prepare != nil && c.Configure != nil:
return c.Prepare(ctx, cfg, c.Configure)
case c.Prepare != nil:
return c.Prepare(ctx, cfg, func(_ context.Context, _ *input.Definition) error {
return c.Prepare(ctx, cfg, func(_ context.Context, _ config.Definition) error {
return nil
})
case c.Configure != nil:
@@ -125,24 +129,25 @@ func (c *Command) Init(ctx context.Context, cfg *input.Definition) error {
// ChainPrepare creates middleware for configures command.
func ChainPrepare(prepare ...Prepare) Prepare {
n := len(prepare)
if n == 1 {
num := len(prepare)
if num == 1 {
return prepare[0]
}
if n > 1 {
lastI := n - 1
if num > 1 {
lastI := num - 1
return func(ctx context.Context, def *input.Definition, next Configure) error {
return func(ctx context.Context, def config.Definition, next Configure) error {
var (
chainHandler func(context.Context, *input.Definition) error
chainHandler func(context.Context, config.Definition) error
curI int
)
chainHandler = func(currentCtx context.Context, currentDef *input.Definition) error {
chainHandler = func(currentCtx context.Context, currentDef config.Definition) error {
if curI == lastI {
return next(currentCtx, currentDef)
}
curI++
err := prepare[curI](currentCtx, currentDef, chainHandler)
curI--
@@ -154,31 +159,32 @@ func ChainPrepare(prepare ...Prepare) Prepare {
}
}
return func(ctx context.Context, cfg *input.Definition, next Configure) error {
return func(ctx context.Context, cfg config.Definition, next Configure) error {
return next(ctx, cfg)
}
}
// ChainHandle creates middleware for executes command.
func ChainHandle(handlers ...Handle) Handle {
n := len(handlers)
if n == 1 {
num := len(handlers)
if num == 1 {
return handlers[0]
}
if n > 1 {
lastI := n - 1
if num > 1 {
lastI := num - 1
return func(ctx context.Context, in input.Input, out output.Output, next Action) error {
return func(ctx context.Context, in config.Provider, out output.Output, next Action) error {
var (
chainHandler func(context.Context, input.Input, output.Output) error
chainHandler func(context.Context, config.Provider, output.Output) error
curI int
)
chainHandler = func(currentCtx context.Context, currentIn input.Input, currentOut output.Output) error {
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--
@@ -190,7 +196,7 @@ func ChainHandle(handlers ...Handle) Handle {
}
}
return func(ctx context.Context, in input.Input, out output.Output, next Action) error {
return func(ctx context.Context, in config.Provider, out output.Output, next Action) error {
return next(ctx, in, out)
}
}

View File

@@ -2,48 +2,69 @@ package console_test
import (
"context"
"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"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/output"
)
//nolint: gochecknoinits
//nolint:gochecknoinits
func init() {
console.MustRegister(Command().With(console.WithName("fdevs:console:test")))
console.MustRegister(command.Args())
console.MustRegister(Command().With(console.WithName("fdevs:console:arg")))
}
func Command() *console.Command {
return &console.Command{
Name: "test:command",
Description: "test command",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
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:", in.Argument(ctx, "test_argument").String(), "\n",
"bool option:", in.Option(ctx, "bool").Bool(), "\n",
"duration option with default:", in.Option(ctx, "duration").Duration(), "\n",
"array string:[", strings.Join(in.Option(ctx, "string").Strings(), ","), "]\n",
"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(ctx context.Context, def *input.Definition) error {
Configure: func(_ context.Context, def config.Definition) error {
def.
SetArguments(
argument.New("test_argument", "test argument"),
).
SetOptions(
option.New("string", "array string", option.Array),
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(time.Second)),
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
@@ -52,70 +73,92 @@ func Command() *console.Command {
}
func TestChainPrepare(t *testing.T) {
var cnt int32
t.Parallel()
var cnt int64
ctx := context.Background()
def := input.NewDefinition()
def := definition.New()
prepare := func(ctx context.Context, def *input.Definition, n console.Configure) error {
atomic.AddInt32(&cnt, 1)
prepare := func(ctx context.Context, def config.Definition, n console.Configure) error {
atomic.AddInt64(&cnt, 1)
return n(ctx, def)
}
configure := func(context.Context, *input.Definition) error {
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([]console.Prepare, i)
for p := 0; p < i; p++ {
for p := range i {
prepares[p] = prepare
}
cnt = 0
chain := console.ChainPrepare(prepares...)
if err := chain(ctx, def, configure); err != nil {
err := chain(ctx, def, configure)
if err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int32(i) {
if cnt != int64(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
}
func TestChainHandle(t *testing.T) {
var cnt int32
t.Parallel()
var cnt int64
ctx := context.Background()
in := &input.Array{}
in := &memory.Map{}
out := output.Stdout()
handle := func(ctx context.Context, in input.Input, out output.Output, next console.Action) error {
atomic.AddInt32(&cnt, 1)
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, input.Input, output.Output) error {
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 := 0; p < i; p++ {
for p := range i {
handles[p] = handle
}
cnt = 0
chain := console.ChainHandle(handles...)
if err := chain(ctx, in, out, action); err != nil {
err := chain(ctx, in, out, action)
if err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int32(i) {
if cnt != int64(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
}
func TestRunEmptyExecute(t *testing.T) {
t.Parallel()
ctx := context.Background()
empty := console.Command{
Name: "empty",
}
in := &memory.Map{}
out := output.Stdout()
err := empty.Run(ctx, in, out)
if !errors.Is(err, console.ErrExecuteNil) {
t.Fatalf("expected: %v, got: %v ", console.ErrExecuteNil, err)
}
}

View File

@@ -3,11 +3,16 @@ package console
import (
"context"
"errors"
"log"
"math"
"os"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/input/value"
"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/output"
"gitoa.ru/go-4devs/console/output/verbosity"
)
@@ -18,6 +23,19 @@ const (
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, opts ...func(*App)) {
opts = append([]func(*App){WithSkipArgs(1)}, opts...)
@@ -25,15 +43,19 @@ func Execute(ctx context.Context, cmd *Command, opts ...func(*App)) {
}
// Run current command by input and output.
func Run(ctx context.Context, cmd *Command, in input.Input, out output.Output) error {
def := input.NewDefinition()
func Run(ctx context.Context, cmd *Command, in config.BindProvider, out output.Output) error {
def := definition.New()
if err := cmd.Init(ctx, def); err != nil {
err := cmd.Init(ctx, def)
if err != nil {
return err
}
if err := in.Bind(ctx, Default(def)); err != nil {
ansi(ctx, in, out).Print(ctx, "<error>\n\n ", err, "\n</error>\n")
def.Add(Default()...)
berr := in.Bind(ctx, config.NewVars(def.Options()...))
if berr != nil {
log.Print(berr)
return showHelp(ctx, cmd, in, output.Ansi(out))
}
@@ -42,7 +64,7 @@ func Run(ctx context.Context, cmd *Command, in input.Input, out output.Output) e
out = verbose(ctx, in, out)
if in.Option(ctx, "version").Bool() {
if ReadValue(ctx, in, OptionVersion).Bool() {
version := cmd.Version
if version == "" {
version = "unknown"
@@ -53,18 +75,18 @@ func Run(ctx context.Context, cmd *Command, in input.Input, out output.Output) e
return nil
}
if in.Option(ctx, "help").Bool() {
if ReadValue(ctx, in, OptionHelp).Bool() {
return showHelp(ctx, cmd, in, out)
}
return cmd.Run(ctx, in, out)
}
func ansi(ctx context.Context, in input.Input, out output.Output) output.Output {
func ansi(ctx context.Context, in config.Provider, out output.Output) output.Output {
switch {
case in.Option(ctx, "ansi").Bool():
case ReadValue(ctx, in, OptionAnsi).Bool():
out = output.Ansi(out)
case in.Option(ctx, "no-ansi").Bool():
case ReadValue(ctx, in, OptionNoAnsi).Bool():
out = output.None(out)
case lookupEnv("NO_COLOR"):
out = output.None(out)
@@ -81,19 +103,21 @@ func lookupEnv(name string) bool {
return has && v == "true"
}
func verbose(ctx context.Context, in input.Input, out output.Output) output.Output {
func verbose(ctx context.Context, in config.Provider, out output.Output) output.Output {
switch {
case in.Option(ctx, "quiet").Bool():
case ReadValue(ctx, in, OptionQuiet).Bool():
out = output.Quiet()
default:
v := in.Option(ctx, "verbose").Bools()
var verb []bool
_ = ReadValue(ctx, in, OptionVerbose).Unmarshal(&verb)
switch {
case len(v) == verboseInfo:
case len(verb) == verboseInfo:
out = output.Verbosity(out, verbosity.Info)
case len(v) == verboseDebug:
case len(verb) == verboseDebug:
out = output.Verbosity(out, verbosity.Debug)
case len(v) >= verboseTrace:
case len(verb) >= verboseTrace:
out = output.Verbosity(out, verbosity.Trace)
default:
out = output.Verbosity(out, verbosity.Norm)
@@ -103,10 +127,10 @@ func verbose(ctx context.Context, in input.Input, out output.Output) output.Outp
return out
}
func showHelp(ctx context.Context, cmd *Command, in input.Input, out output.Output) error {
a := &input.Array{}
a.SetArgument(HelpArgumentCommandName, value.New(cmd.Name))
a.SetOption("help", value.New(false))
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)
@@ -117,21 +141,30 @@ func showHelp(ctx context.Context, cmd *Command, in input.Input, out output.Outp
return err
}
w := input.Chain(a, in)
w := chain.New(arr, in)
return Run(ctx, help, w, out)
}
// Default options and argument command.
func Default(d *input.Definition) *input.Definition {
return d.SetOptions(
option.Bool("no-ansi", "Disable ANSI output"),
option.Bool("ansi", "Do not ask any interactive question"),
option.Bool("version", "Display this application version", option.Short("V")),
option.Bool("help", "Display this help message", option.Short("h")),
option.Bool("verbose",
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.Array),
option.Bool("quiet", "Do not output any message", option.Short("q")),
)
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 {
return value.EmptyValue()
}
return val
}

View File

@@ -2,11 +2,14 @@ package console_test
import (
"context"
"encoding/json"
"fmt"
"log"
"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"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/output"
)
@@ -14,7 +17,7 @@ func ExampleRun() {
cmd := Command()
ctx := context.Background()
out := output.Stdout()
in := &input.Array{}
in := chain.New(&memory.Map{}, &memory.Default{})
err := console.Run(ctx, cmd, in, out)
fmt.Println("err:", err)
@@ -23,23 +26,33 @@ func ExampleRun() {
// bool option:false
// duration option with default:1s
// array string:[]
// group string:group string default value
// log http service:debug
// err: <nil>
}
func ExampleExecute() {
cmd := Command()
ctx := context.Background()
in := &input.Array{}
in := &memory.Map{}
jb, err := json.Marshal([]string{"same value", "other value"})
if err != nil {
log.Print(err)
}
// Run command: ./bin "argument value" -b --string="same value" --string="other value"
in.SetOption("bool", value.New(true))
in.SetOption("string", value.New([]string{"same value", "other value"}))
in.SetArgument("test_argument", value.New("argument value"))
in.SetOption(value.New(true), "bool")
in.SetOption(value.JBytes(jb), "string")
in.SetOption(value.New("argument value"), "test_argument")
in.SetOption(value.New("error"), "log", "http", "level")
console.Execute(ctx, cmd, console.WithInput(in), console.WithExit(func(int) {}))
console.Execute(ctx, cmd, console.WithInput(chain.New(in, &memory.Default{})), console.WithExit(func(int) {}))
// Output:
// test argument:argument value
// bool option:true
// duration option with default:1s
// array string:[same value,other value]
// group string:group string default value
// log http service:error
}

45
doc.go
View File

@@ -2,29 +2,32 @@
// The Console package allows you to create command-line commands.
// Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.
// console application can be written as follows:
// //cmd/console/main.go
// func main() {
// console.New().Execute(context.Background())
// }
//
// //cmd/console/main.go
// func main() {
// console.New().Execute(context.Background())
// }
//
// Then, you can register the commands using Add():
// package main
//
// import (
// "context"
// package main
//
// "gitoa.ru/go-4devs/console"
// "gitoa.ru/go-4devs/console/example/pkg/command"
// )
// import (
// "context"
//
// func main() {
// console.
// New().
// Add(
// command.Hello(),
// command.Args(),
// command.Hidden(),
// command.Namespace(),
// ).
// Execute(context.Background())
// }
// "gitoa.ru/go-4devs/console"
// "gitoa.ru/go-4devs/console/example/pkg/command"
// )
//
// func main() {
// console.
// New().
// Add(
// command.Hello(),
// command.Args(),
// command.Hidden(),
// command.Namespace(),
// ).
// Execute(context.Background())
// }
package console

31
error.go Normal file
View File

@@ -0,0 +1,31 @@
package console
import (
"errors"
"fmt"
"strings"
)
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")
)
type AlternativesError struct {
Alt []string
Err error
}
func (e AlternativesError) Error() string {
return fmt.Sprintf("%s, alternatives: [%s]", e.Err, strings.Join(e.Alt, ","))
}
func (e AlternativesError) Is(err error) bool {
return errors.Is(e.Err, err)
}
func (e AlternativesError) Unwrap() error {
return e.Err
}

Binary file not shown.

View File

@@ -21,5 +21,6 @@ func main() {
<-ch
cancel()
}()
console.Execute(ctx, command.Long())
}

View File

@@ -0,0 +1,35 @@
package main
import (
"context"
"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/example/pkg/command"
)
const (
Namespace = "fdevs"
AppName = "console"
)
// FDEVS_CONSOLE_CAT=env FDEVS_CONSOLE_HIDDEN=2022-09-18T23:07:49+03:00 go run cmd/config/main.go fdevs:console:arg -b tmp.
// FDEVS_CONSOLE_CAT=env go run cmd/config/main.go fdevs:console:arg --hidden=2022-09-18T23:07:49+03:00 -b tmp.
func main() {
console.
New(console.WithInput(
chain.New(
arg.New(arg.WithSkip(0)),
env.New(Namespace, AppName),
&memory.Default{},
),
)).
Add(
command.Long(),
command.Args(),
).
Execute(context.Background())
}

10
example/go.mod Normal file
View File

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

4
example/go.sum Normal file
View File

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

View File

@@ -2,10 +2,11 @@ package command
import (
"context"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/output"
)
@@ -13,19 +14,23 @@ func Args() *console.Command {
return &console.Command{
Name: "fdevs:console:arg",
Description: "Understanding how Console Arguments and Options Are Handled",
Configure: func(ctx context.Context, def *input.Definition) error {
def.SetOptions(
option.Bool("foo", "foo option", option.Short("f")),
option.New("bar", "required bar option", option.Required, option.Short("b")),
option.New("cat", "cat option", option.Short("c")),
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 input.Input, out output.Output) error {
out.Println(ctx, "foo: <info>", in.Option(ctx, "foo").Bool(), "</info>")
out.Println(ctx, "bar: <info>", in.Option(ctx, "bar").String(), "</info>")
out.Println(ctx, "cat: <info>", in.Option(ctx, "cat").String(), "</info>")
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

@@ -3,9 +3,11 @@ package command
import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
argument "gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/output"
)
@@ -14,21 +16,24 @@ func CreateUser(required bool) *console.Command {
Name: "app:create-user",
Description: "Creates a new user.",
Help: "This command allows you to create a user...",
Configure: func(ctx context.Context, cfg *input.Definition) error {
var opts []func(*argument.Argument)
Configure: func(_ context.Context, cfg config.Definition) error {
var opts []param.Option
if required {
opts = append(opts, argument.Required)
opts = append(opts, option.Required)
}
cfg.
SetArgument("username", "The username of the user.", argument.Required).
SetArgument("password", "User password", opts...)
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 input.Input, out output.Output) error {
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: ", in.Argument(ctx, "username").String())
out.Println(ctx, "Username: ", console.ReadValue(ctx, in, "username").String())
return nil
},

View File

@@ -5,18 +5,20 @@ import (
"context"
"testing"
"gitoa.ru/go-4devs/config/provider/memory"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
)
func TestCreateUser(t *testing.T) {
t.Parallel()
ctx := context.Background()
buf := bytes.Buffer{}
out := output.Buffer(&buf)
in := &input.Array{}
in.SetArgument("username", "andrey")
in := &memory.Map{}
in.SetOption("andrey", "username")
err := console.Run(ctx, command.CreateUser(false), in, out)
if err != nil {

View File

@@ -3,9 +3,10 @@ package command
import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/output"
)
@@ -13,19 +14,23 @@ func Hello() *console.Command {
return &console.Command{
Name: "fdevs:console:hello",
Description: "example hello command",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
name := in.Argument(ctx, "name").String()
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 *input.Definition) error {
def.SetArguments(
argument.New("name", "Same name", argument.Default("World")),
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

@@ -3,8 +3,8 @@ package command
import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
)
@@ -13,7 +13,7 @@ func Hidden() *console.Command {
Name: "fdevs:console:hidden",
Description: "hidden command exmale",
Hidden: true,
Execute: func(ctx context.Context, _ input.Input, out output.Output) error {
Execute: func(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "<info> call hidden command</info>")
return nil

View File

@@ -4,11 +4,11 @@ import (
"context"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/validator"
"gitoa.ru/go-4devs/config/value"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/input/validator"
"gitoa.ru/go-4devs/console/input/value/flag"
"gitoa.ru/go-4devs/console/output"
)
@@ -18,11 +18,13 @@ const defaultTimeout = time.Second * 30
func Long() *console.Command {
return &console.Command{
Name: "fdevs:command:long",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
timeout := in.Option(ctx, "timeout").Duration()
Execute: func(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()
for {
select {
case t := <-ticker.C:
@@ -38,11 +40,11 @@ func Long() *console.Command {
}
}
},
Configure: func(ctx context.Context, def *input.Definition) error {
def.SetOptions(option.Duration("timeout", "set duration run command",
option.Default(defaultTimeout),
option.Short("t"),
option.Valid(validator.NotBlank(flag.Duration)),
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

View File

@@ -3,8 +3,8 @@ package command
import (
"context"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
)
@@ -12,7 +12,7 @@ func Namespace() *console.Command {
return &console.Command{
Name: "app:start",
Description: "example command in other namespace",
Execute: func(ctx context.Context, _ input.Input, out output.Output) error {
Execute: func(ctx context.Context, _ config.Provider, out output.Output) error {
out.Println(ctx, "example command in other namespace")
return nil

12
go.mod
View File

@@ -1,3 +1,13 @@
module gitoa.ru/go-4devs/console
go 1.15
go 1.24.0
require gitoa.ru/go-4devs/config v0.0.8
require (
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.40.0 // indirect
)
tool golang.org/x/tools/cmd/stringer

8
go.sum
View File

@@ -0,0 +1,8 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=

61
help.go
View File

@@ -6,23 +6,25 @@ import (
"os"
"strings"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/input/validator"
"gitoa.ru/go-4devs/console/input/value/flag"
"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
//nolint:gochecknoinits
func init() {
MustRegister(help())
}
const (
HelpArgumentCommandName = "command_name"
helpOptFormat = "format"
ArgumentCommandName = "command_name"
OptionFormat = "format"
)
func help() *Command {
@@ -36,25 +38,27 @@ You can also output the help in other formats by using the <comment>--format</co
<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 input.Input, out output.Output) error {
Execute: func(ctx context.Context, in config.Provider, out output.Output) error {
var err error
name := in.Argument(ctx, HelpArgumentCommandName).String()
format := in.Option(ctx, helpOptFormat).String()
name := ReadValue(ctx, in, ArgumentCommandName).String()
format := ReadValue(ctx, in, OptionFormat).String()
des, err := descriptor.Find(format)
if err != nil {
return err
return fmt.Errorf("find descriptor[%v]: %w", format, err)
}
cmd, err := Find(name)
if err != nil {
return err
return fmt.Errorf("find cmd: %w", err)
}
def := input.NewDefinition()
def := definition.New()
def.Add(Default()...)
if err := cmd.Init(ctx, Default(def)); err != nil {
return err
if err := cmd.Init(ctx, def); err != nil {
return fmt.Errorf("init cmd: %w", err)
}
var bin string
@@ -62,26 +66,29 @@ To display the list of available commands, please use the <info>list</info> comm
bin = os.Args[0]
}
return des.Command(ctx, out, descriptor.Command{
derr := des.Command(ctx, out, descriptor.Command{
Bin: bin,
Name: cmd.Name,
Description: cmd.Description,
Help: cmd.Help,
Definition: def,
Options: def.With(param.New(descriptor.TxtStyle())),
})
if derr != nil {
return fmt.Errorf("descriptor help:%w", derr)
}
return nil
},
Configure: func(ctx context.Context, config *input.Definition) error {
Configure: func(_ context.Context, config config.Definition) error {
formats := descriptor.Descriptors()
config.
SetArguments(
argument.New(HelpArgumentCommandName, "The command name", argument.Default("help")),
).
SetOptions(
option.New(helpOptFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
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(formats[0]),
option.Valid(
validator.NotBlank(flag.String),
option.Default(value.New(formats[0])),
validator.Valid(
validator.NotBlank,
validator.Enum(formats...),
),
),

View File

@@ -1,54 +0,0 @@
package argument
import (
"gitoa.ru/go-4devs/console/input/errs"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
func New(name, description string, opts ...func(*Argument)) Argument {
a := Argument{
Name: name,
Description: description,
}
for _, opt := range opts {
opt(&a)
}
return a
}
type Argument struct {
Name string
Description string
Default value.Value
Flag flag.Flag
Valid []func(value.Value) error
}
func (a Argument) HasDefault() bool {
return a.Default != nil
}
func (a Argument) IsBool() bool {
return a.Flag.IsBool()
}
func (a Argument) IsRequired() bool {
return a.Flag.IsRequired()
}
func (a Argument) IsArray() bool {
return a.Flag.IsArray()
}
func (a Argument) Validate(v value.Value) error {
for _, valid := range a.Valid {
if err := valid(v); err != nil {
return errs.Argument(a.Name, err)
}
}
return nil
}

View File

@@ -1,26 +0,0 @@
package argument
import (
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
func Required(a *Argument) {
a.Flag |= flag.Required
}
func Default(v interface{}) func(*Argument) {
return func(a *Argument) {
a.Default = value.New(v)
}
}
func Flag(flag flag.Flag) func(*Argument) {
return func(a *Argument) {
a.Flag = flag
}
}
func Array(a *Argument) {
a.Flag |= flag.Array
}

View File

@@ -1,139 +0,0 @@
package input
import (
"context"
"fmt"
"strings"
"gitoa.ru/go-4devs/console/input/errs"
"gitoa.ru/go-4devs/console/input/option"
)
const doubleDash = `--`
type Argv struct {
Array
Args []string
ErrHandle func(error) error
}
func (i *Argv) Bind(ctx context.Context, def *Definition) error {
options := true
for len(i.Args) > 0 {
var err error
arg := i.Args[0]
i.Args = i.Args[1:]
switch {
case options && arg == doubleDash:
options = false
case options && len(arg) > 2 && arg[0:2] == doubleDash:
err = i.parseLongOption(arg[2:], def)
case options && arg[0:1] == "-":
if len(arg) == 1 {
return fmt.Errorf("%w: option name required given '-'", errs.ErrInvalidName)
}
err = i.parseShortOption(arg[1:], def)
default:
err = i.parseArgument(arg, def)
}
if err != nil && i.ErrHandle != nil {
if herr := i.ErrHandle(err); herr != nil {
return herr
}
}
}
return i.Array.Bind(ctx, def)
}
func (i *Argv) parseLongOption(arg string, def *Definition) error {
var value *string
name := arg
if strings.Contains(arg, "=") {
vals := strings.SplitN(arg, "=", 2)
name = vals[0]
value = &vals[1]
}
opt, err := def.Option(name)
if err != nil {
return errs.Option(name, err)
}
return i.appendOption(name, value, opt)
}
func (i *Argv) appendOption(name string, data *string, opt option.Option) error {
if i.HasOption(name) && !opt.IsArray() {
return fmt.Errorf("%w: got: array, expect: %s", errs.ErrUnexpectedType, opt.Flag.Type())
}
var val string
switch {
case data != nil:
val = *data
case opt.IsBool():
val = "true"
case len(i.Args) > 0 && len(i.Args[0]) > 0 && i.Args[0][0:1] != "-":
val = i.Args[0]
i.Args = i.Args[1:]
default:
return errs.Option(name, errs.ErrRequired)
}
if err := i.AppendOption(opt.Flag, name, val); err != nil {
return errs.Option(name, err)
}
return nil
}
func (i *Argv) parseShortOption(arg string, def *Definition) error {
name := arg
var value string
if len(name) > 1 {
name, value = arg[0:1], arg[1:]
}
opt, err := def.ShortOption(name)
if err != nil {
return err
}
if opt.IsBool() && value != "" {
if err := i.parseShortOption(value, def); err != nil {
return err
}
value = ""
}
if value == "" {
return i.appendOption(opt.Name, nil, opt)
}
return i.appendOption(opt.Name, &value, opt)
}
func (i *Argv) parseArgument(arg string, def *Definition) error {
opt, err := def.Argument(i.LenArguments())
if err != nil {
return err
}
if err := i.AppendArgument(opt.Flag, opt.Name, arg); err != nil {
return errs.Argument(opt.Name, err)
}
return nil
}

View File

@@ -1,108 +0,0 @@
package input
import (
"context"
"gitoa.ru/go-4devs/console/input/errs"
"gitoa.ru/go-4devs/console/input/value"
)
type Array struct {
Map
defaults Map
}
func (a *Array) Option(ctx context.Context, name string) value.Value {
if v := a.Map.Option(ctx, name); !value.IsEmpty(v) {
return v
}
if v := a.defaults.Option(ctx, name); !value.IsEmpty(v) {
return v
}
return value.Empty()
}
func (a *Array) Argument(ctx context.Context, name string) value.Value {
if v := a.Map.Argument(ctx, name); !value.IsEmpty(v) {
return v
}
if v := a.defaults.Argument(ctx, name); !value.IsEmpty(v) {
return v
}
return value.Empty()
}
func (a *Array) Bind(ctx context.Context, d *Definition) error {
if err := a.bindArguments(ctx, d); err != nil {
return err
}
return a.bindOption(ctx, d)
}
func (a *Array) bindOption(ctx context.Context, def *Definition) error {
for _, name := range def.Options() {
opt, err := def.Option(name)
if err != nil {
return err
}
if !a.HasOption(name) {
switch {
case opt.HasDefault():
a.defaults.SetOption(name, opt.Default)
continue
case opt.IsRequired():
return errs.Option(name, errs.ErrRequired)
default:
continue
}
}
v := a.Map.Option(ctx, name)
if value.IsEmpty(v) {
continue
}
if err := opt.Validate(v); err != nil {
return errs.Option(name, err)
}
}
return nil
}
func (a *Array) bindArguments(ctx context.Context, def *Definition) error {
for pos, name := range def.Arguments() {
arg, err := def.Argument(pos)
if err != nil {
return err
}
if !a.HasArgument(name) {
switch {
case arg.HasDefault():
a.defaults.SetArgument(name, arg.Default)
continue
case arg.IsRequired():
return errs.Argument(name, errs.ErrRequired)
default:
continue
}
}
if v := a.Map.Argument(ctx, name); !value.IsEmpty(v) {
if err := arg.Validate(v); err != nil {
return errs.Argument(name, err)
}
}
}
return nil
}

View File

@@ -1,43 +0,0 @@
package input
import (
"context"
"gitoa.ru/go-4devs/console/input/value"
)
func Chain(c ...Input) Input {
return chain(c)
}
type chain []Input
func (c chain) Option(ctx context.Context, name string) value.Value {
for _, in := range c {
if val := in.Option(ctx, name); !value.IsEmpty(val) {
return val
}
}
return value.Empty()
}
func (c chain) Argument(ctx context.Context, name string) value.Value {
for _, in := range c {
if val := in.Argument(ctx, name); !value.IsEmpty(val) {
return val
}
}
return value.Empty()
}
func (c chain) Bind(ctx context.Context, def *Definition) error {
for _, input := range c {
if err := input.Bind(ctx, def); err != nil {
return err
}
}
return nil
}

View File

@@ -1,101 +0,0 @@
package input
import (
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/input/errs"
"gitoa.ru/go-4devs/console/input/option"
)
func NewDefinition() *Definition {
return &Definition{
options: make(map[string]option.Option),
args: make(map[string]argument.Argument),
short: make(map[string]string),
}
}
type Definition struct {
options map[string]option.Option
posOpt []string
args map[string]argument.Argument
posArgs []string
short map[string]string
}
func (d *Definition) Options() []string {
return d.posOpt
}
func (d *Definition) Arguments() []string {
return d.posArgs
}
func (d *Definition) SetOption(name, description string, opts ...func(*option.Option)) *Definition {
return d.SetOptions(option.New(name, description, opts...))
}
func (d *Definition) SetOptions(opts ...option.Option) *Definition {
for _, opt := range opts {
if _, has := d.options[opt.Name]; !has {
d.posOpt = append([]string{opt.Name}, d.posOpt...)
}
d.options[opt.Name] = opt
if opt.HasShort() {
d.short[opt.Short] = opt.Name
}
}
return d
}
func (d *Definition) SetArgument(name, description string, opts ...func(*argument.Argument)) *Definition {
return d.SetArguments(argument.New(name, description, opts...))
}
func (d *Definition) SetArguments(args ...argument.Argument) *Definition {
for _, arg := range args {
if _, ok := d.args[arg.Name]; !ok {
d.posArgs = append(d.posArgs, arg.Name)
}
d.args[arg.Name] = arg
}
return d
}
func (d *Definition) Argument(pos int) (argument.Argument, error) {
if len(d.posArgs) == 0 {
return argument.Argument{}, errs.ErrNoArgs
}
lastPos := len(d.posArgs) - 1
if lastPos < pos {
arg := d.args[d.posArgs[lastPos]]
if arg.IsArray() {
return arg, nil
}
return argument.Argument{}, errs.ErrToManyArgs
}
return d.args[d.posArgs[pos]], nil
}
func (d *Definition) ShortOption(short string) (option.Option, error) {
name, ok := d.short[short]
if !ok {
return option.Option{}, errs.ErrNotFound
}
return d.Option(name)
}
func (d *Definition) Option(name string) (option.Option, error) {
if opt, ok := d.options[name]; ok {
return opt, nil
}
return option.Option{}, errs.ErrNotFound
}

View File

@@ -1,58 +0,0 @@
package errs
import (
"errors"
"fmt"
)
var (
ErrNotFound = errors.New("not found")
ErrNoArgs = errors.New("no arguments expected")
ErrToManyArgs = errors.New("too many arguments")
ErrUnexpectedType = errors.New("unexpected type")
ErrRequired = errors.New("is required")
ErrAppend = errors.New("failed append")
ErrInvalidName = errors.New("invalid name")
)
func New(name, t string, err error) Error {
return Error{
name: name,
t: t,
err: err,
}
}
type Error struct {
name string
err error
t string
}
func (o Error) Error() string {
return fmt.Sprintf("%s: '%s' %s", o.t, o.name, o.err)
}
func (o Error) Is(err error) bool {
return errors.Is(err, o.err)
}
func (o Error) Unwrap() error {
return o.err
}
func Option(name string, err error) Error {
return Error{
name: name,
err: err,
t: "option",
}
}
func Argument(name string, err error) Error {
return Error{
name: name,
err: err,
t: "argument",
}
}

View File

@@ -1,13 +0,0 @@
package input
import (
"context"
"gitoa.ru/go-4devs/console/input/value"
)
type Input interface {
Option(ctx context.Context, name string) value.Value
Argument(ctx context.Context, name string) value.Value
Bind(ctx context.Context, def *Definition) error
}

View File

@@ -1,87 +0,0 @@
package input
import (
"context"
"sync"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Map struct {
opts map[string]value.Append
args map[string]value.Append
sync.Mutex
}
func (m *Map) Option(_ context.Context, name string) value.Value {
m.Lock()
defer m.Unlock()
return m.opts[name]
}
func (m *Map) Argument(_ context.Context, name string) value.Value {
m.Lock()
defer m.Unlock()
return m.args[name]
}
func (m *Map) Bind(_ context.Context, _ *Definition) error {
return nil
}
func (m *Map) LenArguments() int {
return len(m.args)
}
func (m *Map) HasOption(name string) bool {
_, ok := m.opts[name]
return ok
}
func (m *Map) SetOption(name string, v interface{}) {
m.Lock()
defer m.Unlock()
if m.opts == nil {
m.opts = make(map[string]value.Append)
}
m.opts[name] = value.New(v)
}
func (m *Map) HasArgument(name string) bool {
_, ok := m.args[name]
return ok
}
func (m *Map) SetArgument(name string, v interface{}) {
m.Lock()
defer m.Unlock()
if m.args == nil {
m.args = make(map[string]value.Append)
}
m.args[name] = value.New(v)
}
func (m *Map) AppendOption(f flag.Flag, name, val string) error {
if _, ok := m.opts[name]; !ok {
m.SetOption(name, value.ByFlag(f))
}
return m.opts[name].Append(val)
}
func (m *Map) AppendArgument(f flag.Flag, name, val string) error {
if _, ok := m.args[name]; !ok {
m.SetArgument(name, value.ByFlag(f))
}
return m.args[name].Append(val)
}

View File

@@ -1,37 +0,0 @@
package option
import (
"gitoa.ru/go-4devs/console/input/value/flag"
)
func Bool(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Bool))...)
}
func Duration(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Duration))...)
}
func Float64(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Float64))...)
}
func Int(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Int))...)
}
func Int64(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Int64))...)
}
func Time(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Time))...)
}
func Uint(name, description string, opts ...func(*Option)) Option {
return New(name, description, append(opts, Value(flag.Uint))...)
}
func Uint64(name, descriontion string, opts ...func(*Option)) Option {
return New(name, descriontion, append(opts, Value(flag.Uint64))...)
}

View File

@@ -1,97 +0,0 @@
package option
import (
"gitoa.ru/go-4devs/console/input/errs"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
func Required(o *Option) {
o.Flag |= flag.Required
}
func Default(in interface{}) func(*Option) {
return func(o *Option) {
o.Default = value.New(in)
}
}
func Short(s string) func(*Option) {
return func(o *Option) {
o.Short = s
}
}
func Array(o *Option) {
o.Flag |= flag.Array
}
func Value(flag flag.Flag) func(*Option) {
return func(o *Option) {
o.Flag |= flag
}
}
func Flag(in flag.Flag) func(*Option) {
return func(o *Option) {
o.Flag = in
}
}
func Valid(f ...func(value.Value) error) func(*Option) {
return func(o *Option) {
o.Valid = f
}
}
func New(name, description string, opts ...func(*Option)) Option {
o := Option{
Name: name,
Description: description,
}
for _, opt := range opts {
opt(&o)
}
return o
}
type Option struct {
Name string
Description string
Short string
Flag flag.Flag
Default value.Value
Valid []func(value.Value) error
}
func (o Option) HasShort() bool {
return len(o.Short) == 1
}
func (o Option) HasDefault() bool {
return o.Default != nil
}
func (o Option) IsBool() bool {
return o.Flag.IsBool()
}
func (o Option) IsArray() bool {
return o.Flag.IsArray()
}
func (o Option) IsRequired() bool {
return o.Flag.IsRequired()
}
func (o Option) Validate(v value.Value) error {
for _, valid := range o.Valid {
if err := valid(v); err != nil {
return errs.Option(o.Name, err)
}
}
return nil
}

View File

@@ -1,16 +0,0 @@
package validator
import "gitoa.ru/go-4devs/console/input/value"
func Enum(enum ...string) func(value.Value) error {
return func(in value.Value) error {
v := in.String()
for _, e := range enum {
if e == v {
return nil
}
}
return NewError(ErrInvalid, v, enum)
}
}

View File

@@ -1,24 +0,0 @@
package validator_test
import (
"errors"
"testing"
"gitoa.ru/go-4devs/console/input/validator"
"gitoa.ru/go-4devs/console/input/value"
)
func TestEnum(t *testing.T) {
validValue := value.New("valid")
invalidValue := value.New("invalid")
enum := validator.Enum("valid", "other", "three")
if err := enum(validValue); err != nil {
t.Errorf("expected valid value got err:%s", err)
}
if err := enum(invalidValue); !errors.Is(err, validator.ErrInvalid) {
t.Errorf("expected err:%s, got: %s", validator.ErrInvalid, err)
}
}

View File

@@ -1,37 +0,0 @@
package validator
import (
"errors"
"fmt"
)
var (
ErrInvalid = errors.New("invalid value")
ErrNotBlank = errors.New("not blank")
)
func NewError(err error, value, expect interface{}) Error {
return Error{
err: err,
value: value,
expect: expect,
}
}
type Error struct {
err error
value interface{}
expect interface{}
}
func (e Error) Error() string {
return fmt.Sprintf("%s: expext: %s, given: %s", e.err, e.expect, e.value)
}
func (e Error) Is(err error) bool {
return errors.Is(e.err, err)
}
func (e Error) Unwrap() error {
return e.err
}

View File

@@ -1,110 +0,0 @@
package validator
import (
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
//nolint: gocyclo
func NotBlank(f flag.Flag) func(value.Value) error {
return func(in value.Value) error {
switch {
case f.IsAny() && in.Any() != nil:
return nil
case f.IsArray():
return arrayNotBlank(f, in)
case f.IsInt() && in.Int() != 0:
return nil
case f.IsInt64() && in.Int64() != 0:
return nil
case f.IsUint() && in.Uint() != 0:
return nil
case f.IsUint64() && in.Uint64() != 0:
return nil
case f.IsFloat64() && in.Float64() != 0:
return nil
case f.IsDuration() && in.Duration() != 0:
return nil
case f.IsTime() && !in.Time().IsZero():
return nil
case f.IsString() && len(in.String()) > 0:
return nil
}
return ErrNotBlank
}
}
//nolint: gocyclo,gocognit
func arrayNotBlank(f flag.Flag, in value.Value) error {
switch {
case f.IsInt() && len(in.Ints()) > 0:
for _, i := range in.Ints() {
if i == 0 {
return ErrNotBlank
}
}
return nil
case f.IsInt64() && len(in.Int64s()) > 0:
for _, i := range in.Int64s() {
if i == 0 {
return ErrNotBlank
}
}
return nil
case f.IsUint() && len(in.Uints()) > 0:
for _, u := range in.Uints() {
if u == 0 {
return ErrNotBlank
}
}
return nil
case f.IsUint64() && len(in.Uint64s()) > 0:
for _, u := range in.Uint64s() {
if u == 0 {
return ErrNotBlank
}
}
return nil
case f.IsFloat64() && len(in.Float64s()) > 0:
for _, f := range in.Float64s() {
if f == 0 {
return ErrNotBlank
}
}
return nil
case f.IsBool() && len(in.Bools()) > 0:
return nil
case f.IsDuration() && len(in.Durations()) > 0:
for _, d := range in.Durations() {
if d == 0 {
return ErrNotBlank
}
}
return nil
case f.IsTime() && len(in.Times()) > 0:
for _, t := range in.Times() {
if t.IsZero() {
return ErrNotBlank
}
}
return nil
case f.IsString() && len(in.Strings()) > 0:
for _, st := range in.Strings() {
if len(st) == 0 {
return ErrNotBlank
}
}
return nil
}
return ErrNotBlank
}

View File

@@ -1,109 +0,0 @@
package validator_test
import (
"errors"
"testing"
"time"
"gitoa.ru/go-4devs/console/input/validator"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
func TestNotBlank(t *testing.T) {
cases := map[string]struct {
flag flag.Flag
value value.Value
empty value.Value
}{
"any": {flag: flag.Any, value: value.New(float32(1))},
"array int": {
flag: flag.Int | flag.Array,
value: value.New([]int{1}),
empty: value.New([]int{10, 20, 0}),
},
"array int64": {
flag: flag.Int64 | flag.Array,
value: value.New([]int64{1}),
empty: value.New([]int64{0}),
},
"array uint": {
flag: flag.Uint | flag.Array,
value: value.New([]uint{1}),
empty: value.New([]uint{1, 0}),
},
"array uint64": {
flag: flag.Uint64 | flag.Array,
value: value.New([]uint64{1}),
empty: value.New([]uint64{0}),
},
"array float64": {
flag: flag.Float64 | flag.Array,
value: value.New([]float64{0.2}),
empty: value.New([]float64{0}),
},
"array bool": {
flag: flag.Bool | flag.Array,
value: value.New([]bool{true, false}),
empty: value.New([]bool{}),
},
"array duration": {
flag: flag.Duration | flag.Array,
value: value.New([]time.Duration{time.Second}),
empty: value.New([]time.Duration{time.Second, 0}),
},
"array time": {
flag: flag.Time | flag.Array,
value: value.New([]time.Time{time.Now()}),
empty: value.New([]time.Time{{}, time.Now()}),
},
"array string": {
flag: flag.Array,
value: value.New([]string{"value"}),
empty: value.New([]string{""}),
},
"int": {
flag: flag.Int,
value: value.New(int(1)),
},
"int64": {
flag: flag.Int64,
value: value.New(int64(2)),
},
"uint": {
flag: flag.Uint,
value: value.New(uint(1)),
empty: value.New([]uint{1}),
},
"uint64": {
flag: flag.Uint64,
value: value.New(uint64(10)),
},
"float64": {
flag: flag.Float64,
value: value.New(float64(.00001)),
},
"duration": {
flag: flag.Duration,
value: value.New(time.Minute),
empty: value.New("same string"),
},
"time": {flag: flag.Time, value: value.New(time.Now())},
"string": {value: value.New("string"), empty: value.New("")},
}
for name, ca := range cases {
valid := validator.NotBlank(ca.flag)
if err := valid(ca.value); err != nil {
t.Errorf("case: %s, expected error <nil>, got: %s", name, err)
}
if ca.empty == nil {
ca.empty = value.Empty()
}
if err := valid(ca.empty); err == nil || !errors.Is(err, validator.ErrNotBlank) {
t.Errorf("case: %s, expect: %s, got:%s", name, validator.ErrNotBlank, err)
}
}
}

View File

@@ -1,15 +0,0 @@
package validator
import "gitoa.ru/go-4devs/console/input/value"
func Valid(v ...func(value.Value) error) func(value.Value) error {
return func(in value.Value) error {
for _, valid := range v {
if err := valid(in); err != nil {
return err
}
}
return nil
}
}

View File

@@ -1,28 +0,0 @@
package validator_test
import (
"errors"
"testing"
"gitoa.ru/go-4devs/console/input/validator"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
)
func TestValid(t *testing.T) {
validValue := value.New("one")
invalidValue := value.New([]string{"one"})
valid := validator.Valid(
validator.NotBlank(flag.String),
validator.Enum("one", "two"),
)
if err := valid(validValue); err != nil {
t.Errorf("expected valid value, got: %s", err)
}
if err := valid(invalidValue); !errors.Is(err, validator.ErrNotBlank) {
t.Errorf("expected not blank, got:%s", err)
}
}

View File

@@ -1,21 +0,0 @@
package value
import "gitoa.ru/go-4devs/console/input/value/flag"
type Any struct {
empty
Val []interface{}
Flag flag.Flag
}
func (a *Any) Any() interface{} {
if a.Flag.IsArray() {
return a.Val
}
if len(a.Val) > 0 {
return a.Val[0]
}
return nil
}

View File

@@ -1,44 +0,0 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Bool struct {
empty
Val []bool
Flag flag.Flag
}
func (b *Bool) Append(in string) error {
v, err := strconv.ParseBool(in)
if err != nil {
return err
}
b.Val = append(b.Val, v)
return nil
}
func (b *Bool) Bool() bool {
if !b.Flag.IsArray() && len(b.Val) == 1 {
return b.Val[0]
}
return false
}
func (b *Bool) Bools() []bool {
return b.Val
}
func (b *Bool) Any() interface{} {
if b.Flag.IsArray() {
return b.Bools()
}
return b.Bool()
}

View File

@@ -1,44 +0,0 @@
package value
import (
"time"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Duration struct {
empty
Val []time.Duration
Flag flag.Flag
}
func (d *Duration) Append(in string) error {
v, err := time.ParseDuration(in)
if err != nil {
return err
}
d.Val = append(d.Val, v)
return nil
}
func (d *Duration) Duration() time.Duration {
if !d.Flag.IsArray() && len(d.Val) == 1 {
return d.Val[0]
}
return 0
}
func (d *Duration) Durations() []time.Duration {
return d.Val
}
func (d *Duration) Any() interface{} {
if d.Flag.IsArray() {
return d.Durations()
}
return d.Duration()
}

View File

@@ -1,100 +0,0 @@
package value
import (
"time"
)
// nolint: gochecknoglobals
var (
emptyValue = &empty{}
)
func Empty() Value {
return emptyValue
}
func IsEmpty(v Value) bool {
return v == nil || v == emptyValue
}
type empty struct{}
func (e *empty) Append(string) error {
return ErrAppendEmpty
}
func (e *empty) String() string {
return ""
}
func (e *empty) Int() int {
return 0
}
func (e *empty) Int64() int64 {
return 0
}
func (e *empty) Uint() uint {
return 0
}
func (e *empty) Uint64() uint64 {
return 0
}
func (e *empty) Float64() float64 {
return 0
}
func (e *empty) Bool() bool {
return false
}
func (e *empty) Duration() time.Duration {
return 0
}
func (e *empty) Time() time.Time {
return time.Time{}
}
func (e *empty) Strings() []string {
return nil
}
func (e *empty) Ints() []int {
return nil
}
func (e *empty) Int64s() []int64 {
return nil
}
func (e *empty) Uints() []uint {
return nil
}
func (e *empty) Uint64s() []uint64 {
return nil
}
func (e *empty) Float64s() []float64 {
return nil
}
func (e *empty) Bools() []bool {
return nil
}
func (e *empty) Durations() []time.Duration {
return nil
}
func (e *empty) Times() []time.Time {
return nil
}
func (e *empty) Any() interface{} {
return nil
}

View File

@@ -1,97 +0,0 @@
package flag
//go:generate stringer -type=Flag -linecomment
type Flag int
const (
String Flag = 0 // string
Required Flag = 1 << iota // required
Array // array
Int // int
Int64 // int64
Uint // uint
Uint64 // uint64
Float64 // float64
Bool // bool
Duration // duration
Time // time
Any // any
)
func (i Flag) With(v Flag) Flag {
return i | v
}
func (i Flag) IsString() bool {
return i|Required|Array^Required^Array == 0
}
func (i Flag) IsRequired() bool {
return i&Required > 0
}
func (i Flag) IsArray() bool {
return i&Array > 0
}
func (i Flag) IsInt() bool {
return i&Int > 0
}
func (i Flag) IsInt64() bool {
return i&Int64 > 0
}
func (i Flag) IsUint() bool {
return i&Uint > 0
}
func (i Flag) IsUint64() bool {
return i&Uint64 > 0
}
func (i Flag) IsFloat64() bool {
return i&Float64 > 0
}
func (i Flag) IsBool() bool {
return i&Bool > 0
}
func (i Flag) IsDuration() bool {
return i&Duration > 0
}
func (i Flag) IsTime() bool {
return i&Time > 0
}
func (i Flag) IsAny() bool {
return i&Any > 0
}
func (i Flag) Type() Flag {
switch {
case i.IsInt():
return Int
case i.IsInt64():
return Int64
case i.IsUint():
return Uint
case i.IsUint64():
return Uint64
case i.IsFloat64():
return Float64
case i.IsBool():
return Bool
case i.IsDuration():
return Duration
case i.IsTime():
return Time
case i.IsAny():
return Any
default:
return String
}
}

View File

@@ -1,47 +0,0 @@
// Code generated by "stringer -type=Flag -linecomment"; DO NOT EDIT.
package flag
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[String-0]
_ = x[Required-2]
_ = x[Array-4]
_ = x[Int-8]
_ = x[Int64-16]
_ = x[Uint-32]
_ = x[Uint64-64]
_ = x[Float64-128]
_ = x[Bool-256]
_ = x[Duration-512]
_ = x[Time-1024]
_ = x[Any-2048]
}
const _Flag_name = "stringrequiredarrayintint64uintuint64float64booldurationtimeany"
var _Flag_map = map[Flag]string{
0: _Flag_name[0:6],
2: _Flag_name[6:14],
4: _Flag_name[14:19],
8: _Flag_name[19:22],
16: _Flag_name[22:27],
32: _Flag_name[27:31],
64: _Flag_name[31:37],
128: _Flag_name[37:44],
256: _Flag_name[44:48],
512: _Flag_name[48:56],
1024: _Flag_name[56:60],
2048: _Flag_name[60:63],
}
func (i Flag) String() string {
if str, ok := _Flag_map[i]; ok {
return str
}
return "Flag(" + strconv.FormatInt(int64(i), 10) + ")"
}

View File

@@ -1,44 +0,0 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Float64 struct {
empty
Val []float64
Flag flag.Flag
}
func (f *Float64) Append(in string) error {
v, err := strconv.ParseFloat(in, 64)
if err != nil {
return err
}
f.Val = append(f.Val, v)
return nil
}
func (f *Float64) Float64() float64 {
if !f.Flag.IsArray() && len(f.Val) == 1 {
return f.Val[0]
}
return 0
}
func (f *Float64) Float64s() []float64 {
return f.Val
}
func (f *Float64) Any() interface{} {
if f.Flag.IsArray() {
return f.Float64s()
}
return f.Float64()
}

View File

@@ -1,44 +0,0 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Int struct {
empty
Val []int
Flag flag.Flag
}
func (i *Int) Append(in string) error {
v, err := strconv.Atoi(in)
if err != nil {
return err
}
i.Val = append(i.Val, v)
return nil
}
func (i *Int) Int() int {
if !i.Flag.IsArray() && len(i.Val) == 1 {
return i.Val[0]
}
return 0
}
func (i *Int) Ints() []int {
return i.Val
}
func (i *Int) Any() interface{} {
if i.Flag.IsArray() {
return i.Ints()
}
return i.Int()
}

View File

@@ -1,44 +0,0 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Int64 struct {
empty
Val []int64
Flag flag.Flag
}
func (i *Int64) Int64() int64 {
if !i.Flag.IsArray() && len(i.Val) == 1 {
return i.Val[0]
}
return 0
}
func (i *Int64) Int64s() []int64 {
return i.Val
}
func (i *Int64) Any() interface{} {
if i.Flag.IsArray() {
return i.Int64s()
}
return i.Int64()
}
func (i *Int64) Append(in string) error {
v, err := strconv.ParseInt(in, 10, 64)
if err != nil {
return err
}
i.Val = append(i.Val, v)
return nil
}

View File

@@ -1,20 +0,0 @@
package value
import (
"errors"
)
var _ Append = (*Read)(nil)
var (
ErrAppendRead = errors.New("invalid append data to read value")
ErrAppendEmpty = errors.New("invalid apped data to empty value")
)
type Read struct {
Value
}
func (r *Read) Append(string) error {
return ErrAppendRead
}

View File

@@ -1,39 +0,0 @@
package value
import "gitoa.ru/go-4devs/console/input/value/flag"
type String struct {
empty
Val []string
Flag flag.Flag
}
func (s *String) Append(in string) error {
s.Val = append(s.Val, in)
return nil
}
func (s *String) String() string {
if s.Flag.IsArray() {
return ""
}
if len(s.Val) == 1 {
return s.Val[0]
}
return ""
}
func (s *String) Strings() []string {
return s.Val
}
func (s *String) Any() interface{} {
if s.Flag.IsArray() {
return s.Strings()
}
return s.String()
}

View File

@@ -1,44 +0,0 @@
package value
import (
"time"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Time struct {
empty
Val []time.Time
Flag flag.Flag
}
func (t *Time) Append(in string) error {
v, err := time.Parse(time.RFC3339, in)
if err != nil {
return err
}
t.Val = append(t.Val, v)
return nil
}
func (t *Time) Time() time.Time {
if !t.Flag.IsArray() && len(t.Val) == 1 {
return t.Val[0]
}
return time.Time{}
}
func (t *Time) Times() []time.Time {
return t.Val
}
func (t *Time) Amy() interface{} {
if t.Flag.IsArray() {
return t.Times()
}
return t.Time()
}

View File

@@ -1,44 +0,0 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Uint struct {
empty
Val []uint
Flag flag.Flag
}
func (u *Uint) Append(in string) error {
v, err := strconv.ParseUint(in, 10, 64)
if err != nil {
return err
}
u.Val = append(u.Val, uint(v))
return nil
}
func (u *Uint) Uint() uint {
if !u.Flag.IsArray() && len(u.Val) == 1 {
return u.Val[0]
}
return 0
}
func (u *Uint) Uints() []uint {
return u.Val
}
func (u *Uint) Any() interface{} {
if u.Flag.IsArray() {
return u.Uints()
}
return u.Uint()
}

View File

@@ -1,44 +0,0 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Uint64 struct {
empty
Val []uint64
Flag flag.Flag
}
func (u *Uint64) Append(in string) error {
v, err := strconv.ParseUint(in, 10, 64)
if err != nil {
return err
}
u.Val = append(u.Val, v)
return nil
}
func (u *Uint64) Uint64() uint64 {
if !u.Flag.IsArray() && len(u.Val) == 1 {
return u.Val[0]
}
return 0
}
func (u *Uint64) Uint64s() []uint64 {
return u.Val
}
func (u *Uint64) Any() interface{} {
if u.Flag.IsArray() {
return u.Uint64s()
}
return u.Uint64()
}

View File

@@ -1,114 +0,0 @@
package value
import (
"time"
"gitoa.ru/go-4devs/console/input/value/flag"
)
type Value interface {
String() string
Int() int
Int64() int64
Uint() uint
Uint64() uint64
Float64() float64
Bool() bool
Duration() time.Duration
Time() time.Time
Any() interface{}
Strings() []string
Ints() []int
Int64s() []int64
Uints() []uint
Uint64s() []uint64
Float64s() []float64
Bools() []bool
Durations() []time.Duration
Times() []time.Time
}
type Append interface {
Value
Append(string) error
}
//nolint: gocyclo
func New(v interface{}) Append {
switch val := v.(type) {
case string:
return &String{Val: []string{val}, Flag: flag.String}
case int:
return &Int{Val: []int{val}, Flag: flag.Int}
case int64:
return &Int64{Val: []int64{val}, Flag: flag.Int64}
case uint:
return &Uint{Val: []uint{val}, Flag: flag.Uint}
case uint64:
return &Uint64{Val: []uint64{val}, Flag: flag.Uint64}
case float64:
return &Float64{Val: []float64{val}, Flag: flag.Float64}
case bool:
return &Bool{Val: []bool{val}, Flag: flag.Bool}
case time.Duration:
return &Duration{Val: []time.Duration{val}, Flag: flag.Duration}
case time.Time:
return &Time{Val: []time.Time{val}, Flag: flag.Time}
case []int64:
return &Int64{Val: val, Flag: flag.Int64 | flag.Array}
case []uint:
return &Uint{Val: val, Flag: flag.Uint | flag.Array}
case []uint64:
return &Uint64{Val: val, Flag: flag.Uint64 | flag.Array}
case []float64:
return &Float64{Val: val, Flag: flag.Float64 | flag.Array}
case []bool:
return &Bool{Val: val, Flag: flag.Bool | flag.Array}
case []time.Duration:
return &Duration{Val: val, Flag: flag.Duration | flag.Array}
case []time.Time:
return &Time{Val: val, Flag: flag.Time | flag.Array}
case []string:
return &String{Val: val, Flag: flag.String | flag.Array}
case []int:
return &Int{Val: val, Flag: flag.Int | flag.Array}
case []interface{}:
return &Any{Val: val, Flag: flag.Any | flag.Array}
case Append:
return val
case Value:
return &Read{Value: val}
default:
if v != nil {
return &Any{Val: []interface{}{v}, Flag: flag.Any}
}
return &empty{}
}
}
func ByFlag(f flag.Flag) Append {
switch {
case f.IsInt():
return &Int{Flag: f | flag.Int}
case f.IsInt64():
return &Int64{Flag: f | flag.Int64}
case f.IsUint():
return &Uint{Flag: f | flag.Uint}
case f.IsUint64():
return &Uint64{Flag: f | flag.Uint64}
case f.IsFloat64():
return &Float64{Flag: f | flag.Float64}
case f.IsBool():
return &Bool{Flag: f | flag.Bool}
case f.IsDuration():
return &Duration{Flag: f | flag.Duration}
case f.IsTime():
return &Time{Flag: f | flag.Time}
case f.IsAny():
return &Any{Flag: f | flag.Any}
default:
return &String{}
}
}

159
list.go
View File

@@ -5,21 +5,28 @@ import (
"fmt"
"strings"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/input/validator"
"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
//nolint:gochecknoinits
func init() {
MustRegister(list())
}
const (
ArgumentNamespace = "namespace"
)
func list() *Command {
return &Command{
Name: CommandList,
@@ -32,75 +39,17 @@ You can also display the commands for a specific namespace:
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>{{ .Bin }} {{ .Name }} --format=xml</info>
`,
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
ns := in.Argument(ctx, "namespace").String()
format := in.Option(ctx, helpOptFormat).String()
des, err := descriptor.Find(format)
if err != nil {
return err
}
cmds := Commands()
commands := descriptor.Commands{
Namespace: ns,
Definition: Default(input.NewDefinition()),
}
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, ":", 2)
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)
}
return des.Commands(ctx, out, commands)
},
Configure: func(ctx context.Context, config *input.Definition) error {
Execute: executeList,
Configure: func(_ context.Context, cfg config.Definition) error {
formats := descriptor.Descriptors()
config.
SetArguments(
argument.New("namespace", "The namespace name"),
).
SetOptions(
option.New(helpOptFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
cfg.
Add(
arg.String(ArgumentNamespace, "The namespace name"),
option.String(OptionFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
option.Required,
option.Default(formats[0]),
option.Valid(
validator.NotBlank(0),
option.Default(value.New(formats[0])),
validator.Valid(
validator.NotBlank,
validator.Enum(formats...),
),
),
@@ -110,3 +59,69 @@ You can also output the information in other formats by using the <comment>--for
},
}
}
//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

@@ -5,13 +5,13 @@ import (
"errors"
"sync"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/console/output"
)
var ErrDescriptorNotFound = errors.New("descriptor not found")
//nolint: gochecknoglobals
//nolint:gochecknoglobals
var (
descriptors = map[string]Descriptor{
"txt": &txt{},
@@ -20,17 +20,19 @@ var (
)
type Command struct {
config.Options
Bin string
Name string
Description string
Help string
Definition *input.Definition
}
type Commands struct {
Namespace string
Definition *input.Definition
Commands []NSCommand
config.Options
Namespace string
Commands []NSCommand
}
type NSCommand struct {

View File

@@ -4,20 +4,19 @@ import (
"bytes"
"context"
"fmt"
"strconv"
"strings"
"text/template"
"time"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/value/flag"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/console/output"
)
const (
defaultSpace = 2
infoLen = 13
defaultSpace = 2
dashDelimiter = "-"
)
//nolint:gochecknoglobals
@@ -38,27 +37,41 @@ var (
{{ end -}}
<comment>Usage:</comment>
{{ .Name }} {{ synopsis .Definition }}
{{- definition .Definition }}
{{ .Name }} {{ synopsis .Options }}
{{ definition .Options }}
{{- help . }}
`))
txtListTempkate = template.Must(template.New("txt_list").
txtListTemplate = template.Must(template.New("txt_list").
Funcs(txtFunc).
Parse(`<comment>Usage:</comment>
command [options] [arguments]
{{- definition .Definition }}
{{ definition .Options }}
{{- commands .Commands -}}
`))
)
func TxtStyle() param.Option {
return arg.WithStyle(
arg.Style{
Start: "<comment>",
End: "</comment>",
},
arg.Style{
Start: "<info>",
End: "</info>",
},
)
}
type txt struct{}
func (t *txt) Command(ctx context.Context, out output.Output, cmd Command) error {
var tpl bytes.Buffer
if err := txtHelpTemplate.Execute(&tpl, cmd); err != nil {
return err
err := txtHelpTemplate.Execute(&tpl, cmd)
if err != nil {
return fmt.Errorf("execute txt help tpl:%w", err)
}
out.Println(ctx, tpl.String())
@@ -69,8 +82,9 @@ func (t *txt) Command(ctx context.Context, out output.Output, cmd Command) error
func (t *txt) Commands(ctx context.Context, out output.Output, cmds Commands) error {
var buf bytes.Buffer
if err := txtListTempkate.Execute(&buf, cmds); err != nil {
return err
err := txtListTemplate.Execute(&buf, cmds)
if err != nil {
return fmt.Errorf("execute txt list tpl:%w", err)
}
out.Println(ctx, buf.String())
@@ -78,78 +92,8 @@ func (t *txt) Commands(ctx context.Context, out output.Output, cmds Commands) er
return nil
}
func txtDefaultArray(v value.Value, f flag.Flag) string {
st := v.Strings()
switch {
case f.IsInt():
for _, i := range v.Ints() {
st = append(st, strconv.Itoa(i))
}
case f.IsInt64():
for _, i := range v.Int64s() {
st = append(st, strconv.FormatInt(i, 10))
}
case f.IsUint():
for _, u := range v.Uints() {
st = append(st, strconv.FormatUint(uint64(u), 10))
}
case f.IsUint64():
for _, u := range v.Uint64s() {
st = append(st, strconv.FormatUint(u, 10))
}
case f.IsFloat64():
for _, f := range v.Float64s() {
st = append(st, strconv.FormatFloat(f, 'g', -1, 64))
}
case f.IsDuration():
for _, d := range v.Durations() {
st = append(st, d.String())
}
case f.IsTime():
for _, d := range v.Times() {
st = append(st, d.Format(time.RFC3339))
}
}
return strings.Join(st, ",")
}
func txtDefault(v value.Value, f flag.Flag) []byte {
var buf bytes.Buffer
buf.WriteString("<comment> [default: ")
switch {
case f.IsArray():
buf.WriteString(txtDefaultArray(v, f))
case f.IsInt():
buf.WriteString(strconv.Itoa(v.Int()))
case f.IsInt64():
buf.WriteString(strconv.FormatInt(v.Int64(), 10))
case f.IsUint():
buf.WriteString(strconv.FormatUint(uint64(v.Uint()), 10))
case f.IsUint64():
buf.WriteString(strconv.FormatUint(v.Uint64(), 10))
case f.IsFloat64():
buf.WriteString(strconv.FormatFloat(v.Float64(), 'g', -1, 64))
case f.IsDuration():
buf.WriteString(v.Duration().String())
case f.IsTime():
buf.WriteString(v.Time().Format(time.RFC3339))
case f.IsAny():
buf.WriteString(fmt.Sprint(v.Any()))
default:
buf.WriteString(v.String())
}
buf.WriteString("]</comment>")
return buf.Bytes()
}
func txtCommands(cmds []NSCommand) string {
max := commandsTotalWidth(cmds)
width := commandsTotalWidth(cmds)
showNS := len(cmds) > 1
var buf bytes.Buffer
@@ -175,7 +119,7 @@ func txtCommands(cmds []NSCommand) string {
buf.WriteString(" <info>")
buf.WriteString(cmd.Name)
buf.WriteString("</info>")
buf.WriteString(strings.Repeat(" ", max-len(cmd.Name)+defaultSpace))
buf.WriteString(strings.Repeat(" ", width-len(cmd.Name)+defaultSpace))
buf.WriteString(cmd.Description)
buf.WriteString("\n")
}
@@ -199,124 +143,48 @@ func txtHelp(cmd Command) string {
return buf.String()
}
func txtDefinitionOption(maxLen int, def *input.Definition) string {
buf := bytes.Buffer{}
opts := def.Options()
buf.WriteString("\n\n<comment>Options:</comment>\n")
for _, name := range opts {
opt, _ := def.Option(name)
var op bytes.Buffer
op.WriteString(" <info>")
if opt.HasShort() {
op.WriteString("-")
op.WriteString(opt.Short)
op.WriteString(", ")
} else {
op.WriteString(" ")
}
op.WriteString("--")
op.WriteString(opt.Name)
if !opt.IsBool() {
if !opt.IsRequired() {
op.WriteString("[")
}
op.WriteString("=")
op.WriteString(strings.ToUpper(opt.Name))
if !opt.IsRequired() {
op.WriteString("]")
}
}
op.WriteString("</info>")
buf.Write(op.Bytes())
buf.WriteString(strings.Repeat(" ", maxLen+17-op.Len()))
buf.WriteString(opt.Description)
if opt.HasDefault() {
buf.Write(txtDefault(opt.Default, opt.Flag))
}
if opt.IsArray() {
buf.WriteString("<comment> (multiple values allowed)</comment>")
}
buf.WriteString("\n")
}
return buf.String()
}
func txtDefinition(def *input.Definition) string {
max := totalWidth(def)
func txtDefinition(options config.Options) string {
var buf bytes.Buffer
if args := def.Arguments(); len(args) > 0 {
buf.WriteString("\n\n<comment>Arguments:</comment>\n")
for pos := range args {
var ab bytes.Buffer
arg, _ := def.Argument(pos)
ab.WriteString(" <info>")
ab.WriteString(arg.Name)
ab.WriteString("</info>")
ab.WriteString(strings.Repeat(" ", max+infoLen+defaultSpace-ab.Len()))
buf.Write(ab.Bytes())
buf.WriteString(arg.Description)
if arg.HasDefault() {
buf.Write(txtDefault(arg.Default, arg.Flag))
}
}
}
if opts := def.Options(); len(opts) > 0 {
buf.WriteString(txtDefinitionOption(max, def))
err := arg.NewDump().Reference(&buf, options)
if err != nil {
return err.Error()
}
return buf.String()
}
func txtSynopsis(def *input.Definition) string {
func txtSynopsis(options config.Options) string {
def := arg.NewViews(options, nil)
var buf bytes.Buffer
if len(def.Options()) > 0 {
buf.WriteString("[options] ")
}
if buf.Len() > 0 && len(def.Arguments()) > 0 {
args := def.Arguments()
if buf.Len() > 0 && len(args) > 0 {
buf.WriteString("[--]")
}
var opt int
for pos := range def.Arguments() {
for _, arg := range args {
buf.WriteString(" ")
arg, _ := def.Argument(pos)
if !arg.IsRequired() {
if !option.IsRequired(arg) {
buf.WriteString("[")
opt++
}
buf.WriteString("<")
buf.WriteString(arg.Name)
buf.WriteString(arg.Name(dashDelimiter))
buf.WriteString(">")
if arg.IsArray() {
if option.IsSlice(arg) {
buf.WriteString("...")
}
}
@@ -327,47 +195,15 @@ func txtSynopsis(def *input.Definition) string {
}
func commandsTotalWidth(cmds []NSCommand) int {
var max int
var width int
for _, ns := range cmds {
for _, cmd := range ns.Commands {
if len(cmd.Name) > max {
max = len(cmd.Name)
if len(cmd.Name) > width {
width = len(cmd.Name)
}
}
}
return max
}
func totalWidth(def *input.Definition) int {
var max int
for pos := range def.Arguments() {
arg, _ := def.Argument(pos)
l := len(arg.Name)
if l > max {
max = l
}
}
for _, name := range def.Options() {
opt, _ := def.Option(name)
l := len(opt.Name) + 6
if !opt.IsBool() {
l = l*2 + 1
}
if opt.HasDefault() {
l += 2
}
if l > max {
max = l
}
}
return max
return width
}

View File

@@ -8,7 +8,6 @@ import (
"gitoa.ru/go-4devs/console/output/style"
)
//nolint: gochecknoglobals
var re = regexp.MustCompile(`<(([a-z][^<>]+)|/([a-z][^<>]+)?)>`)
func WithStyle(styles func(string) (style.Style, error)) func(*Formatter) {
@@ -18,22 +17,22 @@ func WithStyle(styles func(string) (style.Style, error)) func(*Formatter) {
}
func New(opts ...func(*Formatter)) *Formatter {
f := &Formatter{
formatter := &Formatter{
styles: style.Find,
}
for _, opt := range opts {
opt(f)
opt(formatter)
}
return f
return formatter
}
type Formatter struct {
styles func(string) (style.Style, error)
}
func (a *Formatter) Format(ctx context.Context, msg string) string {
func (a *Formatter) Format(_ context.Context, msg string) string {
var (
out bytes.Buffer
cur int
@@ -51,8 +50,8 @@ func (a *Formatter) Format(ctx context.Context, msg string) string {
err error
)
switch {
case tag[0:1] == "/":
switch tag[0:1] {
case "/":
st, err = a.styles(tag[1:])
if err == nil {
out.WriteString(st.Set(style.ActionUnset))

View File

@@ -8,6 +8,8 @@ import (
)
func TestFormatter(t *testing.T) {
t.Parallel()
ctx := context.Background()
formatter := formatter.New()

View File

@@ -8,6 +8,8 @@ import (
)
func TestNone(t *testing.T) {
t.Parallel()
ctx := context.Background()
none := formatter.None()

View File

@@ -2,7 +2,7 @@ package label
type Key string
func (k Key) Any(v interface{}) KeyValue {
func (k Key) Any(v any) KeyValue {
return KeyValue{
Key: k,
Value: AnyValue(v),

View File

@@ -30,7 +30,7 @@ func (k KeyValue) String() string {
return string(k.Key) + "=\"" + k.Value.String() + "\""
}
func Any(k string, v interface{}) KeyValue {
func Any(k string, v any) KeyValue {
return Key(k).Any(v)
}

View File

@@ -17,14 +17,14 @@ const (
type Value struct {
vtype Type
value interface{}
value any
}
func (v Value) String() string {
return fmt.Sprint(v.value)
}
func AnyValue(v interface{}) Value {
func AnyValue(v any) Value {
return Value{vtype: TypeAny, value: v}
}

View File

@@ -18,7 +18,7 @@ func writeError(_ int, err error) {
type Output func(ctx context.Context, verb verbosity.Verbosity, msg string, args ...label.KeyValue) (int, error)
func (o Output) Print(ctx context.Context, args ...interface{}) {
func (o Output) Print(ctx context.Context, args ...any) {
writeError(o(ctx, verbosity.Norm, fmt.Sprint(args...)))
}
@@ -26,15 +26,15 @@ func (o Output) PrintKV(ctx context.Context, msg string, kv ...label.KeyValue) {
writeError(o(ctx, verbosity.Norm, msg, kv...))
}
func (o Output) Printf(ctx context.Context, format string, args ...interface{}) {
func (o Output) Printf(ctx context.Context, format string, args ...any) {
writeError(o(ctx, verbosity.Norm, fmt.Sprintf(format, args...)))
}
func (o Output) Println(ctx context.Context, args ...interface{}) {
func (o Output) Println(ctx context.Context, args ...any) {
writeError(o(ctx, verbosity.Norm, fmt.Sprintln(args...)))
}
func (o Output) Info(ctx context.Context, args ...interface{}) {
func (o Output) Info(ctx context.Context, args ...any) {
writeError(o(ctx, verbosity.Info, fmt.Sprint(args...)))
}
@@ -42,7 +42,7 @@ func (o Output) InfoKV(ctx context.Context, msg string, kv ...label.KeyValue) {
writeError(o(ctx, verbosity.Info, msg, kv...))
}
func (o Output) Debug(ctx context.Context, args ...interface{}) {
func (o Output) Debug(ctx context.Context, args ...any) {
writeError(o(ctx, verbosity.Debug, fmt.Sprint(args...)))
}
@@ -50,7 +50,7 @@ func (o Output) DebugKV(ctx context.Context, msg string, kv ...label.KeyValue) {
writeError(o(ctx, verbosity.Debug, msg, kv...))
}
func (o Output) Trace(ctx context.Context, args ...interface{}) {
func (o Output) Trace(ctx context.Context, args ...any) {
writeError(o(ctx, verbosity.Trace, fmt.Sprint(args...)))
}
@@ -63,15 +63,13 @@ func (o Output) Write(b []byte) (int, error) {
}
func (o Output) Writer(ctx context.Context, verb verbosity.Verbosity) io.Writer {
return verbosityWriter{ctx, o, verb}
return verbosityWriter(func(b []byte) (int, error) {
return o(ctx, verb, string(b))
})
}
type verbosityWriter struct {
ctx context.Context
out Output
verb verbosity.Verbosity
}
type verbosityWriter func(b []byte) (int, error)
func (w verbosityWriter) Write(b []byte) (int, error) {
return w.out(w.ctx, w.verb, string(b))
return w(b)
}

View File

@@ -28,13 +28,13 @@ const (
type Option string
func (o Option) Apply(action int) string {
v := string(o)
out := string(o)
switch action {
case ActionSet:
return v[0:1]
return out[0:1]
case ActionUnset:
return v[1:]
return out[1:]
}
return ""

View File

@@ -7,7 +7,7 @@ import (
"sync"
)
//nolint: gochecknoglobals
//nolint:gochecknoglobals
var (
styles = map[string]Style{
"error": {Foreground: White, Background: Red},
@@ -50,7 +50,8 @@ func Register(name string, style Style) error {
}
func MustRegister(name string, style Style) {
if err := Register(name, style); err != nil {
err := Register(name, style)
if err != nil {
panic(err)
}
}

View File

@@ -1,6 +1,6 @@
package verbosity
//go:generate stringer -type=Verbosity -linecomment
//go:generate go tool stringer -type=Verbosity -linecomment
type Verbosity int

View File

@@ -20,9 +20,9 @@ const _Verbosity_name = "quietnorminfodebugtrace"
var _Verbosity_index = [...]uint8{0, 5, 9, 13, 18, 23}
func (i Verbosity) String() string {
i -= -1
if i < 0 || i >= Verbosity(len(_Verbosity_index)-1) {
return "Verbosity(" + strconv.FormatInt(int64(i+-1), 10) + ")"
idx := int(i) - -1
if i < -1 || idx >= len(_Verbosity_index)-1 {
return "Verbosity(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Verbosity_name[_Verbosity_index[i]:_Verbosity_index[i+1]]
return _Verbosity_name[_Verbosity_index[idx]:_Verbosity_index[idx+1]]
}

View File

@@ -40,7 +40,12 @@ func FormatString(_ verbosity.Verbosity, msg string, kv ...label.KeyValue) strin
}
func New(w io.Writer, format func(verb verbosity.Verbosity, msg string, kv ...label.KeyValue) string) Output {
return func(ctx context.Context, verb verbosity.Verbosity, msg string, kv ...label.KeyValue) (int, error) {
return fmt.Fprint(w, format(verb, msg, kv...))
return func(_ context.Context, verb verbosity.Verbosity, msg string, kv ...label.KeyValue) (int, error) {
out, err := fmt.Fprint(w, format(verb, msg, kv...))
if err != nil {
return 0, fmt.Errorf("writer fprint:%w", err)
}
return out, nil
}
}

View File

@@ -10,6 +10,8 @@ import (
)
func TestNew(t *testing.T) {
t.Parallel()
ctx := context.Background()
buf := bytes.Buffer{}
wr := output.New(&buf, output.FormatString)

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"regexp"
"sort"
"strings"
"sync"
)
@@ -14,43 +13,17 @@ const (
CommandList = "list"
)
var (
ErrNotFound = errors.New("command not found")
ErrCommandNil = errors.New("console: Register command is nil")
ErrCommandDuplicate = errors.New("console: duplicate command")
)
//nolint: gochecknoglobals
//nolint:gochecknoglobals
var (
commandsMu sync.RWMutex
commands = make(map[string]*Command)
findCommand = regexp.MustCompile("([^:]+|)")
)
type ErrorAlternatives struct {
alt []string
err error
}
func (e ErrorAlternatives) Error() string {
return fmt.Sprintf("%s, alternatives: [%s]", e.err, strings.Join(e.alt, ","))
}
func (e ErrorAlternatives) Is(err error) bool {
return errors.Is(e.err, err)
}
func (e ErrorAlternatives) Unwrap() error {
return e.err
}
func (e ErrorAlternatives) Alternatives() []string {
return e.alt
}
// MustRegister register command or panic if err.
func MustRegister(cmd *Command) {
if err := Register(cmd); err != nil {
err := Register(cmd)
if err != nil {
panic(err)
}
}
@@ -115,7 +88,7 @@ func Find(name string) (*Command, error) {
cmdRegexp, err := regexp.Compile("^" + nameRegexp + "$")
if err != nil {
return nil, err
return nil, fmt.Errorf("find by regexp:%w", err)
}
for name := range commands {
@@ -134,7 +107,7 @@ func Find(name string) (*Command, error) {
names[i] = findCommands[i].Name
}
return nil, ErrorAlternatives{alt: names, err: ErrNotFound}
return nil, AlternativesError{Alt: names, Err: ErrNotFound}
}
return nil, ErrNotFound

View File

@@ -7,6 +7,8 @@ import (
)
func TestFind(t *testing.T) {
t.Parallel()
cases := map[string]string{
"fdevs:console:test": "fdevs:console:test",
"fd:c:t": "fdevs:console:test",
@@ -18,13 +20,13 @@ func TestFind(t *testing.T) {
for name, ex := range cases {
res, err := console.Find(name)
if err != nil {
t.Errorf("expect <nil> err, got:%s", err)
t.Errorf("%v expect <nil> err, got:%s", name, err)
continue
}
if res.Name != ex {
t.Errorf("expect: %s, got: %s", ex, res)
t.Errorf("%v expect: %s, got: %s", name, ex, res)
}
}
}