move chinampa into its own package
This commit is contained in:
parent
ba63cc2419
commit
7e4e6eb02d
40
.golangci.yml
Normal file
40
.golangci.yml
Normal file
@ -0,0 +1,40 @@
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 21
|
||||
tagliatelle:
|
||||
case:
|
||||
rules:
|
||||
yaml: kebab
|
||||
|
||||
linters:
|
||||
fast: false
|
||||
enable:
|
||||
- deadcode
|
||||
- errcheck
|
||||
- exportloopref
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ifshort
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- prealloc
|
||||
- revive
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- tagliatelle
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
@ -17,14 +17,14 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/command"
|
||||
"git.rob.mx/nidito/joao/internal/registry"
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
"git.rob.mx/nidito/joao/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register(fetchCommand)
|
||||
chinampa.Register(fetchCommand)
|
||||
}
|
||||
|
||||
var fetchCommand = (&command.Command{
|
||||
|
@ -15,15 +15,15 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/command"
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
opclient "git.rob.mx/nidito/joao/internal/op-client"
|
||||
"git.rob.mx/nidito/joao/internal/registry"
|
||||
"git.rob.mx/nidito/joao/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register(flushCommand)
|
||||
chinampa.Register(flushCommand)
|
||||
}
|
||||
|
||||
var flushCommand = (&command.Command{
|
||||
|
@ -19,15 +19,15 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/command"
|
||||
"git.rob.mx/nidito/joao/internal/registry"
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
"git.rob.mx/nidito/joao/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register(gCommand)
|
||||
chinampa.Register(gCommand)
|
||||
}
|
||||
|
||||
func keyFinder(cmd *command.Command, currentValue string) ([]string, cobra.ShellCompDirective, error) {
|
||||
|
@ -18,15 +18,15 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/command"
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/command"
|
||||
opclient "git.rob.mx/nidito/joao/internal/op-client"
|
||||
"git.rob.mx/nidito/joao/internal/registry"
|
||||
"git.rob.mx/nidito/joao/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register(setCommand)
|
||||
chinampa.Register(setCommand)
|
||||
}
|
||||
|
||||
var setCommand = (&command.Command{
|
||||
@ -102,7 +102,7 @@ Will read values from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH
|
||||
return fmt.Errorf("cannot set a --secret that is JSON encoded, encode individual values instead")
|
||||
}
|
||||
|
||||
if delete && input != "" {
|
||||
if delete && input != "/dev/stdin" {
|
||||
logrus.Warn("Ignoring --file while deleting")
|
||||
}
|
||||
|
||||
|
13
go.mod
13
go.mod
@ -3,15 +3,12 @@ module git.rob.mx/nidito/joao
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.rob.mx/nidito/chinampa v0.0.0-20221219030434-c622ba72beb5
|
||||
github.com/1Password/connect-sdk-go v1.5.0
|
||||
github.com/alessio/shellescape v1.4.1
|
||||
github.com/charmbracelet/glamour v0.6.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/go-playground/validator/v10 v10.11.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@ -19,9 +16,12 @@ require (
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/glamour v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
@ -36,13 +36,14 @@ require (
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
github.com/yuin/goldmark v1.5.2 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -1,4 +1,6 @@
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.rob.mx/nidito/chinampa v0.0.0-20221219030434-c622ba72beb5 h1:5ynI2LRpxLD9tfiBLP60ChAvrBFhHytOHGqN7tUZyjc=
|
||||
git.rob.mx/nidito/chinampa v0.0.0-20221219030434-c622ba72beb5/go.mod h1:nQlQqIQ6UuP6spFFZvfVT1MhQJYEA7B3Y2EtM2Fha3Y=
|
||||
github.com/1Password/connect-sdk-go v1.5.0 h1:F0WJcLSzGg3iXEDY49/ULdszYKsQLGTzn+2cyYXqiyk=
|
||||
github.com/1Password/connect-sdk-go v1.5.0/go.mod h1:TdynFeyvaRoackENbJ8RfJokH+WAowAu1MLmUbdMq6s=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
|
@ -1,283 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func contains(haystack []string, needle string) bool {
|
||||
for _, validValue := range haystack {
|
||||
if needle == validValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Arguments is an ordered list of Argument.
|
||||
type Arguments []*Argument
|
||||
|
||||
func (args *Arguments) AllKnown() map[string]any {
|
||||
col := map[string]any{}
|
||||
for _, arg := range *args {
|
||||
col[arg.Name] = arg.ToValue()
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
func (args *Arguments) AllKnownStr() map[string]string {
|
||||
col := map[string]string{}
|
||||
for _, arg := range *args {
|
||||
col[arg.Name] = arg.ToString()
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
func (args *Arguments) Parse(supplied []string) {
|
||||
for idx, arg := range *args {
|
||||
argumentProvided := idx < len(supplied)
|
||||
|
||||
if !argumentProvided {
|
||||
if arg.Default != nil {
|
||||
if arg.Variadic {
|
||||
defaultSlice := []string{}
|
||||
for _, valI := range arg.Default.([]any) {
|
||||
defaultSlice = append(defaultSlice, valI.(string))
|
||||
}
|
||||
arg.provided = &defaultSlice
|
||||
} else {
|
||||
defaultString := arg.Default.(string)
|
||||
if defaultString != "" {
|
||||
arg.provided = &[]string{defaultString}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if arg.Variadic {
|
||||
values := append([]string{}, supplied[idx:]...)
|
||||
arg.SetValue(values)
|
||||
} else {
|
||||
arg.SetValue([]string{supplied[idx]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (args *Arguments) AreValid() error {
|
||||
for _, arg := range *args {
|
||||
if err := arg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompletionFunction is called by cobra when asked to complete arguments.
|
||||
func (args *Arguments) CompletionFunction(cc *cobra.Command, provided []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
expectedArgLen := len(*args)
|
||||
values := []string{}
|
||||
directive := cobra.ShellCompDirectiveError
|
||||
|
||||
if expectedArgLen > 0 {
|
||||
argsCompleted := len(provided)
|
||||
lastArg := (*args)[len(*args)-1]
|
||||
hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic
|
||||
lastArg.Command.Options.Parse(cc.Flags())
|
||||
args.Parse(provided)
|
||||
|
||||
directive = cobra.ShellCompDirectiveDefault
|
||||
if argsCompleted < expectedArgLen || hasVariadicArg {
|
||||
var arg *Argument
|
||||
if hasVariadicArg && argsCompleted >= expectedArgLen {
|
||||
// completing a variadic argument
|
||||
arg = lastArg
|
||||
} else {
|
||||
// completing regular argument (maybe variadic!)
|
||||
arg = (*args)[argsCompleted]
|
||||
}
|
||||
|
||||
if arg.Values != nil {
|
||||
var err error
|
||||
arg.Values.command = lastArg.Command
|
||||
arg.Command = lastArg.Command
|
||||
values, directive, err = arg.Resolve(toComplete)
|
||||
if err != nil {
|
||||
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
} else {
|
||||
directive = cobra.ShellCompDirectiveError
|
||||
}
|
||||
values = cobra.AppendActiveHelp(values, arg.Description)
|
||||
}
|
||||
|
||||
if toComplete != "" {
|
||||
filtered := []string{}
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, toComplete) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
values = filtered
|
||||
}
|
||||
}
|
||||
|
||||
return values, directive
|
||||
}
|
||||
|
||||
// Argument represents a single command-line argument.
|
||||
type Argument struct {
|
||||
// Name is how this variable will be exposed to the underlying command.
|
||||
Name string `json:"name" yaml:"name" validate:"required,excludesall=!$\\/%^@#?:'\""`
|
||||
// Description is what this argument is for.
|
||||
Description string `json:"description" yaml:"description" validate:"required"`
|
||||
// Default is the default value for this argument if none is provided.
|
||||
Default any `json:"default,omitempty" yaml:"default,omitempty" validate:"excluded_with=Required"`
|
||||
// Variadic makes an argument a list of all values from this one on.
|
||||
Variadic bool `json:"variadic" yaml:"variadic"`
|
||||
// Required raises an error if an argument is not provided.
|
||||
Required bool `json:"required" yaml:"required" validate:"excluded_with=Default"`
|
||||
// Values describes autocompletion and validation for an argument
|
||||
Values *ValueSource `json:"values,omitempty" yaml:"values" validate:"omitempty"`
|
||||
Command *Command `json:"-" yaml:"-" validate:"-"`
|
||||
provided *[]string
|
||||
}
|
||||
|
||||
func (arg *Argument) EnvName() string {
|
||||
return strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))
|
||||
}
|
||||
|
||||
func (arg *Argument) SetValue(value []string) {
|
||||
arg.provided = &value
|
||||
}
|
||||
|
||||
func (arg *Argument) IsKnown() bool {
|
||||
return arg.provided != nil && len(*arg.provided) > 0
|
||||
}
|
||||
|
||||
func (arg *Argument) ToString() string {
|
||||
val := arg.ToValue()
|
||||
|
||||
if arg.Variadic {
|
||||
val := val.([]string)
|
||||
return strings.Join(val, " ")
|
||||
}
|
||||
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
func (arg *Argument) ToValue() any {
|
||||
var value any
|
||||
if arg.IsKnown() {
|
||||
if arg.Variadic {
|
||||
value = *arg.provided
|
||||
} else {
|
||||
vals := *arg.provided
|
||||
value = vals[0]
|
||||
}
|
||||
} else {
|
||||
if arg.Default != nil {
|
||||
if arg.Variadic {
|
||||
defaultSlice := []string{}
|
||||
for _, valI := range arg.Default.([]any) {
|
||||
valStr := valI.(string)
|
||||
defaultSlice = append(defaultSlice, valStr)
|
||||
}
|
||||
|
||||
value = defaultSlice
|
||||
} else {
|
||||
value = arg.Default.(string)
|
||||
}
|
||||
} else {
|
||||
if arg.Variadic {
|
||||
value = []string{}
|
||||
} else {
|
||||
value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (arg *Argument) Validate() error {
|
||||
if !arg.IsKnown() {
|
||||
if arg.Required {
|
||||
return errors.BadArguments{Msg: fmt.Sprintf("Missing argument for %s", strings.ToUpper(arg.Name))}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !arg.Validates() {
|
||||
return nil
|
||||
}
|
||||
|
||||
validValues, _, err := arg.Resolve(strings.Join(*arg.provided, " "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if arg.Variadic {
|
||||
for _, current := range *arg.provided {
|
||||
if !contains(validValues, current) {
|
||||
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current := arg.ToValue().(string)
|
||||
if !contains(validValues, current) {
|
||||
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validates tells if the user-supplied value needs validation.
|
||||
func (arg *Argument) Validates() bool {
|
||||
return arg.Values != nil && arg.Values.Validates()
|
||||
}
|
||||
|
||||
// ToDesc prints out the description of an argument for help and docs.
|
||||
func (arg *Argument) ToDesc() string {
|
||||
spec := arg.EnvName()
|
||||
if arg.Variadic {
|
||||
spec = fmt.Sprintf("%s...", spec)
|
||||
}
|
||||
|
||||
if !arg.Required {
|
||||
spec = fmt.Sprintf("[%s]", spec)
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
// Resolve returns autocomplete values for an argument.
|
||||
func (arg *Argument) Resolve(current string) (values []string, flag cobra.ShellCompDirective, err error) {
|
||||
if arg.Values != nil {
|
||||
values, flag, err = arg.Values.Resolve(current)
|
||||
if err != nil {
|
||||
flag = cobra.ShellCompDirectiveError
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -1,425 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "git.rob.mx/nidito/joao/internal/command"
|
||||
)
|
||||
|
||||
func testCommand() *Command {
|
||||
return (&Command{
|
||||
Arguments: []*Argument{
|
||||
{
|
||||
Name: "first",
|
||||
Default: "default",
|
||||
},
|
||||
{
|
||||
Name: "variadic",
|
||||
Default: []any{"defaultVariadic0", "defaultVariadic1"},
|
||||
Variadic: true,
|
||||
},
|
||||
},
|
||||
Options: Options{
|
||||
"option": {
|
||||
Default: "default",
|
||||
Type: "string",
|
||||
},
|
||||
"bool": {
|
||||
Type: "bool",
|
||||
Default: false,
|
||||
},
|
||||
},
|
||||
}).SetBindings()
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
cmd := testCommand()
|
||||
cmd.Arguments.Parse([]string{"asdf", "one", "two", "three"})
|
||||
known := cmd.Arguments.AllKnown()
|
||||
|
||||
if !cmd.Arguments[0].IsKnown() {
|
||||
t.Fatalf("first argument isn't known")
|
||||
}
|
||||
val, exists := known["first"]
|
||||
if !exists {
|
||||
t.Fatalf("first argument isn't on AllKnown map: %v", known)
|
||||
}
|
||||
|
||||
if val != "asdf" {
|
||||
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
|
||||
}
|
||||
|
||||
if !cmd.Arguments[1].IsKnown() {
|
||||
t.Fatalf("variadic argument isn't known")
|
||||
}
|
||||
val, exists = known["variadic"]
|
||||
if !exists {
|
||||
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(val, []string{"one", "two", "three"}) {
|
||||
t.Fatalf("Known argument does not match. expected: %s, got %s", "one two three", val)
|
||||
}
|
||||
|
||||
cmd = testCommand()
|
||||
cmd.Arguments.Parse([]string{"asdf"})
|
||||
known = cmd.Arguments.AllKnown()
|
||||
|
||||
if !cmd.Arguments[0].IsKnown() {
|
||||
t.Fatalf("first argument is not known")
|
||||
}
|
||||
|
||||
val, exists = known["first"]
|
||||
if !exists {
|
||||
t.Fatalf("first argument isn't on AllKnown map: %v", known)
|
||||
}
|
||||
|
||||
if val != "asdf" {
|
||||
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
|
||||
}
|
||||
|
||||
val, exists = known["variadic"]
|
||||
if !exists {
|
||||
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
|
||||
}
|
||||
|
||||
expected := []string{"defaultVariadic0", "defaultVariadic1"}
|
||||
if !reflect.DeepEqual(val, expected) {
|
||||
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeforeParse(t *testing.T) {
|
||||
cmd := testCommand()
|
||||
known := cmd.Arguments.AllKnown()
|
||||
|
||||
if cmd.Arguments[0].IsKnown() {
|
||||
t.Fatalf("first argument is known")
|
||||
}
|
||||
|
||||
val, exists := known["first"]
|
||||
if !exists {
|
||||
t.Fatalf("first argument isn't on AllKnown map: %v", known)
|
||||
}
|
||||
|
||||
if val != "default" {
|
||||
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
|
||||
}
|
||||
|
||||
val, exists = known["variadic"]
|
||||
if !exists {
|
||||
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
|
||||
}
|
||||
|
||||
expected := []string{"defaultVariadic0", "defaultVariadic1"}
|
||||
if !reflect.DeepEqual(val, expected) {
|
||||
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidate(t *testing.T) {
|
||||
staticArgument := func(name string, def string, values []string, variadic bool) *Argument {
|
||||
return &Argument{
|
||||
Name: name,
|
||||
Default: def,
|
||||
Variadic: variadic,
|
||||
Required: def == "",
|
||||
Values: &ValueSource{
|
||||
Static: &values,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
Command *Command
|
||||
Args []string
|
||||
ErrorSuffix string
|
||||
Env []string
|
||||
}{
|
||||
{
|
||||
Command: (&Command{
|
||||
// Name: []string{"test", "required", "failure"},
|
||||
Arguments: []*Argument{
|
||||
{
|
||||
Name: "first",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}).SetBindings(),
|
||||
ErrorSuffix: "Missing argument for FIRST",
|
||||
},
|
||||
{
|
||||
Args: []string{"bad"},
|
||||
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: good, default",
|
||||
Command: (&Command{
|
||||
// Name: []string{"test", "script", "bad"},
|
||||
Arguments: []*Argument{
|
||||
{
|
||||
Name: "first",
|
||||
Default: "default",
|
||||
Values: &ValueSource{
|
||||
Script: "echo good; echo default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}).SetBindings(),
|
||||
},
|
||||
{
|
||||
Args: []string{"bad"},
|
||||
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: default, good",
|
||||
Command: (&Command{
|
||||
// Name: []string{"test", "static", "errors"},
|
||||
Arguments: []*Argument{staticArgument("first", "default", []string{"default", "good"}, false)},
|
||||
}).SetBindings(),
|
||||
},
|
||||
{
|
||||
Args: []string{"default", "good", "bad"},
|
||||
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: default, good",
|
||||
Command: (&Command{
|
||||
// Name: []string{"test", "static", "errors"},
|
||||
Arguments: []*Argument{staticArgument("first", "default", []string{"default", "good"}, true)},
|
||||
}).SetBindings(),
|
||||
},
|
||||
{
|
||||
Args: []string{"good"},
|
||||
ErrorSuffix: "could not validate argument for command test script bad-exit, ran",
|
||||
Command: (&Command{
|
||||
Path: []string{"test", "script", "bad-exit"},
|
||||
Arguments: []*Argument{
|
||||
{
|
||||
Name: "first",
|
||||
Default: "default",
|
||||
Values: &ValueSource{
|
||||
Script: "echo good; echo default; exit 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}).SetBindings(),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("good command is good", func(t *testing.T) {
|
||||
cmd := testCommand()
|
||||
cmd.Arguments[0] = staticArgument("first", "default", []string{"default", "good"}, false)
|
||||
cmd.Arguments[1] = staticArgument("second", "", []string{"one", "two", "three"}, true)
|
||||
cmd.SetBindings()
|
||||
|
||||
cmd.Arguments.Parse([]string{"first", "one", "three", "two"})
|
||||
|
||||
err := cmd.Arguments.AreValid()
|
||||
if err == nil {
|
||||
t.Fatalf("Unexpected failure validating: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Command.FullName(), func(t *testing.T) {
|
||||
c.Command.Arguments.Parse(c.Args)
|
||||
|
||||
err := c.Command.Arguments.AreValid()
|
||||
if err == nil {
|
||||
t.Fatalf("Expected failure but got none")
|
||||
}
|
||||
if !strings.HasPrefix(err.Error(), c.ErrorSuffix) {
|
||||
t.Fatalf("Could not find error <%s> got <%s>", c.ErrorSuffix, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// func TestArgumentsToEnv(t *testing.T) {
|
||||
// cases := []struct {
|
||||
// Command *Command
|
||||
// Args []string
|
||||
// Expect []string
|
||||
// Env []string
|
||||
// }{
|
||||
// {
|
||||
// Args: []string{"something"},
|
||||
// Expect: []string{"export MILPA_ARG_FIRST=something"},
|
||||
// Command: &Command{
|
||||
// // Name: []string{"test", "required", "present"},
|
||||
// Arguments: []*Argument{
|
||||
// {
|
||||
// Name: "first",
|
||||
// Required: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Args: []string{},
|
||||
// Expect: []string{"export MILPA_ARG_FIRST=default"},
|
||||
// Command: &Command{
|
||||
// // Name: []string{"test", "default", "present"},
|
||||
// Arguments: []*Argument{
|
||||
// {
|
||||
// Name: "first",
|
||||
// Default: "default",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Args: []string{"zero", "one", "two", "three"},
|
||||
// Expect: []string{
|
||||
// "export MILPA_ARG_FIRST=zero",
|
||||
// "declare -a MILPA_ARG_VARIADIC='( one two three )'",
|
||||
// },
|
||||
// Command: &Command{
|
||||
// // Name: []string{"test", "variadic"},
|
||||
// Arguments: []*Argument{
|
||||
// {
|
||||
// Name: "first",
|
||||
// Default: "default",
|
||||
// },
|
||||
// {
|
||||
// Name: "variadic",
|
||||
// Variadic: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Args: []string{},
|
||||
// Expect: []string{"export MILPA_ARG_FIRST=default"},
|
||||
// Command: &Command{
|
||||
// // Name: []string{"test", "static", "default"},
|
||||
// Arguments: []*Argument{
|
||||
// {
|
||||
// Name: "first",
|
||||
// Default: "default",
|
||||
// Values: &ValueSource{
|
||||
// Static: &[]string{
|
||||
// "default",
|
||||
// "good",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Args: []string{"good"},
|
||||
// Expect: []string{"export MILPA_ARG_FIRST=good"},
|
||||
// Command: &Command{
|
||||
// // Name: []string{"test", "static", "good"},
|
||||
// Arguments: []*Argument{
|
||||
// {
|
||||
// Name: "first",
|
||||
// Default: "default",
|
||||
// Values: &ValueSource{
|
||||
// Static: &[]string{
|
||||
// "default",
|
||||
// "good",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Args: []string{"good"},
|
||||
// Expect: []string{"export MILPA_ARG_FIRST=good"},
|
||||
// Command: &Command{
|
||||
// // Name: []string{"test", "script", "good"},
|
||||
// Arguments: []*Argument{
|
||||
// {
|
||||
// Name: "first",
|
||||
// Default: "default",
|
||||
// Values: &ValueSource{
|
||||
// Script: "echo good; echo default",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
||||
// for _, c := range cases {
|
||||
// t.Run(c.Command.FullName(), func(t *testing.T) {
|
||||
// dst := []string{}
|
||||
// c.Command.SetBindings()
|
||||
// c.Command.Arguments.Parse(c.Args)
|
||||
// c.Command.Arguments.ToEnv(c.Command, &dst, "export ")
|
||||
|
||||
// err := c.Command.Arguments.AreValid()
|
||||
// if err != nil {
|
||||
// t.Fatalf("Unexpected failure validating: %s", err)
|
||||
// }
|
||||
|
||||
// for _, expected := range c.Expect {
|
||||
// found := false
|
||||
// for _, actual := range dst {
|
||||
// if strings.HasPrefix(actual, expected) {
|
||||
// found = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if !found {
|
||||
// t.Fatalf("Expected line %v not found in %v", expected, dst)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestArgumentToDesc(t *testing.T) {
|
||||
cases := []struct {
|
||||
Arg *Argument
|
||||
Spec string
|
||||
}{
|
||||
{
|
||||
Arg: &Argument{
|
||||
Name: "regular",
|
||||
},
|
||||
Spec: "[REGULAR]",
|
||||
},
|
||||
{
|
||||
Arg: &Argument{
|
||||
Name: "required",
|
||||
Required: true,
|
||||
},
|
||||
Spec: "REQUIRED",
|
||||
},
|
||||
{
|
||||
Arg: &Argument{
|
||||
Name: "variadic-regular",
|
||||
Variadic: true,
|
||||
},
|
||||
Spec: "[VARIADIC_REGULAR...]",
|
||||
},
|
||||
{
|
||||
Arg: &Argument{
|
||||
Name: "variadic-required",
|
||||
Variadic: true,
|
||||
Required: true,
|
||||
},
|
||||
Spec: "VARIADIC_REQUIRED...",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Arg.Name, func(t *testing.T) {
|
||||
res := c.Arg.ToDesc()
|
||||
if res != c.Spec {
|
||||
t.Fatalf("Expected %s got %s", c.Spec, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type HelpFunc func(printLinks bool) string
|
||||
type Action func(cmd *Command) error
|
||||
|
||||
type Command struct {
|
||||
Path []string
|
||||
// Summary is a short description of a command, on supported shells this is part of the autocomplete prompt
|
||||
Summary string `json:"summary" yaml:"summary" validate:"required"`
|
||||
// Description is a long form explanation of how a command works its magic. Markdown is supported
|
||||
Description string `json:"description" yaml:"description" validate:"required"`
|
||||
// A list of arguments for a command
|
||||
Arguments Arguments `json:"arguments" yaml:"arguments" validate:"dive"`
|
||||
// A map of option names to option definitions
|
||||
Options Options `json:"options" yaml:"options" validate:"dive"`
|
||||
HelpFunc HelpFunc `json:"-" yaml:"-"`
|
||||
// The action to take upon running
|
||||
Action Action
|
||||
runtimeFlags *pflag.FlagSet
|
||||
Cobra *cobra.Command
|
||||
}
|
||||
|
||||
func (cmd *Command) SetBindings() *Command {
|
||||
ptr := cmd
|
||||
for _, opt := range cmd.Options {
|
||||
opt.Command = ptr
|
||||
if opt.Validates() {
|
||||
opt.Values.command = ptr
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range cmd.Arguments {
|
||||
arg.Command = ptr
|
||||
if arg.Validates() {
|
||||
arg.Values.command = ptr
|
||||
}
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
func (cmd *Command) Name() string {
|
||||
return cmd.Path[len(cmd.Path)-1]
|
||||
}
|
||||
|
||||
func (cmd *Command) FullName() string {
|
||||
return strings.Join(cmd.Path, " ")
|
||||
}
|
||||
|
||||
func (cmd *Command) FlagSet() *pflag.FlagSet {
|
||||
if cmd.runtimeFlags == nil {
|
||||
fs := pflag.NewFlagSet(strings.Join(cmd.Path, " "), pflag.ContinueOnError)
|
||||
fs.SortFlags = false
|
||||
fs.Usage = func() {}
|
||||
|
||||
for name, opt := range cmd.Options {
|
||||
switch opt.Type {
|
||||
case ValueTypeBoolean:
|
||||
def := false
|
||||
if opt.Default != nil {
|
||||
def = opt.Default.(bool)
|
||||
}
|
||||
fs.Bool(name, def, opt.Description)
|
||||
case ValueTypeDefault, ValueTypeString:
|
||||
opt.Type = ValueTypeString
|
||||
def := ""
|
||||
if opt.Default != nil {
|
||||
def = fmt.Sprintf("%s", opt.Default)
|
||||
}
|
||||
fs.String(name, def, opt.Description)
|
||||
default:
|
||||
// ignore flag
|
||||
logrus.Warnf("Ignoring unknown option type <%s> for option <%s>", opt.Type, name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
cmd.runtimeFlags = fs
|
||||
}
|
||||
return cmd.runtimeFlags
|
||||
}
|
||||
|
||||
func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error {
|
||||
cmd.Arguments.Parse(args)
|
||||
skipValidation, _ := cc.Flags().GetBool("skip-validation")
|
||||
cmd.Options.Parse(cc.Flags())
|
||||
if !skipValidation {
|
||||
logrus.Debug("Validating arguments")
|
||||
if err := cmd.Arguments.AreValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debug("Validating flags")
|
||||
if err := cmd.Options.AreValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *Command) Run(cc *cobra.Command, args []string) error {
|
||||
logrus.Debugf("running command %s", cmd.FullName())
|
||||
|
||||
if err := cmd.ParseInput(cc, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cmd.Action(cmd)
|
||||
}
|
||||
|
||||
func (cmd *Command) SetCobra(cc *cobra.Command) {
|
||||
cmd.Cobra = cc
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/render"
|
||||
"git.rob.mx/nidito/joao/internal/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type combinedCommand struct {
|
||||
Spec *Command
|
||||
Command *cobra.Command
|
||||
GlobalOptions Options
|
||||
HTMLOutput bool
|
||||
}
|
||||
|
||||
func (cmd *Command) HasAdditionalHelp() bool {
|
||||
return cmd.HelpFunc != nil
|
||||
}
|
||||
|
||||
func (cmd *Command) AdditionalHelp(printLinks bool) *string {
|
||||
if cmd.HelpFunc != nil {
|
||||
str := cmd.HelpFunc(printLinks)
|
||||
return &str
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *Command) HelpRenderer(globalOptions Options) func(cc *cobra.Command, args []string) {
|
||||
return func(cc *cobra.Command, args []string) {
|
||||
// some commands don't have a binding until help is rendered
|
||||
// like virtual ones (sub command groups)
|
||||
cmd.SetCobra(cc)
|
||||
content, err := cmd.ShowHelp(globalOptions, args)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = cc.OutOrStderr().Write(content)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *Command) ShowHelp(globalOptions Options, args []string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
c := &combinedCommand{
|
||||
Spec: cmd,
|
||||
Command: cmd.Cobra,
|
||||
GlobalOptions: globalOptions,
|
||||
HTMLOutput: runtime.UnstyledHelpEnabled(),
|
||||
}
|
||||
err := _c.TemplateCommandHelp.Execute(&buf, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
colorEnabled := runtime.ColorEnabled()
|
||||
flags := cmd.Cobra.Flags()
|
||||
ncf := cmd.Cobra.Flag("no-color") // nolint:ifshort
|
||||
cf := cmd.Cobra.Flag("color") // nolint:ifshort
|
||||
|
||||
if noColorFlag, err := flags.GetBool("no-color"); err == nil && ncf.Changed {
|
||||
colorEnabled = !noColorFlag
|
||||
} else if colorFlag, err := flags.GetBool("color"); err == nil && cf.Changed {
|
||||
colorEnabled = colorFlag
|
||||
}
|
||||
|
||||
content, err := render.Markdown(buf.Bytes(), colorEnabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Options is a map of name to Option.
|
||||
type Options map[string]*Option
|
||||
|
||||
func (opts *Options) AllKnown() map[string]any {
|
||||
col := map[string]any{}
|
||||
for name, opt := range *opts {
|
||||
col[name] = opt.ToValue()
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
func (opts *Options) AllKnownStr() map[string]string {
|
||||
col := map[string]string{}
|
||||
for name, opt := range *opts {
|
||||
col[name] = opt.ToString()
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
// func envValue(opts Options, f *pflag.Flag) (*string, *string) {
|
||||
// name := f.Name
|
||||
// if name == _c.HelpCommandName {
|
||||
// return nil, nil
|
||||
// }
|
||||
// envName := ""
|
||||
// value := f.Value.String()
|
||||
|
||||
// if cname, ok := _c.EnvFlagNames[name]; ok {
|
||||
// if value == "false" {
|
||||
// return nil, nil
|
||||
// }
|
||||
// envName = cname
|
||||
// } else {
|
||||
// envName = fmt.Sprintf("%s%s", _c.OutputPrefixOpt, strings.ToUpper(strings.ReplaceAll(name, "-", "_")))
|
||||
// opt := opts[name]
|
||||
// if opt != nil {
|
||||
// value = opt.ToString(true)
|
||||
// }
|
||||
|
||||
// if value == "false" && opt.Type == ValueTypeBoolean {
|
||||
// // makes dealing with false flags in shell easier
|
||||
// value = ""
|
||||
// }
|
||||
// }
|
||||
|
||||
// return &envName, &value
|
||||
// }
|
||||
|
||||
// // ToEnv writes shell variables to dst.
|
||||
// func (opts *Options) ToEnv(command *Command, dst *[]string, prefix string) {
|
||||
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
// envName, value := envValue(*opts, f)
|
||||
// if envName != nil && value != nil {
|
||||
// *dst = append(*dst, fmt.Sprintf("%s%s=%s", prefix, *envName, *value))
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// func (opts *Options) EnvMap(command *Command, dst *map[string]string) {
|
||||
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
// envName, value := envValue(*opts, f)
|
||||
// if envName != nil && value != nil {
|
||||
// (*dst)[*envName] = *value
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
func (opts *Options) Parse(supplied *pflag.FlagSet) {
|
||||
// logrus.Debugf("Parsing supplied flags, %v", supplied)
|
||||
for name, opt := range *opts {
|
||||
switch opt.Type {
|
||||
case ValueTypeBoolean:
|
||||
if val, err := supplied.GetBool(name); err == nil {
|
||||
opt.provided = val
|
||||
continue
|
||||
}
|
||||
default:
|
||||
opt.Type = ValueTypeString
|
||||
if val, err := supplied.GetString(name); err == nil {
|
||||
opt.provided = val
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *Options) AreValid() error {
|
||||
for name, opt := range *opts {
|
||||
if err := opt.Validate(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Option represents a command line flag.
|
||||
type Option struct {
|
||||
ShortName string `json:"short-name,omitempty" yaml:"short-name,omitempty"` // nolint:tagliatelle
|
||||
Type ValueType `json:"type" yaml:"type" validate:"omitempty,oneof=string bool"`
|
||||
Description string `json:"description" yaml:"description" validate:"required"`
|
||||
Default any `json:"default,omitempty" yaml:"default,omitempty"`
|
||||
Values *ValueSource `json:"values,omitempty" yaml:"values,omitempty" validate:"omitempty"`
|
||||
Repeated bool `json:"repeated" yaml:"repeated" validate:"omitempty"`
|
||||
Command *Command `json:"-" yaml:"-" validate:"-"`
|
||||
provided any
|
||||
}
|
||||
|
||||
func (opt *Option) IsKnown() bool {
|
||||
return opt.provided != nil
|
||||
}
|
||||
|
||||
func (opt *Option) ToValue() any {
|
||||
if opt.IsKnown() {
|
||||
return opt.provided
|
||||
}
|
||||
return opt.Default
|
||||
}
|
||||
|
||||
func (opt *Option) ToString() string {
|
||||
value := opt.ToValue()
|
||||
stringValue := ""
|
||||
if opt.Type == "bool" {
|
||||
if value == nil {
|
||||
stringValue = ""
|
||||
} else {
|
||||
stringValue = strconv.FormatBool(value.(bool))
|
||||
}
|
||||
} else {
|
||||
if value != nil {
|
||||
stringValue = value.(string)
|
||||
}
|
||||
}
|
||||
|
||||
return stringValue
|
||||
}
|
||||
|
||||
func (opt *Option) Validate(name string) error {
|
||||
if !opt.Validates() {
|
||||
return nil
|
||||
}
|
||||
|
||||
current := opt.ToString() // nolint:ifshort
|
||||
|
||||
if current == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
validValues, _, err := opt.Resolve(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !contains(validValues, current) {
|
||||
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for option <%s>. Valid options are: %s", current, name, strings.Join(validValues, ", "))}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validates tells if the user-supplied value needs validation.
|
||||
func (opt *Option) Validates() bool {
|
||||
return opt.Values != nil && opt.Values.Validates()
|
||||
}
|
||||
|
||||
// providesAutocomplete tells if this option provides autocomplete values.
|
||||
func (opt *Option) providesAutocomplete() bool {
|
||||
return opt.Values != nil
|
||||
}
|
||||
|
||||
// Resolve returns autocomplete values for an option.
|
||||
func (opt *Option) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
|
||||
if opt.Values != nil {
|
||||
if opt.Values.command == nil {
|
||||
opt.Values.command = opt.Command
|
||||
}
|
||||
return opt.Values.Resolve(currentValue)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CompletionFunction is called by cobra when asked to complete an option.
|
||||
func (opt *Option) CompletionFunction(cmd *cobra.Command, args []string, toComplete string) (values []string, flag cobra.ShellCompDirective) {
|
||||
if !opt.providesAutocomplete() {
|
||||
flag = cobra.ShellCompDirectiveNoFileComp
|
||||
return
|
||||
}
|
||||
|
||||
opt.Command.Arguments.Parse(args)
|
||||
opt.Command.Options.Parse(cmd.Flags())
|
||||
|
||||
var err error
|
||||
values, flag, err = opt.Resolve(toComplete)
|
||||
if err != nil {
|
||||
return values, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
if toComplete != "" {
|
||||
filtered := []string{}
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, toComplete) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
values = filtered
|
||||
}
|
||||
|
||||
return cobra.AppendActiveHelp(values, opt.Description), flag
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/runtime"
|
||||
)
|
||||
|
||||
var Root = &Command{
|
||||
Summary: "Helps organize config for roberto",
|
||||
Description: `﹅joao﹅ makes yaml, json, 1password and vault play along nicely.`,
|
||||
Path: []string{"joao"},
|
||||
Options: Options{
|
||||
_c.HelpCommandName: &Option{
|
||||
ShortName: "h",
|
||||
Type: "bool",
|
||||
Description: "Display help for any command",
|
||||
},
|
||||
"verbose": &Option{
|
||||
ShortName: "v",
|
||||
Type: "bool",
|
||||
Default: runtime.VerboseEnabled(),
|
||||
Description: "Log verbose output to stderr",
|
||||
},
|
||||
"no-color": &Option{
|
||||
Type: "bool",
|
||||
Description: "Disable printing of colors to stderr",
|
||||
Default: !runtime.ColorEnabled(),
|
||||
},
|
||||
"color": &Option{
|
||||
Type: "bool",
|
||||
Description: "Always print colors to stderr",
|
||||
Default: runtime.ColorEnabled(),
|
||||
},
|
||||
"silent": &Option{
|
||||
Type: "bool",
|
||||
Description: "Silence non-error logging",
|
||||
},
|
||||
"skip-validation": &Option{
|
||||
Type: "bool",
|
||||
Description: "Do not validate any arguments or options",
|
||||
},
|
||||
},
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type varSearchMap struct {
|
||||
Status int
|
||||
Name string
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (cmd *Command) Validate() (report map[string]int) {
|
||||
report = map[string]int{}
|
||||
|
||||
validate := validator.New()
|
||||
if err := validate.Struct(cmd); err != nil {
|
||||
verrs := err.(validator.ValidationErrors)
|
||||
for _, issue := range verrs {
|
||||
// todo: output better errors, see validator.FieldError
|
||||
report[fmt.Sprint(issue)] = 1
|
||||
}
|
||||
}
|
||||
|
||||
vars := map[string]map[string]*varSearchMap{
|
||||
"argument": {},
|
||||
"option": {},
|
||||
}
|
||||
|
||||
for _, arg := range cmd.Arguments {
|
||||
vars["argument"][strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))] = &varSearchMap{2, arg.Name, ""}
|
||||
}
|
||||
|
||||
for name := range cmd.Options {
|
||||
vars["option"][strings.ToUpper(strings.ReplaceAll(name, "-", "_"))] = &varSearchMap{2, name, ""}
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/exec"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ValueType represent the kinds of or option.
|
||||
type ValueType string
|
||||
|
||||
const (
|
||||
// ValueTypeDefault is the empty string, maps to ValueTypeString.
|
||||
ValueTypeDefault ValueType = ""
|
||||
// ValueTypeString a value treated like a string.
|
||||
ValueTypeString ValueType = "string"
|
||||
// ValueTypeBoolean is a value treated like a boolean.
|
||||
ValueTypeBoolean ValueType = "bool"
|
||||
)
|
||||
|
||||
type SourceCommand struct {
|
||||
Path []string
|
||||
Args string
|
||||
}
|
||||
|
||||
type CompletionFunc func(cmd *Command, currentValue string) (values []string, flag cobra.ShellCompDirective, err error)
|
||||
|
||||
// ValueSource represents the source for an auto-completed and/or validated option/argument.
|
||||
type ValueSource struct {
|
||||
// Directories prompts for directories with the given prefix.
|
||||
Directories *string `json:"dirs,omitempty" yaml:"dirs,omitempty" validate:"omitempty,excluded_with=Command Files Func Script Static"`
|
||||
// Files prompts for files with the given extensions
|
||||
Files *[]string `json:"files,omitempty" yaml:"files,omitempty" validate:"omitempty,excluded_with=Command Func Directories Script Static"`
|
||||
// Script runs the provided command with `bash -c "$script"` and returns an option for every line of stdout.
|
||||
Script string `json:"script,omitempty" yaml:"script,omitempty" validate:"omitempty,excluded_with=Command Directories Files Func Static"`
|
||||
// Static returns the given list.
|
||||
Static *[]string `json:"static,omitempty" yaml:"static,omitempty" validate:"omitempty,excluded_with=Command Directories Files Func Script"`
|
||||
// Command runs a subcommand and returns an option for every line of stdout.
|
||||
Command *SourceCommand `json:"command,omitempty" yaml:"command,omitempty" validate:"omitempty,excluded_with=Directories Files Func Script Static"`
|
||||
// Func runs a function
|
||||
Func CompletionFunc `json:"func,omitempty" yaml:"func,omitempty" validate:"omitempty,excluded_with=Command Directories Files Script Static"`
|
||||
// Timeout is the maximum amount of time we will wait for a Script, Command, or Func before giving up on completions/validations.
|
||||
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty" validate:"omitempty,excluded_with=Directories Files Static"`
|
||||
// Suggestion if provided will only suggest autocomplete values but will not perform validation of a given value
|
||||
Suggestion bool `json:"suggest-only" yaml:"suggest-only" validate:"omitempty"` // nolint:tagliatelle
|
||||
// SuggestRaw if provided the shell will not add a space after autocompleting
|
||||
SuggestRaw bool `json:"suggest-raw" yaml:"suggest-raw" validate:"omitempty"` // nolint:tagliatelle
|
||||
command *Command `json:"-" yaml:"-" validate:"-"`
|
||||
computed *[]string
|
||||
flag cobra.ShellCompDirective
|
||||
}
|
||||
|
||||
// Validates tells if a value needs to be validated.
|
||||
func (vs *ValueSource) Validates() bool {
|
||||
if vs.Directories != nil || vs.Files != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return !vs.Suggestion
|
||||
}
|
||||
|
||||
// Resolve returns the values for autocomplete and validation.
|
||||
func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
|
||||
if vs.computed != nil {
|
||||
return *vs.computed, vs.flag, nil
|
||||
}
|
||||
|
||||
if vs.Timeout == 0 {
|
||||
vs.Timeout = 5
|
||||
}
|
||||
|
||||
flag = cobra.ShellCompDirectiveDefault
|
||||
timeout := time.Duration(vs.Timeout)
|
||||
|
||||
switch {
|
||||
case vs.Static != nil:
|
||||
values = *vs.Static
|
||||
case vs.Files != nil:
|
||||
flag = cobra.ShellCompDirectiveFilterFileExt
|
||||
values = *vs.Files
|
||||
case vs.Directories != nil:
|
||||
flag = cobra.ShellCompDirectiveFilterDirs
|
||||
values = []string{*vs.Directories}
|
||||
case vs.Func != nil:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
panicChan := make(chan any, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicChan <- p
|
||||
}
|
||||
}()
|
||||
|
||||
values, flag, err = vs.Func(vs.command, currentValue)
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case err = <-done:
|
||||
return
|
||||
case p := <-panicChan:
|
||||
panic(p)
|
||||
case <-ctx.Done():
|
||||
flag = cobra.ShellCompDirectiveError
|
||||
err = ctx.Err()
|
||||
return
|
||||
}
|
||||
|
||||
case vs.Command != nil:
|
||||
if vs.command == nil {
|
||||
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("bug: command is nil")
|
||||
}
|
||||
argString, err := vs.command.ResolveTemplate(vs.Command.Args, currentValue)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError, err
|
||||
}
|
||||
args := strings.Split(argString, " ")
|
||||
sub, _, err := vs.command.Cobra.Root().Find(vs.Command.Path)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("could not find a command named %s", vs.Command.Path)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
|
||||
defer cancel() // The cancel should be deferred so resources are cleaned up
|
||||
|
||||
sub.SetArgs(args)
|
||||
var stdout bytes.Buffer
|
||||
sub.SetOut(&stdout)
|
||||
var stderr bytes.Buffer
|
||||
sub.SetErr(&stderr)
|
||||
err = sub.ExecuteContext(ctx)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError, err
|
||||
}
|
||||
|
||||
values = strings.Split(stdout.String(), "\n")
|
||||
flag = cobra.ShellCompDirectiveDefault
|
||||
case vs.Script != "":
|
||||
if vs.command == nil {
|
||||
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("bug: command is nil")
|
||||
}
|
||||
cmd, err := vs.command.ResolveTemplate(vs.Script, currentValue)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError, err
|
||||
}
|
||||
|
||||
args := append([]string{"/bin/bash", "-c"}, cmd)
|
||||
|
||||
values, flag, err = exec.Exec(vs.command.FullName(), args, os.Environ(), timeout*time.Second)
|
||||
if err != nil {
|
||||
return nil, flag, err
|
||||
}
|
||||
}
|
||||
|
||||
vs.computed = &values
|
||||
|
||||
if vs.SuggestRaw {
|
||||
flag |= cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
|
||||
vs.flag = flag
|
||||
return values, flag, err
|
||||
}
|
||||
|
||||
type AutocompleteTemplate struct {
|
||||
Args map[string]string
|
||||
Opts map[string]string
|
||||
}
|
||||
|
||||
func (tpl *AutocompleteTemplate) Opt(name string) string {
|
||||
if val, ok := tpl.Opts[name]; ok {
|
||||
return fmt.Sprintf("--%s %s", name, val)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (tpl *AutocompleteTemplate) Arg(name string) string {
|
||||
return tpl.Args[name]
|
||||
}
|
||||
|
||||
func (cmd *Command) ResolveTemplate(templateString string, currentValue string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
tplData := &AutocompleteTemplate{
|
||||
Args: cmd.Arguments.AllKnownStr(),
|
||||
Opts: cmd.Options.AllKnownStr(),
|
||||
}
|
||||
|
||||
fnMap := template.FuncMap{
|
||||
"Opt": tplData.Opt,
|
||||
"Arg": tplData.Arg,
|
||||
"Current": func() string { return currentValue },
|
||||
}
|
||||
|
||||
for k, v := range _c.TemplateFuncs {
|
||||
fnMap[k] = v
|
||||
}
|
||||
|
||||
tpl, err := template.New("subcommand").Funcs(fnMap).Parse(templateString)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = tpl.Execute(&buf, tplData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "git.rob.mx/nidito/joao/internal/command"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func TestResolveTemplate(t *testing.T) {
|
||||
overrideFlags := &pflag.FlagSet{}
|
||||
overrideFlags.String("option", "override", "stuff")
|
||||
overrideFlags.Bool("bool", false, "stuff")
|
||||
overrideFlags.Bool("help", false, "stuff")
|
||||
overrideFlags.Bool("no-color", false, "stuff")
|
||||
overrideFlags.Bool("skip-validation", false, "stuff")
|
||||
err := overrideFlags.Parse([]string{"--option", "override", "--bool", "--help", "--no-color", "--skip-validation"})
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse test flags")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
Tpl string
|
||||
Expected string
|
||||
Args []string
|
||||
Flags *pflag.FlagSet
|
||||
Errors bool
|
||||
}{
|
||||
{
|
||||
Tpl: "adds nothing to nothing",
|
||||
Expected: "adds nothing to nothing",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `prints default option as {{ Opt "option" }}`,
|
||||
Expected: "prints default option as --option default",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `prints default option value as {{ .Opts.option }}`,
|
||||
Expected: "prints default option value as default",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `prints default argument as {{ Arg "argument_0" }}`,
|
||||
Expected: "prints default argument as default",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `prints default argument value as {{ .Args.argument_0 }}`,
|
||||
Expected: "prints default argument value as default",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `overrides default option as {{ Opt "option" }}`,
|
||||
Expected: "overrides default option as --option override",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: overrideFlags,
|
||||
},
|
||||
{
|
||||
Tpl: `overrides default argument as {{ Arg "argument_0" }}`,
|
||||
Expected: "overrides default argument as override",
|
||||
Errors: false,
|
||||
Args: []string{"override"},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `combines defaults as {{ Opt "option" }} {{ Opt "bool"}} {{ Arg "argument_0" }}`,
|
||||
Expected: "combines defaults as --option default --bool false default",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `combines overrides as {{ Opt "option" }} {{ Opt "bool" }} {{ Arg "argument_0" }}`,
|
||||
Expected: "combines overrides as --option override --bool true twice",
|
||||
Errors: false,
|
||||
Args: []string{"twice"},
|
||||
Flags: overrideFlags,
|
||||
},
|
||||
{
|
||||
Tpl: `prints variadic as {{ Arg "argument_0" }} {{ Arg "argument_n" }}`,
|
||||
Expected: "prints variadic as override a b",
|
||||
Errors: false,
|
||||
Args: []string{"override", "a", "b"},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `doesn't error on bad names {{ Opt "bad-option" }} {{ Arg "bad-argument" }}`,
|
||||
Expected: "doesn't error on bad names ",
|
||||
Errors: false,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
},
|
||||
{
|
||||
Tpl: `errors on bad templates {{ BadFunc }}`,
|
||||
Args: []string{},
|
||||
Flags: &pflag.FlagSet{},
|
||||
Errors: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
test := test
|
||||
t.Run(test.Expected, func(t *testing.T) {
|
||||
cmd := (&Command{
|
||||
Arguments: []*Argument{
|
||||
{
|
||||
Name: "argument_0",
|
||||
Default: "default",
|
||||
},
|
||||
{
|
||||
Name: "argument_n",
|
||||
Variadic: true,
|
||||
},
|
||||
},
|
||||
Options: Options{
|
||||
"option": {
|
||||
Default: "default",
|
||||
Type: "string",
|
||||
},
|
||||
"bool": {
|
||||
Type: "bool",
|
||||
Default: false,
|
||||
},
|
||||
},
|
||||
}).SetBindings()
|
||||
cmd.Arguments.Parse(test.Args)
|
||||
cmd.Options.Parse(test.Flags)
|
||||
res, err := cmd.ResolveTemplate(test.Tpl, "")
|
||||
|
||||
if err != nil && !test.Errors {
|
||||
t.Fatalf("good template failed: %s", err)
|
||||
}
|
||||
|
||||
if res != test.Expected {
|
||||
t.Fatalf("expected '%s' got '%s'", test.Expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package constants
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
// Embed requires an import so the compiler knows what's up. Golint requires a comment. Gotta please em both.
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
const HelpCommandName = "help"
|
||||
|
||||
// Environment Variables.
|
||||
const EnvVarHelpUnstyled = "MILPA_PLAIN_HELP"
|
||||
const EnvVarHelpStyle = "MILPA_HELP_STYLE"
|
||||
const EnvVarMilpaRoot = "MILPA_ROOT"
|
||||
const EnvVarMilpaPath = "MILPA_PATH"
|
||||
const EnvVarMilpaPathParsed = "MILPA_PATH_PARSED"
|
||||
const EnvVarMilpaVerbose = "MILPA_VERBOSE"
|
||||
const EnvVarMilpaSilent = "MILPA_SILENT"
|
||||
const EnvVarMilpaUnstyled = "NO_COLOR"
|
||||
const EnvVarMilpaForceColor = "COLOR"
|
||||
const EnvVarValidationDisabled = "MILPA_SKIP_VALIDATION"
|
||||
const EnvVarCompaOut = "COMPA_OUT"
|
||||
const EnvVarDebug = "DEBUG"
|
||||
const EnvVarLookupGitDisabled = "MILPA_DISABLE_GIT"
|
||||
const EnvVarLookupUserReposDisabled = "MILPA_DISABLE_USER_REPOS"
|
||||
const EnvVarLookupGlobalReposDisabled = "MILPA_DISABLE_GLOBAL_REPOS"
|
||||
|
||||
// EnvFlagNames are flags also available as environment variables.
|
||||
var EnvFlagNames = map[string]string{
|
||||
"no-color": EnvVarMilpaUnstyled,
|
||||
"color": EnvVarMilpaForceColor,
|
||||
"silent": EnvVarMilpaSilent,
|
||||
"verbose": EnvVarMilpaVerbose,
|
||||
"skip-validation": EnvVarValidationDisabled,
|
||||
}
|
||||
|
||||
// Exit statuses
|
||||
// see man sysexits || grep "#define EX" /usr/include/sysexits.h
|
||||
// and https://tldp.org/LDP/abs/html/exitcodes.html
|
||||
|
||||
// 0 means everything is fine.
|
||||
const ExitStatusOk = 0
|
||||
|
||||
// 42 provides answers to life, the universe and everything; also, renders help.
|
||||
const ExitStatusRenderHelp = 42
|
||||
|
||||
// 64 bad arguments
|
||||
// EX_USAGE The command was used incorrectly, e.g., with the wrong number of arguments, a bad flag, a bad syntax in a parameter, or whatever.
|
||||
const ExitStatusUsage = 64
|
||||
|
||||
// EX_SOFTWARE An internal software error has been detected. This should be limited to non-operating system related errors as possible.
|
||||
const ExitStatusProgrammerError = 70
|
||||
|
||||
// EX_CONFIG Something was found in an unconfigured or misconfigured state.
|
||||
const ExitStatusConfigError = 78
|
||||
|
||||
// 127 command not found.
|
||||
const ExitStatusNotFound = 127
|
||||
|
||||
// ContextKeyRuntimeIndex is the string key used to store context in a cobra Command.
|
||||
const ContextKeyRuntimeIndex = "x-joao-runtime-index"
|
||||
|
||||
//go:embed help.md
|
||||
var helpTemplateText string
|
||||
|
||||
// TemplateFuncs is a FuncMap with aliases to the strings package.
|
||||
var TemplateFuncs = template.FuncMap{
|
||||
"contains": strings.Contains,
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"replace": strings.ReplaceAll,
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"trim": strings.TrimSpace,
|
||||
"trimSuffix": strings.TrimSuffix,
|
||||
"trimPrefix": strings.TrimPrefix,
|
||||
}
|
||||
|
||||
// TemplateCommandHelp holds a template for rendering command help.
|
||||
var TemplateCommandHelp = template.Must(template.New("help").Funcs(TemplateFuncs).Parse(helpTemplateText))
|
@ -1,70 +0,0 @@
|
||||
{{- if not .HTMLOutput }}
|
||||
# {{ if and (not (eq .Spec.FullName "joao")) (not (eq .Command.Name "help")) }}joao {{ end }}{{ .Spec.FullName }}{{if eq .Command.Name "help"}} help{{end}}
|
||||
{{- else }}
|
||||
---
|
||||
description: {{ .Command.Short }}
|
||||
---
|
||||
{{- end }}
|
||||
|
||||
{{ .Command.Short }}
|
||||
|
||||
## Usage
|
||||
|
||||
﹅{{ replace .Command.UseLine " [flags]" "" }}{{if .Command.HasAvailableSubCommands}} SUBCOMMAND{{end}}﹅
|
||||
|
||||
{{ if .Command.HasAvailableSubCommands -}}
|
||||
## Subcommands
|
||||
|
||||
{{ $hh := .HTMLOutput -}}
|
||||
{{ range .Command.Commands -}}
|
||||
{{- if (or .IsAvailableCommand (eq .Name "help")) -}}
|
||||
- {{ if $hh -}}
|
||||
[﹅{{ .Name }}﹅]({{.Name}})
|
||||
{{- else -}}
|
||||
﹅{{ .Name }}﹅
|
||||
{{- end }} - {{.Short}}
|
||||
{{ end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if .Spec.Arguments -}}
|
||||
## Arguments
|
||||
|
||||
{{ range .Spec.Arguments -}}
|
||||
|
||||
- ﹅{{ .Name | toUpper }}{{ if .Variadic}}...{{ end }}﹅{{ if .Required }} _required_{{ end }} - {{ .Description }}
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{ if and (eq .Spec.FullName "joao") (not (eq .Command.Name "help")) }}
|
||||
## Description
|
||||
|
||||
{{ .Spec.Description }}
|
||||
{{ end -}}
|
||||
{{- if .Spec.HasAdditionalHelp }}
|
||||
{{ .Spec.AdditionalHelp .HTMLOutput }}
|
||||
{{ end -}}
|
||||
|
||||
|
||||
{{- if .Command.HasAvailableLocalFlags}}
|
||||
## Options
|
||||
|
||||
{{ range $name, $opt := .Spec.Options -}}
|
||||
- ﹅--{{ $name }}﹅ (_{{$opt.Type}}_): {{ trimSuffix $opt.Description "."}}.{{ if $opt.Default }} Default: _{{ $opt.Default }}_.{{ end }}
|
||||
{{ end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if not (eq .Spec.FullName "joao") }}
|
||||
## Description
|
||||
|
||||
{{ if not (eq .Command.Long "") }}{{ .Command.Long }}{{ else }}{{ .Spec.Description }}{{end}}
|
||||
{{ end }}
|
||||
|
||||
{{- if .Command.HasAvailableInheritedFlags }}
|
||||
## Global Options
|
||||
|
||||
{{ range $name, $opt := .GlobalOptions -}}
|
||||
- ﹅--{{ $name }}﹅ (_{{$opt.Type}}_): {{$opt.Description}}.{{ if $opt.Default }} Default: _{{ $opt.Default }}_.{{ end }}
|
||||
{{ end -}}
|
||||
{{end}}
|
@ -1,70 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type NotFound struct {
|
||||
Msg string
|
||||
Group []string
|
||||
}
|
||||
|
||||
type BadArguments struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
type NotExecutable struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
type ConfigError struct {
|
||||
Err error
|
||||
Config string
|
||||
}
|
||||
|
||||
type EnvironmentError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
type SubCommandExit struct {
|
||||
Err error
|
||||
ExitCode int
|
||||
}
|
||||
|
||||
func (err NotFound) Error() string {
|
||||
return err.Msg
|
||||
}
|
||||
|
||||
func (err BadArguments) Error() string {
|
||||
return err.Msg
|
||||
}
|
||||
|
||||
func (err NotExecutable) Error() string {
|
||||
return err.Msg
|
||||
}
|
||||
|
||||
func (err SubCommandExit) Error() string {
|
||||
if err.Err != nil {
|
||||
return err.Err.Error()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (err ConfigError) Error() string {
|
||||
return fmt.Sprintf("Invalid configuration %s: %v", err.Config, err.Err)
|
||||
}
|
||||
|
||||
func (err EnvironmentError) Error() string {
|
||||
return fmt.Sprintf("Invalid MILPA_ environment: %v", err.Err)
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
)
|
||||
|
||||
func showHelp(cmd *cobra.Command) {
|
||||
if cmd.Name() != _c.HelpCommandName {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
os.Exit(_c.ExitStatusProgrammerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleCobraExit(cmd *cobra.Command, err error) {
|
||||
if err == nil {
|
||||
ok, err := cmd.Flags().GetBool(_c.HelpCommandName)
|
||||
if cmd.Name() == _c.HelpCommandName || err == nil && ok {
|
||||
os.Exit(_c.ExitStatusRenderHelp)
|
||||
}
|
||||
|
||||
os.Exit(_c.ExitStatusOk)
|
||||
}
|
||||
|
||||
switch tErr := err.(type) {
|
||||
case SubCommandExit:
|
||||
logrus.Debugf("Sub-command failed with: %s", err.Error())
|
||||
os.Exit(tErr.ExitCode)
|
||||
case BadArguments:
|
||||
showHelp(cmd)
|
||||
logrus.Error(err)
|
||||
os.Exit(_c.ExitStatusUsage)
|
||||
case NotFound:
|
||||
showHelp(cmd)
|
||||
logrus.Error(err)
|
||||
os.Exit(_c.ExitStatusNotFound)
|
||||
case ConfigError:
|
||||
showHelp(cmd)
|
||||
logrus.Error(err)
|
||||
os.Exit(_c.ExitStatusConfigError)
|
||||
case EnvironmentError:
|
||||
logrus.Error(err)
|
||||
os.Exit(_c.ExitStatusConfigError)
|
||||
default:
|
||||
if strings.HasPrefix(err.Error(), "unknown command") {
|
||||
showHelp(cmd)
|
||||
os.Exit(_c.ExitStatusNotFound)
|
||||
} else if strings.HasPrefix(err.Error(), "unknown flag") || strings.HasPrefix(err.Error(), "unknown shorthand flag") {
|
||||
showHelp(cmd)
|
||||
logrus.Error(err)
|
||||
os.Exit(_c.ExitStatusUsage)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Errorf("Unknown error: %s", err)
|
||||
os.Exit(2)
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
os_exec "os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ExecFunc is replaced in tests.
|
||||
var ExecFunc = WithSubshell
|
||||
|
||||
func WithSubshell(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||
cmd := os_exec.CommandContext(ctx, executable, args...) // #nosec G204
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = env
|
||||
return stdout, stderr, cmd.Run()
|
||||
}
|
||||
|
||||
// Exec runs a subprocess and returns a list of lines from stdout.
|
||||
func Exec(name string, args []string, env []string, timeout time.Duration) ([]string, cobra.ShellCompDirective, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel() // The cancel should be deferred so resources are cleaned up
|
||||
|
||||
logrus.Debugf("executing %s", args)
|
||||
executable := args[0]
|
||||
args = args[1:]
|
||||
|
||||
stdout, _, err := ExecFunc(ctx, env, executable, args...)
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
fmt.Println("Sub-command timed out")
|
||||
logrus.Debugf("timeout running %s %s: %s", executable, args, stdout.String())
|
||||
return []string{}, cobra.ShellCompDirectiveError, fmt.Errorf("timed out resolving %s %s", executable, args)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.Debugf("error running %s %s: %s", executable, args, err)
|
||||
return []string{}, cobra.ShellCompDirectiveError, errors.BadArguments{Msg: fmt.Sprintf("could not validate argument for command %s, ran <%s %s> failed: %s", name, executable, strings.Join(args, " "), err)}
|
||||
}
|
||||
|
||||
logrus.Debugf("done running %s %s: %s", executable, args, stdout.String())
|
||||
return strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n"), cobra.ShellCompDirectiveDefault, nil
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package exec_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "git.rob.mx/nidito/joao/internal/exec"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestSubshellExec(t *testing.T) {
|
||||
ExecFunc = WithSubshell
|
||||
stdout, directive, err := Exec("test-command", []string{"bash", "-c", `echo "stdout"; echo "stderr" >&2;`}, []string{}, 1*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("good subshell errored: %v", err)
|
||||
}
|
||||
|
||||
if len(stdout) != 1 && stdout[0] == "stdout" {
|
||||
t.Fatalf("good subshell returned wrong stdout: %v", stdout)
|
||||
}
|
||||
|
||||
if directive != cobra.ShellCompDirectiveDefault {
|
||||
t.Fatalf("good subshell returned wrong directive: %v", directive)
|
||||
}
|
||||
|
||||
stdout, directive, err = Exec("test-command", []string{"bash", "-c", `echo "stdout"; echo "stderr" >&2; exit 2`}, []string{}, 1*time.Second)
|
||||
if err == nil {
|
||||
t.Fatalf("bad subshell did not error; stdout: %v", stdout)
|
||||
}
|
||||
|
||||
if len(stdout) != 0 {
|
||||
t.Fatalf("bad subshell returned non-empty stdout: %v", stdout)
|
||||
}
|
||||
|
||||
if directive != cobra.ShellCompDirectiveError {
|
||||
t.Fatalf("bad subshell returned wrong directive: %v", directive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecTimesOut(t *testing.T) {
|
||||
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||
time.Sleep(100 * time.Nanosecond)
|
||||
return bytes.Buffer{}, bytes.Buffer{}, context.DeadlineExceeded
|
||||
}
|
||||
_, _, err := Exec("test-command", []string{"bash", "-c", "sleep", "2"}, []string{}, 10*time.Nanosecond)
|
||||
if err == nil {
|
||||
t.Fatalf("timeout didn't happen after 10ms: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecWorksFine(t *testing.T) {
|
||||
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||
var out bytes.Buffer
|
||||
fmt.Fprint(&out, strings.Join([]string{
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
}, "\n"))
|
||||
return out, bytes.Buffer{}, nil
|
||||
}
|
||||
args := []string{"a", "b", "c"}
|
||||
res, directive, err := Exec("test-command", append([]string{"bash", "-c", "echo"}, args...), []string{}, 1*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("good command failed: %v", err)
|
||||
}
|
||||
|
||||
if directive != 0 {
|
||||
t.Fatalf("good command resulted in wrong directive, expected %d, got %d", 0, directive)
|
||||
}
|
||||
|
||||
if strings.Join(args, "-") != strings.Join(res, "-") {
|
||||
t.Fatalf("good command resulted in wrong results, expected %v, got %v", res, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecErrors(t *testing.T) {
|
||||
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
||||
return bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("bad command is bad")
|
||||
}
|
||||
res, directive, err := Exec("test-command", []string{"bash", "-c", "bad-command"}, []string{}, 1*time.Second)
|
||||
if err == fmt.Errorf("bad command is bad") {
|
||||
t.Fatalf("bad command didn't fail: %v", res)
|
||||
}
|
||||
|
||||
if directive != cobra.ShellCompDirectiveError {
|
||||
t.Fatalf("bad command resulted in wrong directive, expected %d, got %d", cobra.ShellCompDirectiveError, directive)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
t.Fatalf("bad command returned values, got %v", res)
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/command"
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/errors"
|
||||
"git.rob.mx/nidito/joao/internal/runtime"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func toCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command {
|
||||
localName := cmd.Name()
|
||||
useSpec := []string{localName, "[options]"}
|
||||
for _, arg := range cmd.Arguments {
|
||||
useSpec = append(useSpec, arg.ToDesc())
|
||||
}
|
||||
|
||||
cc := &cobra.Command{
|
||||
Use: strings.Join(useSpec, " "),
|
||||
Short: cmd.Summary,
|
||||
DisableAutoGenTag: true,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Annotations: map[string]string{
|
||||
_c.ContextKeyRuntimeIndex: cmd.FullName(),
|
||||
},
|
||||
Args: func(cc *cobra.Command, supplied []string) error {
|
||||
skipValidation, _ := cc.Flags().GetBool("skip-validation")
|
||||
if !skipValidation && runtime.ValidationEnabled() {
|
||||
cmd.Arguments.Parse(supplied)
|
||||
return cmd.Arguments.AreValid()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: cmd.Run,
|
||||
}
|
||||
|
||||
cc.SetFlagErrorFunc(func(c *cobra.Command, e error) error {
|
||||
return errors.BadArguments{Msg: e.Error()}
|
||||
})
|
||||
|
||||
cc.ValidArgsFunction = cmd.Arguments.CompletionFunction
|
||||
|
||||
cc.Flags().AddFlagSet(cmd.FlagSet())
|
||||
|
||||
for name, opt := range cmd.Options {
|
||||
if err := cc.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
|
||||
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmd.FullName())
|
||||
}
|
||||
}
|
||||
|
||||
cc.SetHelpFunc(cmd.HelpRenderer(globalOptions))
|
||||
cmd.SetCobra(cc)
|
||||
return cc
|
||||
}
|
||||
|
||||
func fromCobra(cc *cobra.Command) *command.Command {
|
||||
rtidx, hasAnnotation := cc.Annotations[_c.ContextKeyRuntimeIndex]
|
||||
if hasAnnotation {
|
||||
return Get(rtidx)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.rob.mx/nidito/joao/internal/command"
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/errors"
|
||||
"github.com/fatih/color"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var registry = &CommandRegistry{
|
||||
kv: map[string]*command.Command{},
|
||||
}
|
||||
|
||||
type ByPath []*command.Command
|
||||
|
||||
func (cmds ByPath) Len() int { return len(cmds) }
|
||||
func (cmds ByPath) Swap(i, j int) { cmds[i], cmds[j] = cmds[j], cmds[i] }
|
||||
func (cmds ByPath) Less(i, j int) bool { return cmds[i].FullName() < cmds[j].FullName() }
|
||||
|
||||
type CommandTree struct {
|
||||
Command *command.Command `json:"command"`
|
||||
Children []*CommandTree `json:"children"`
|
||||
}
|
||||
|
||||
func (t *CommandTree) Traverse(fn func(cmd *command.Command) error) error {
|
||||
for _, child := range t.Children {
|
||||
if err := fn(child.Command); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := child.Traverse(fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CommandRegistry struct {
|
||||
kv map[string]*command.Command
|
||||
byPath []*command.Command
|
||||
tree *CommandTree
|
||||
}
|
||||
|
||||
func Register(cmd *command.Command) {
|
||||
logrus.Debugf("Registering %s", cmd.FullName())
|
||||
registry.kv[cmd.FullName()] = cmd
|
||||
}
|
||||
|
||||
func Get(id string) *command.Command {
|
||||
return registry.kv[id]
|
||||
}
|
||||
|
||||
func CommandList() []*command.Command {
|
||||
if len(registry.byPath) == 0 {
|
||||
list := []*command.Command{}
|
||||
for _, v := range registry.kv {
|
||||
list = append(list, v)
|
||||
}
|
||||
sort.Sort(ByPath(list))
|
||||
registry.byPath = list
|
||||
}
|
||||
|
||||
return registry.byPath
|
||||
}
|
||||
|
||||
func BuildTree(cc *cobra.Command, depth int) {
|
||||
tree := &CommandTree{
|
||||
Command: fromCobra(cc),
|
||||
Children: []*CommandTree{},
|
||||
}
|
||||
|
||||
var populateTree func(cmd *cobra.Command, ct *CommandTree, maxDepth int, depth int)
|
||||
populateTree = func(cmd *cobra.Command, ct *CommandTree, maxDepth int, depth int) {
|
||||
newDepth := depth + 1
|
||||
for _, subcc := range cmd.Commands() {
|
||||
if subcc.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
if cmd := fromCobra(subcc); cmd != nil {
|
||||
leaf := &CommandTree{Children: []*CommandTree{}}
|
||||
leaf.Command = cmd
|
||||
ct.Children = append(ct.Children, leaf)
|
||||
|
||||
if newDepth < maxDepth {
|
||||
populateTree(subcc, leaf, maxDepth, newDepth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
populateTree(cc, tree, depth, 0)
|
||||
|
||||
registry.tree = tree
|
||||
}
|
||||
|
||||
func SerializeTree(serializationFn func(any) ([]byte, error)) (string, error) {
|
||||
bytes, err := serializationFn(registry.tree)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func ChildrenNames() []string {
|
||||
if registry.tree == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
ret := make([]string, len(registry.tree.Children))
|
||||
for idx, cmd := range registry.tree.Children {
|
||||
ret[idx] = cmd.Command.Name()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func Execute(version string) error {
|
||||
cmdRoot := command.Root
|
||||
ccRoot.Short = cmdRoot.Summary
|
||||
ccRoot.Long = cmdRoot.Description
|
||||
ccRoot.Annotations["version"] = version
|
||||
ccRoot.CompletionOptions.DisableDefaultCmd = true
|
||||
ccRoot.Flags().AddFlagSet(cmdRoot.FlagSet())
|
||||
|
||||
for name, opt := range cmdRoot.Options {
|
||||
if err := ccRoot.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
|
||||
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmdRoot.FullName())
|
||||
}
|
||||
}
|
||||
ccRoot.SetHelpFunc(cmdRoot.HelpRenderer(cmdRoot.Options))
|
||||
|
||||
for _, cmd := range CommandList() {
|
||||
cmd := cmd
|
||||
leaf := toCobra(cmd, cmdRoot.Options)
|
||||
container := ccRoot
|
||||
for idx, cp := range cmd.Path {
|
||||
if idx == len(cmd.Path)-1 {
|
||||
// logrus.Debugf("adding command %s to %s", leaf.Name(), cmd.Path[0:idx])
|
||||
container.AddCommand(leaf)
|
||||
break
|
||||
}
|
||||
|
||||
query := []string{cp}
|
||||
if cc, _, err := container.Find(query); err == nil && cc != container {
|
||||
container = cc
|
||||
} else {
|
||||
groupName := strings.Join(query, " ")
|
||||
groupPath := append(cmd.Path[0:idx], query...) // nolint:gocritic
|
||||
cc := &cobra.Command{
|
||||
Use: cp,
|
||||
Short: fmt.Sprintf("%s subcommands", groupName),
|
||||
DisableAutoGenTag: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Annotations: map[string]string{
|
||||
_c.ContextKeyRuntimeIndex: strings.Join(groupPath, " "),
|
||||
},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cobra.OnlyValidArgs(cmd, args); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
suggestions := []string{}
|
||||
bold := color.New(color.Bold)
|
||||
for _, l := range cmd.SuggestionsFor(args[len(args)-1]) {
|
||||
suggestions = append(suggestions, bold.Sprint(l))
|
||||
}
|
||||
last := len(args) - 1
|
||||
parent := cmd.CommandPath()
|
||||
errMessage := fmt.Sprintf("Unknown subcommand %s of known command %s", bold.Sprint(args[last]), bold.Sprint(parent))
|
||||
if len(suggestions) > 0 {
|
||||
errMessage += ". Perhaps you meant " + strings.Join(suggestions, ", ") + "?"
|
||||
}
|
||||
return errors.NotFound{Msg: errMessage, Group: []string{}}
|
||||
},
|
||||
ValidArgs: []string{""},
|
||||
RunE: func(cc *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
|
||||
}
|
||||
os.Exit(_c.ExitStatusNotFound)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
groupParent := &command.Command{
|
||||
Path: cmd.Path[0 : len(cmd.Path)-1],
|
||||
Summary: fmt.Sprintf("%s subcommands", groupName),
|
||||
Description: fmt.Sprintf("Runs subcommands within %s", groupName),
|
||||
Arguments: command.Arguments{},
|
||||
Options: command.Options{},
|
||||
}
|
||||
Register(groupParent)
|
||||
cc.SetHelpFunc(groupParent.HelpRenderer(command.Options{}))
|
||||
container.AddCommand(cc)
|
||||
container = cc
|
||||
}
|
||||
}
|
||||
}
|
||||
cmdRoot.SetCobra(ccRoot)
|
||||
|
||||
return ccRoot.Execute()
|
||||
}
|
||||
|
||||
var ccRoot = &cobra.Command{
|
||||
Use: "joao [--silent|-v|--verbose] [--[no-]color] [-h|--help] [--version]",
|
||||
Annotations: map[string]string{
|
||||
_c.ContextKeyRuntimeIndex: "joao",
|
||||
},
|
||||
DisableAutoGenTag: true,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
ValidArgs: []string{""},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
err := cobra.OnlyValidArgs(cmd, args)
|
||||
if err != nil {
|
||||
|
||||
suggestions := []string{}
|
||||
bold := color.New(color.Bold)
|
||||
for _, l := range cmd.SuggestionsFor(args[len(args)-1]) {
|
||||
suggestions = append(suggestions, bold.Sprint(l))
|
||||
}
|
||||
errMessage := fmt.Sprintf("Unknown subcommand %s", bold.Sprint(strings.Join(args, " ")))
|
||||
if len(suggestions) > 0 {
|
||||
errMessage += ". Perhaps you meant " + strings.Join(suggestions, ", ") + "?"
|
||||
}
|
||||
return errors.NotFound{Msg: errMessage, Group: []string{}}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
// if ok, err := cmd.Flags().GetBool("version"); err == nil && ok {
|
||||
// vc, _, err := cmd.Root().Find([]string{versionName()})
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return vc.RunE(vc, []string{})
|
||||
// }
|
||||
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/runtime"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func addBackticks(str []byte) []byte {
|
||||
return bytes.ReplaceAll(str, []byte("﹅"), []byte("`"))
|
||||
}
|
||||
|
||||
func Markdown(content []byte, withColor bool) ([]byte, error) {
|
||||
content = addBackticks(content)
|
||||
|
||||
if runtime.UnstyledHelpEnabled() {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
width, _, err := term.GetSize(0)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get terminal width")
|
||||
width = 80
|
||||
}
|
||||
|
||||
var styleFunc glamour.TermRendererOption
|
||||
|
||||
if withColor {
|
||||
style := os.Getenv(_c.EnvVarHelpStyle)
|
||||
switch style {
|
||||
case "dark":
|
||||
styleFunc = glamour.WithStandardStyle("dark")
|
||||
case "light":
|
||||
styleFunc = glamour.WithStandardStyle("light")
|
||||
default:
|
||||
styleFunc = glamour.WithStandardStyle("auto")
|
||||
}
|
||||
} else {
|
||||
styleFunc = glamour.WithStandardStyle("notty")
|
||||
}
|
||||
|
||||
renderer, err := glamour.NewTermRenderer(
|
||||
styleFunc,
|
||||
glamour.WithEmoji(),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return content, err
|
||||
}
|
||||
|
||||
return renderer.RenderBytes(content)
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package render_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
"git.rob.mx/nidito/joao/internal/render"
|
||||
)
|
||||
|
||||
func TestMarkdownUnstyled(t *testing.T) {
|
||||
content := []byte("# hello")
|
||||
os.Setenv(_c.EnvVarHelpUnstyled, "true")
|
||||
res, err := render.Markdown(content, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %s", err)
|
||||
}
|
||||
|
||||
expected := []byte("# hello") // nolint:ifshort
|
||||
if !reflect.DeepEqual(res, expected) {
|
||||
t.Fatalf("Unexpected response ---\n%s\n---\n wanted:\n---\n%s\n---", res, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownNoColor(t *testing.T) {
|
||||
os.Unsetenv(_c.EnvVarHelpUnstyled)
|
||||
content := []byte("# hello ﹅world﹅")
|
||||
res, err := render.Markdown(content, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %s", err)
|
||||
}
|
||||
|
||||
// account for 80 character width word wrapping
|
||||
// our string is 15 characters, there's 2 for padding at the start
|
||||
spaces := " "
|
||||
|
||||
expected := []byte("\n # hello `world`" + spaces + "\n\n") // nolint:ifshort
|
||||
if !reflect.DeepEqual(res, expected) {
|
||||
t.Fatalf("Unexpected response ---\n%s\n---\n wanted:\n---\n%s\n---", res, expected)
|
||||
}
|
||||
}
|
||||
|
||||
var autoStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;63;1m\x1b[0m \x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;228;48;5;63;1mhello\x1b[0m\x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;252m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[0m\n\x1b[0m\n"
|
||||
|
||||
const lightStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;63;1m\x1b[0m \x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;228;48;5;63;1mhello\x1b[0m\x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;234m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[0m\n\x1b[0m\n"
|
||||
|
||||
func TestMarkdownColor(t *testing.T) {
|
||||
os.Unsetenv(_c.EnvVarHelpUnstyled)
|
||||
content := []byte("# hello")
|
||||
|
||||
styles := map[string][]byte{
|
||||
"": []byte(autoStyleTestRender),
|
||||
"dark": []byte(autoStyleTestRender),
|
||||
"auto": []byte(autoStyleTestRender),
|
||||
"light": []byte(lightStyleTestRender),
|
||||
}
|
||||
for style, expected := range styles {
|
||||
t.Run(fmt.Sprintf("style %s", style), func(t *testing.T) {
|
||||
os.Setenv(_c.EnvVarHelpStyle, style)
|
||||
res, err := render.Markdown(content, true)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, expected) {
|
||||
t.Fatalf("Unexpected response ---\n%v\n---\n wanted:\n---\n%v\n---", res, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
)
|
||||
|
||||
var MilpaPath = ParseMilpaPath()
|
||||
|
||||
// ParseMilpaPath turns MILPA_PATH into a string slice.
|
||||
func ParseMilpaPath() []string {
|
||||
return strings.Split(os.Getenv(_c.EnvVarMilpaPath), ":")
|
||||
}
|
||||
|
||||
var falseIshValues = []string{
|
||||
"",
|
||||
"0",
|
||||
"no",
|
||||
"false",
|
||||
"disable",
|
||||
"disabled",
|
||||
"off",
|
||||
"never",
|
||||
}
|
||||
|
||||
var trueIshValues = []string{
|
||||
"1",
|
||||
"yes",
|
||||
"true",
|
||||
"enable",
|
||||
"enabled",
|
||||
"on",
|
||||
"always",
|
||||
}
|
||||
|
||||
func isFalseIsh(val string) bool {
|
||||
for _, negative := range falseIshValues {
|
||||
if val == negative {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isTrueIsh(val string) bool {
|
||||
for _, positive := range trueIshValues {
|
||||
if val == positive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func DoctorModeEnabled() bool {
|
||||
count := len(os.Args)
|
||||
if count < 2 {
|
||||
return false
|
||||
}
|
||||
first := os.Args[1]
|
||||
|
||||
return first == "__doctor" || count >= 2 && (first == "itself" && os.Args[2] == "doctor")
|
||||
}
|
||||
|
||||
func DebugEnabled() bool {
|
||||
return isTrueIsh(os.Getenv(_c.EnvVarDebug))
|
||||
}
|
||||
|
||||
func ValidationEnabled() bool {
|
||||
return isFalseIsh(os.Getenv(_c.EnvVarValidationDisabled))
|
||||
}
|
||||
|
||||
func VerboseEnabled() bool {
|
||||
return isTrueIsh(os.Getenv(_c.EnvVarMilpaVerbose))
|
||||
}
|
||||
|
||||
func ColorEnabled() bool {
|
||||
return isFalseIsh(os.Getenv(_c.EnvVarMilpaUnstyled)) && !UnstyledHelpEnabled()
|
||||
}
|
||||
|
||||
func UnstyledHelpEnabled() bool {
|
||||
return isTrueIsh(os.Getenv(_c.EnvVarHelpUnstyled))
|
||||
}
|
||||
|
||||
func CheckMilpaPathSet() error {
|
||||
if len(MilpaPath) == 0 {
|
||||
return fmt.Errorf("no %s set on the environment", _c.EnvVarMilpaPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnvironmentMap returns the resolved environment map.
|
||||
func EnvironmentMap() map[string]string {
|
||||
env := map[string]string{}
|
||||
env[_c.EnvVarMilpaPath] = strings.Join(MilpaPath, ":")
|
||||
trueString := strconv.FormatBool(true)
|
||||
env[_c.EnvVarMilpaPathParsed] = trueString
|
||||
|
||||
if !ColorEnabled() {
|
||||
env[_c.EnvVarMilpaUnstyled] = trueString
|
||||
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaForceColor)) {
|
||||
env[_c.EnvVarMilpaForceColor] = "always"
|
||||
}
|
||||
|
||||
if DebugEnabled() {
|
||||
env[_c.EnvVarDebug] = trueString
|
||||
}
|
||||
|
||||
if VerboseEnabled() {
|
||||
env[_c.EnvVarMilpaVerbose] = trueString
|
||||
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaSilent)) {
|
||||
env[_c.EnvVarMilpaSilent] = trueString
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package runtime_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_c "git.rob.mx/nidito/joao/internal/constants"
|
||||
. "git.rob.mx/nidito/joao/internal/runtime"
|
||||
)
|
||||
|
||||
func TestCheckMilpaPathSet(t *testing.T) {
|
||||
MilpaPath = []string{"a", "b"}
|
||||
|
||||
if err := CheckMilpaPathSet(); err != nil {
|
||||
t.Fatalf("Got error with set MILPA_PATH: %v", err)
|
||||
}
|
||||
|
||||
MilpaPath = []string{}
|
||||
if err := CheckMilpaPathSet(); err == nil {
|
||||
t.Fatalf("Got no error with unset MILPA_PATH")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnabled(t *testing.T) {
|
||||
defer func() { os.Setenv(_c.EnvVarMilpaVerbose, "") }()
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Func func() bool
|
||||
Expects bool
|
||||
}{
|
||||
{
|
||||
Name: _c.EnvVarMilpaVerbose,
|
||||
Func: VerboseEnabled,
|
||||
Expects: true,
|
||||
},
|
||||
{
|
||||
Name: _c.EnvVarValidationDisabled,
|
||||
Func: ValidationEnabled,
|
||||
},
|
||||
{
|
||||
Name: _c.EnvVarMilpaUnstyled,
|
||||
Func: ColorEnabled,
|
||||
},
|
||||
{
|
||||
Name: _c.EnvVarHelpUnstyled,
|
||||
Func: ColorEnabled,
|
||||
},
|
||||
{
|
||||
Name: _c.EnvVarDebug,
|
||||
Func: DebugEnabled,
|
||||
Expects: true,
|
||||
},
|
||||
{
|
||||
Name: _c.EnvVarHelpUnstyled,
|
||||
Func: UnstyledHelpEnabled,
|
||||
Expects: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
fname := runtime.FuncForPC(reflect.ValueOf(c.Func).Pointer()).Name()
|
||||
name := fmt.Sprintf("%v/%s", fname, c.Name)
|
||||
enabled := []string{
|
||||
"yes", "true", "1", "enabled",
|
||||
}
|
||||
for _, val := range enabled {
|
||||
t.Run("enabled-"+val, func(t *testing.T) {
|
||||
os.Setenv(c.Name, val)
|
||||
if c.Func() != c.Expects {
|
||||
t.Fatalf("%s wasn't enabled with a valid value: %s", name, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
disabled := []string{"", "no", "false", "0", "disabled"}
|
||||
for _, val := range disabled {
|
||||
t.Run("disabled-"+val, func(t *testing.T) {
|
||||
os.Setenv(c.Name, val)
|
||||
if c.Func() == c.Expects {
|
||||
t.Fatalf("%s was enabled with falsy value: %s", name, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorMode(t *testing.T) {
|
||||
cases := []struct {
|
||||
Args []string
|
||||
Expects bool
|
||||
}{
|
||||
{
|
||||
Args: []string{},
|
||||
},
|
||||
{
|
||||
Args: []string{""},
|
||||
},
|
||||
{
|
||||
Args: []string{"something", "doctor"},
|
||||
},
|
||||
{
|
||||
Args: []string{"__doctor"},
|
||||
Expects: true,
|
||||
},
|
||||
{
|
||||
Args: []string{"__doctor", "whatever"},
|
||||
Expects: true,
|
||||
},
|
||||
{
|
||||
Args: []string{"itself", "doctor"},
|
||||
Expects: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(strings.Join(c.Args, " "), func(t *testing.T) {
|
||||
os.Args = append([]string{"compa"}, c.Args...)
|
||||
res := DoctorModeEnabled()
|
||||
if res != c.Expects {
|
||||
t.Fatalf("Expected %v for %v and got %v", c.Expects, c.Args, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentMapEnabled(t *testing.T) {
|
||||
MilpaPath = []string{"something"}
|
||||
trueString := strconv.FormatBool(true)
|
||||
os.Setenv(_c.EnvVarMilpaForceColor, trueString)
|
||||
os.Setenv(_c.EnvVarDebug, trueString)
|
||||
os.Setenv(_c.EnvVarMilpaVerbose, trueString)
|
||||
|
||||
res := EnvironmentMap()
|
||||
if res == nil {
|
||||
t.Fatalf("Expected map, got nil")
|
||||
}
|
||||
|
||||
expected := map[string]string{
|
||||
_c.EnvVarMilpaPath: "something",
|
||||
_c.EnvVarMilpaForceColor: "always",
|
||||
_c.EnvVarMilpaPathParsed: trueString,
|
||||
_c.EnvVarDebug: trueString,
|
||||
_c.EnvVarMilpaVerbose: trueString,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, expected) {
|
||||
t.Fatalf("Unexpected result from enabled environment. Wanted %v, got %v", res, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentMapDisabled(t *testing.T) {
|
||||
MilpaPath = []string{"something"}
|
||||
trueString := strconv.FormatBool(true)
|
||||
// clear COLOR
|
||||
os.Unsetenv(_c.EnvVarMilpaForceColor)
|
||||
// set NO_COLOR
|
||||
os.Setenv(_c.EnvVarMilpaUnstyled, trueString)
|
||||
os.Unsetenv(_c.EnvVarDebug)
|
||||
os.Unsetenv(_c.EnvVarMilpaVerbose)
|
||||
os.Setenv(_c.EnvVarMilpaSilent, trueString)
|
||||
|
||||
res := EnvironmentMap()
|
||||
if res == nil {
|
||||
t.Fatalf("Expected map, got nil")
|
||||
}
|
||||
|
||||
expected := map[string]string{
|
||||
_c.EnvVarMilpaPath: "something",
|
||||
_c.EnvVarMilpaUnstyled: trueString,
|
||||
_c.EnvVarMilpaPathParsed: trueString,
|
||||
_c.EnvVarMilpaSilent: trueString,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, expected) {
|
||||
t.Fatalf("Unexpected result from disabled environment. Wanted %v, got %v", res, expected)
|
||||
}
|
||||
}
|
16
main.go
16
main.go
@ -13,9 +13,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.rob.mx/nidito/chinampa"
|
||||
"git.rob.mx/nidito/chinampa/pkg/runtime"
|
||||
_ "git.rob.mx/nidito/joao/cmd"
|
||||
"git.rob.mx/nidito/joao/internal/registry"
|
||||
"git.rob.mx/nidito/joao/internal/runtime"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -28,11 +30,13 @@ func main() {
|
||||
ForceColors: runtime.ColorEnabled(),
|
||||
})
|
||||
|
||||
if runtime.DebugEnabled() {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
logrus.Debug("Debugging enabled")
|
||||
}
|
||||
|
||||
err := registry.Execute(version)
|
||||
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
if err := chinampa.Execute(version); err != nil {
|
||||
logrus.Error(err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user