move chinampa into its own package

This commit is contained in:
Roberto Hidalgo 2022-12-18 21:09:05 -06:00
parent ba63cc2419
commit 7e4e6eb02d
29 changed files with 73 additions and 2996 deletions

40
.golangci.yml Normal file
View File

@ -0,0 +1,40 @@
linters-settings:
gocyclo:
min-complexity: 21
tagliatelle:
case:
rules:
yaml: kebab
linters:
fast: false
enable:
- deadcode
- errcheck
- exportloopref
- goconst
- gocritic
- gocyclo
- godot
- gofmt
- goimports
- gosec
- gosimple
- govet
- ifshort
- ineffassign
- misspell
- nakedret
- nilerr
- prealloc
- revive
- staticcheck
- structcheck
- stylecheck
- tagliatelle
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace

View File

@ -17,14 +17,14 @@ import (
"io/fs" "io/fs"
"os" "os"
"git.rob.mx/nidito/joao/internal/command" "git.rob.mx/nidito/chinampa"
"git.rob.mx/nidito/joao/internal/registry" "git.rob.mx/nidito/chinampa/pkg/command"
"git.rob.mx/nidito/joao/pkg/config" "git.rob.mx/nidito/joao/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func init() { func init() {
registry.Register(fetchCommand) chinampa.Register(fetchCommand)
} }
var fetchCommand = (&command.Command{ var fetchCommand = (&command.Command{

View File

@ -15,15 +15,15 @@ package cmd
import ( import (
"fmt" "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" opclient "git.rob.mx/nidito/joao/internal/op-client"
"git.rob.mx/nidito/joao/internal/registry"
"git.rob.mx/nidito/joao/pkg/config" "git.rob.mx/nidito/joao/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func init() { func init() {
registry.Register(flushCommand) chinampa.Register(flushCommand)
} }
var flushCommand = (&command.Command{ var flushCommand = (&command.Command{

View File

@ -19,15 +19,15 @@ import (
"sort" "sort"
"strings" "strings"
"git.rob.mx/nidito/joao/internal/command" "git.rob.mx/nidito/chinampa"
"git.rob.mx/nidito/joao/internal/registry" "git.rob.mx/nidito/chinampa/pkg/command"
"git.rob.mx/nidito/joao/pkg/config" "git.rob.mx/nidito/joao/pkg/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func init() { func init() {
registry.Register(gCommand) chinampa.Register(gCommand)
} }
func keyFinder(cmd *command.Command, currentValue string) ([]string, cobra.ShellCompDirective, error) { func keyFinder(cmd *command.Command, currentValue string) ([]string, cobra.ShellCompDirective, error) {

View File

@ -18,15 +18,15 @@ import (
"os" "os"
"strings" "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" opclient "git.rob.mx/nidito/joao/internal/op-client"
"git.rob.mx/nidito/joao/internal/registry"
"git.rob.mx/nidito/joao/pkg/config" "git.rob.mx/nidito/joao/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func init() { func init() {
registry.Register(setCommand) chinampa.Register(setCommand)
} }
var setCommand = (&command.Command{ 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") 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") logrus.Warn("Ignoring --file while deleting")
} }

13
go.mod
View File

@ -3,15 +3,12 @@ module git.rob.mx/nidito/joao
go 1.18 go 1.18
require ( require (
git.rob.mx/nidito/chinampa v0.0.0-20221219030434-c622ba72beb5
github.com/1Password/connect-sdk-go v1.5.0 github.com/1Password/connect-sdk-go v1.5.0
github.com/alessio/shellescape v1.4.1 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/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@ -19,9 +16,12 @@ require (
github.com/alecthomas/chroma v0.10.0 // indirect github.com/alecthomas/chroma v0.10.0 // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/aymerick/douceur v0.2.0 // 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/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/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.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/gorilla/css v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/leodido/go-urn v1.2.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/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // 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-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/yuin/goldmark v1.5.2 // indirect github.com/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect
go.uber.org/atomic v1.9.0 // 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/net v0.0.0-20221002022538-bcab6841153b // indirect
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // 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 golang.org/x/text v0.3.7 // indirect
) )

2
go.sum
View File

@ -1,4 +1,6 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 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 h1:F0WJcLSzGg3iXEDY49/ULdszYKsQLGTzn+2cyYXqiyk=
github.com/1Password/connect-sdk-go v1.5.0/go.mod h1:TdynFeyvaRoackENbJ8RfJokH+WAowAu1MLmUbdMq6s= 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= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=

View File

@ -1,283 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"strings"
"git.rob.mx/nidito/joao/internal/errors"
"github.com/spf13/cobra"
)
func contains(haystack []string, needle string) bool {
for _, validValue := range haystack {
if needle == validValue {
return true
}
}
return false
}
// Arguments is an ordered list of Argument.
type Arguments []*Argument
func (args *Arguments) AllKnown() map[string]any {
col := map[string]any{}
for _, arg := range *args {
col[arg.Name] = arg.ToValue()
}
return col
}
func (args *Arguments) AllKnownStr() map[string]string {
col := map[string]string{}
for _, arg := range *args {
col[arg.Name] = arg.ToString()
}
return col
}
func (args *Arguments) Parse(supplied []string) {
for idx, arg := range *args {
argumentProvided := idx < len(supplied)
if !argumentProvided {
if arg.Default != nil {
if arg.Variadic {
defaultSlice := []string{}
for _, valI := range arg.Default.([]any) {
defaultSlice = append(defaultSlice, valI.(string))
}
arg.provided = &defaultSlice
} else {
defaultString := arg.Default.(string)
if defaultString != "" {
arg.provided = &[]string{defaultString}
}
}
}
continue
}
if arg.Variadic {
values := append([]string{}, supplied[idx:]...)
arg.SetValue(values)
} else {
arg.SetValue([]string{supplied[idx]})
}
}
}
func (args *Arguments) AreValid() error {
for _, arg := range *args {
if err := arg.Validate(); err != nil {
return err
}
}
return nil
}
// CompletionFunction is called by cobra when asked to complete arguments.
func (args *Arguments) CompletionFunction(cc *cobra.Command, provided []string, toComplete string) ([]string, cobra.ShellCompDirective) {
expectedArgLen := len(*args)
values := []string{}
directive := cobra.ShellCompDirectiveError
if expectedArgLen > 0 {
argsCompleted := len(provided)
lastArg := (*args)[len(*args)-1]
hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic
lastArg.Command.Options.Parse(cc.Flags())
args.Parse(provided)
directive = cobra.ShellCompDirectiveDefault
if argsCompleted < expectedArgLen || hasVariadicArg {
var arg *Argument
if hasVariadicArg && argsCompleted >= expectedArgLen {
// completing a variadic argument
arg = lastArg
} else {
// completing regular argument (maybe variadic!)
arg = (*args)[argsCompleted]
}
if arg.Values != nil {
var err error
arg.Values.command = lastArg.Command
arg.Command = lastArg.Command
values, directive, err = arg.Resolve(toComplete)
if err != nil {
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
}
} else {
directive = cobra.ShellCompDirectiveError
}
values = cobra.AppendActiveHelp(values, arg.Description)
}
if toComplete != "" {
filtered := []string{}
for _, value := range values {
if strings.HasPrefix(value, toComplete) {
filtered = append(filtered, value)
}
}
values = filtered
}
}
return values, directive
}
// Argument represents a single command-line argument.
type Argument struct {
// Name is how this variable will be exposed to the underlying command.
Name string `json:"name" yaml:"name" validate:"required,excludesall=!$\\/%^@#?:'\""`
// Description is what this argument is for.
Description string `json:"description" yaml:"description" validate:"required"`
// Default is the default value for this argument if none is provided.
Default any `json:"default,omitempty" yaml:"default,omitempty" validate:"excluded_with=Required"`
// Variadic makes an argument a list of all values from this one on.
Variadic bool `json:"variadic" yaml:"variadic"`
// Required raises an error if an argument is not provided.
Required bool `json:"required" yaml:"required" validate:"excluded_with=Default"`
// Values describes autocompletion and validation for an argument
Values *ValueSource `json:"values,omitempty" yaml:"values" validate:"omitempty"`
Command *Command `json:"-" yaml:"-" validate:"-"`
provided *[]string
}
func (arg *Argument) EnvName() string {
return strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))
}
func (arg *Argument) SetValue(value []string) {
arg.provided = &value
}
func (arg *Argument) IsKnown() bool {
return arg.provided != nil && len(*arg.provided) > 0
}
func (arg *Argument) ToString() string {
val := arg.ToValue()
if arg.Variadic {
val := val.([]string)
return strings.Join(val, " ")
}
return val.(string)
}
func (arg *Argument) ToValue() any {
var value any
if arg.IsKnown() {
if arg.Variadic {
value = *arg.provided
} else {
vals := *arg.provided
value = vals[0]
}
} else {
if arg.Default != nil {
if arg.Variadic {
defaultSlice := []string{}
for _, valI := range arg.Default.([]any) {
valStr := valI.(string)
defaultSlice = append(defaultSlice, valStr)
}
value = defaultSlice
} else {
value = arg.Default.(string)
}
} else {
if arg.Variadic {
value = []string{}
} else {
value = ""
}
}
}
return value
}
func (arg *Argument) Validate() error {
if !arg.IsKnown() {
if arg.Required {
return errors.BadArguments{Msg: fmt.Sprintf("Missing argument for %s", strings.ToUpper(arg.Name))}
}
return nil
}
if !arg.Validates() {
return nil
}
validValues, _, err := arg.Resolve(strings.Join(*arg.provided, " "))
if err != nil {
return err
}
if arg.Variadic {
for _, current := range *arg.provided {
if !contains(validValues, current) {
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
}
}
} else {
current := arg.ToValue().(string)
if !contains(validValues, current) {
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
}
}
return nil
}
// Validates tells if the user-supplied value needs validation.
func (arg *Argument) Validates() bool {
return arg.Values != nil && arg.Values.Validates()
}
// ToDesc prints out the description of an argument for help and docs.
func (arg *Argument) ToDesc() string {
spec := arg.EnvName()
if arg.Variadic {
spec = fmt.Sprintf("%s...", spec)
}
if !arg.Required {
spec = fmt.Sprintf("[%s]", spec)
}
return spec
}
// Resolve returns autocomplete values for an argument.
func (arg *Argument) Resolve(current string) (values []string, flag cobra.ShellCompDirective, err error) {
if arg.Values != nil {
values, flag, err = arg.Values.Resolve(current)
if err != nil {
flag = cobra.ShellCompDirectiveError
return
}
}
return
}

View File

@ -1,425 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command_test
import (
"reflect"
"strings"
"testing"
. "git.rob.mx/nidito/joao/internal/command"
)
func testCommand() *Command {
return (&Command{
Arguments: []*Argument{
{
Name: "first",
Default: "default",
},
{
Name: "variadic",
Default: []any{"defaultVariadic0", "defaultVariadic1"},
Variadic: true,
},
},
Options: Options{
"option": {
Default: "default",
Type: "string",
},
"bool": {
Type: "bool",
Default: false,
},
},
}).SetBindings()
}
func TestParse(t *testing.T) {
cmd := testCommand()
cmd.Arguments.Parse([]string{"asdf", "one", "two", "three"})
known := cmd.Arguments.AllKnown()
if !cmd.Arguments[0].IsKnown() {
t.Fatalf("first argument isn't known")
}
val, exists := known["first"]
if !exists {
t.Fatalf("first argument isn't on AllKnown map: %v", known)
}
if val != "asdf" {
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
}
if !cmd.Arguments[1].IsKnown() {
t.Fatalf("variadic argument isn't known")
}
val, exists = known["variadic"]
if !exists {
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
}
if !reflect.DeepEqual(val, []string{"one", "two", "three"}) {
t.Fatalf("Known argument does not match. expected: %s, got %s", "one two three", val)
}
cmd = testCommand()
cmd.Arguments.Parse([]string{"asdf"})
known = cmd.Arguments.AllKnown()
if !cmd.Arguments[0].IsKnown() {
t.Fatalf("first argument is not known")
}
val, exists = known["first"]
if !exists {
t.Fatalf("first argument isn't on AllKnown map: %v", known)
}
if val != "asdf" {
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
}
val, exists = known["variadic"]
if !exists {
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
}
expected := []string{"defaultVariadic0", "defaultVariadic1"}
if !reflect.DeepEqual(val, expected) {
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
}
}
func TestBeforeParse(t *testing.T) {
cmd := testCommand()
known := cmd.Arguments.AllKnown()
if cmd.Arguments[0].IsKnown() {
t.Fatalf("first argument is known")
}
val, exists := known["first"]
if !exists {
t.Fatalf("first argument isn't on AllKnown map: %v", known)
}
if val != "default" {
t.Fatalf("first argument does not match. expected: %s, got %s", "asdf", val)
}
val, exists = known["variadic"]
if !exists {
t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
}
expected := []string{"defaultVariadic0", "defaultVariadic1"}
if !reflect.DeepEqual(val, expected) {
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
}
}
func TestArgumentsValidate(t *testing.T) {
staticArgument := func(name string, def string, values []string, variadic bool) *Argument {
return &Argument{
Name: name,
Default: def,
Variadic: variadic,
Required: def == "",
Values: &ValueSource{
Static: &values,
},
}
}
cases := []struct {
Command *Command
Args []string
ErrorSuffix string
Env []string
}{
{
Command: (&Command{
// Name: []string{"test", "required", "failure"},
Arguments: []*Argument{
{
Name: "first",
Required: true,
},
},
}).SetBindings(),
ErrorSuffix: "Missing argument for FIRST",
},
{
Args: []string{"bad"},
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: good, default",
Command: (&Command{
// Name: []string{"test", "script", "bad"},
Arguments: []*Argument{
{
Name: "first",
Default: "default",
Values: &ValueSource{
Script: "echo good; echo default",
},
},
},
}).SetBindings(),
},
{
Args: []string{"bad"},
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: default, good",
Command: (&Command{
// Name: []string{"test", "static", "errors"},
Arguments: []*Argument{staticArgument("first", "default", []string{"default", "good"}, false)},
}).SetBindings(),
},
{
Args: []string{"default", "good", "bad"},
ErrorSuffix: "bad is not a valid value for argument <first>. Valid options are: default, good",
Command: (&Command{
// Name: []string{"test", "static", "errors"},
Arguments: []*Argument{staticArgument("first", "default", []string{"default", "good"}, true)},
}).SetBindings(),
},
{
Args: []string{"good"},
ErrorSuffix: "could not validate argument for command test script bad-exit, ran",
Command: (&Command{
Path: []string{"test", "script", "bad-exit"},
Arguments: []*Argument{
{
Name: "first",
Default: "default",
Values: &ValueSource{
Script: "echo good; echo default; exit 2",
},
},
},
}).SetBindings(),
},
}
t.Run("good command is good", func(t *testing.T) {
cmd := testCommand()
cmd.Arguments[0] = staticArgument("first", "default", []string{"default", "good"}, false)
cmd.Arguments[1] = staticArgument("second", "", []string{"one", "two", "three"}, true)
cmd.SetBindings()
cmd.Arguments.Parse([]string{"first", "one", "three", "two"})
err := cmd.Arguments.AreValid()
if err == nil {
t.Fatalf("Unexpected failure validating: %s", err)
}
})
for _, c := range cases {
t.Run(c.Command.FullName(), func(t *testing.T) {
c.Command.Arguments.Parse(c.Args)
err := c.Command.Arguments.AreValid()
if err == nil {
t.Fatalf("Expected failure but got none")
}
if !strings.HasPrefix(err.Error(), c.ErrorSuffix) {
t.Fatalf("Could not find error <%s> got <%s>", c.ErrorSuffix, err)
}
})
}
}
// func TestArgumentsToEnv(t *testing.T) {
// cases := []struct {
// Command *Command
// Args []string
// Expect []string
// Env []string
// }{
// {
// Args: []string{"something"},
// Expect: []string{"export MILPA_ARG_FIRST=something"},
// Command: &Command{
// // Name: []string{"test", "required", "present"},
// Arguments: []*Argument{
// {
// Name: "first",
// Required: true,
// },
// },
// },
// },
// {
// Args: []string{},
// Expect: []string{"export MILPA_ARG_FIRST=default"},
// Command: &Command{
// // Name: []string{"test", "default", "present"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// },
// },
// },
// },
// {
// Args: []string{"zero", "one", "two", "three"},
// Expect: []string{
// "export MILPA_ARG_FIRST=zero",
// "declare -a MILPA_ARG_VARIADIC='( one two three )'",
// },
// Command: &Command{
// // Name: []string{"test", "variadic"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// },
// {
// Name: "variadic",
// Variadic: true,
// },
// },
// },
// },
// {
// Args: []string{},
// Expect: []string{"export MILPA_ARG_FIRST=default"},
// Command: &Command{
// // Name: []string{"test", "static", "default"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// Values: &ValueSource{
// Static: &[]string{
// "default",
// "good",
// },
// },
// },
// },
// },
// },
// {
// Args: []string{"good"},
// Expect: []string{"export MILPA_ARG_FIRST=good"},
// Command: &Command{
// // Name: []string{"test", "static", "good"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// Values: &ValueSource{
// Static: &[]string{
// "default",
// "good",
// },
// },
// },
// },
// },
// },
// {
// Args: []string{"good"},
// Expect: []string{"export MILPA_ARG_FIRST=good"},
// Command: &Command{
// // Name: []string{"test", "script", "good"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// Values: &ValueSource{
// Script: "echo good; echo default",
// },
// },
// },
// },
// },
// }
// for _, c := range cases {
// t.Run(c.Command.FullName(), func(t *testing.T) {
// dst := []string{}
// c.Command.SetBindings()
// c.Command.Arguments.Parse(c.Args)
// c.Command.Arguments.ToEnv(c.Command, &dst, "export ")
// err := c.Command.Arguments.AreValid()
// if err != nil {
// t.Fatalf("Unexpected failure validating: %s", err)
// }
// for _, expected := range c.Expect {
// found := false
// for _, actual := range dst {
// if strings.HasPrefix(actual, expected) {
// found = true
// break
// }
// }
// if !found {
// t.Fatalf("Expected line %v not found in %v", expected, dst)
// }
// }
// })
// }
// }
func TestArgumentToDesc(t *testing.T) {
cases := []struct {
Arg *Argument
Spec string
}{
{
Arg: &Argument{
Name: "regular",
},
Spec: "[REGULAR]",
},
{
Arg: &Argument{
Name: "required",
Required: true,
},
Spec: "REQUIRED",
},
{
Arg: &Argument{
Name: "variadic-regular",
Variadic: true,
},
Spec: "[VARIADIC_REGULAR...]",
},
{
Arg: &Argument{
Name: "variadic-required",
Variadic: true,
Required: true,
},
Spec: "VARIADIC_REQUIRED...",
},
}
for _, c := range cases {
t.Run(c.Arg.Name, func(t *testing.T) {
res := c.Arg.ToDesc()
if res != c.Spec {
t.Fatalf("Expected %s got %s", c.Spec, res)
}
})
}
}

View File

@ -1,134 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type HelpFunc func(printLinks bool) string
type Action func(cmd *Command) error
type Command struct {
Path []string
// Summary is a short description of a command, on supported shells this is part of the autocomplete prompt
Summary string `json:"summary" yaml:"summary" validate:"required"`
// Description is a long form explanation of how a command works its magic. Markdown is supported
Description string `json:"description" yaml:"description" validate:"required"`
// A list of arguments for a command
Arguments Arguments `json:"arguments" yaml:"arguments" validate:"dive"`
// A map of option names to option definitions
Options Options `json:"options" yaml:"options" validate:"dive"`
HelpFunc HelpFunc `json:"-" yaml:"-"`
// The action to take upon running
Action Action
runtimeFlags *pflag.FlagSet
Cobra *cobra.Command
}
func (cmd *Command) SetBindings() *Command {
ptr := cmd
for _, opt := range cmd.Options {
opt.Command = ptr
if opt.Validates() {
opt.Values.command = ptr
}
}
for _, arg := range cmd.Arguments {
arg.Command = ptr
if arg.Validates() {
arg.Values.command = ptr
}
}
return ptr
}
func (cmd *Command) Name() string {
return cmd.Path[len(cmd.Path)-1]
}
func (cmd *Command) FullName() string {
return strings.Join(cmd.Path, " ")
}
func (cmd *Command) FlagSet() *pflag.FlagSet {
if cmd.runtimeFlags == nil {
fs := pflag.NewFlagSet(strings.Join(cmd.Path, " "), pflag.ContinueOnError)
fs.SortFlags = false
fs.Usage = func() {}
for name, opt := range cmd.Options {
switch opt.Type {
case ValueTypeBoolean:
def := false
if opt.Default != nil {
def = opt.Default.(bool)
}
fs.Bool(name, def, opt.Description)
case ValueTypeDefault, ValueTypeString:
opt.Type = ValueTypeString
def := ""
if opt.Default != nil {
def = fmt.Sprintf("%s", opt.Default)
}
fs.String(name, def, opt.Description)
default:
// ignore flag
logrus.Warnf("Ignoring unknown option type <%s> for option <%s>", opt.Type, name)
continue
}
}
cmd.runtimeFlags = fs
}
return cmd.runtimeFlags
}
func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error {
cmd.Arguments.Parse(args)
skipValidation, _ := cc.Flags().GetBool("skip-validation")
cmd.Options.Parse(cc.Flags())
if !skipValidation {
logrus.Debug("Validating arguments")
if err := cmd.Arguments.AreValid(); err != nil {
return err
}
logrus.Debug("Validating flags")
if err := cmd.Options.AreValid(); err != nil {
return err
}
}
return nil
}
func (cmd *Command) Run(cc *cobra.Command, args []string) error {
logrus.Debugf("running command %s", cmd.FullName())
if err := cmd.ParseInput(cc, args); err != nil {
return err
}
return cmd.Action(cmd)
}
func (cmd *Command) SetCobra(cc *cobra.Command) {
cmd.Cobra = cc
}

View File

@ -1,88 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"bytes"
_c "git.rob.mx/nidito/joao/internal/constants"
"git.rob.mx/nidito/joao/internal/render"
"git.rob.mx/nidito/joao/internal/runtime"
"github.com/spf13/cobra"
)
type combinedCommand struct {
Spec *Command
Command *cobra.Command
GlobalOptions Options
HTMLOutput bool
}
func (cmd *Command) HasAdditionalHelp() bool {
return cmd.HelpFunc != nil
}
func (cmd *Command) AdditionalHelp(printLinks bool) *string {
if cmd.HelpFunc != nil {
str := cmd.HelpFunc(printLinks)
return &str
}
return nil
}
func (cmd *Command) HelpRenderer(globalOptions Options) func(cc *cobra.Command, args []string) {
return func(cc *cobra.Command, args []string) {
// some commands don't have a binding until help is rendered
// like virtual ones (sub command groups)
cmd.SetCobra(cc)
content, err := cmd.ShowHelp(globalOptions, args)
if err != nil {
panic(err)
}
_, err = cc.OutOrStderr().Write(content)
if err != nil {
panic(err)
}
}
}
func (cmd *Command) ShowHelp(globalOptions Options, args []string) ([]byte, error) {
var buf bytes.Buffer
c := &combinedCommand{
Spec: cmd,
Command: cmd.Cobra,
GlobalOptions: globalOptions,
HTMLOutput: runtime.UnstyledHelpEnabled(),
}
err := _c.TemplateCommandHelp.Execute(&buf, c)
if err != nil {
return nil, err
}
colorEnabled := runtime.ColorEnabled()
flags := cmd.Cobra.Flags()
ncf := cmd.Cobra.Flag("no-color") // nolint:ifshort
cf := cmd.Cobra.Flag("color") // nolint:ifshort
if noColorFlag, err := flags.GetBool("no-color"); err == nil && ncf.Changed {
colorEnabled = !noColorFlag
} else if colorFlag, err := flags.GetBool("color"); err == nil && cf.Changed {
colorEnabled = colorFlag
}
content, err := render.Markdown(buf.Bytes(), colorEnabled)
if err != nil {
return nil, err
}
return content, nil
}

View File

@ -1,234 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"strconv"
"strings"
"git.rob.mx/nidito/joao/internal/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Options is a map of name to Option.
type Options map[string]*Option
func (opts *Options) AllKnown() map[string]any {
col := map[string]any{}
for name, opt := range *opts {
col[name] = opt.ToValue()
}
return col
}
func (opts *Options) AllKnownStr() map[string]string {
col := map[string]string{}
for name, opt := range *opts {
col[name] = opt.ToString()
}
return col
}
// func envValue(opts Options, f *pflag.Flag) (*string, *string) {
// name := f.Name
// if name == _c.HelpCommandName {
// return nil, nil
// }
// envName := ""
// value := f.Value.String()
// if cname, ok := _c.EnvFlagNames[name]; ok {
// if value == "false" {
// return nil, nil
// }
// envName = cname
// } else {
// envName = fmt.Sprintf("%s%s", _c.OutputPrefixOpt, strings.ToUpper(strings.ReplaceAll(name, "-", "_")))
// opt := opts[name]
// if opt != nil {
// value = opt.ToString(true)
// }
// if value == "false" && opt.Type == ValueTypeBoolean {
// // makes dealing with false flags in shell easier
// value = ""
// }
// }
// return &envName, &value
// }
// // ToEnv writes shell variables to dst.
// func (opts *Options) ToEnv(command *Command, dst *[]string, prefix string) {
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
// envName, value := envValue(*opts, f)
// if envName != nil && value != nil {
// *dst = append(*dst, fmt.Sprintf("%s%s=%s", prefix, *envName, *value))
// }
// })
// }
// func (opts *Options) EnvMap(command *Command, dst *map[string]string) {
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
// envName, value := envValue(*opts, f)
// if envName != nil && value != nil {
// (*dst)[*envName] = *value
// }
// })
// }
func (opts *Options) Parse(supplied *pflag.FlagSet) {
// logrus.Debugf("Parsing supplied flags, %v", supplied)
for name, opt := range *opts {
switch opt.Type {
case ValueTypeBoolean:
if val, err := supplied.GetBool(name); err == nil {
opt.provided = val
continue
}
default:
opt.Type = ValueTypeString
if val, err := supplied.GetString(name); err == nil {
opt.provided = val
continue
}
}
}
}
func (opts *Options) AreValid() error {
for name, opt := range *opts {
if err := opt.Validate(name); err != nil {
return err
}
}
return nil
}
// Option represents a command line flag.
type Option struct {
ShortName string `json:"short-name,omitempty" yaml:"short-name,omitempty"` // nolint:tagliatelle
Type ValueType `json:"type" yaml:"type" validate:"omitempty,oneof=string bool"`
Description string `json:"description" yaml:"description" validate:"required"`
Default any `json:"default,omitempty" yaml:"default,omitempty"`
Values *ValueSource `json:"values,omitempty" yaml:"values,omitempty" validate:"omitempty"`
Repeated bool `json:"repeated" yaml:"repeated" validate:"omitempty"`
Command *Command `json:"-" yaml:"-" validate:"-"`
provided any
}
func (opt *Option) IsKnown() bool {
return opt.provided != nil
}
func (opt *Option) ToValue() any {
if opt.IsKnown() {
return opt.provided
}
return opt.Default
}
func (opt *Option) ToString() string {
value := opt.ToValue()
stringValue := ""
if opt.Type == "bool" {
if value == nil {
stringValue = ""
} else {
stringValue = strconv.FormatBool(value.(bool))
}
} else {
if value != nil {
stringValue = value.(string)
}
}
return stringValue
}
func (opt *Option) Validate(name string) error {
if !opt.Validates() {
return nil
}
current := opt.ToString() // nolint:ifshort
if current == "" {
return nil
}
validValues, _, err := opt.Resolve(current)
if err != nil {
return err
}
if !contains(validValues, current) {
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for option <%s>. Valid options are: %s", current, name, strings.Join(validValues, ", "))}
}
return nil
}
// Validates tells if the user-supplied value needs validation.
func (opt *Option) Validates() bool {
return opt.Values != nil && opt.Values.Validates()
}
// providesAutocomplete tells if this option provides autocomplete values.
func (opt *Option) providesAutocomplete() bool {
return opt.Values != nil
}
// Resolve returns autocomplete values for an option.
func (opt *Option) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
if opt.Values != nil {
if opt.Values.command == nil {
opt.Values.command = opt.Command
}
return opt.Values.Resolve(currentValue)
}
return
}
// CompletionFunction is called by cobra when asked to complete an option.
func (opt *Option) CompletionFunction(cmd *cobra.Command, args []string, toComplete string) (values []string, flag cobra.ShellCompDirective) {
if !opt.providesAutocomplete() {
flag = cobra.ShellCompDirectiveNoFileComp
return
}
opt.Command.Arguments.Parse(args)
opt.Command.Options.Parse(cmd.Flags())
var err error
values, flag, err = opt.Resolve(toComplete)
if err != nil {
return values, cobra.ShellCompDirectiveError
}
if toComplete != "" {
filtered := []string{}
for _, value := range values {
if strings.HasPrefix(value, toComplete) {
filtered = append(filtered, value)
}
}
values = filtered
}
return cobra.AppendActiveHelp(values, opt.Description), flag
}

View File

@ -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",
},
},
}

View File

@ -1,54 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
type varSearchMap struct {
Status int
Name string
Usage string
}
func (cmd *Command) Validate() (report map[string]int) {
report = map[string]int{}
validate := validator.New()
if err := validate.Struct(cmd); err != nil {
verrs := err.(validator.ValidationErrors)
for _, issue := range verrs {
// todo: output better errors, see validator.FieldError
report[fmt.Sprint(issue)] = 1
}
}
vars := map[string]map[string]*varSearchMap{
"argument": {},
"option": {},
}
for _, arg := range cmd.Arguments {
vars["argument"][strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))] = &varSearchMap{2, arg.Name, ""}
}
for name := range cmd.Options {
vars["option"][strings.ToUpper(strings.ReplaceAll(name, "-", "_"))] = &varSearchMap{2, name, ""}
}
return report
}

View File

@ -1,234 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command
import (
"bytes"
"context"
"fmt"
"os"
"strings"
"text/template"
"time"
_c "git.rob.mx/nidito/joao/internal/constants"
"git.rob.mx/nidito/joao/internal/exec"
"github.com/spf13/cobra"
)
// ValueType represent the kinds of or option.
type ValueType string
const (
// ValueTypeDefault is the empty string, maps to ValueTypeString.
ValueTypeDefault ValueType = ""
// ValueTypeString a value treated like a string.
ValueTypeString ValueType = "string"
// ValueTypeBoolean is a value treated like a boolean.
ValueTypeBoolean ValueType = "bool"
)
type SourceCommand struct {
Path []string
Args string
}
type CompletionFunc func(cmd *Command, currentValue string) (values []string, flag cobra.ShellCompDirective, err error)
// ValueSource represents the source for an auto-completed and/or validated option/argument.
type ValueSource struct {
// Directories prompts for directories with the given prefix.
Directories *string `json:"dirs,omitempty" yaml:"dirs,omitempty" validate:"omitempty,excluded_with=Command Files Func Script Static"`
// Files prompts for files with the given extensions
Files *[]string `json:"files,omitempty" yaml:"files,omitempty" validate:"omitempty,excluded_with=Command Func Directories Script Static"`
// Script runs the provided command with `bash -c "$script"` and returns an option for every line of stdout.
Script string `json:"script,omitempty" yaml:"script,omitempty" validate:"omitempty,excluded_with=Command Directories Files Func Static"`
// Static returns the given list.
Static *[]string `json:"static,omitempty" yaml:"static,omitempty" validate:"omitempty,excluded_with=Command Directories Files Func Script"`
// Command runs a subcommand and returns an option for every line of stdout.
Command *SourceCommand `json:"command,omitempty" yaml:"command,omitempty" validate:"omitempty,excluded_with=Directories Files Func Script Static"`
// Func runs a function
Func CompletionFunc `json:"func,omitempty" yaml:"func,omitempty" validate:"omitempty,excluded_with=Command Directories Files Script Static"`
// Timeout is the maximum amount of time we will wait for a Script, Command, or Func before giving up on completions/validations.
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty" validate:"omitempty,excluded_with=Directories Files Static"`
// Suggestion if provided will only suggest autocomplete values but will not perform validation of a given value
Suggestion bool `json:"suggest-only" yaml:"suggest-only" validate:"omitempty"` // nolint:tagliatelle
// SuggestRaw if provided the shell will not add a space after autocompleting
SuggestRaw bool `json:"suggest-raw" yaml:"suggest-raw" validate:"omitempty"` // nolint:tagliatelle
command *Command `json:"-" yaml:"-" validate:"-"`
computed *[]string
flag cobra.ShellCompDirective
}
// Validates tells if a value needs to be validated.
func (vs *ValueSource) Validates() bool {
if vs.Directories != nil || vs.Files != nil {
return false
}
return !vs.Suggestion
}
// Resolve returns the values for autocomplete and validation.
func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
if vs.computed != nil {
return *vs.computed, vs.flag, nil
}
if vs.Timeout == 0 {
vs.Timeout = 5
}
flag = cobra.ShellCompDirectiveDefault
timeout := time.Duration(vs.Timeout)
switch {
case vs.Static != nil:
values = *vs.Static
case vs.Files != nil:
flag = cobra.ShellCompDirectiveFilterFileExt
values = *vs.Files
case vs.Directories != nil:
flag = cobra.ShellCompDirectiveFilterDirs
values = []string{*vs.Directories}
case vs.Func != nil:
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
defer cancel()
done := make(chan error, 1)
panicChan := make(chan any, 1)
go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
values, flag, err = vs.Func(vs.command, currentValue)
done <- err
}()
select {
case err = <-done:
return
case p := <-panicChan:
panic(p)
case <-ctx.Done():
flag = cobra.ShellCompDirectiveError
err = ctx.Err()
return
}
case vs.Command != nil:
if vs.command == nil {
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("bug: command is nil")
}
argString, err := vs.command.ResolveTemplate(vs.Command.Args, currentValue)
if err != nil {
return nil, cobra.ShellCompDirectiveError, err
}
args := strings.Split(argString, " ")
sub, _, err := vs.command.Cobra.Root().Find(vs.Command.Path)
if err != nil {
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("could not find a command named %s", vs.Command.Path)
}
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
defer cancel() // The cancel should be deferred so resources are cleaned up
sub.SetArgs(args)
var stdout bytes.Buffer
sub.SetOut(&stdout)
var stderr bytes.Buffer
sub.SetErr(&stderr)
err = sub.ExecuteContext(ctx)
if err != nil {
return nil, cobra.ShellCompDirectiveError, err
}
values = strings.Split(stdout.String(), "\n")
flag = cobra.ShellCompDirectiveDefault
case vs.Script != "":
if vs.command == nil {
return nil, cobra.ShellCompDirectiveError, fmt.Errorf("bug: command is nil")
}
cmd, err := vs.command.ResolveTemplate(vs.Script, currentValue)
if err != nil {
return nil, cobra.ShellCompDirectiveError, err
}
args := append([]string{"/bin/bash", "-c"}, cmd)
values, flag, err = exec.Exec(vs.command.FullName(), args, os.Environ(), timeout*time.Second)
if err != nil {
return nil, flag, err
}
}
vs.computed = &values
if vs.SuggestRaw {
flag |= cobra.ShellCompDirectiveNoSpace
}
vs.flag = flag
return values, flag, err
}
type AutocompleteTemplate struct {
Args map[string]string
Opts map[string]string
}
func (tpl *AutocompleteTemplate) Opt(name string) string {
if val, ok := tpl.Opts[name]; ok {
return fmt.Sprintf("--%s %s", name, val)
}
return ""
}
func (tpl *AutocompleteTemplate) Arg(name string) string {
return tpl.Args[name]
}
func (cmd *Command) ResolveTemplate(templateString string, currentValue string) (string, error) {
var buf bytes.Buffer
tplData := &AutocompleteTemplate{
Args: cmd.Arguments.AllKnownStr(),
Opts: cmd.Options.AllKnownStr(),
}
fnMap := template.FuncMap{
"Opt": tplData.Opt,
"Arg": tplData.Arg,
"Current": func() string { return currentValue },
}
for k, v := range _c.TemplateFuncs {
fnMap[k] = v
}
tpl, err := template.New("subcommand").Funcs(fnMap).Parse(templateString)
if err != nil {
return "", err
}
err = tpl.Execute(&buf, tplData)
if err != nil {
return "", err
}
return buf.String(), nil
}

View File

@ -1,164 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package command_test
import (
"testing"
. "git.rob.mx/nidito/joao/internal/command"
"github.com/spf13/pflag"
)
func TestResolveTemplate(t *testing.T) {
overrideFlags := &pflag.FlagSet{}
overrideFlags.String("option", "override", "stuff")
overrideFlags.Bool("bool", false, "stuff")
overrideFlags.Bool("help", false, "stuff")
overrideFlags.Bool("no-color", false, "stuff")
overrideFlags.Bool("skip-validation", false, "stuff")
err := overrideFlags.Parse([]string{"--option", "override", "--bool", "--help", "--no-color", "--skip-validation"})
if err != nil {
t.Fatalf("Could not parse test flags")
}
cases := []struct {
Tpl string
Expected string
Args []string
Flags *pflag.FlagSet
Errors bool
}{
{
Tpl: "adds nothing to nothing",
Expected: "adds nothing to nothing",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `prints default option as {{ Opt "option" }}`,
Expected: "prints default option as --option default",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `prints default option value as {{ .Opts.option }}`,
Expected: "prints default option value as default",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `prints default argument as {{ Arg "argument_0" }}`,
Expected: "prints default argument as default",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `prints default argument value as {{ .Args.argument_0 }}`,
Expected: "prints default argument value as default",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `overrides default option as {{ Opt "option" }}`,
Expected: "overrides default option as --option override",
Errors: false,
Args: []string{},
Flags: overrideFlags,
},
{
Tpl: `overrides default argument as {{ Arg "argument_0" }}`,
Expected: "overrides default argument as override",
Errors: false,
Args: []string{"override"},
Flags: &pflag.FlagSet{},
},
{
Tpl: `combines defaults as {{ Opt "option" }} {{ Opt "bool"}} {{ Arg "argument_0" }}`,
Expected: "combines defaults as --option default --bool false default",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `combines overrides as {{ Opt "option" }} {{ Opt "bool" }} {{ Arg "argument_0" }}`,
Expected: "combines overrides as --option override --bool true twice",
Errors: false,
Args: []string{"twice"},
Flags: overrideFlags,
},
{
Tpl: `prints variadic as {{ Arg "argument_0" }} {{ Arg "argument_n" }}`,
Expected: "prints variadic as override a b",
Errors: false,
Args: []string{"override", "a", "b"},
Flags: &pflag.FlagSet{},
},
{
Tpl: `doesn't error on bad names {{ Opt "bad-option" }} {{ Arg "bad-argument" }}`,
Expected: "doesn't error on bad names ",
Errors: false,
Args: []string{},
Flags: &pflag.FlagSet{},
},
{
Tpl: `errors on bad templates {{ BadFunc }}`,
Args: []string{},
Flags: &pflag.FlagSet{},
Errors: true,
},
}
for _, test := range cases {
test := test
t.Run(test.Expected, func(t *testing.T) {
cmd := (&Command{
Arguments: []*Argument{
{
Name: "argument_0",
Default: "default",
},
{
Name: "argument_n",
Variadic: true,
},
},
Options: Options{
"option": {
Default: "default",
Type: "string",
},
"bool": {
Type: "bool",
Default: false,
},
},
}).SetBindings()
cmd.Arguments.Parse(test.Args)
cmd.Options.Parse(test.Flags)
res, err := cmd.ResolveTemplate(test.Tpl, "")
if err != nil && !test.Errors {
t.Fatalf("good template failed: %s", err)
}
if res != test.Expected {
t.Fatalf("expected '%s' got '%s'", test.Expected, res)
}
})
}
}

View File

@ -1,94 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package constants
import (
"strings"
"text/template"
// Embed requires an import so the compiler knows what's up. Golint requires a comment. Gotta please em both.
_ "embed"
)
const HelpCommandName = "help"
// Environment Variables.
const EnvVarHelpUnstyled = "MILPA_PLAIN_HELP"
const EnvVarHelpStyle = "MILPA_HELP_STYLE"
const EnvVarMilpaRoot = "MILPA_ROOT"
const EnvVarMilpaPath = "MILPA_PATH"
const EnvVarMilpaPathParsed = "MILPA_PATH_PARSED"
const EnvVarMilpaVerbose = "MILPA_VERBOSE"
const EnvVarMilpaSilent = "MILPA_SILENT"
const EnvVarMilpaUnstyled = "NO_COLOR"
const EnvVarMilpaForceColor = "COLOR"
const EnvVarValidationDisabled = "MILPA_SKIP_VALIDATION"
const EnvVarCompaOut = "COMPA_OUT"
const EnvVarDebug = "DEBUG"
const EnvVarLookupGitDisabled = "MILPA_DISABLE_GIT"
const EnvVarLookupUserReposDisabled = "MILPA_DISABLE_USER_REPOS"
const EnvVarLookupGlobalReposDisabled = "MILPA_DISABLE_GLOBAL_REPOS"
// EnvFlagNames are flags also available as environment variables.
var EnvFlagNames = map[string]string{
"no-color": EnvVarMilpaUnstyled,
"color": EnvVarMilpaForceColor,
"silent": EnvVarMilpaSilent,
"verbose": EnvVarMilpaVerbose,
"skip-validation": EnvVarValidationDisabled,
}
// Exit statuses
// see man sysexits || grep "#define EX" /usr/include/sysexits.h
// and https://tldp.org/LDP/abs/html/exitcodes.html
// 0 means everything is fine.
const ExitStatusOk = 0
// 42 provides answers to life, the universe and everything; also, renders help.
const ExitStatusRenderHelp = 42
// 64 bad arguments
// EX_USAGE The command was used incorrectly, e.g., with the wrong number of arguments, a bad flag, a bad syntax in a parameter, or whatever.
const ExitStatusUsage = 64
// EX_SOFTWARE An internal software error has been detected. This should be limited to non-operating system related errors as possible.
const ExitStatusProgrammerError = 70
// EX_CONFIG Something was found in an unconfigured or misconfigured state.
const ExitStatusConfigError = 78
// 127 command not found.
const ExitStatusNotFound = 127
// ContextKeyRuntimeIndex is the string key used to store context in a cobra Command.
const ContextKeyRuntimeIndex = "x-joao-runtime-index"
//go:embed help.md
var helpTemplateText string
// TemplateFuncs is a FuncMap with aliases to the strings package.
var TemplateFuncs = template.FuncMap{
"contains": strings.Contains,
"hasSuffix": strings.HasSuffix,
"hasPrefix": strings.HasPrefix,
"replace": strings.ReplaceAll,
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
"trim": strings.TrimSpace,
"trimSuffix": strings.TrimSuffix,
"trimPrefix": strings.TrimPrefix,
}
// TemplateCommandHelp holds a template for rendering command help.
var TemplateCommandHelp = template.Must(template.New("help").Funcs(TemplateFuncs).Parse(helpTemplateText))

View File

@ -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}}

View File

@ -1,70 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import "fmt"
type NotFound struct {
Msg string
Group []string
}
type BadArguments struct {
Msg string
}
type NotExecutable struct {
Msg string
}
type ConfigError struct {
Err error
Config string
}
type EnvironmentError struct {
Err error
}
type SubCommandExit struct {
Err error
ExitCode int
}
func (err NotFound) Error() string {
return err.Msg
}
func (err BadArguments) Error() string {
return err.Msg
}
func (err NotExecutable) Error() string {
return err.Msg
}
func (err SubCommandExit) Error() string {
if err.Err != nil {
return err.Err.Error()
}
return ""
}
func (err ConfigError) Error() string {
return fmt.Sprintf("Invalid configuration %s: %v", err.Config, err.Err)
}
func (err EnvironmentError) Error() string {
return fmt.Sprintf("Invalid MILPA_ environment: %v", err.Err)
}

View File

@ -1,76 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import (
"os"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
_c "git.rob.mx/nidito/joao/internal/constants"
)
func showHelp(cmd *cobra.Command) {
if cmd.Name() != _c.HelpCommandName {
err := cmd.Help()
if err != nil {
os.Exit(_c.ExitStatusProgrammerError)
}
}
}
func HandleCobraExit(cmd *cobra.Command, err error) {
if err == nil {
ok, err := cmd.Flags().GetBool(_c.HelpCommandName)
if cmd.Name() == _c.HelpCommandName || err == nil && ok {
os.Exit(_c.ExitStatusRenderHelp)
}
os.Exit(_c.ExitStatusOk)
}
switch tErr := err.(type) {
case SubCommandExit:
logrus.Debugf("Sub-command failed with: %s", err.Error())
os.Exit(tErr.ExitCode)
case BadArguments:
showHelp(cmd)
logrus.Error(err)
os.Exit(_c.ExitStatusUsage)
case NotFound:
showHelp(cmd)
logrus.Error(err)
os.Exit(_c.ExitStatusNotFound)
case ConfigError:
showHelp(cmd)
logrus.Error(err)
os.Exit(_c.ExitStatusConfigError)
case EnvironmentError:
logrus.Error(err)
os.Exit(_c.ExitStatusConfigError)
default:
if strings.HasPrefix(err.Error(), "unknown command") {
showHelp(cmd)
os.Exit(_c.ExitStatusNotFound)
} else if strings.HasPrefix(err.Error(), "unknown flag") || strings.HasPrefix(err.Error(), "unknown shorthand flag") {
showHelp(cmd)
logrus.Error(err)
os.Exit(_c.ExitStatusUsage)
}
}
logrus.Errorf("Unknown error: %s", err)
os.Exit(2)
}

View File

@ -1,65 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package exec
import (
"bytes"
"context"
"fmt"
os_exec "os/exec"
"strings"
"time"
"git.rob.mx/nidito/joao/internal/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// ExecFunc is replaced in tests.
var ExecFunc = WithSubshell
func WithSubshell(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
cmd := os_exec.CommandContext(ctx, executable, args...) // #nosec G204
var stdout bytes.Buffer
cmd.Stdout = &stdout
var stderr bytes.Buffer
cmd.Stderr = &stderr
cmd.Env = env
return stdout, stderr, cmd.Run()
}
// Exec runs a subprocess and returns a list of lines from stdout.
func Exec(name string, args []string, env []string, timeout time.Duration) ([]string, cobra.ShellCompDirective, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() // The cancel should be deferred so resources are cleaned up
logrus.Debugf("executing %s", args)
executable := args[0]
args = args[1:]
stdout, _, err := ExecFunc(ctx, env, executable, args...)
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Sub-command timed out")
logrus.Debugf("timeout running %s %s: %s", executable, args, stdout.String())
return []string{}, cobra.ShellCompDirectiveError, fmt.Errorf("timed out resolving %s %s", executable, args)
}
if err != nil {
logrus.Debugf("error running %s %s: %s", executable, args, err)
return []string{}, cobra.ShellCompDirectiveError, errors.BadArguments{Msg: fmt.Sprintf("could not validate argument for command %s, ran <%s %s> failed: %s", name, executable, strings.Join(args, " "), err)}
}
logrus.Debugf("done running %s %s: %s", executable, args, stdout.String())
return strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n"), cobra.ShellCompDirectiveDefault, nil
}

View File

@ -1,108 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package exec_test
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"time"
. "git.rob.mx/nidito/joao/internal/exec"
"github.com/spf13/cobra"
)
func TestSubshellExec(t *testing.T) {
ExecFunc = WithSubshell
stdout, directive, err := Exec("test-command", []string{"bash", "-c", `echo "stdout"; echo "stderr" >&2;`}, []string{}, 1*time.Second)
if err != nil {
t.Fatalf("good subshell errored: %v", err)
}
if len(stdout) != 1 && stdout[0] == "stdout" {
t.Fatalf("good subshell returned wrong stdout: %v", stdout)
}
if directive != cobra.ShellCompDirectiveDefault {
t.Fatalf("good subshell returned wrong directive: %v", directive)
}
stdout, directive, err = Exec("test-command", []string{"bash", "-c", `echo "stdout"; echo "stderr" >&2; exit 2`}, []string{}, 1*time.Second)
if err == nil {
t.Fatalf("bad subshell did not error; stdout: %v", stdout)
}
if len(stdout) != 0 {
t.Fatalf("bad subshell returned non-empty stdout: %v", stdout)
}
if directive != cobra.ShellCompDirectiveError {
t.Fatalf("bad subshell returned wrong directive: %v", directive)
}
}
func TestExecTimesOut(t *testing.T) {
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
time.Sleep(100 * time.Nanosecond)
return bytes.Buffer{}, bytes.Buffer{}, context.DeadlineExceeded
}
_, _, err := Exec("test-command", []string{"bash", "-c", "sleep", "2"}, []string{}, 10*time.Nanosecond)
if err == nil {
t.Fatalf("timeout didn't happen after 10ms: %v", err)
}
}
func TestExecWorksFine(t *testing.T) {
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
var out bytes.Buffer
fmt.Fprint(&out, strings.Join([]string{
"a",
"b",
"c",
}, "\n"))
return out, bytes.Buffer{}, nil
}
args := []string{"a", "b", "c"}
res, directive, err := Exec("test-command", append([]string{"bash", "-c", "echo"}, args...), []string{}, 1*time.Second)
if err != nil {
t.Fatalf("good command failed: %v", err)
}
if directive != 0 {
t.Fatalf("good command resulted in wrong directive, expected %d, got %d", 0, directive)
}
if strings.Join(args, "-") != strings.Join(res, "-") {
t.Fatalf("good command resulted in wrong results, expected %v, got %v", res, args)
}
}
func TestExecErrors(t *testing.T) {
ExecFunc = func(ctx context.Context, env []string, executable string, args ...string) (bytes.Buffer, bytes.Buffer, error) {
return bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("bad command is bad")
}
res, directive, err := Exec("test-command", []string{"bash", "-c", "bad-command"}, []string{}, 1*time.Second)
if err == fmt.Errorf("bad command is bad") {
t.Fatalf("bad command didn't fail: %v", res)
}
if directive != cobra.ShellCompDirectiveError {
t.Fatalf("bad command resulted in wrong directive, expected %d, got %d", cobra.ShellCompDirectiveError, directive)
}
if len(res) > 0 {
t.Fatalf("bad command returned values, got %v", res)
}
}

View File

@ -1,78 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"strings"
"git.rob.mx/nidito/joao/internal/command"
_c "git.rob.mx/nidito/joao/internal/constants"
"git.rob.mx/nidito/joao/internal/errors"
"git.rob.mx/nidito/joao/internal/runtime"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func toCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command {
localName := cmd.Name()
useSpec := []string{localName, "[options]"}
for _, arg := range cmd.Arguments {
useSpec = append(useSpec, arg.ToDesc())
}
cc := &cobra.Command{
Use: strings.Join(useSpec, " "),
Short: cmd.Summary,
DisableAutoGenTag: true,
SilenceUsage: true,
SilenceErrors: true,
Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: cmd.FullName(),
},
Args: func(cc *cobra.Command, supplied []string) error {
skipValidation, _ := cc.Flags().GetBool("skip-validation")
if !skipValidation && runtime.ValidationEnabled() {
cmd.Arguments.Parse(supplied)
return cmd.Arguments.AreValid()
}
return nil
},
RunE: cmd.Run,
}
cc.SetFlagErrorFunc(func(c *cobra.Command, e error) error {
return errors.BadArguments{Msg: e.Error()}
})
cc.ValidArgsFunction = cmd.Arguments.CompletionFunction
cc.Flags().AddFlagSet(cmd.FlagSet())
for name, opt := range cmd.Options {
if err := cc.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmd.FullName())
}
}
cc.SetHelpFunc(cmd.HelpRenderer(globalOptions))
cmd.SetCobra(cc)
return cc
}
func fromCobra(cc *cobra.Command) *command.Command {
rtidx, hasAnnotation := cc.Annotations[_c.ContextKeyRuntimeIndex]
if hasAnnotation {
return Get(rtidx)
}
return nil
}

View File

@ -1,265 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"fmt"
"os"
"sort"
"strings"
"git.rob.mx/nidito/joao/internal/command"
_c "git.rob.mx/nidito/joao/internal/constants"
"git.rob.mx/nidito/joao/internal/errors"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var registry = &CommandRegistry{
kv: map[string]*command.Command{},
}
type ByPath []*command.Command
func (cmds ByPath) Len() int { return len(cmds) }
func (cmds ByPath) Swap(i, j int) { cmds[i], cmds[j] = cmds[j], cmds[i] }
func (cmds ByPath) Less(i, j int) bool { return cmds[i].FullName() < cmds[j].FullName() }
type CommandTree struct {
Command *command.Command `json:"command"`
Children []*CommandTree `json:"children"`
}
func (t *CommandTree) Traverse(fn func(cmd *command.Command) error) error {
for _, child := range t.Children {
if err := fn(child.Command); err != nil {
return err
}
if err := child.Traverse(fn); err != nil {
return err
}
}
return nil
}
type CommandRegistry struct {
kv map[string]*command.Command
byPath []*command.Command
tree *CommandTree
}
func Register(cmd *command.Command) {
logrus.Debugf("Registering %s", cmd.FullName())
registry.kv[cmd.FullName()] = cmd
}
func Get(id string) *command.Command {
return registry.kv[id]
}
func CommandList() []*command.Command {
if len(registry.byPath) == 0 {
list := []*command.Command{}
for _, v := range registry.kv {
list = append(list, v)
}
sort.Sort(ByPath(list))
registry.byPath = list
}
return registry.byPath
}
func BuildTree(cc *cobra.Command, depth int) {
tree := &CommandTree{
Command: fromCobra(cc),
Children: []*CommandTree{},
}
var populateTree func(cmd *cobra.Command, ct *CommandTree, maxDepth int, depth int)
populateTree = func(cmd *cobra.Command, ct *CommandTree, maxDepth int, depth int) {
newDepth := depth + 1
for _, subcc := range cmd.Commands() {
if subcc.Hidden {
continue
}
if cmd := fromCobra(subcc); cmd != nil {
leaf := &CommandTree{Children: []*CommandTree{}}
leaf.Command = cmd
ct.Children = append(ct.Children, leaf)
if newDepth < maxDepth {
populateTree(subcc, leaf, maxDepth, newDepth)
}
}
}
}
populateTree(cc, tree, depth, 0)
registry.tree = tree
}
func SerializeTree(serializationFn func(any) ([]byte, error)) (string, error) {
bytes, err := serializationFn(registry.tree)
if err != nil {
return "", err
}
return string(bytes), nil
}
func ChildrenNames() []string {
if registry.tree == nil {
return []string{}
}
ret := make([]string, len(registry.tree.Children))
for idx, cmd := range registry.tree.Children {
ret[idx] = cmd.Command.Name()
}
return ret
}
func Execute(version string) error {
cmdRoot := command.Root
ccRoot.Short = cmdRoot.Summary
ccRoot.Long = cmdRoot.Description
ccRoot.Annotations["version"] = version
ccRoot.CompletionOptions.DisableDefaultCmd = true
ccRoot.Flags().AddFlagSet(cmdRoot.FlagSet())
for name, opt := range cmdRoot.Options {
if err := ccRoot.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmdRoot.FullName())
}
}
ccRoot.SetHelpFunc(cmdRoot.HelpRenderer(cmdRoot.Options))
for _, cmd := range CommandList() {
cmd := cmd
leaf := toCobra(cmd, cmdRoot.Options)
container := ccRoot
for idx, cp := range cmd.Path {
if idx == len(cmd.Path)-1 {
// logrus.Debugf("adding command %s to %s", leaf.Name(), cmd.Path[0:idx])
container.AddCommand(leaf)
break
}
query := []string{cp}
if cc, _, err := container.Find(query); err == nil && cc != container {
container = cc
} else {
groupName := strings.Join(query, " ")
groupPath := append(cmd.Path[0:idx], query...) // nolint:gocritic
cc := &cobra.Command{
Use: cp,
Short: fmt.Sprintf("%s subcommands", groupName),
DisableAutoGenTag: true,
SuggestionsMinimumDistance: 2,
SilenceUsage: true,
SilenceErrors: true,
Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: strings.Join(groupPath, " "),
},
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.OnlyValidArgs(cmd, args); err == nil {
return nil
}
suggestions := []string{}
bold := color.New(color.Bold)
for _, l := range cmd.SuggestionsFor(args[len(args)-1]) {
suggestions = append(suggestions, bold.Sprint(l))
}
last := len(args) - 1
parent := cmd.CommandPath()
errMessage := fmt.Sprintf("Unknown subcommand %s of known command %s", bold.Sprint(args[last]), bold.Sprint(parent))
if len(suggestions) > 0 {
errMessage += ". Perhaps you meant " + strings.Join(suggestions, ", ") + "?"
}
return errors.NotFound{Msg: errMessage, Group: []string{}}
},
ValidArgs: []string{""},
RunE: func(cc *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
}
os.Exit(_c.ExitStatusNotFound)
return nil
},
}
groupParent := &command.Command{
Path: cmd.Path[0 : len(cmd.Path)-1],
Summary: fmt.Sprintf("%s subcommands", groupName),
Description: fmt.Sprintf("Runs subcommands within %s", groupName),
Arguments: command.Arguments{},
Options: command.Options{},
}
Register(groupParent)
cc.SetHelpFunc(groupParent.HelpRenderer(command.Options{}))
container.AddCommand(cc)
container = cc
}
}
}
cmdRoot.SetCobra(ccRoot)
return ccRoot.Execute()
}
var ccRoot = &cobra.Command{
Use: "joao [--silent|-v|--verbose] [--[no-]color] [-h|--help] [--version]",
Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: "joao",
},
DisableAutoGenTag: true,
SilenceUsage: true,
SilenceErrors: true,
ValidArgs: []string{""},
Args: func(cmd *cobra.Command, args []string) error {
err := cobra.OnlyValidArgs(cmd, args)
if err != nil {
suggestions := []string{}
bold := color.New(color.Bold)
for _, l := range cmd.SuggestionsFor(args[len(args)-1]) {
suggestions = append(suggestions, bold.Sprint(l))
}
errMessage := fmt.Sprintf("Unknown subcommand %s", bold.Sprint(strings.Join(args, " ")))
if len(suggestions) > 0 {
errMessage += ". Perhaps you meant " + strings.Join(suggestions, ", ") + "?"
}
return errors.NotFound{Msg: errMessage, Group: []string{}}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// if ok, err := cmd.Flags().GetBool("version"); err == nil && ok {
// vc, _, err := cmd.Root().Find([]string{versionName()})
// if err != nil {
// return err
// }
// return vc.RunE(vc, []string{})
// }
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
}
return nil
},
}

View File

@ -1,70 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package render
import (
"bytes"
"os"
_c "git.rob.mx/nidito/joao/internal/constants"
"git.rob.mx/nidito/joao/internal/runtime"
"github.com/charmbracelet/glamour"
"github.com/sirupsen/logrus"
"golang.org/x/term"
)
func addBackticks(str []byte) []byte {
return bytes.ReplaceAll(str, []byte("﹅"), []byte("`"))
}
func Markdown(content []byte, withColor bool) ([]byte, error) {
content = addBackticks(content)
if runtime.UnstyledHelpEnabled() {
return content, nil
}
width, _, err := term.GetSize(0)
if err != nil {
logrus.Debugf("Could not get terminal width")
width = 80
}
var styleFunc glamour.TermRendererOption
if withColor {
style := os.Getenv(_c.EnvVarHelpStyle)
switch style {
case "dark":
styleFunc = glamour.WithStandardStyle("dark")
case "light":
styleFunc = glamour.WithStandardStyle("light")
default:
styleFunc = glamour.WithStandardStyle("auto")
}
} else {
styleFunc = glamour.WithStandardStyle("notty")
}
renderer, err := glamour.NewTermRenderer(
styleFunc,
glamour.WithEmoji(),
glamour.WithWordWrap(width),
)
if err != nil {
return content, err
}
return renderer.RenderBytes(content)
}

View File

@ -1,87 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package render_test
import (
"fmt"
"os"
"reflect"
"testing"
_c "git.rob.mx/nidito/joao/internal/constants"
"git.rob.mx/nidito/joao/internal/render"
)
func TestMarkdownUnstyled(t *testing.T) {
content := []byte("# hello")
os.Setenv(_c.EnvVarHelpUnstyled, "true")
res, err := render.Markdown(content, false)
if err != nil {
t.Fatalf("Unexpected error %s", err)
}
expected := []byte("# hello") // nolint:ifshort
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Unexpected response ---\n%s\n---\n wanted:\n---\n%s\n---", res, expected)
}
}
func TestMarkdownNoColor(t *testing.T) {
os.Unsetenv(_c.EnvVarHelpUnstyled)
content := []byte("# hello ﹅world﹅")
res, err := render.Markdown(content, false)
if err != nil {
t.Fatalf("Unexpected error %s", err)
}
// account for 80 character width word wrapping
// our string is 15 characters, there's 2 for padding at the start
spaces := " "
expected := []byte("\n # hello `world`" + spaces + "\n\n") // nolint:ifshort
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Unexpected response ---\n%s\n---\n wanted:\n---\n%s\n---", res, expected)
}
}
var autoStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;63;1m\x1b[0m \x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;228;48;5;63;1mhello\x1b[0m\x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;252m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[38;5;252m \x1b[0m\x1b[0m\n\x1b[0m\n"
const lightStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;63;1m\x1b[0m \x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;228;48;5;63;1mhello\x1b[0m\x1b[38;5;228;48;5;63;1m \x1b[0m\x1b[38;5;234m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[38;5;234m \x1b[0m\x1b[0m\n\x1b[0m\n"
func TestMarkdownColor(t *testing.T) {
os.Unsetenv(_c.EnvVarHelpUnstyled)
content := []byte("# hello")
styles := map[string][]byte{
"": []byte(autoStyleTestRender),
"dark": []byte(autoStyleTestRender),
"auto": []byte(autoStyleTestRender),
"light": []byte(lightStyleTestRender),
}
for style, expected := range styles {
t.Run(fmt.Sprintf("style %s", style), func(t *testing.T) {
os.Setenv(_c.EnvVarHelpStyle, style)
res, err := render.Markdown(content, true)
if err != nil {
t.Fatalf("Unexpected error %s", err)
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Unexpected response ---\n%v\n---\n wanted:\n---\n%v\n---", res, expected)
}
})
}
}

View File

@ -1,133 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtime
import (
"fmt"
"os"
"strconv"
"strings"
_c "git.rob.mx/nidito/joao/internal/constants"
)
var MilpaPath = ParseMilpaPath()
// ParseMilpaPath turns MILPA_PATH into a string slice.
func ParseMilpaPath() []string {
return strings.Split(os.Getenv(_c.EnvVarMilpaPath), ":")
}
var falseIshValues = []string{
"",
"0",
"no",
"false",
"disable",
"disabled",
"off",
"never",
}
var trueIshValues = []string{
"1",
"yes",
"true",
"enable",
"enabled",
"on",
"always",
}
func isFalseIsh(val string) bool {
for _, negative := range falseIshValues {
if val == negative {
return true
}
}
return false
}
func isTrueIsh(val string) bool {
for _, positive := range trueIshValues {
if val == positive {
return true
}
}
return false
}
func DoctorModeEnabled() bool {
count := len(os.Args)
if count < 2 {
return false
}
first := os.Args[1]
return first == "__doctor" || count >= 2 && (first == "itself" && os.Args[2] == "doctor")
}
func DebugEnabled() bool {
return isTrueIsh(os.Getenv(_c.EnvVarDebug))
}
func ValidationEnabled() bool {
return isFalseIsh(os.Getenv(_c.EnvVarValidationDisabled))
}
func VerboseEnabled() bool {
return isTrueIsh(os.Getenv(_c.EnvVarMilpaVerbose))
}
func ColorEnabled() bool {
return isFalseIsh(os.Getenv(_c.EnvVarMilpaUnstyled)) && !UnstyledHelpEnabled()
}
func UnstyledHelpEnabled() bool {
return isTrueIsh(os.Getenv(_c.EnvVarHelpUnstyled))
}
func CheckMilpaPathSet() error {
if len(MilpaPath) == 0 {
return fmt.Errorf("no %s set on the environment", _c.EnvVarMilpaPath)
}
return nil
}
// EnvironmentMap returns the resolved environment map.
func EnvironmentMap() map[string]string {
env := map[string]string{}
env[_c.EnvVarMilpaPath] = strings.Join(MilpaPath, ":")
trueString := strconv.FormatBool(true)
env[_c.EnvVarMilpaPathParsed] = trueString
if !ColorEnabled() {
env[_c.EnvVarMilpaUnstyled] = trueString
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaForceColor)) {
env[_c.EnvVarMilpaForceColor] = "always"
}
if DebugEnabled() {
env[_c.EnvVarDebug] = trueString
}
if VerboseEnabled() {
env[_c.EnvVarMilpaVerbose] = trueString
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaSilent)) {
env[_c.EnvVarMilpaSilent] = trueString
}
return env
}

View File

@ -1,195 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtime_test
import (
"fmt"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
_c "git.rob.mx/nidito/joao/internal/constants"
. "git.rob.mx/nidito/joao/internal/runtime"
)
func TestCheckMilpaPathSet(t *testing.T) {
MilpaPath = []string{"a", "b"}
if err := CheckMilpaPathSet(); err != nil {
t.Fatalf("Got error with set MILPA_PATH: %v", err)
}
MilpaPath = []string{}
if err := CheckMilpaPathSet(); err == nil {
t.Fatalf("Got no error with unset MILPA_PATH")
}
}
func TestEnabled(t *testing.T) {
defer func() { os.Setenv(_c.EnvVarMilpaVerbose, "") }()
cases := []struct {
Name string
Func func() bool
Expects bool
}{
{
Name: _c.EnvVarMilpaVerbose,
Func: VerboseEnabled,
Expects: true,
},
{
Name: _c.EnvVarValidationDisabled,
Func: ValidationEnabled,
},
{
Name: _c.EnvVarMilpaUnstyled,
Func: ColorEnabled,
},
{
Name: _c.EnvVarHelpUnstyled,
Func: ColorEnabled,
},
{
Name: _c.EnvVarDebug,
Func: DebugEnabled,
Expects: true,
},
{
Name: _c.EnvVarHelpUnstyled,
Func: UnstyledHelpEnabled,
Expects: true,
},
}
for _, c := range cases {
fname := runtime.FuncForPC(reflect.ValueOf(c.Func).Pointer()).Name()
name := fmt.Sprintf("%v/%s", fname, c.Name)
enabled := []string{
"yes", "true", "1", "enabled",
}
for _, val := range enabled {
t.Run("enabled-"+val, func(t *testing.T) {
os.Setenv(c.Name, val)
if c.Func() != c.Expects {
t.Fatalf("%s wasn't enabled with a valid value: %s", name, val)
}
})
}
disabled := []string{"", "no", "false", "0", "disabled"}
for _, val := range disabled {
t.Run("disabled-"+val, func(t *testing.T) {
os.Setenv(c.Name, val)
if c.Func() == c.Expects {
t.Fatalf("%s was enabled with falsy value: %s", name, val)
}
})
}
}
}
func TestDoctorMode(t *testing.T) {
cases := []struct {
Args []string
Expects bool
}{
{
Args: []string{},
},
{
Args: []string{""},
},
{
Args: []string{"something", "doctor"},
},
{
Args: []string{"__doctor"},
Expects: true,
},
{
Args: []string{"__doctor", "whatever"},
Expects: true,
},
{
Args: []string{"itself", "doctor"},
Expects: true,
},
}
for _, c := range cases {
t.Run(strings.Join(c.Args, " "), func(t *testing.T) {
os.Args = append([]string{"compa"}, c.Args...)
res := DoctorModeEnabled()
if res != c.Expects {
t.Fatalf("Expected %v for %v and got %v", c.Expects, c.Args, res)
}
})
}
}
func TestEnvironmentMapEnabled(t *testing.T) {
MilpaPath = []string{"something"}
trueString := strconv.FormatBool(true)
os.Setenv(_c.EnvVarMilpaForceColor, trueString)
os.Setenv(_c.EnvVarDebug, trueString)
os.Setenv(_c.EnvVarMilpaVerbose, trueString)
res := EnvironmentMap()
if res == nil {
t.Fatalf("Expected map, got nil")
}
expected := map[string]string{
_c.EnvVarMilpaPath: "something",
_c.EnvVarMilpaForceColor: "always",
_c.EnvVarMilpaPathParsed: trueString,
_c.EnvVarDebug: trueString,
_c.EnvVarMilpaVerbose: trueString,
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Unexpected result from enabled environment. Wanted %v, got %v", res, expected)
}
}
func TestEnvironmentMapDisabled(t *testing.T) {
MilpaPath = []string{"something"}
trueString := strconv.FormatBool(true)
// clear COLOR
os.Unsetenv(_c.EnvVarMilpaForceColor)
// set NO_COLOR
os.Setenv(_c.EnvVarMilpaUnstyled, trueString)
os.Unsetenv(_c.EnvVarDebug)
os.Unsetenv(_c.EnvVarMilpaVerbose)
os.Setenv(_c.EnvVarMilpaSilent, trueString)
res := EnvironmentMap()
if res == nil {
t.Fatalf("Expected map, got nil")
}
expected := map[string]string{
_c.EnvVarMilpaPath: "something",
_c.EnvVarMilpaUnstyled: trueString,
_c.EnvVarMilpaPathParsed: trueString,
_c.EnvVarMilpaSilent: trueString,
}
if !reflect.DeepEqual(res, expected) {
t.Fatalf("Unexpected result from disabled environment. Wanted %v, got %v", res, expected)
}
}

16
main.go
View File

@ -13,9 +13,11 @@
package main package main
import ( 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/cmd"
"git.rob.mx/nidito/joao/internal/registry"
"git.rob.mx/nidito/joao/internal/runtime"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -28,11 +30,13 @@ func main() {
ForceColors: runtime.ColorEnabled(), ForceColors: runtime.ColorEnabled(),
}) })
if runtime.DebugEnabled() {
logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
logrus.Debug("Debugging enabled")
}
err := registry.Execute(version) if err := chinampa.Execute(version); err != nil {
logrus.Error(err)
if err != nil { os.Exit(2)
logrus.Fatal(err)
} }
} }