first commit
This commit is contained in:
24
.drone.yml
Normal file
24
.drone.yml
Normal file
@@ -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
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -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
Normal file
36
.golangci.yml
Normal file
@@ -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
Normal file
19
LICENSE
Normal file
@@ -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
Normal file
187
README.md
Normal file
@@ -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
Normal file
141
app.go
Normal file
@@ -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
Normal file
87
app_test.go
Normal file
@@ -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
Normal file
196
command.go
Normal file
@@ -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
Normal file
122
command_test.go
Normal file
@@ -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
Normal file
135
console.go
Normal file
@@ -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
Normal file
45
console_test.go
Normal file
@@ -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
Normal file
89
descriptor/descriptor.go
Normal file
@@ -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
Normal file
371
descriptor/txt.go
Normal file
@@ -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
Normal file
30
doc.go
Normal file
@@ -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
Executable file
BIN
example/bin/console
Executable file
Binary file not shown.
25
example/cmd/cancel/main.go
Normal file
25
example/cmd/cancel/main.go
Normal file
@@ -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
Normal file
21
example/cmd/console/main.go
Normal file
@@ -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
Normal file
12
example/cmd/single/main.go
Normal file
@@ -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
Normal file
33
example/pkg/command/args.go
Normal file
@@ -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
Normal file
36
example/pkg/command/create_user.go
Normal file
@@ -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
Normal file
30
example/pkg/command/create_user_test.go
Normal file
@@ -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
Normal file
34
example/pkg/command/hello.go
Normal file
@@ -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
Normal file
22
example/pkg/command/hidden.go
Normal file
@@ -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
Normal file
50
example/pkg/command/long.go
Normal file
@@ -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
Normal file
21
example/pkg/command/namespace.go
Normal file
@@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
92
help.go
Normal file
92
help.go
Normal file
@@ -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
Normal file
48
input/argument.go
Normal file
@@ -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
Normal file
26
input/argument/option.go
Normal file
@@ -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
Normal file
211
input/argv/input.go
Normal file
@@ -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
Normal file
87
input/array/input.go
Normal file
@@ -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
Normal file
95
input/definition.go
Normal file
@@ -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
Normal file
50
input/error.go
Normal file
@@ -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
Normal file
76
input/flag.go
Normal file
@@ -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
Normal file
47
input/flag_string.go
Normal file
@@ -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
Normal file
21
input/input.go
Normal file
@@ -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
Normal file
53
input/option.go
Normal file
@@ -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
Normal file
35
input/option/helpers.go
Normal file
@@ -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
Normal file
44
input/option/option.go
Normal file
@@ -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
Normal file
58
input/value.go
Normal file
@@ -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
Normal file
21
input/value/any.go
Normal file
@@ -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
Normal file
44
input/value/bool.go
Normal file
@@ -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
Normal file
44
input/value/duration.go
Normal file
@@ -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
Normal file
90
input/value/empty.go
Normal file
@@ -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
Normal file
44
input/value/float64.go
Normal file
@@ -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
Normal file
44
input/value/int.go
Normal file
@@ -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
Normal file
44
input/value/int64.go
Normal file
@@ -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
Normal file
17
input/value/read.go
Normal file
@@ -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
Normal file
39
input/value/string.go
Normal file
@@ -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
Normal file
44
input/value/time.go
Normal file
@@ -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
Normal file
44
input/value/uint.go
Normal file
@@ -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
Normal file
44
input/value/uint64.go
Normal file
@@ -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
Normal file
84
input/value/value.go
Normal file
@@ -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
Normal file
105
input/wrap/input.go
Normal file
@@ -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
Normal file
111
list.go
Normal file
@@ -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
Normal file
5
output/formatter/ansi.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package formatter
|
||||||
|
|
||||||
|
func Ansi() *Formatter {
|
||||||
|
return New()
|
||||||
|
}
|
||||||
79
output/formatter/formatter.go
Normal file
79
output/formatter/formatter.go
Normal file
@@ -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
Normal file
26
output/formatter/formatter_test.go
Normal file
@@ -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
Normal file
14
output/formatter/none.go
Normal file
@@ -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
Normal file
27
output/formatter/none_test.go
Normal file
@@ -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
Normal file
59
output/key.go
Normal file
@@ -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
Normal file
63
output/kv.go
Normal file
@@ -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
Normal file
77
output/output.go
Normal file
@@ -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
Normal file
51
output/style/color.go
Normal file
@@ -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
Normal file
88
output/style/style.go
Normal file
@@ -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
Normal file
57
output/value.go
Normal file
@@ -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
Normal file
23
output/verbosity/norm.go
Normal file
@@ -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
Normal file
22
output/wrap/formatter.go
Normal file
@@ -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
Normal file
44
output/writer/output.go
Normal file
@@ -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
Normal file
49
output/writer/output_test.go
Normal file
@@ -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
Normal file
141
register.go
Normal file
@@ -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
Normal file
30
register_test.go
Normal file
@@ -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
Normal file
16
validator/enum.go
Normal file
@@ -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
Normal file
24
validator/enum_test.go
Normal file
@@ -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
Normal file
37
validator/error.go
Normal file
@@ -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
Normal file
109
validator/not_blank.go
Normal file
@@ -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
Normal file
109
validator/not_blank_test.go
Normal file
@@ -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
Normal file
15
validator/valid.go
Normal file
@@ -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
Normal file
28
validator/valid_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user