Browse Source

first commit

pull/2/head
andrey1s 4 years ago
commit
0bd6f67397
  1. 24
      .drone.yml
  2. 17
      .gitignore
  3. 36
      .golangci.yml
  4. 19
      LICENSE
  5. 187
      README.md
  6. 141
      app.go
  7. 87
      app_test.go
  8. 196
      command.go
  9. 122
      command_test.go
  10. 135
      console.go
  11. 45
      console_test.go
  12. 89
      descriptor/descriptor.go
  13. 371
      descriptor/txt.go
  14. 30
      doc.go
  15. BIN
      example/bin/console
  16. 25
      example/cmd/cancel/main.go
  17. 21
      example/cmd/console/main.go
  18. 12
      example/cmd/single/main.go
  19. 33
      example/pkg/command/args.go
  20. 36
      example/pkg/command/create_user.go
  21. 30
      example/pkg/command/create_user_test.go
  22. 34
      example/pkg/command/hello.go
  23. 22
      example/pkg/command/hidden.go
  24. 50
      example/pkg/command/long.go
  25. 21
      example/pkg/command/namespace.go
  26. 3
      go.mod
  27. 0
      go.sum
  28. 92
      help.go
  29. 48
      input/argument.go
  30. 26
      input/argument/option.go
  31. 211
      input/argv/input.go
  32. 87
      input/array/input.go
  33. 95
      input/definition.go
  34. 50
      input/error.go
  35. 76
      input/flag.go
  36. 47
      input/flag_string.go
  37. 21
      input/input.go
  38. 53
      input/option.go
  39. 35
      input/option/helpers.go
  40. 44
      input/option/option.go
  41. 58
      input/value.go
  42. 21
      input/value/any.go
  43. 44
      input/value/bool.go
  44. 44
      input/value/duration.go
  45. 90
      input/value/empty.go
  46. 44
      input/value/float64.go
  47. 44
      input/value/int.go
  48. 44
      input/value/int64.go
  49. 17
      input/value/read.go
  50. 39
      input/value/string.go
  51. 44
      input/value/time.go
  52. 44
      input/value/uint.go
  53. 44
      input/value/uint64.go
  54. 84
      input/value/value.go
  55. 105
      input/wrap/input.go
  56. 111
      list.go
  57. 5
      output/formatter/ansi.go
  58. 79
      output/formatter/formatter.go
  59. 26
      output/formatter/formatter_test.go
  60. 14
      output/formatter/none.go
  61. 27
      output/formatter/none_test.go
  62. 59
      output/key.go
  63. 63
      output/kv.go
  64. 77
      output/output.go
  65. 51
      output/style/color.go
  66. 88
      output/style/style.go
  67. 57
      output/value.go
  68. 23
      output/verbosity/norm.go
  69. 22
      output/wrap/formatter.go
  70. 44
      output/writer/output.go
  71. 49
      output/writer/output_test.go
  72. 141
      register.go
  73. 30
      register_test.go
  74. 16
      validator/enum.go
  75. 24
      validator/enum_test.go
  76. 37
      validator/error.go
  77. 109
      validator/not_blank.go
  78. 109
      validator/not_blank_test.go
  79. 15
      validator/valid.go
  80. 28
      validator/valid_test.go

24
.drone.yml

@ -0,0 +1,24 @@
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: {}

17
.gitignore

@ -0,0 +1,17 @@
# ---> Go
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

36
.golangci.yml

@ -0,0 +1,36 @@
linters-settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
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

19
LICENSE

@ -0,0 +1,19 @@
MIT License Copyright (c) 2020 go-4devs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

187
README.md

@ -0,0 +1,187 @@
# Console
## Creating a Command
Commands are defined in struct extending `pkg/command/create_user.go`. For example, you may want a command to create a user:
```go
package command
import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
)
func Createuser() *console.Command {
return &console.Command{
Name: "app:create-user",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
return nil
},
}
}
```
## Configure command
```go
func Createuser() *console.Command {
return &console.Command{
//...
Description: "Creates a new user.",
Help: "This command allows you to create a user...",
}
}
```
## Add arguments
```go
func Createuser(required bool) *console.Command {
return &console.Command{
//....
Configure: func(ctx context.Context, cfg *input.Definition) error {
var opts []func(*input.Argument)
if required {
opts = append(opts, argument.Required)
}
cfg.SetArgument("password", "User password", opts...)
return nil
},
}
}
```
## Registering the Command
`cmd/console/main.go`
```go
package main
import (
"context"
"gitoa.ru/go-4devs/console"
"pkg/command"
)
func main() {
console.
New().
Add(
command.Createuser(false),
).
Execute(context.Background())
}
```
## Executing the Command
build command `go build -o bin/console cmd/console/main.go`
run command `bin/console app:create-user``
## Console Output
The Execute field has access to the output stream to write messages to the console:
```go
func Createuser(required bool) *console.Command {
return &console.Command{
// ....
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
// outputs a message followed by a "\n"
out.Println(ctx, "User Creator")
out.Println(ctx, "Whoa!")
// outputs a message without adding a "\n" at the end of the line
out.Print(ctx, "You are about to ", "create a user.")
return nil
},
}
}
```
Now, try build and executing the command:
```bash
bin/console app:create-user
User Creator
Whoa!
You are about to create a user.
```
## Console Input
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 {
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...)
return nil
},
Execute: func(ctx context.Context, in input.Input, 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())
return nil
},
}
}
```
Now, you can pass the username to the command:
```bash
bin/console app:create-user AwesomeUsername
User Creator
Username: AwesomeUsername
```
## Testing Commands
```go
package command_test
import (
"bytes"
"context"
"testing"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
"gitoa.ru/go-4devs/console/input/array"
"gitoa.ru/go-4devs/console/output/writer"
)
func TestCreateUser(t *testing.T) {
ctx := context.Background()
in := array.New(array.Argument("username", "andrey"))
buf := bytes.Buffer{}
out := writer.Buffer(&buf)
console.Run(ctx, command.CreateUser(false), in, out)
expect := `User Creator
Username: andrey
`
if expect != buf.String() {
t.Errorf("expect: %s, got:%s", expect, buf.String())
}
}
```

141
app.go

@ -0,0 +1,141 @@
package console
import (
"context"
"os"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argv"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/writer"
)
// WithOutput sets outpu,^ by default output os.Stdout.
func WithOutput(out output.Output) func(*App) {
return func(a *App) {
a.out = out
}
}
// WithInput sets input, by default creates inpur by os.Args.
func WithInput(in input.Input) func(*App) {
return func(a *App) {
a.in = in
}
}
// 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
}
}
// WithExit sets exit callback by default os.Exit.
func WithExit(f func(int)) func(*App) {
return func(a *App) {
a.exit = f
}
}
// New creates and configure new console app.
func New(opts ...func(*App)) *App {
a := &App{
out: writer.Stdout(),
exit: os.Exit,
}
for _, opt := range opts {
opt(a)
}
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 = argv.New(os.Args[skip:])
}
return a
}
// App is collection of command and configure env.
type App struct {
cmds []*Command
out output.Output
in input.Input
skipArgv int
exit func(int)
}
// Add add or replace command.
func (a *App) Add(cmds ...*Command) *App {
a.cmds = append(a.cmds, cmds...)
return a
}
// Execute run the command by name and arguments.
func (a *App) Execute(ctx context.Context) {
for _, cmd := range a.cmds {
register(cmd)
}
cmd, err := a.find(ctx)
if err != nil {
a.printError(ctx, err)
if err := a.list(ctx); err != nil {
a.printError(ctx, err)
}
a.exit(1)
}
a.exec(ctx, cmd)
}
func (a *App) exec(ctx context.Context, cmd *Command) {
if err := Run(ctx, cmd, a.in, a.out); err != nil {
a.printError(ctx, err)
a.exit(1)
}
a.exit(0)
}
func (a *App) find(_ context.Context) (*Command, error) {
if len(os.Args) < 2 || os.Args[1][1] == '-' {
return Find(CommandList)
}
name := os.Args[1]
return Find(name)
}
func (a *App) list(ctx context.Context) error {
cmd, err := Find(CommandHelp)
if err != nil {
return err
}
a.in.SetArgument("command_name", value.New(CommandList))
return Run(ctx, cmd, a.in, a.out)
}
func (a *App) printError(ctx context.Context, err error) {
a.out.Println(ctx, "<error>\n\n ", err, "\n</error>")
}

87
app_test.go

@ -0,0 +1,87 @@
package console_test
import (
"context"
"os"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
)
//nolint: lll
func ExampleNew_help() {
ctx := context.Background()
os.Args = []string{
"bin/console",
"test:command",
"-h",
"--no-ansi",
}
console.New(console.WithExit(func(int) {})).
Add(
Command(),
).
Execute(ctx)
// Output:
// Description:
// test command
//
// Usage:
// test:command [options] [--] [<test_argument>]
//
// Arguments:
// 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
}
func ExampleNew_list() {
ctx := context.Background()
os.Args = []string{
"bin/console",
"--no-ansi",
}
console.New(console.WithExit(func(int) {})).
Add(
Command(),
command.Hello(),
command.Args(),
command.Namespace(),
).
Execute(ctx)
// Output:
// Usage:
// 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
//
// Available commands:
// help Displays help for a command
// list Lists commands
// app
// app:start example command in other namespace
// fdevs
// fdevs:console:arg Understanding how Console Arguments and Options Are Handled
// fdevs:console:hello example hello command
// fdevs:console:test test command
// test
// test:command test command
}

196
command.go

@ -0,0 +1,196 @@
package console
import (
"context"
"fmt"
"gitoa.ru/go-4devs/console/input"
"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
Option func(*Command)
)
// WithPrepare append middleware for configuration command.
func WithPrepare(p ...Prepare) Option {
return func(c *Command) {
if c.Prepare != nil {
p = append([]Prepare{c.Prepare}, p...)
}
c.Prepare = ChainPrepare(p...)
}
}
// WithHandle append middleware for executed command.
func WithHandle(h ...Handle) Option {
return func(c *Command) {
if c.Handle != nil {
h = append([]Handle{c.Handle}, h...)
}
c.Handle = ChainHandle(h...)
}
}
// WithHidden sets hidden command.
func WithHidden(v bool) Option {
return func(c *Command) {
c.Hidden = v
}
}
// WithName sets name command.
func WithName(n string) Option {
return func(c *Command) {
c.Name = n
}
}
type Command struct {
// The name of the command.
Name string
// A short description of the usage of this command.
Description string
// A longer explanation of how the command works.
Help string
// Vervion command.
Version string
// Boolean to hide this command from help or completion.
Hidden bool
// Configures the current command.
Configure Configure
// The middleware for configures current command.
Prepare Prepare
// The function to call when this command is invoked.
Execute Action
// The middleware for executes current command.
Handle Handle
}
func (c *Command) String() string {
return fmt.Sprintf("name: %s, version: %s", c.Name, c.Version)
}
// With creates new command by parent and options.
func (c *Command) With(opts ...Option) *Command {
cmd := &Command{
Name: c.Name,
Description: c.Description,
Help: c.Help,
Version: c.Version,
Hidden: c.Hidden,
Configure: c.Configure,
Prepare: c.Prepare,
Execute: c.Execute,
Handle: c.Handle,
}
for _, opt := range opts {
opt(cmd)
}
return cmd
}
// Run run command with input and output.
func (c *Command) Run(ctx context.Context, in input.Input, out output.Output) error {
if c.Handle != nil {
return c.Handle(ctx, in, out, c.Execute)
}
return c.Execute(ctx, in, out)
}
// Init configures command.
func (c *Command) Init(ctx context.Context, cfg *input.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 nil
})
case c.Configure != nil:
return c.Configure(ctx, cfg)
default:
return nil
}
}
// ChainPrepare creates middleware for configures command.
func ChainPrepare(prepare ...Prepare) Prepare {
n := len(prepare)
if n == 1 {
return prepare[0]
}
if n > 1 {
lastI := n - 1
return func(ctx context.Context, def *input.Definition, next Configure) error {
var (
chainHandler func(context.Context, *input.Definition) error
curI int
)
chainHandler = func(currentCtx context.Context, currentDef *input.Definition) error {
if curI == lastI {
return next(currentCtx, currentDef)
}
curI++
err := prepare[curI](currentCtx, currentDef, chainHandler)
curI--
return err
}
return prepare[0](ctx, def, chainHandler)
}
}
return func(ctx context.Context, cfg *input.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 {
return handlers[0]
}
if n > 1 {
lastI := n - 1
return func(ctx context.Context, in input.Input, out output.Output, next Action) error {
var (
chainHandler func(context.Context, input.Input, output.Output) error
curI int
)
chainHandler = func(currentCtx context.Context, currentIn input.Input, currentOut output.Output) error {
if curI == lastI {
return next(currentCtx, currentIn, currentOut)
}
curI++
err := handlers[curI](currentCtx, currentIn, currentOut, chainHandler)
curI--
return err
}
return handlers[0](ctx, in, out, chainHandler)
}
}
return func(ctx context.Context, in input.Input, out output.Output, next Action) error {
return next(ctx, in, out)
}
}

122
command_test.go

@ -0,0 +1,122 @@
package console_test
import (
"context"
"strings"
"sync/atomic"
"testing"
"time"
"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/array"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/writer"
)
//nolint: gochecknoinits
func init() {
console.MustRegister(Command().With(console.WithName("fdevs:console:test")))
console.MustRegister(command.Args())
}
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 {
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",
)
return nil
},
Configure: func(ctx context.Context, def *input.Definition) error {
def.
SetArguments(
input.NewArgument("test_argument", "test argument"),
).
SetOptions(
input.NewOption("string", "array string", option.Array),
option.Bool("bool", "test bool option"),
option.Duration("duration", "test duration with default", option.Default(time.Second)),
)
return nil
},
}
}
func TestChainPrepare(t *testing.T) {
var cnt int32
ctx := context.Background()
def := input.NewDefinition()
prepare := func(ctx context.Context, def *input.Definition, n console.Configure) error {
atomic.AddInt32(&cnt, 1)
return n(ctx, def)
}
configure := func(context.Context, *input.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++ {
prepares[p] = prepare
}
cnt = 0
chain := console.ChainPrepare(prepares...)
if err := chain(ctx, def, configure); err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int32(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
}
func TestChainHandle(t *testing.T) {
var cnt int32
ctx := context.Background()
in := array.New()
out := writer.Stdout()
handle := func(ctx context.Context, in input.Input, out output.Output, next console.Action) error {
atomic.AddInt32(&cnt, 1)
return next(ctx, in, out)
}
action := func(context.Context, input.Input, 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++ {
handles[p] = handle
}
cnt = 0
chain := console.ChainHandle(handles...)
if err := chain(ctx, in, out, action); err != nil {
t.Errorf("expected nil err, got: %s", err)
}
if cnt != int32(i) {
t.Fatalf("expected: call prepare 1, got: %d ", cnt)
}
}
}

135
console.go

@ -0,0 +1,135 @@
package console
import (
"context"
"errors"
"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/console/output"
"gitoa.ru/go-4devs/console/output/verbosity"
"gitoa.ru/go-4devs/console/output/wrap"
)
const (
verboseTrace = 3
verboseDebug = 2
verboseInfo = 3
)
// Execute the current command with option.
func Execute(ctx context.Context, cmd *Command, opts ...func(*App)) {
opts = append([]func(*App){WithSkipArgs(1)}, opts...)
New(opts...).exec(ctx, cmd)
}
// Run current command by input and output/
func Run(ctx context.Context, cmd *Command, in input.Input, out output.Output) error {
def := input.NewDefinition()
if err := cmd.Init(ctx, def); 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")
return showHelp(ctx, cmd, in, wrap.Ansi(out))
}
out = ansi(ctx, in, out)
out = verbose(ctx, in, out)
if in.Option(ctx, "version").Bool() {
version := cmd.Version
if version == "" {
version = "unknown"
}
out.Println(ctx, "command <comment>", cmd.Name, "</comment> version: <info>", version, "</info>")
return nil
}
if in.Option(ctx, "help").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 {
switch {
case in.Option(ctx, "ansi").Bool():
out = wrap.Ansi(out)
case in.Option(ctx, "no-ansi").Bool():
out = wrap.None(out)
case lookupEnv("NO_COLOR"):
out = wrap.None(out)
default:
out = wrap.Ansi(out)
}
return out
}
func lookupEnv(name string) bool {
v, has := os.LookupEnv(name)
return has && v == "true"
}
func verbose(ctx context.Context, in input.Input, out output.Output) output.Output {
switch {
case in.Option(ctx, "quiet").Bool():
out = verbosity.Quiet()
default:
v := in.Option(ctx, "verbose").Bools()
switch {
case len(v) == verboseInfo:
out = verbosity.Verb(out, output.VerbosityInfo)
case len(v) == verboseDebug:
out = verbosity.Verb(out, output.VerbosityDebug)
case len(v) >= verboseTrace:
out = verbosity.Verb(out, output.VerbosityTrace)
default:
out = verbosity.Verb(out, output.VerbosityNorm)
}
}
return out
}
func showHelp(ctx context.Context, cmd *Command, in input.Input, out output.Output) error {
in.SetArgument(HelpArgumentCommandName, value.New(cmd.Name))
in.SetOption("help", value.New(false))
if _, err := Find(cmd.Name); errors.Is(err, ErrNotFound) {
register(cmd)
}
help, err := Find(CommandHelp)
if err != nil {
return err
}
return Run(ctx, help, in, 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",
"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")),
)
}

45
console_test.go

@ -0,0 +1,45 @@
package console_test
import (
"context"
"fmt"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input/array"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/output/writer"
)
func ExampleRun() {
cmd := Command()
ctx := context.Background()
out := writer.Stdout()
in := array.New()
err := console.Run(ctx, cmd, in, out)
fmt.Println("err:", err)
// Output:
// test argument:
// bool option:false
// duration option with default:1s
// array string:[]
// err: <nil>
}
func ExampleExecute() {
cmd := Command()
ctx := context.Background()
in := array.New()
// 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"))
console.Execute(ctx, cmd, console.WithInput(in), console.WithExit(func(int) {}))
// Output:
// test argument:argument value
// bool option:true
// duration option with default:1s
// array string:[same value,other value]
}

89
descriptor/descriptor.go

@ -0,0 +1,89 @@
package descriptor
import (
"context"
"errors"
"sync"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
)
var ErrDescriptorNotFound = errors.New("descriptor not found")
//nolint: gochecknoglobals
var (
descriptors = map[string]Descriptor{
"txt": &txt{},
}
descriptorMu sync.Mutex
)
type Command struct {
Bin string
Name string
Description string
Help string
Definition *input.Definition
}
type Commands struct {
Namespace string
Definition *input.Definition
Commands []NSCommand
}
type NSCommand struct {
Name string
Commands []ShortCommand
}
func (n *NSCommand) Append(name, desc string) {
n.Commands = append(n.Commands, ShortCommand{Name: name, Description: desc})
}
type ShortCommand struct {
Name string
Description string
}
type Descriptor interface {
Command(ctx context.Context, out output.Output, cmd Command) error
Commands(ctx context.Context, out output.Output, cmds Commands) error
}
func Find(name string) (Descriptor, error) {
descriptorMu.Lock()
defer descriptorMu.Unlock()
if d, has := descriptors[name]; has {
return d, nil
}
return nil, ErrDescriptorNotFound
}
func Descriptors() []string {
names := make([]string, 0, len(descriptors))
for name := range descriptors {
names = append(names, name)
}
return names
}
func Register(name string, descriptor Descriptor) {
descriptorMu.Lock()
defer descriptorMu.Unlock()
if descriptor == nil {
panic("console: Register descriptor is nil")
}
if _, has := descriptors[name]; has {
panic("console: Register called twice for descriptor " + name)
}
descriptors[name] = descriptor
}

371
descriptor/txt.go

@ -0,0 +1,371 @@
package descriptor
import (
"bytes"
"context"
"fmt"
"strconv"
"strings"
"text/template"
"time"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/output"
)
const (
defaultSpace = 2
infoLen = 13
)
//nolint:gochecknoglobals
var (
txtFunc = template.FuncMap{
"synopsis": txtSynopsis,
"definition": txtDefinition,
"help": txtHelp,
"commands": txtCommands,
}
txtHelpTemplate = template.Must(template.New("txt_template").
Funcs(txtFunc).
Parse(`
{{- if .Description -}}
<comment>Description:</comment>
{{ .Description }}
{{ end -}}
<comment>Usage:</comment>
{{ .Name }} {{ synopsis .Definition }}
{{- definition .Definition }}
{{- help . }}
`))
txtListTempkate = template.Must(template.New("txt_list").
Funcs(txtFunc).
Parse(`<comment>Usage:</comment>
command [options] [arguments]
{{- definition .Definition }}
{{- commands .Commands -}}
`))
)
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
}
out.Println(ctx, tpl.String())
return nil
}
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
}
out.Println(ctx, buf.String())
return nil
}
func txtDefaultArray(val input.Value, flag input.Flag) string {
st := val.Strings()
switch {
case flag.IsInt():
for _, i := range val.Ints() {
st = append(st, strconv.Itoa(i))
}
case flag.IsInt64():
for _, i := range val.Int64s() {
st = append(st, strconv.FormatInt(i, 10))
}
case flag.IsUint():
for _, u := range val.Uints() {
st = append(st, strconv.FormatUint(uint64(u), 10))
}
case flag.IsUint64():
for _, u := range val.Uint64s() {
st = append(st, strconv.FormatUint(u, 10))
}
case flag.IsFloat64():
for _, f := range val.Float64s() {
st = append(st, strconv.FormatFloat(f, 'g', -1, 64))
}
case flag.IsDuration():
for _, d := range val.Durations() {
st = append(st, d.String())
}
case flag.IsTime():
for _, d := range val.Times() {
st = append(st, d.Format(time.RFC3339))
}
}
return strings.Join(st, ",")
}
func txtDefault(val input.Value, flag input.Flag) []byte {
var buf bytes.Buffer
buf.WriteString("<comment> [default: ")
switch {
case flag.IsArray():
buf.WriteString(txtDefaultArray(val, flag))
case flag.IsInt():
buf.WriteString(strconv.Itoa(val.Int()))
case flag.IsInt64():
buf.WriteString(strconv.FormatInt(val.Int64(), 10))
case flag.IsUint():
buf.WriteString(strconv.FormatUint(uint64(val.Uint()), 10))
case flag.IsUint64():
buf.WriteString(strconv.FormatUint(val.Uint64(), 10))
case flag.IsFloat64():
buf.WriteString(strconv.FormatFloat(val.Float64(), 'g', -1, 64))
case flag.IsDuration():
buf.WriteString(val.Duration().String())
case flag.IsTime():
buf.WriteString(val.Time().Format(time.RFC3339))
case flag.IsAny():
buf.WriteString(fmt.Sprint(val.Any()))
default:
buf.WriteString(val.String())
}
buf.WriteString("]</comment>")
return buf.Bytes()
}
func txtCommands(cmds []NSCommand) string {
max := commandsTotalWidth(cmds)
showNS := len(cmds) > 1
var buf bytes.Buffer
buf.WriteString("\n<comment>Available commands")
if len(cmds) == 1 && cmds[0].Name != "" {
buf.WriteString("for the \"")
buf.WriteString(cmds[0].Name)
buf.WriteString(`" namespace`)
}
buf.WriteString(":</comment>\n")
for _, ns := range cmds {
if ns.Name != "" && showNS {
buf.WriteString("<comment>")
buf.WriteString(ns.Name)
buf.WriteString("</comment>\n")
}
for _, cmd := range ns.Commands {
buf.WriteString(" <info>")
buf.WriteString(cmd.Name)
buf.WriteString("</info>")
buf.WriteString(strings.Repeat(" ", max-len(cmd.Name)+defaultSpace))
buf.WriteString(cmd.Description)
buf.WriteString("\n")
}
}
return buf.String()
}
func txtHelp(cmd Command) string {
if cmd.Help == "" {
return ""
}
tpl := template.Must(template.New("help").Parse(cmd.Help))
var buf bytes.Buffer
buf.WriteString("\n<comment>Help:</comment>")
_ = tpl.Execute(&buf, cmd)
return buf.String()
}
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)
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))
}
return buf.String()
}
func txtSynopsis(def *input.Definition) string {
var buf bytes.Buffer
if len(def.Options()) > 0 {
buf.WriteString("[options] ")
}
if buf.Len() > 0 && len(def.Arguments()) > 0 {
buf.WriteString("[--]")
}
var opt int
for pos := range def.Arguments() {
buf.WriteString(" ")
arg, _ := def.Argument(pos)
if !arg.IsRequired() {
buf.WriteString("[")
opt++
}
buf.WriteString("<")
buf.WriteString(arg.Name)
buf.WriteString(">")
if arg.IsArray() {
buf.WriteString("...")
}
}
buf.WriteString(strings.Repeat("]", opt))
return buf.String()
}
func commandsTotalWidth(cmds []NSCommand) int {
var max int
for _, ns := range cmds {
for _, cmd := range ns.Commands {
if len(cmd.Name) > max {
max = 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
}

30
doc.go

@ -0,0 +1,30 @@
// Package console eases the creation of beautiful and testable command line interfaces.
// 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())
// }
// Then, you can register the commands using Add():
// package main
//
// import (
// "context"
//
// "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

BIN
example/bin/console

Binary file not shown.

25
example/cmd/cancel/main.go

@ -0,0 +1,25 @@
package main
import (
"context"
"os"
"os/signal"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan os.Signal, 1)
defer close(ch)
signal.Notify(ch, os.Interrupt)
go func() {
<-ch
cancel()
}()
console.Execute(ctx, command.Long())
}

21
example/cmd/console/main.go

@ -0,0 +1,21 @@
package main
import (
"context"
"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(),
command.CreateUser(false),
).
Execute(context.Background())
}

12
example/cmd/single/main.go

@ -0,0 +1,12 @@
package main
import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
)
func main() {
console.Execute(context.Background(), command.Hello())
}

33
example/pkg/command/args.go

@ -0,0 +1,33 @@
package command
import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/output"
)
func Args() *console.Command {
return &console.Command{
Name: "fdevs:console:arg",
Description: "Understanding how Console Arguments and Options Are Handled",
Configure: func(ctx context.Context, def *input.Definition) error {
def.SetOptions(
option.Bool("foo", "foo option", option.Short("f")),
input.NewOption("bar", "required bar option", option.Required, option.Short("b")),
input.NewOption("cat", "cat option", option.Short("c")),
)
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>")
return nil
},
}
}

36
example/pkg/command/create_user.go

@ -0,0 +1,36 @@
package command
import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/output"
)
func CreateUser(required bool) *console.Command {
return &console.Command{
Name: "app:create-user",
Description: "Creates a new user.",
Help: "This command allows you to create a user...",
Configure: func(ctx context.Context, cfg *input.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...)
return nil
},
Execute: func(ctx context.Context, in input.Input, 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())
return nil
},
}
}

30
example/pkg/command/create_user_test.go

@ -0,0 +1,30 @@
package command_test
import (
"bytes"
"context"
"testing"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/example/pkg/command"
"gitoa.ru/go-4devs/console/input/array"
"gitoa.ru/go-4devs/console/output/writer"
)
func TestCreateUser(t *testing.T) {
ctx := context.Background()
in := array.New(array.Argument("username", "andrey"))
buf := bytes.Buffer{}
out := writer.Buffer(&buf)
err := console.Run(ctx, command.CreateUser(false), in, out)
if err != nil {
t.Fatalf("expect nil err, got :%s", err)
}
expect := "User Creator\nUsername: andrey\n"
if expect != buf.String() {
t.Errorf("expect: %s, got:%s", expect, buf.String())
}
}

34
example/pkg/command/hello.go

@ -0,0 +1,34 @@
package command
import (
"context"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/argument"
"gitoa.ru/go-4devs/console/output"
)
func Hello() *console.Command {
return &console.Command{
Name: "fdevs:console:hello",
Description: "example hello command",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
name := in.Argument(ctx, "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")
return nil
},
Configure: func(_ context.Context, def *input.Definition) error {
def.SetArguments(
input.NewArgument("name", "Same name", argument.Default("World")),
)
return nil
},
}
}

22
example/pkg/command/hidden.go

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

50
example/pkg/command/long.go

@ -0,0 +1,50 @@
package command
import (
"context"
"time"
"gitoa.ru/go-4devs/console"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/validator"
)
const defaultTimeout = time.Second * 30
// Long example of a command that takes a long time to run.
func Long() *console.Command {
return &console.Command{
Name: "fdevs:command:long",
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
timeout := in.Option(ctx, "timeout").Duration()
timer := time.NewTimer(timeout)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
out.Println(ctx, "ticker: <info>", t, "</info>")
case <-timer.C:
out.Println(ctx, "<error>stop timer</error>")
return nil
case <-ctx.Done():
out.Println(ctx, "<info>cancel context</info>")
return nil
}
}
},
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(input.ValueDuration)),
))
return nil
},
}
}

21
example/pkg/command/namespace.go

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

3
go.mod

@ -0,0 +1,3 @@
module gitoa.ru/go-4devs/console
go 1.15

0
go.sum

92
help.go

@ -0,0 +1,92 @@
package console
import (
"context"
"fmt"
"os"
"strings"
"gitoa.ru/go-4devs/console/descriptor"
"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"
"gitoa.ru/go-4devs/console/validator"
)
//nolint: gochecknoinits
func init() {
MustRegister(help())
}
const (
HelpArgumentCommandName = "command_name"
helpOptFormat = "format"
)
func help() *Command {
return &Command{
Name: CommandHelp,
Description: `Displays help for a command`,
Help: `
The <info>{{ .Name }}</info> command displays help for a given command:
<info>{{ .Bin }} {{ .Name }} list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>{{ .Bin }} {{ .Name }} --format=xml list</info>
To display the list of available commands, please use the <info>list</info> command.
`,
Execute: func(ctx context.Context, in input.Input, out output.Output) error {
var err error
name := in.Argument(ctx, HelpArgumentCommandName).String()
format := in.Option(ctx, helpOptFormat).String()
des, err := descriptor.Find(format)
if err != nil {
return err
}
cmd, err := Find(name)
if err != nil {
return err
}
def := input.NewDefinition()
if err := cmd.Init(ctx, Default(def)); err != nil {
return err
}
var bin string
if len(os.Args) > 0 {
bin = os.Args[0]
}
return des.Command(ctx, out, descriptor.Command{
Bin: bin,
Name: cmd.Name,
Description: cmd.Description,
Help: cmd.Help,
Definition: def,
})
},
Configure: func(ctx context.Context, config *input.Definition) error {
formats := descriptor.Descriptors()
config.
SetArguments(
input.NewArgument(HelpArgumentCommandName, "The command name", argument.Default("help")),
).
SetOptions(
input.NewOption(helpOptFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
option.Required,
option.Default(formats[0]),
option.Valid(
validator.NotBlank(input.ValueString),
validator.Enum(formats...),
),
),
)
return nil
},
}
}

48
input/argument.go

@ -0,0 +1,48 @@
package input
func NewArgument(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
Flag Flag
Valid []func(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) error {
for _, valid := range a.Valid {
if err := valid(v); err != nil {
return ErrorArgument(a.Name, err)
}
}
return nil
}

26
input/argument/option.go

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

211
input/argv/input.go

@ -0,0 +1,211 @@
package argv
import (
"context"
"fmt"
"strings"
"sync"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/wrap"
)
const doubleDash = `--`
var _ input.ReadInput = (*Input)(nil)
func WithErrorHandle(h func(error) error) func(*Input) {
return func(i *Input) {
i.errorHandle = h
}
}
func New(args []string, opts ...func(*Input)) *wrap.Input {
i := &Input{
args: args,
arguments: make(map[string]input.AppendValue),
options: make(map[string]input.AppendValue),
errorHandle: func(err error) error {
return err
},
}
for _, opt := range opts {
opt(i)
}
return &wrap.Input{ReadInput: i}
}
type Input struct {
args []string
arguments map[string]input.AppendValue
options map[string]input.AppendValue
mu sync.RWMutex
errorHandle func(error) error
}
func (i *Input) ReadOption(ctx context.Context, name string) (input.Value, error) {
if v, ok := i.options[name]; ok {
return v, nil
}
return nil, input.ErrNotFound
}
func (i *Input) SetOption(name string, val input.Value) {
i.mu.Lock()
defer i.mu.Unlock()
i.options[name] = &value.Read{Value: val}
}
func (i *Input) ReadArgument(ctx context.Context, name string) (input.Value, error) {
if v, ok := i.arguments[name]; ok {
return v, nil
}
return nil, input.ErrNotFound
}
func (i *Input) SetArgument(name string, val input.Value) {
i.mu.Lock()
defer i.mu.Unlock()
i.arguments[name] = &value.Read{Value: val}
}
func (i *Input) Bind(ctx context.Context, def *input.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 '-'", input.ErrInvalidName)
}
err = i.parseShortOption(arg[1:], def)
default:
err = i.parseArgument(arg, def)
}
if err != nil {
if herr := i.errorHandle(err); herr != nil {
return herr
}
}
}
return nil
}
func (i *Input) parseLongOption(arg string, def *input.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 input.ErrorOption(name, err)
}
return i.appendOption(name, value, opt)
}
func (i *Input) appendOption(name string, data *string, opt input.Option) error {
v, ok := i.options[name]
if ok && !opt.IsArray() {
return fmt.Errorf("%w: got: array, expect: %s", input.ErrUnexpectedType, input.Type(opt.Flag))
}
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 input.ErrorOption(name, input.ErrRequired)
}
if !ok {
v = value.ByFlag(opt.Flag)
i.options[name] = v
}
if err := v.Append(val); err != nil {
return input.ErrorOption(name, err)
}
return nil
}
func (i *Input) parseShortOption(arg string, def *input.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 *Input) parseArgument(arg string, def *input.Definition) error {
opt, err := def.Argument(len(i.arguments))
if err != nil {
return err
}
v, ok := i.arguments[opt.Name]
if !ok {
v = value.ByFlag(opt.Flag)
i.arguments[opt.Name] = v
}
if err := v.Append(arg); err != nil {
return input.ErrorArgument(opt.Name, err)
}
return nil
}

87
input/array/input.go

@ -0,0 +1,87 @@
package array
import (
"context"
"sync"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/input/wrap"
)
var _ input.ReadInput = (*Input)(nil)
func Argument(name string, v interface{}) func(*Input) {
return func(i *Input) {
i.args[name] = value.New(v)
}
}
func Option(name string, v interface{}) func(*Input) {
return func(i *Input) {
i.opt[name] = value.New(v)
}
}
func New(opts ...func(*Input)) *wrap.Input {
i := &Input{
args: make(map[string]input.Value),
opt: make(map[string]input.Value),
}
for _, opt := range opts {
opt(i)
}
return &wrap.Input{ReadInput: i}
}
type Input struct {
args map[string]input.Value
opt map[string]input.Value
mu sync.Mutex
}
func (i *Input) ReadOption(_ context.Context, name string) (input.Value, error) {
if o, has := i.opt[name]; has {
return o, nil
}
return nil, input.ErrorOption(name, input.ErrNotFound)
}
func (i *Input) HasOption(name string) bool {
_, has := i.opt[name]
return has
}
func (i *Input) SetOption(name string, val input.Value) {
i.mu.Lock()
i.opt[name] = val
i.mu.Unlock()
}
func (i *Input) ReadArgument(_ context.Context, name string) (input.Value, error) {
if a, has := i.args[name]; has {
return a, nil
}
return nil, input.ErrorArgument(name, input.ErrNotFound)
}
func (i *Input) HasArgument(name string) bool {
_, has := i.args[name]
return has
}
func (i *Input) SetArgument(name string, val input.Value) {
i.mu.Lock()
i.args[name] = val
i.mu.Unlock()
}
func (i *Input) Bind(_ context.Context, def *input.Definition) error {
return nil
}

95
input/definition.go

@ -0,0 +1,95 @@
package input
func NewDefinition() *Definition {
return &Definition{
options: make(map[string]Option),
args: make(map[string]Argument),
short: make(map[string]string),
}
}
type Definition struct {
options map[string]Option
posOpt []string
args map[string]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)) *Definition {
return d.SetOptions(NewOption(name, description, opts...))
}
func (d *Definition) SetOptions(opts ...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)) *Definition {
return d.SetArguments(NewArgument(name, description, opts...))
}
func (d *Definition) SetArguments(args ...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, error) {
if len(d.posArgs) == 0 {
return Argument{}, ErrNoArgs
}
lastPos := len(d.posArgs) - 1
if lastPos < pos {
arg := d.args[d.posArgs[lastPos]]
if arg.IsArray() {
return arg, nil
}
return Argument{}, ErrToManyArgs
}
return d.args[d.posArgs[pos]], nil
}
func (d *Definition) ShortOption(short string) (Option, error) {
name, ok := d.short[short]
if !ok {
return Option{}, ErrNotFound
}
return d.Option(name)
}
func (d *Definition) Option(name string) (Option, error) {
if opt, ok := d.options[name]; ok {
return opt, nil
}
return Option{}, ErrNotFound
}

50
input/error.go

@ -0,0 +1,50 @@
package input
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")
)
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 ErrorOption(name string, err error) Error {
return Error{
name: name,
err: err,
t: "option",
}
}
func ErrorArgument(name string, err error) Error {
return Error{
name: name,
err: err,
t: "argument",
}
}

76
input/flag.go

@ -0,0 +1,76 @@
package input
//go:generate stringer -type=Flag -linecomment
type Flag int
const (
ValueString Flag = 0 // string
ValueRequired Flag = 1 << iota // required
ValueArray // array
ValueInt // int
ValueInt64 // int64
ValueUint // uint
ValueUint64 // uint64
ValueFloat64 // float64
ValueBool // bool
ValueDuration // duration
ValueTime // time
ValueAny // any
)
func (f Flag) Type() Flag {
return Type(f)
}
func (f Flag) With(v Flag) Flag {
return f | v
}
func (f Flag) IsString() bool {
return f|ValueRequired|ValueArray^ValueRequired^ValueArray == 0
}
func (f Flag) IsRequired() bool {
return f&ValueRequired > 0
}
func (f Flag) IsArray() bool {
return f&ValueArray > 0
}
func (f Flag) IsInt() bool {
return f&ValueInt > 0
}
func (f Flag) IsInt64() bool {
return f&ValueInt64 > 0
}
func (f Flag) IsUint() bool {
return f&ValueUint > 0
}
func (f Flag) IsUint64() bool {
return f&ValueUint64 > 0
}
func (f Flag) IsFloat64() bool {
return f&ValueFloat64 > 0
}
func (f Flag) IsBool() bool {
return f&ValueBool > 0
}
func (f Flag) IsDuration() bool {
return f&ValueDuration > 0
}
func (f Flag) IsTime() bool {
return f&ValueTime > 0
}
func (f Flag) IsAny() bool {
return f&ValueAny > 0
}

47
input/flag_string.go

@ -0,0 +1,47 @@
// Code generated by "stringer -type=Flag -linecomment"; DO NOT EDIT.
package input
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[ValueString-0]
_ = x[ValueRequired-2]
_ = x[ValueArray-4]
_ = x[ValueInt-8]
_ = x[ValueInt64-16]
_ = x[ValueUint-32]
_ = x[ValueUint64-64]
_ = x[ValueFloat64-128]
_ = x[ValueBool-256]
_ = x[ValueDuration-512]
_ = x[ValueTime-1024]
_ = x[ValueAny-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) + ")"
}

21
input/input.go

@ -0,0 +1,21 @@
package input
import (
"context"
)
type ReadInput interface {
Bind(ctx context.Context, def *Definition) error
ReadOption(ctx context.Context, name string) (Value, error)
SetOption(name string, value Value)
ReadArgument(ctx context.Context, name string) (Value, error)
SetArgument(name string, value Value)
}
type Input interface {
Option(ctx context.Context, name string) Value
Argument(ctx context.Context, name string) Value
ReadInput
}

53
input/option.go

@ -0,0 +1,53 @@
package input
func NewOption(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
Default Value
Valid []func(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) error {
for _, valid := range o.Valid {
if err := valid(v); err != nil {
return ErrorOption(o.Name, err)
}
}
return nil
}

35
input/option/helpers.go

@ -0,0 +1,35 @@
package option
import "gitoa.ru/go-4devs/console/input"
func Bool(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueBool))...)
}
func Duration(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueDuration))...)
}
func Float64(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueFloat64))...)
}
func Int(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueInt))...)
}
func Int64(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueInt64))...)
}
func Time(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueTime))...)
}
func Uint(name, description string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, description, append(opts, Value(input.ValueUint))...)
}
func Uint64(name, descriontion string, opts ...func(*input.Option)) input.Option {
return input.NewOption(name, descriontion, append(opts, Value(input.ValueUint64))...)
}

44
input/option/option.go

@ -0,0 +1,44 @@
package option
import (
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
)
func Required(o *input.Option) {
o.Flag |= input.ValueRequired
}
func Default(in interface{}) func(*input.Option) {
return func(o *input.Option) {
o.Default = value.New(in)
}
}
func Short(s string) func(*input.Option) {
return func(o *input.Option) {
o.Short = s
}
}
func Array(o *input.Option) {
o.Flag |= input.ValueArray
}
func Value(flag input.Flag) func(*input.Option) {
return func(o *input.Option) {
o.Flag |= flag
}
}
func Flag(in input.Flag) func(*input.Option) {
return func(o *input.Option) {
o.Flag = in
}
}
func Valid(f ...func(input.Value) error) func(*input.Option) {
return func(o *input.Option) {
o.Valid = f
}
}

58
input/value.go

@ -0,0 +1,58 @@
package input
import (
"time"
)
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 AppendValue interface {
Value
Append(string) error
}
func Type(flag Flag) Flag {
switch {
case (flag & ValueInt) > 0:
return ValueInt
case (flag & ValueInt64) > 0:
return ValueInt64
case (flag & ValueUint) > 0:
return ValueUint
case (flag & ValueUint64) > 0:
return ValueUint64
case (flag & ValueFloat64) > 0:
return ValueFloat64
case (flag & ValueBool) > 0:
return ValueBool
case (flag & ValueDuration) > 0:
return ValueDuration
case (flag & ValueTime) > 0:
return ValueTime
case (flag & ValueAny) > 0:
return ValueAny
default:
return ValueString
}
}

21
input/value/any.go

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

44
input/value/bool.go

@ -0,0 +1,44 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input"
)
type Bool struct {
Empty
Val []bool
Flag input.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&input.ValueArray > 0 {
return b.Bools()
}
return b.Bool()
}

44
input/value/duration.go

@ -0,0 +1,44 @@
package value
import (
"time"
"gitoa.ru/go-4devs/console/input"
)
type Duration struct {
Empty
Val []time.Duration
Flag input.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&input.ValueArray > 0 {
return d.Durations()
}
return d.Duration()
}

90
input/value/empty.go

@ -0,0 +1,90 @@
package value
import (
"fmt"
"time"
"gitoa.ru/go-4devs/console/input"
)
type Empty struct{}
func (e *Empty) Append(string) error {
return fmt.Errorf("%w: in empty value", input.ErrInvalidName)
}
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
}

44
input/value/float64.go

@ -0,0 +1,44 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input"
)
type Float64 struct {
Empty
Val []float64
Flag input.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&input.ValueFloat64 > 0 {
return f.Float64s()
}
return f.Float64()
}

44
input/value/int.go

@ -0,0 +1,44 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input"
)
type Int struct {
Empty
Val []int
Flag input.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&input.ValueArray > 0 {
return i.Ints()
}
return i.Int()
}

44
input/value/int64.go

@ -0,0 +1,44 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input"
)
type Int64 struct {
Empty
Val []int64
Flag input.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&input.ValueArray > 0 {
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
}

17
input/value/read.go

@ -0,0 +1,17 @@
package value
import (
"fmt"
"gitoa.ru/go-4devs/console/input"
)
var _ input.AppendValue = (*Read)(nil)
type Read struct {
input.Value
}
func (r *Read) Append(string) error {
return fmt.Errorf("%w: read value", input.ErrInvalidName)
}

39
input/value/string.go

@ -0,0 +1,39 @@
package value
import "gitoa.ru/go-4devs/console/input"
type String struct {
Empty
Val []string
Flag input.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()
}

44
input/value/time.go

@ -0,0 +1,44 @@
package value
import (
"time"
"gitoa.ru/go-4devs/console/input"
)
type Time struct {
Empty
Val []time.Time
Flag input.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&input.ValueArray > 0 {
return t.Times()
}
return t.Time()
}

44
input/value/uint.go

@ -0,0 +1,44 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input"
)
type Uint struct {
Empty
Val []uint
Flag input.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&input.ValueArray > 0 {
return u.Uints()
}
return u.Uint()
}

44
input/value/uint64.go

@ -0,0 +1,44 @@
package value
import (
"strconv"
"gitoa.ru/go-4devs/console/input"
)
type Uint64 struct {
Empty
Val []uint64
Flag input.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&input.ValueArray > 0 {
return u.Uint64s()
}
return u.Uint64()
}

84
input/value/value.go

@ -0,0 +1,84 @@
package value
import (
"time"
"gitoa.ru/go-4devs/console/input"
)
//nolint: gocyclo
func New(v interface{}) input.Value {
switch val := v.(type) {
case string:
return &String{Val: []string{val}, Flag: input.ValueString}
case int:
return &Int{Val: []int{val}, Flag: input.ValueInt}
case int64:
return &Int64{Val: []int64{val}, Flag: input.ValueInt64}
case uint:
return &Uint{Val: []uint{val}, Flag: input.ValueUint}
case uint64:
return &Uint64{Val: []uint64{val}, Flag: input.ValueUint64}
case float64:
return &Float64{Val: []float64{val}, Flag: input.ValueFloat64}
case bool:
return &Bool{Val: []bool{val}, Flag: input.ValueBool}
case time.Duration:
return &Duration{Val: []time.Duration{val}, Flag: input.ValueDuration}
case time.Time:
return &Time{Val: []time.Time{val}, Flag: input.ValueTime}
case []int64:
return &Int64{Val: val, Flag: input.ValueInt64 | input.ValueArray}
case []uint:
return &Uint{Val: val, Flag: input.ValueUint | input.ValueArray}
case []uint64:
return &Uint64{Val: val, Flag: input.ValueUint64 | input.ValueArray}
case []float64:
return &Float64{Val: val, Flag: input.ValueFloat64 | input.ValueArray}
case []bool:
return &Bool{Val: val, Flag: input.ValueBool | input.ValueArray}
case []time.Duration:
return &Duration{Val: val, Flag: input.ValueDuration | input.ValueArray}
case []time.Time:
return &Time{Val: val, Flag: input.ValueTime | input.ValueArray}
case []string:
return &String{Val: val, Flag: input.ValueString | input.ValueArray}
case []int:
return &Int{Val: val, Flag: input.ValueInt | input.ValueArray}
case []interface{}:
return &Any{Val: val, Flag: input.ValueAny | input.ValueArray}
case input.Value:
return val
default:
if v != nil {
return &Any{Val: []interface{}{v}, Flag: input.ValueAny}
}
return &Empty{}
}
}
func ByFlag(flag input.Flag) input.AppendValue {
switch {
case flag.IsInt():
return &Int{Flag: flag | input.ValueInt}
case flag.IsInt64():
return &Int64{Flag: flag | input.ValueInt64}
case flag.IsUint():
return &Uint{Flag: flag | input.ValueUint}
case flag.IsUint64():
return &Uint64{Flag: flag | input.ValueUint64}
case flag.IsFloat64():
return &Float64{Flag: flag | input.ValueFloat64}
case flag.IsBool():
return &Bool{Flag: flag | input.ValueBool}
case flag.IsDuration():
return &Duration{Flag: flag | input.ValueDuration}
case flag.IsTime():
return &Time{Flag: flag | input.ValueTime}
case flag.IsAny():
return &Any{Flag: flag | input.ValueAny}
default:
return &String{}
}
}

105
input/wrap/input.go

@ -0,0 +1,105 @@
package wrap
import (
"context"
"errors"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
)
type Input struct {
input.ReadInput
}
func (i *Input) Option(ctx context.Context, name string) input.Value {
if v, err := i.ReadOption(ctx, name); err == nil {
return v
}
return &value.Empty{}
}
func (i *Input) Argument(ctx context.Context, name string) input.Value {
if v, err := i.ReadArgument(ctx, name); err == nil {
return v
}
return &value.Empty{}
}
func (i *Input) Bind(ctx context.Context, def *input.Definition) error {
if err := i.ReadInput.Bind(ctx, def); err != nil {
return err
}
if err := i.bindArguments(ctx, def); err != nil {
return err
}
return i.bindOptions(ctx, def)
}
func (i *Input) bindOptions(ctx context.Context, def *input.Definition) error {
for _, name := range def.Options() {
opt, err := def.Option(name)
if err != nil {
return err
}
v, err := i.ReadOption(ctx, name)
if err != nil && !errors.Is(err, input.ErrNotFound) {
return input.ErrorOption(name, err)
}
if err == nil {
if err := opt.Validate(v); err != nil {
return input.ErrorOption(name, err)
}
continue
}
if opt.IsRequired() && !opt.HasDefault() {
return input.ErrorOption(name, input.ErrRequired)
}
if opt.HasDefault() {
i.SetOption(name, opt.Default)
}
}
return nil
}
func (i *Input) bindArguments(ctx context.Context, def *input.Definition) error {
for pos, name := range def.Arguments() {
arg, err := def.Argument(pos)
if err != nil {
return err
}
v, err := i.ReadArgument(ctx, name)
if err != nil && !errors.Is(err, input.ErrNotFound) {
return input.ErrorArgument(name, err)
}
if err == nil {
if err := arg.Validate(v); err != nil {
return input.ErrorArgument(name, err)
}
continue
}
if arg.IsRequired() && !arg.HasDefault() {
return input.ErrorArgument(name, input.ErrRequired)
}
if arg.HasDefault() {
i.SetArgument(name, arg.Default)
}
}
return nil
}

111
list.go

@ -0,0 +1,111 @@
package console
import (
"context"
"fmt"
"strings"
"gitoa.ru/go-4devs/console/descriptor"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/option"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/validator"
)
const defaultLenNamespace = 2
//nolint: gochecknoinits
func init() {
MustRegister(list())
}
func list() *Command {
return &Command{
Name: CommandList,
Description: "Lists commands",
Help: `
The <info>{{ .Name }}</info> command lists all commands:
<info>{{ .Bin }} {{ .Name }}</info>
You can also display the commands for a specific namespace:
<info>{{ .Bin }} {{ .Name }} test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>{{ .Bin }} {{ .Name }} --format=xml</info>
`,
Execute: 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 {
formats := descriptor.Descriptors()
config.
SetArguments(
input.NewArgument("namespace", "The namespace name"),
).
SetOptions(
input.NewOption(helpOptFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")),
option.Required,
option.Default(formats[0]),
option.Valid(
validator.NotBlank(0),
validator.Enum(formats...),
),
),
)
return nil
},
}
}

5
output/formatter/ansi.go

@ -0,0 +1,5 @@
package formatter
func Ansi() *Formatter {
return New()
}

79
output/formatter/formatter.go

@ -0,0 +1,79 @@
package formatter
import (
"bytes"
"context"
"regexp"
"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) {
return func(f *Formatter) {
f.styles = styles
}
}
func New(opts ...func(*Formatter)) *Formatter {
f := &Formatter{
styles: style.Find,
}
for _, opt := range opts {
opt(f)
}
return f
}
type Formatter struct {
styles func(string) (style.Style, error)
}
func (a *Formatter) Format(ctx context.Context, msg string) string {
var (
out bytes.Buffer
cur int
)
for _, idx := range re.FindAllStringIndex(msg, -1) {
tag := msg[idx[0]+1 : idx[1]-1]
if cur < idx[0] {
out.WriteString(msg[cur:idx[0]])
}
var (
st style.Style
err error
)
switch {
case tag[0:1] == "/":
st, err = a.styles(tag[1:])
if err == nil {
out.WriteString(st.Set(style.ActionUnset))
}
default:
st, err = a.styles(tag)
if err == nil {
out.WriteString(st.Set(style.ActionSet))
}
}
if err != nil {
cur = idx[0]
} else {
cur = idx[1]
}
}
if len(msg) > cur {
out.WriteString(msg[cur:])
}
return out.String()
}

26
output/formatter/formatter_test.go

@ -0,0 +1,26 @@
package formatter_test
import (
"context"
"testing"
"gitoa.ru/go-4devs/console/output/formatter"
)
func TestFormatter(t *testing.T) {
ctx := context.Background()
formatter := formatter.New()
cases := map[string]string{
"<info>info message</info>": "\x1b[32minfo message\x1b[39m",
"<info><command></info>": "\x1b[32m<command>\x1b[39m",
"<html>...</html>": "<html>...</html>",
}
for msg, ex := range cases {
got := formatter.Format(ctx, msg)
if ex != got {
t.Errorf("ivalid expected:%#v, got: %#v", ex, got)
}
}
}

14
output/formatter/none.go

@ -0,0 +1,14 @@
package formatter
import "gitoa.ru/go-4devs/console/output/style"
func None() *Formatter {
return New(
WithStyle(func(name string) (style.Style, error) {
if _, err := style.Find(name); err != nil {
return style.Empty(), err
}
return style.Empty(), nil
}))
}

27
output/formatter/none_test.go

@ -0,0 +1,27 @@
package formatter_test
import (
"context"
"testing"
"gitoa.ru/go-4devs/console/output/formatter"
)
func TestNone(t *testing.T) {
ctx := context.Background()
none := formatter.None()
cases := map[string]string{
"<info>message info</info>": "message info",
"<error>message error</error>": "message error",
"<comment><scheme></comment>": "<scheme>",
"<body>body</body>": "<body>body</body>",
}
for msg, ex := range cases {
got := none.Format(ctx, msg)
if ex != got {
t.Errorf("expect:%#v, got:%#v", ex, got)
}
}
}

59
output/key.go

@ -0,0 +1,59 @@
package output
type Key string
func (k Key) Any(v interface{}) KeyValue {
return KeyValue{
Key: k,
Value: AnyValue(v),
}
}
func (k Key) Bool(v bool) KeyValue {
return KeyValue{
Key: k,
Value: BoolValue(v),
}
}
func (k Key) Int(v int) KeyValue {
return KeyValue{
Key: k,
Value: IntValue(v),
}
}
func (k Key) Int64(v int64) KeyValue {
return KeyValue{
Key: k,
Value: Int64Value(v),
}
}
func (k Key) Uint(v uint) KeyValue {
return KeyValue{
Key: k,
Value: UintValue(v),
}
}
func (k Key) Uint64(v uint64) KeyValue {
return KeyValue{
Key: k,
Value: Uint64Value(v),
}
}
func (k Key) Float64(v float64) KeyValue {
return KeyValue{
Key: k,
Value: Float64Value(v),
}
}
func (k Key) String(v string) KeyValue {
return KeyValue{
Key: k,
Value: StringValue(v),
}
}

63
output/kv.go

@ -0,0 +1,63 @@
package output
import (
"fmt"
"strings"
)
var (
_ fmt.Stringer = KeyValue{}
_ fmt.Stringer = KeyValues{}
)
type KeyValues []KeyValue
func (kv KeyValues) String() string {
s := make([]string, len(kv))
for i, v := range kv {
s[i] = v.String()
}
return strings.Join(s, ", ")
}
type KeyValue struct {
Key Key
Value Value
}
func (k KeyValue) String() string {
return string(k.Key) + "=\"" + k.Value.String() + "\""
}
func Any(k string, v interface{}) KeyValue {
return Key(k).Any(v)
}
func Bool(k string, v bool) KeyValue {
return Key(k).Bool(v)
}
func Int(k string, v int) KeyValue {
return Key(k).Int(v)
}
func Int64(k string, v int64) KeyValue {
return Key(k).Int64(v)
}
func Uint(k string, v uint) KeyValue {
return Key(k).Uint(v)
}
func Uint64(k string, v uint64) KeyValue {
return Key(k).Uint64(v)
}
func Float64(k string, v float64) KeyValue {
return Key(k).Float64(v)
}
func String(k string, v string) KeyValue {
return Key(k).String(v)
}

77
output/output.go

@ -0,0 +1,77 @@
package output
import (
"context"
"fmt"
"io"
)
type Verbosity int
const (
VerbosityQuiet Verbosity = iota - 1
VerbosityNorm
VerbosityInfo
VerbosityDebug
VerbosityTrace
)
type Output func(ctx context.Context, verb Verbosity, msg string, args ...KeyValue) (int, error)
func (o Output) Print(ctx context.Context, args ...interface{}) {
o(ctx, VerbosityNorm, fmt.Sprint(args...))
}
func (o Output) PrintKV(ctx context.Context, msg string, kv ...KeyValue) {
o(ctx, VerbosityNorm, msg, kv...)
}
func (o Output) Printf(ctx context.Context, format string, args ...interface{}) {
o(ctx, VerbosityNorm, fmt.Sprintf(format, args...))
}
func (o Output) Println(ctx context.Context, args ...interface{}) {
o(ctx, VerbosityNorm, fmt.Sprintln(args...))
}
func (o Output) Info(ctx context.Context, args ...interface{}) {
o(ctx, VerbosityInfo, fmt.Sprint(args...))
}
func (o Output) InfoKV(ctx context.Context, msg string, kv ...KeyValue) {
o(ctx, VerbosityInfo, msg, kv...)
}
func (o Output) Debug(ctx context.Context, args ...interface{}) {
o(ctx, VerbosityDebug, fmt.Sprint(args...))
}
func (o Output) DebugKV(ctx context.Context, msg string, kv ...KeyValue) {
o(ctx, VerbosityDebug, msg, kv...)
}
func (o Output) Trace(ctx context.Context, args ...interface{}) {
o(ctx, VerbosityTrace, fmt.Sprint(args...))
}
func (o Output) TraceKV(ctx context.Context, msg string, kv ...KeyValue) {
o(ctx, VerbosityTrace, msg, kv...)
}
func (o Output) Write(b []byte) (int, error) {
return o(context.Background(), VerbosityNorm, string(b))
}
func (o Output) Writer(ctx context.Context, verb Verbosity) io.Writer {
return verbosityWriter{ctx, o, verb}
}
type verbosityWriter struct {
ctx context.Context
out Output
verb Verbosity
}
func (w verbosityWriter) Write(b []byte) (int, error) {
return w.out(w.ctx, w.verb, string(b))
}

51
output/style/color.go

@ -0,0 +1,51 @@
package style
const (
Black Color = "0"
Red Color = "1"
Green Color = "2"
Yellow Color = "3"
Blue Color = "4"
Magenta Color = "5"
Cyan Color = "6"
White Color = "7"
Default Color = "9"
)
const (
Bold Option = "122"
Underscore Option = "424"
Blink Option = "525"
Reverse Option = "727"
Conseal Option = "828"
)
const (
ActionSet = 1
ActionUnset = 2
)
type Option string
func (o Option) Apply(action int) string {
v := string(o)
switch action {
case ActionSet:
return v[0:1]
case ActionUnset:
return v[1:]
}
return ""
}
type Color string
func (c Color) Apply(action int) string {
if action == ActionSet {
return string(c)
}
return string(Default)
}

88
output/style/style.go

@ -0,0 +1,88 @@
package style
import (
"errors"
"fmt"
"strings"
"sync"
)
//nolint: gochecknoglobals
var (
styles = map[string]Style{
"error": {Foreground: White, Background: Red},
"info": {Foreground: Green},
"comment": {Foreground: Yellow},
"question": {Foreground: Black, Background: Cyan},
}
stylesMu sync.Mutex
empty = Style{}
)
var (
ErrNotFound = errors.New("console: style not found")
ErrDuplicateStyle = errors.New("console: Register called twice")
)
func Empty() Style {
return empty
}
func Find(name string) (Style, error) {
if st, has := styles[name]; has {
return st, nil
}
return empty, ErrNotFound
}
func Register(name string, style Style) error {
stylesMu.Lock()
defer stylesMu.Unlock()
if _, has := styles[name]; has {
return fmt.Errorf("%w for style %s", ErrDuplicateStyle, name)
}
styles[name] = style
return nil
}
func MustRegister(name string, style Style) {
if err := Register(name, style); err != nil {
panic(err)
}
}
type Style struct {
Background Color
Foreground Color
Options []Option
}
func (s Style) Apply(msg string) string {
return s.Set(ActionSet) + msg + s.Set(ActionUnset)
}
func (s Style) Set(action int) string {
style := make([]string, 0, len(s.Options))
if s.Foreground != "" {
style = append(style, "3"+s.Foreground.Apply(action))
}
if s.Background != "" {
style = append(style, "4"+s.Background.Apply(action))
}
for _, opt := range s.Options {
style = append(style, opt.Apply(action))
}
if len(style) == 0 {
return ""
}
return "\033[" + strings.Join(style, ";") + "m"
}

57
output/value.go

@ -0,0 +1,57 @@
package output
import "fmt"
type Type int
const (
TypeAny Type = iota
TypeBool
TypeInt
TypeInt64
TypeUint
TypeUint64
TypeFloat64
TypeString
)
type Value struct {
vtype Type
value interface{}
}
func (v Value) String() string {
return fmt.Sprint(v.value)
}
func AnyValue(v interface{}) Value {
return Value{vtype: TypeAny, value: v}
}
func BoolValue(v bool) Value {
return Value{vtype: TypeBool, value: v}
}
func IntValue(v int) Value {
return Value{vtype: TypeInt, value: v}
}
func Int64Value(v int64) Value {
return Value{vtype: TypeInt64, value: v}
}
func UintValue(v uint) Value {
return Value{vtype: TypeUint, value: v}
}
func Uint64Value(v uint64) Value {
return Value{vtype: TypeUint64, value: v}
}
func Float64Value(v float64) Value {
return Value{vtype: TypeFloat64, value: v}
}
func StringValue(v string) Value {
return Value{vtype: TypeString, value: v}
}

23
output/verbosity/norm.go

@ -0,0 +1,23 @@
package verbosity
import (
"context"
"gitoa.ru/go-4devs/console/output"
)
func Verb(out output.Output, verb output.Verbosity) output.Output {
return func(ctx context.Context, v output.Verbosity, msg string, kv ...output.KeyValue) (int, error) {
if verb >= v {
return out(ctx, v, msg, kv...)
}
return 0, nil
}
}
func Quiet() output.Output {
return func(context.Context, output.Verbosity, string, ...output.KeyValue) (int, error) {
return 0, nil
}
}

22
output/wrap/formatter.go

@ -0,0 +1,22 @@
package wrap
import (
"context"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/formatter"
)
func Format(out output.Output, format *formatter.Formatter) output.Output {
return func(ctx context.Context, v output.Verbosity, msg string, kv ...output.KeyValue) (int, error) {
return out(ctx, v, format.Format(ctx, msg), kv...)
}
}
func Ansi(out output.Output) output.Output {
return Format(out, formatter.Ansi())
}
func None(out output.Output) output.Output {
return Format(out, formatter.None())
}

44
output/writer/output.go

@ -0,0 +1,44 @@
package writer
import (
"bytes"
"context"
"fmt"
"io"
"os"
"strings"
"gitoa.ru/go-4devs/console/output"
)
func Stderr() output.Output {
return New(os.Stderr, String)
}
func Stdout() output.Output {
return New(os.Stdout, String)
}
func Buffer(buf *bytes.Buffer) output.Output {
return New(buf, String)
}
func String(_ output.Verbosity, msg string, kv ...output.KeyValue) string {
if len(kv) > 0 {
newline := ""
if msg[len(msg)-1:] == "\n" {
newline = "\n"
}
return "msg=\"" + strings.TrimSpace(msg) + "\", " + output.KeyValues(kv).String() + newline
}
return msg
}
func New(w io.Writer, format func(verb output.Verbosity, msg string, kv ...output.KeyValue) string) output.Output {
return func(ctx context.Context, verb output.Verbosity, msg string, kv ...output.KeyValue) (int, error) {
return fmt.Fprint(w, format(verb, msg, kv...))
}
}

49
output/writer/output_test.go

@ -0,0 +1,49 @@
package writer_test
import (
"bytes"
"context"
"testing"
"gitoa.ru/go-4devs/console/output"
"gitoa.ru/go-4devs/console/output/writer"
)
func TestNew(t *testing.T) {
ctx := context.Background()
buf := bytes.Buffer{}
wr := writer.New(&buf, writer.String)
cases := map[string]struct {
ex string
kv []output.KeyValue
}{
"message": {
ex: "message",
},
"msg with kv": {
ex: "msg=\"msg with kv\", string key=\"string value\", bool key=\"false\", int key=\"42\"",
kv: []output.KeyValue{
output.String("string key", "string value"),
output.Bool("bool key", false),
output.Int("int key", 42),
},
},
"msg with newline \n": {
ex: "msg=\"msg with newline\", int=\"42\"\n",
kv: []output.KeyValue{
output.Int("int", 42),
},
},
}
for msg, data := range cases {
wr.InfoKV(ctx, msg, data.kv...)
if data.ex != buf.String() {
t.Errorf("message not equals expext:%s, got:%s", data.ex, buf.String())
}
buf.Reset()
}
}

141
register.go

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

30
register_test.go

@ -0,0 +1,30 @@
package console_test
import (
"testing"
"gitoa.ru/go-4devs/console"
)
func TestFind(t *testing.T) {
cases := map[string]string{
"fdevs:console:test": "fdevs:console:test",
"fd:c:t": "fdevs:console:test",
"fd::t": "fdevs:console:test",
"f:c:t": "fdevs:console:test",
"f:c:a": "fdevs:console:arg",
}
for name, ex := range cases {
res, err := console.Find(name)
if err != nil {
t.Errorf("expect <nil> err, got:%s", err)
continue
}
if res.Name != ex {
t.Errorf("expect: %s, got: %s", ex, res)
}
}
}

16
validator/enum.go

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

24
validator/enum_test.go

@ -0,0 +1,24 @@
package validator_test
import (
"errors"
"testing"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/validator"
)
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)
}
}

37
validator/error.go

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

109
validator/not_blank.go

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

109
validator/not_blank_test.go

@ -0,0 +1,109 @@
package validator_test
import (
"errors"
"testing"
"time"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/validator"
)
func TestNotBlank(t *testing.T) {
cases := map[string]struct {
flag input.Flag
value input.Value
empty input.Value
}{
"any": {flag: input.ValueAny, value: value.New(float32(1))},
"array int": {
flag: input.ValueInt | input.ValueArray,
value: value.New([]int{1}),
empty: value.New([]int{10, 20, 0}),
},
"array int64": {
flag: input.ValueInt64 | input.ValueArray,
value: value.New([]int64{1}),
empty: value.New([]int64{0}),
},
"array uint": {
flag: input.ValueUint | input.ValueArray,
value: value.New([]uint{1}),
empty: value.New([]uint{1, 0}),
},
"array uint64": {
flag: input.ValueUint64 | input.ValueArray,
value: value.New([]uint64{1}),
empty: value.New([]uint64{0}),
},
"array float64": {
flag: input.ValueFloat64 | input.ValueArray,
value: value.New([]float64{0.2}),
empty: value.New([]float64{0}),
},
"array bool": {
flag: input.ValueBool | input.ValueArray,
value: value.New([]bool{true, false}),
empty: value.New([]bool{}),
},
"array duration": {
flag: input.ValueDuration | input.ValueArray,
value: value.New([]time.Duration{time.Second}),
empty: value.New([]time.Duration{time.Second, 0}),
},
"array time": {
flag: input.ValueTime | input.ValueArray,
value: value.New([]time.Time{time.Now()}),
empty: value.New([]time.Time{{}, time.Now()}),
},
"array string": {
flag: input.ValueArray,
value: value.New([]string{"value"}),
empty: value.New([]string{""}),
},
"int": {
flag: input.ValueInt,
value: value.New(int(1)),
},
"int64": {
flag: input.ValueInt64,
value: value.New(int64(2)),
},
"uint": {
flag: input.ValueUint,
value: value.New(uint(1)),
empty: value.New([]uint{1}),
},
"uint64": {
flag: input.ValueUint64,
value: value.New(uint64(10)),
},
"float64": {
flag: input.ValueFloat64,
value: value.New(float64(.00001)),
},
"duration": {
flag: input.ValueDuration,
value: value.New(time.Minute),
empty: value.New("same string"),
},
"time": {flag: input.ValueTime, 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)
}
}
}

15
validator/valid.go

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

28
validator/valid_test.go

@ -0,0 +1,28 @@
package validator_test
import (
"errors"
"testing"
"gitoa.ru/go-4devs/console/input"
"gitoa.ru/go-4devs/console/input/value"
"gitoa.ru/go-4devs/console/validator"
)
func TestValid(t *testing.T) {
validValue := value.New("one")
invalidValue := value.New([]string{"one"})
valid := validator.Valid(
validator.NotBlank(input.ValueString),
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)
}
}
Loading…
Cancel
Save