From 7e4e6eb02d4259ec9a8111a932fc947e4f39179b Mon Sep 17 00:00:00 2001 From: Roberto Hidalgo Date: Sun, 18 Dec 2022 21:09:05 -0600 Subject: [PATCH] move chinampa into its own package --- .golangci.yml | 40 +++ cmd/fetch.go | 6 +- cmd/flush.go | 6 +- cmd/get.go | 6 +- cmd/set.go | 8 +- go.mod | 13 +- go.sum | 2 + internal/command/arguments.go | 283 ------------------- internal/command/arguments_test.go | 425 ----------------------------- internal/command/command.go | 134 --------- internal/command/help.go | 88 ------ internal/command/options.go | 234 ---------------- internal/command/root.go | 43 --- internal/command/validation.go | 54 ---- internal/command/value.go | 234 ---------------- internal/command/value_test.go | 164 ----------- internal/constants/constants.go | 94 ------- internal/constants/help.md | 70 ----- internal/errors/errors.go | 70 ----- internal/errors/handler.go | 76 ------ internal/exec/exec.go | 65 ----- internal/exec/exec_test.go | 108 -------- internal/registry/cobra.go | 78 ------ internal/registry/registry.go | 265 ------------------ internal/render/render.go | 70 ----- internal/render/render_test.go | 87 ------ internal/runtime/runtime.go | 133 --------- internal/runtime/runtime_test.go | 195 ------------- main.go | 18 +- 29 files changed, 73 insertions(+), 2996 deletions(-) create mode 100644 .golangci.yml delete mode 100644 internal/command/arguments.go delete mode 100644 internal/command/arguments_test.go delete mode 100644 internal/command/command.go delete mode 100644 internal/command/help.go delete mode 100644 internal/command/options.go delete mode 100644 internal/command/root.go delete mode 100644 internal/command/validation.go delete mode 100644 internal/command/value.go delete mode 100644 internal/command/value_test.go delete mode 100644 internal/constants/constants.go delete mode 100644 internal/constants/help.md delete mode 100644 internal/errors/errors.go delete mode 100644 internal/errors/handler.go delete mode 100644 internal/exec/exec.go delete mode 100644 internal/exec/exec_test.go delete mode 100644 internal/registry/cobra.go delete mode 100644 internal/registry/registry.go delete mode 100644 internal/render/render.go delete mode 100644 internal/render/render_test.go delete mode 100644 internal/runtime/runtime.go delete mode 100644 internal/runtime/runtime_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..fa3240e --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/cmd/fetch.go b/cmd/fetch.go index b3821f7..b9de03c 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -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{ diff --git a/cmd/flush.go b/cmd/flush.go index 1946f9a..906d832 100644 --- a/cmd/flush.go +++ b/cmd/flush.go @@ -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{ diff --git a/cmd/get.go b/cmd/get.go index e3b39ab..01afb6a 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -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) { diff --git a/cmd/set.go b/cmd/set.go index aa3c60d..bce1c50 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -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") } diff --git a/go.mod b/go.mod index b69639a..e7d9cd0 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index dde8adf..e515d59 100644 --- a/go.sum +++ b/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= diff --git a/internal/command/arguments.go b/internal/command/arguments.go deleted file mode 100644 index 7c41f50..0000000 --- a/internal/command/arguments.go +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/command/arguments_test.go b/internal/command/arguments_test.go deleted file mode 100644 index 7219e21..0000000 --- a/internal/command/arguments_test.go +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 . 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 . 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 . 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) - } - }) - } -} diff --git a/internal/command/command.go b/internal/command/command.go deleted file mode 100644 index 860716b..0000000 --- a/internal/command/command.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/command/help.go b/internal/command/help.go deleted file mode 100644 index fea08e0..0000000 --- a/internal/command/help.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/command/options.go b/internal/command/options.go deleted file mode 100644 index e6e7556..0000000 --- a/internal/command/options.go +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/command/root.go b/internal/command/root.go deleted file mode 100644 index e424a05..0000000 --- a/internal/command/root.go +++ /dev/null @@ -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", - }, - }, -} diff --git a/internal/command/validation.go b/internal/command/validation.go deleted file mode 100644 index 8b87fab..0000000 --- a/internal/command/validation.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/command/value.go b/internal/command/value.go deleted file mode 100644 index 232ed1f..0000000 --- a/internal/command/value.go +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/command/value_test.go b/internal/command/value_test.go deleted file mode 100644 index 562dd01..0000000 --- a/internal/command/value_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) - } - }) - } -} diff --git a/internal/constants/constants.go b/internal/constants/constants.go deleted file mode 100644 index b1caad0..0000000 --- a/internal/constants/constants.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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)) diff --git a/internal/constants/help.md b/internal/constants/help.md deleted file mode 100644 index f884171..0000000 --- a/internal/constants/help.md +++ /dev/null @@ -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}} diff --git a/internal/errors/errors.go b/internal/errors/errors.go deleted file mode 100644 index de75145..0000000 --- a/internal/errors/errors.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) -} diff --git a/internal/errors/handler.go b/internal/errors/handler.go deleted file mode 100644 index 9745e58..0000000 --- a/internal/errors/handler.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) -} diff --git a/internal/exec/exec.go b/internal/exec/exec.go deleted file mode 100644 index 9f187f4..0000000 --- a/internal/exec/exec.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/exec/exec_test.go b/internal/exec/exec_test.go deleted file mode 100644 index 4e2c0fc..0000000 --- a/internal/exec/exec_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) - } -} diff --git a/internal/registry/cobra.go b/internal/registry/cobra.go deleted file mode 100644 index 4f18f66..0000000 --- a/internal/registry/cobra.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/registry/registry.go b/internal/registry/registry.go deleted file mode 100644 index bc5d222..0000000 --- a/internal/registry/registry.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 - }, -} diff --git a/internal/render/render.go b/internal/render/render.go deleted file mode 100644 index d0c5ff8..0000000 --- a/internal/render/render.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) -} diff --git a/internal/render/render_test.go b/internal/render/render_test.go deleted file mode 100644 index 49b98db..0000000 --- a/internal/render/render_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) - } - }) - } -} diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go deleted file mode 100644 index f16c7f1..0000000 --- a/internal/runtime/runtime.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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 -} diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go deleted file mode 100644 index 74155ae..0000000 --- a/internal/runtime/runtime_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright © 2022 Roberto Hidalgo -// -// 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) - } -} diff --git a/main.go b/main.go index 47edced..8274b3d 100644 --- a/main.go +++ b/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(), }) - logrus.SetLevel(logrus.DebugLevel) + 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) } }