moar milpa stuff

This commit is contained in:
Roberto Hidalgo 2022-12-30 23:53:24 -06:00
parent 831e68c7b7
commit 8ea5f42ef8
30 changed files with 605 additions and 515 deletions

View File

@ -12,3 +12,10 @@ indent_style = space
indent_size = 2 indent_size = 2
max_line_length = 120 max_line_length = 120
[*.go]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
indent_size = 4
max_line_length = 120

1
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (

4
go.sum
View File

@ -29,9 +29,11 @@ github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7P
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
@ -62,6 +64,7 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
@ -106,6 +109,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright © 2021 Roberto Hidalgo <chinampa@un.rob.mx>
package commands
import (
"os"
"github.com/spf13/cobra"
)
var GenerateCompletions = &cobra.Command{
Use: "__generate_completions [bash|zsh|fish]",
Short: "Outputs a shell-specific script for autocompletions that can be piped into a file",
Hidden: true,
DisableAutoGenTag: true,
SilenceUsage: true,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletionV2(os.Stdout, true)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
}
return
},
}

74
internal/commands/help.go Normal file
View File

@ -0,0 +1,74 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package commands
import (
"fmt"
"os"
"strings"
_c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/pkg/env"
"git.rob.mx/nidito/chinampa/pkg/runtime"
"git.rob.mx/nidito/chinampa/pkg/statuscode"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var Help = &cobra.Command{
Use: _c.HelpCommandName + " [command]",
Short: "Display usage information for any command",
Long: `Help provides the valid arguments and options for any command known to ` + runtime.Executable + `. By default, ﹅` + runtime.Executable + ` help﹅ will query the environment variable ﹅COLORFGBG﹅ to decide which style to use when rendering help, except if ﹅` + env.HelpUnstyled + `﹅ is set. Valid styles are: **light**, **dark**, and **auto**.`,
ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var completions []string
cmd, _, e := c.Root().Find(args)
if e != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if cmd == nil {
// Root help command.
cmd = c.Root()
}
for _, subCmd := range cmd.Commands() {
if subCmd.IsAvailableCommand() || subCmd.Name() == _c.HelpCommandName {
if strings.HasPrefix(subCmd.Name(), toComplete) {
completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short))
}
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
},
Run: func(c *cobra.Command, args []string) {
cmd, _, e := c.Root().Find(args)
if cmd == nil || e != nil || (len(args) > 0 && cmd != nil && cmd.Name() != args[len(args)-1]) {
if cmd == nil {
err := c.Root().Help()
if err != nil {
logrus.Error(err)
os.Exit(statuscode.ProgrammerError)
}
logrus.Errorf("Unknown help topic %s", args)
os.Exit(statuscode.NotFound)
} else {
err := cmd.Help()
if err != nil {
logrus.Error(err)
os.Exit(statuscode.ProgrammerError)
}
if len(args) > 1 {
logrus.Errorf("Unknown help topic %s for %s", args[1], args[0])
} else {
logrus.Errorf("Unknown help topic %s for %s", runtime.Executable, args[0])
}
os.Exit(statuscode.NotFound)
}
} else {
cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown
cobra.CheckErr(cmd.Help())
}
os.Exit(statuscode.RenderHelp)
},
}

View File

@ -0,0 +1,37 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package commands
import (
"os"
"git.rob.mx/nidito/chinampa/pkg/statuscode"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var Version = &cobra.Command{
Use: "version",
Short: "Display program version",
Hidden: false,
DisableAutoGenTag: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
output := cmd.ErrOrStderr()
version := cmd.Root().Annotations["version"]
if cmd.CalledAs() == "" {
// user asked for --version directly
output = cmd.OutOrStderr()
version += "\n"
}
_, err := output.Write([]byte(version))
if err != nil {
logrus.Errorf("version error: %s", err)
return err
}
os.Exit(statuscode.Ok)
return nil
},
}

View File

@ -1,91 +1,13 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package constants
import ( import (
"strings"
"text/template"
// Embed requires an import so the compiler knows what's up. Golint requires a comment. Gotta please em both. // Embed requires an import so the compiler knows what's up. Golint requires a comment. Gotta please em both.
_ "embed" _ "embed"
) )
const HelpCommandName = "help" const HelpCommandName = "help"
// Environment Variables.
const EnvVarHelpUnstyled = "HELP_STYLE_PLAIN"
const EnvVarHelpStyle = "HELP_STYLE"
const EnvVarMilpaVerbose = "VERBOSE"
const EnvVarMilpaSilent = "SILENT"
const EnvVarMilpaUnstyled = "NO_COLOR"
const EnvVarMilpaForceColor = "COLOR"
const EnvVarValidationDisabled = "SKIP_VALIDATION"
const EnvVarDebug = "DEBUG"
// 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
const (
// 0 means everything is fine.
ExitStatusOk = 0
// 42 provides answers to life, the universe and everything; also, renders help.
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.
ExitStatusUsage = 64
// EX_SOFTWARE An internal software error has been detected. This should be limited to non-operating system related errors as possible.
ExitStatusProgrammerError = 70
// EX_CONFIG Something was found in an unconfigured or misconfigured state.
ExitStatusConfigError = 78
// 127 command not found.
ExitStatusNotFound = 127
)
// ContextKeyRuntimeIndex is the string key used to store context in a cobra Command.
const ContextKeyRuntimeIndex = "x-chinampa-runtime-index"
//go:embed help.md //go:embed help.md
var helpTemplateText string var HelpTemplate 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.Template
func HelpTemplate(executableName string) *template.Template {
if TemplateCommandHelp == nil {
TemplateCommandHelp = template.Must(template.New("help").Funcs(TemplateFuncs).Parse(strings.ReplaceAll(helpTemplateText, "@chinampa@", executableName)))
}
return TemplateCommandHelp
}

View File

@ -1,24 +1,13 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package registry
import ( import (
"fmt" "fmt"
"strings" "strings"
_c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/internal/errors"
"git.rob.mx/nidito/chinampa/pkg/command" "git.rob.mx/nidito/chinampa/pkg/command"
"git.rob.mx/nidito/chinampa/pkg/errors"
"git.rob.mx/nidito/chinampa/pkg/runtime" "git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -29,7 +18,7 @@ func newCobraRoot(root *command.Command) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: root.Name() + " [--silent|-v|--verbose] [--[no-]color] [-h|--help] [--version]", Use: root.Name() + " [--silent|-v|--verbose] [--[no-]color] [-h|--help] [--version]",
Annotations: map[string]string{ Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: root.Name(), ContextKeyRuntimeIndex: root.Name(),
}, },
Short: root.Summary, Short: root.Summary,
Long: root.Description, Long: root.Description,
@ -62,13 +51,12 @@ func newCobraRoot(root *command.Command) *cobra.Command {
} }
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}} return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
} }
return nil return nil
}, },
} }
} }
func toCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command { func ToCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command {
localName := cmd.Name() localName := cmd.Name()
useSpec := []string{localName, "[options]"} useSpec := []string{localName, "[options]"}
for _, arg := range cmd.Arguments { for _, arg := range cmd.Arguments {
@ -81,8 +69,9 @@ func toCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command
DisableAutoGenTag: true, DisableAutoGenTag: true,
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
Hidden: cmd.Hidden,
Annotations: map[string]string{ Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: cmd.FullName(), ContextKeyRuntimeIndex: cmd.FullName(),
}, },
Args: func(cc *cobra.Command, supplied []string) error { Args: func(cc *cobra.Command, supplied []string) error {
skipValidation, _ := cc.Flags().GetBool("skip-validation") skipValidation, _ := cc.Flags().GetBool("skip-validation")
@ -114,8 +103,8 @@ func toCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command
return cc return cc
} }
func fromCobra(cc *cobra.Command) *command.Command { func FromCobra(cc *cobra.Command) *command.Command {
rtidx, hasAnnotation := cc.Annotations[_c.ContextKeyRuntimeIndex] rtidx, hasAnnotation := cc.Annotations[ContextKeyRuntimeIndex]
if hasAnnotation { if hasAnnotation {
return Get(rtidx) return Get(rtidx)
} }

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package registry
import ( import (
@ -18,14 +8,18 @@ import (
"sort" "sort"
"strings" "strings"
_c "git.rob.mx/nidito/chinampa/internal/constants" "git.rob.mx/nidito/chinampa/internal/commands"
"git.rob.mx/nidito/chinampa/internal/errors"
"git.rob.mx/nidito/chinampa/pkg/command" "git.rob.mx/nidito/chinampa/pkg/command"
"git.rob.mx/nidito/chinampa/pkg/errors"
"git.rob.mx/nidito/chinampa/pkg/statuscode"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// ContextKeyRuntimeIndex is the string key used to store context in a cobra Command.
const ContextKeyRuntimeIndex = "x-chinampa-runtime-index"
var registry = &CommandRegistry{ var registry = &CommandRegistry{
kv: map[string]*command.Command{}, kv: map[string]*command.Command{},
} }
@ -36,28 +30,9 @@ 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) 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() } 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 { type CommandRegistry struct {
kv map[string]*command.Command kv map[string]*command.Command
byPath []*command.Command byPath []*command.Command
tree *CommandTree
} }
func Register(cmd *command.Command) { func Register(cmd *command.Command) {
@ -82,87 +57,54 @@ func CommandList() []*command.Command {
return registry.byPath 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 { func Execute(version string) error {
cmdRoot := command.Root cmdRoot := command.Root
ccRoot := newCobraRoot(command.Root) ccRoot := newCobraRoot(command.Root)
ccRoot.Annotations["version"] = version ccRoot.Annotations["version"] = version
ccRoot.CompletionOptions.HiddenDefaultCmd = true ccRoot.CompletionOptions.HiddenDefaultCmd = true
ccRoot.PersistentFlags().AddFlagSet(cmdRoot.FlagSet()) ccRoot.PersistentFlags().AddFlagSet(cmdRoot.FlagSet())
ccRoot.SetHelpCommand(commands.Help)
ccRoot.AddCommand(commands.Version)
ccRoot.AddCommand(commands.GenerateCompletions)
for name, opt := range cmdRoot.Options { for name, opt := range cmdRoot.Options {
if err := ccRoot.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil { if err := ccRoot.RegisterFlagCompletionFunc(name, opt.CompletionFunction); err != nil {
logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmdRoot.FullName()) logrus.Errorf("Failed setting up autocompletion for option <%s> of command <%s>", name, cmdRoot.FullName())
} }
} }
// ccRoot.SetHelpFunc(func(cc *cobra.Command, args []string) {
// cmdRoot.HelpRenderer(cmdRoot.Options)(cc, args)
// os.Exit(statuscode.RenderHelp)
// })
ccRoot.SetHelpFunc(cmdRoot.HelpRenderer(cmdRoot.Options)) ccRoot.SetHelpFunc(cmdRoot.HelpRenderer(cmdRoot.Options))
for _, cmd := range CommandList() { for _, cmd := range CommandList() {
cmd := cmd cmd := cmd
leaf := toCobra(cmd, cmdRoot.Options)
container := ccRoot container := ccRoot
for idx, cp := range cmd.Path { for idx, cp := range cmd.Path {
if idx == len(cmd.Path)-1 { if idx == len(cmd.Path)-1 {
leaf := ToCobra(cmd, cmdRoot.Options)
logrus.Debugf("adding command %s to %s", leaf.Name(), container.Name()) logrus.Debugf("adding command %s to %s", leaf.Name(), container.Name())
container.AddCommand(leaf) container.AddCommand(leaf)
break break
} }
query := []string{cp} query := []string{cp}
if cc, _, err := container.Find(query); err == nil && cc != container { found := false
container = cc if cp == "help" && container == ccRoot {
container = commands.Help
} else { } else {
for _, sub := range container.Commands() {
if sub.Name() == cp {
container = sub
found = true
}
}
}
if !found {
groupName := strings.Join(query, " ") groupName := strings.Join(query, " ")
groupPath := append(cmd.Path[0:idx], query...) // nolint:gocritic groupPath := append(cmdRoot.Path, append(cmd.Path[0:idx], query...)...) // nolint:gocritic
cc := &cobra.Command{ cc := &cobra.Command{
Use: cp, Use: cp,
Short: fmt.Sprintf("%s subcommands", groupName), Short: fmt.Sprintf("%s subcommands", groupName),
@ -171,7 +113,7 @@ func Execute(version string) error {
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
Annotations: map[string]string{ Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: strings.Join(groupPath, " "), ContextKeyRuntimeIndex: strings.Join(groupPath, " "),
}, },
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.OnlyValidArgs(cmd, args); err == nil { if err := cobra.OnlyValidArgs(cmd, args); err == nil {
@ -196,13 +138,13 @@ func Execute(version string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}} return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
} }
os.Exit(_c.ExitStatusNotFound) os.Exit(statuscode.NotFound)
return nil return nil
}, },
} }
groupParent := &command.Command{ groupParent := &command.Command{
Path: cmd.Path[0 : len(cmd.Path)-1], Path: groupPath,
Summary: fmt.Sprintf("%s subcommands", groupName), Summary: fmt.Sprintf("%s subcommands", groupName),
Description: fmt.Sprintf("Runs subcommands within %s", groupName), Description: fmt.Sprintf("Runs subcommands within %s", groupName),
Arguments: command.Arguments{}, Arguments: command.Arguments{},
@ -219,12 +161,21 @@ func Execute(version string) error {
} }
cmdRoot.SetCobra(ccRoot) cmdRoot.SetCobra(ccRoot)
current, _, err := ccRoot.Find(os.Args[1:]) current, remaining, err := ccRoot.Find(os.Args[1:])
if err != nil { if err != nil {
current = ccRoot current = ccRoot
} }
logrus.Debugf("Chinampa found command %s, remaining %s", current.Name(), remaining)
// if current.HasSubCommands() && current.
if sub, _, err := current.Find(remaining); err == nil && sub != current {
logrus.Debugf("Chinampa found sub-command %s, of %s", sub.Name(), current.Name())
current = sub
}
logrus.Debugf("Chinampa is going to call command %s", current.Name())
err = current.Execute() err = current.Execute()
if err != nil { if err != nil {
logrus.Debugf("Chinampa found error calling command %s", current.Name())
errors.HandleCobraExit(current, err) errors.HandleCobraExit(current, err)
} }

12
main.go
View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 chinampa package chinampa
import ( import (

View File

@ -1,22 +1,12 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (
"fmt" "fmt"
"strings" "strings"
"git.rob.mx/nidito/chinampa/internal/errors" "git.rob.mx/nidito/chinampa/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -55,10 +45,11 @@ func (args *Arguments) Parse(supplied []string) {
if !argumentProvided { if !argumentProvided {
if arg.Default != nil { if arg.Default != nil {
if arg.Variadic { if arg.Variadic {
defaultSlice := []string{} defaultSlice := arg.Default.([]string)
for _, valI := range arg.Default.([]any) { // defaultSlice := []string{}
defaultSlice = append(defaultSlice, valI.(string)) // for _, valI := range arg.Default.([]string) {
} // defaultSlice = append(defaultSlice, valI.(string))
// }
arg.provided = &defaultSlice arg.provided = &defaultSlice
} else { } else {
defaultString := arg.Default.(string) defaultString := arg.Default.(string)
@ -194,11 +185,11 @@ func (arg *Argument) ToValue() any {
} else { } else {
if arg.Default != nil { if arg.Default != nil {
if arg.Variadic { if arg.Variadic {
defaultSlice := []string{} defaultSlice := arg.Default
for _, valI := range arg.Default.([]any) { // for _, valI := range arg.Default.([]any) {
valStr := valI.(string) // valStr := valI.(string)
defaultSlice = append(defaultSlice, valStr) // defaultSlice = append(defaultSlice, valStr)
} // }
value = defaultSlice value = defaultSlice
} else { } else {

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command_test
import ( import (

View File

@ -1,22 +1,13 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"git.rob.mx/nidito/chinampa/internal/errors" "git.rob.mx/nidito/chinampa/pkg/errors"
"git.rob.mx/nidito/chinampa/pkg/runtime" "git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -38,9 +29,12 @@ type Command struct {
Options Options `json:"options" yaml:"options" validate:"dive"` Options Options `json:"options" yaml:"options" validate:"dive"`
HelpFunc HelpFunc `json:"-" yaml:"-"` HelpFunc HelpFunc `json:"-" yaml:"-"`
// The action to take upon running // The action to take upon running
Action Action Action Action `json:"-" yaml:"-"`
runtimeFlags *pflag.FlagSet runtimeFlags *pflag.FlagSet
Cobra *cobra.Command Cobra *cobra.Command `json:"-" yaml:"-"`
// Meta stores application specific stuff
Meta any `json:"meta" yaml:"meta"`
Hidden bool `json:"-" yaml:"-"`
} }
func (cmd *Command) IsRoot() bool { func (cmd *Command) IsRoot() bool {
@ -88,6 +82,22 @@ func (cmd *Command) FlagSet() *pflag.FlagSet {
} }
fs.BoolP(name, opt.ShortName, def, opt.Description) fs.BoolP(name, opt.ShortName, def, opt.Description)
case ValueTypeInt:
def := -1
if opt.Default != nil {
switch val := opt.Default.(type) {
case int:
def = opt.Default.(int)
case string:
casted, err := strconv.Atoi(val)
if err != nil {
logrus.Warnf("Could not parse default with value <%s> as integer for option <%s>", val, name)
}
def = casted
}
}
fs.IntP(name, opt.ShortName, def, opt.Description)
case ValueTypeDefault, ValueTypeString: case ValueTypeDefault, ValueTypeString:
opt.Type = ValueTypeString opt.Type = ValueTypeString
def := "" def := ""
@ -119,7 +129,7 @@ func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error {
logrus.Debug("Validating flags") logrus.Debug("Validating flags")
if err := cmd.Options.AreValid(); err != nil { if err := cmd.Options.AreValid(); err != nil {
logrus.Debugf("Invalid flags for %s: %w", cmd.FullName(), err) logrus.Debugf("Invalid flags for %s: %s", cmd.FullName(), err)
return err return err
} }
} }

View File

@ -1,22 +1,11 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (
"bytes" "bytes"
_c "git.rob.mx/nidito/chinampa/internal/constants" "git.rob.mx/nidito/chinampa/pkg/render"
"git.rob.mx/nidito/chinampa/internal/render"
"git.rob.mx/nidito/chinampa/pkg/runtime" "git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -64,7 +53,7 @@ func (cmd *Command) ShowHelp(globalOptions Options, args []string) ([]byte, erro
GlobalOptions: globalOptions, GlobalOptions: globalOptions,
HTMLOutput: runtime.UnstyledHelpEnabled(), HTMLOutput: runtime.UnstyledHelpEnabled(),
} }
err := _c.HelpTemplate(runtime.Executable).Execute(&buf, c) err := render.HelpTemplate(runtime.Executable).Execute(&buf, c)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (
@ -17,7 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.rob.mx/nidito/chinampa/internal/errors" "git.rob.mx/nidito/chinampa/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@ -41,54 +31,6 @@ func (opts *Options) AllKnownStr() map[string]string {
return col 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) { func (opts *Options) Parse(supplied *pflag.FlagSet) {
// logrus.Debugf("Parsing supplied flags, %v", supplied) // logrus.Debugf("Parsing supplied flags, %v", supplied)
for name, opt := range *opts { for name, opt := range *opts {
@ -98,6 +40,11 @@ func (opts *Options) Parse(supplied *pflag.FlagSet) {
opt.provided = val opt.provided = val
continue continue
} }
case ValueTypeInt:
if val, err := supplied.GetInt(name); err == nil {
opt.provided = val
continue
}
default: default:
opt.Type = ValueTypeString opt.Type = ValueTypeString
if val, err := supplied.GetString(name); err == nil { if val, err := supplied.GetString(name); err == nil {
@ -121,7 +68,7 @@ func (opts *Options) AreValid() error {
// Option represents a command line flag. // Option represents a command line flag.
type Option struct { type Option struct {
ShortName string `json:"short-name,omitempty" yaml:"short-name,omitempty"` // nolint:tagliatelle ShortName string `json:"short-name,omitempty" yaml:"short-name,omitempty"` // nolint:tagliatelle
Type ValueType `json:"type" yaml:"type" validate:"omitempty,oneof=string bool"` Type ValueType `json:"type" yaml:"type" validate:"omitempty,oneof=string bool int"`
Description string `json:"description" yaml:"description" validate:"required"` Description string `json:"description" yaml:"description" validate:"required"`
Default any `json:"default,omitempty" yaml:"default,omitempty"` Default any `json:"default,omitempty" yaml:"default,omitempty"`
Values *ValueSource `json:"values,omitempty" yaml:"values,omitempty" validate:"omitempty"` Values *ValueSource `json:"values,omitempty" yaml:"values,omitempty" validate:"omitempty"`
@ -144,13 +91,20 @@ func (opt *Option) ToValue() any {
func (opt *Option) ToString() string { func (opt *Option) ToString() string {
value := opt.ToValue() value := opt.ToValue()
stringValue := "" stringValue := ""
if opt.Type == "bool" { switch opt.Type {
case ValueTypeBoolean:
if value == nil { if value == nil {
stringValue = "" stringValue = ""
} else { } else {
stringValue = strconv.FormatBool(value.(bool)) stringValue = strconv.FormatBool(value.(bool))
} }
} else { case ValueTypeInt:
if value == nil {
stringValue = ""
} else {
stringValue = fmt.Sprintf("%i", value)
}
default:
if value != nil { if value != nil {
stringValue = value.(string) stringValue = value.(string)
} }

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command
import ( import (
@ -21,9 +11,11 @@ import (
"text/template" "text/template"
"time" "time"
_c "git.rob.mx/nidito/chinampa/internal/constants" "git.rob.mx/nidito/chinampa/pkg/exec"
"git.rob.mx/nidito/chinampa/internal/exec" "git.rob.mx/nidito/chinampa/pkg/render"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3"
) )
// ValueType represent the kinds of or option. // ValueType represent the kinds of or option.
@ -36,6 +28,8 @@ const (
ValueTypeString ValueType = "string" ValueTypeString ValueType = "string"
// ValueTypeBoolean is a value treated like a boolean. // ValueTypeBoolean is a value treated like a boolean.
ValueTypeBoolean ValueType = "bool" ValueTypeBoolean ValueType = "bool"
// ValueTypeBoolean is a value treated like an integer.
ValueTypeInt ValueType = "int"
) )
type SourceCommand struct { type SourceCommand struct {
@ -43,7 +37,7 @@ type SourceCommand struct {
Args string Args string
} }
type CompletionFunc func(cmd *Command, currentValue string) (values []string, flag cobra.ShellCompDirective, err error) type CompletionFunc func(cmd *Command, currentValue string, config string) (values []string, flag cobra.ShellCompDirective, err error)
// ValueSource represents the source for an auto-completed and/or validated option/argument. // ValueSource represents the source for an auto-completed and/or validated option/argument.
type ValueSource struct { type ValueSource struct {
@ -58,7 +52,7 @@ type ValueSource struct {
// Command runs a subcommand and returns an option for every line of stdout. // 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"` Command *SourceCommand `json:"command,omitempty" yaml:"command,omitempty" validate:"omitempty,excluded_with=Directories Files Func Script Static"`
// Func runs a function // Func runs a function
Func CompletionFunc `json:"func,omitempty" yaml:"func,omitempty" validate:"omitempty,excluded_with=Command Directories Files Script Static"` Func CompletionFunc `json:"-" yaml:"-" 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 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"` 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 if provided will only suggest autocomplete values but will not perform validation of a given value
@ -68,6 +62,7 @@ type ValueSource struct {
command *Command `json:"-" yaml:"-" validate:"-"` command *Command `json:"-" yaml:"-" validate:"-"`
computed *[]string computed *[]string
flag cobra.ShellCompDirective flag cobra.ShellCompDirective
custom string // The app-defined key's value
} }
// Validates tells if a value needs to be validated. // Validates tells if a value needs to be validated.
@ -92,6 +87,7 @@ func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra
flag = cobra.ShellCompDirectiveDefault flag = cobra.ShellCompDirectiveDefault
timeout := time.Duration(vs.Timeout) timeout := time.Duration(vs.Timeout)
logrus.Errorf("resolving: %s", vs)
switch { switch {
case vs.Static != nil: case vs.Static != nil:
values = *vs.Static values = *vs.Static
@ -114,7 +110,7 @@ func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra
} }
}() }()
values, flag, err = vs.Func(vs.command, currentValue) values, flag, err = vs.Func(vs.command, currentValue, vs.custom)
done <- err done <- err
}() }()
select { select {
@ -172,6 +168,8 @@ func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra
if err != nil { if err != nil {
return nil, flag, err return nil, flag, err
} }
default:
return nil, flag, fmt.Errorf("Empty value source")
} }
vs.computed = &values vs.computed = &values
@ -215,7 +213,7 @@ func (cmd *Command) ResolveTemplate(templateString string, currentValue string)
"Current": func() string { return currentValue }, "Current": func() string { return currentValue },
} }
for k, v := range _c.TemplateFuncs { for k, v := range render.TemplateFuncs {
fnMap[k] = v fnMap[k] = v
} }
@ -232,3 +230,91 @@ func (cmd *Command) ResolveTemplate(templateString string, currentValue string)
return buf.String(), nil return buf.String(), nil
} }
func (vs *ValueSource) UnmarshalYAML(node *yaml.Node) error {
vs.Timeout = 0
vs.Suggestion = false
vs.SuggestRaw = false
vs.computed = nil
intermediate := map[string]yaml.Node{}
if err := node.Decode(&intermediate); err != nil {
logrus.Errorf("could not decode valuesource: %s", err)
return err
}
if t, ok := intermediate["timeout"]; ok {
if err := t.Decode(&vs.Timeout); err != nil {
logrus.Errorf("could not decode timeout: %s", err)
return err
}
delete(intermediate, "timeout")
}
if t, ok := intermediate["suggest-only"]; ok {
if err := t.Decode(&vs.Suggestion); err != nil {
logrus.Errorf("could not decode suggest-only: %s", err)
return err
}
delete(intermediate, "suggest-only")
}
if t, ok := intermediate["suggest-raw"]; ok {
if err := t.Decode(&vs.SuggestRaw); err != nil {
logrus.Errorf("could not decode suggest-raw: %s", err)
return err
}
delete(intermediate, "suggest-raw")
}
for key, node := range intermediate {
if cfn, ok := customCompleters[key]; ok {
if err := node.Decode(&vs.custom); err != nil {
logrus.Errorf("could not decode custom: %s", err)
return err
}
vs.Func = cfn
break
}
switch key {
case "dirs":
var res string
if err := node.Decode(&res); err != nil {
return err
}
vs.Directories = &res
case "files":
res := []string{}
if err := node.Decode(&res); err != nil {
return err
}
vs.Files = &res
case "script":
if err := node.Decode(&vs.Script); err != nil {
return err
}
case "static":
static := []string{}
if err := node.Decode(&static); err != nil {
logrus.Errorf("could not decode static: %s", err)
return err
}
vs.Static = &static
case "command":
if err := node.Decode(&vs.Command); err != nil {
return err
}
default:
return fmt.Errorf("unknown value source key: %s", key)
}
}
return nil
}
var customCompleters = map[string]CompletionFunc{}
func RegisterValueSource(key string, completion CompletionFunc) {
customCompleters[key] = completion
}

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package command_test
import ( import (

22
pkg/env/env.go vendored Normal file
View File

@ -0,0 +1,22 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package env
// Environment Variables.
var HelpUnstyled = "HELP_STYLE_PLAIN"
var HelpStyle = "HELP_STYLE"
var Verbose = "VERBOSE"
var Silent = "SILENT"
var NoColor = "NO_COLOR"
var ForceColor = "COLOR"
var ValidationDisabled = "SKIP_VALIDATION"
var Debug = "DEBUG"
// FlagNames are flags also available as environment variables.
var FlagNames = map[string]string{
"no-color": NoColor,
"color": ForceColor,
"silent": Silent,
"verbose": Verbose,
"skip-validation": ValidationDisabled,
}

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package errors
import "fmt" import "fmt"

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package errors
import ( import (
@ -20,13 +10,14 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
_c "git.rob.mx/nidito/chinampa/internal/constants" _c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/pkg/statuscode"
) )
func showHelp(cmd *cobra.Command) { func showHelp(cmd *cobra.Command) {
if cmd.Name() != _c.HelpCommandName { if cmd.Name() != _c.HelpCommandName {
err := cmd.Help() err := cmd.Help()
if err != nil { if err != nil {
os.Exit(_c.ExitStatusProgrammerError) os.Exit(statuscode.ProgrammerError)
} }
} }
} }
@ -35,7 +26,7 @@ func HandleCobraExit(cmd *cobra.Command, err error) {
if err == nil { if err == nil {
ok, err := cmd.Flags().GetBool(_c.HelpCommandName) ok, err := cmd.Flags().GetBool(_c.HelpCommandName)
if cmd.Name() == _c.HelpCommandName || err == nil && ok { if cmd.Name() == _c.HelpCommandName || err == nil && ok {
os.Exit(_c.ExitStatusRenderHelp) os.Exit(statuscode.RenderHelp)
} }
os.Exit(42) os.Exit(42)
@ -48,26 +39,26 @@ func HandleCobraExit(cmd *cobra.Command, err error) {
case BadArguments: case BadArguments:
showHelp(cmd) showHelp(cmd)
logrus.Error(err) logrus.Error(err)
os.Exit(_c.ExitStatusUsage) os.Exit(statuscode.Usage)
case NotFound: case NotFound:
showHelp(cmd) showHelp(cmd)
logrus.Error(err) logrus.Error(err)
os.Exit(_c.ExitStatusNotFound) os.Exit(statuscode.NotFound)
case ConfigError: case ConfigError:
showHelp(cmd) showHelp(cmd)
logrus.Error(err) logrus.Error(err)
os.Exit(_c.ExitStatusConfigError) os.Exit(statuscode.ConfigError)
case EnvironmentError: case EnvironmentError:
logrus.Error(err) logrus.Error(err)
os.Exit(_c.ExitStatusConfigError) os.Exit(statuscode.ConfigError)
default: default:
if strings.HasPrefix(err.Error(), "unknown command") { if strings.HasPrefix(err.Error(), "unknown command") {
showHelp(cmd) showHelp(cmd)
os.Exit(_c.ExitStatusNotFound) os.Exit(statuscode.NotFound)
} else if strings.HasPrefix(err.Error(), "unknown flag") || strings.HasPrefix(err.Error(), "unknown shorthand flag") { } else if strings.HasPrefix(err.Error(), "unknown flag") || strings.HasPrefix(err.Error(), "unknown shorthand flag") {
showHelp(cmd) showHelp(cmd)
logrus.Error(err) logrus.Error(err)
os.Exit(_c.ExitStatusUsage) os.Exit(statuscode.Usage)
} }
} }

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package exec
import ( import (
@ -20,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"git.rob.mx/nidito/chinampa/internal/errors" "git.rob.mx/nidito/chinampa/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package exec_test
import ( import (
@ -20,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
. "git.rob.mx/nidito/chinampa/internal/exec" . "git.rob.mx/nidito/chinampa/pkg/exec"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -1,22 +1,15 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package render
import ( import (
"bytes" "bytes"
"os" "os"
"strings"
"text/template"
_c "git.rob.mx/nidito/chinampa/internal/constants" _c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/pkg/env"
"git.rob.mx/nidito/chinampa/pkg/runtime" "git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -43,7 +36,7 @@ func Markdown(content []byte, withColor bool) ([]byte, error) {
var styleFunc glamour.TermRendererOption var styleFunc glamour.TermRendererOption
if withColor { if withColor {
style := os.Getenv(_c.EnvVarHelpStyle) style := os.Getenv(env.HelpStyle)
switch style { switch style {
case "dark": case "dark":
styleFunc = glamour.WithStandardStyle("dark") styleFunc = glamour.WithStandardStyle("dark")
@ -68,3 +61,27 @@ func Markdown(content []byte, withColor bool) ([]byte, error) {
return renderer.RenderBytes(content) return renderer.RenderBytes(content)
} }
// 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.Template
func HelpTemplate(executableName string) *template.Template {
if TemplateCommandHelp == nil {
TemplateCommandHelp = template.Must(template.New("help").Funcs(TemplateFuncs).Parse(strings.ReplaceAll(_c.HelpTemplate, "@chinampa@", executableName)))
}
return TemplateCommandHelp
}

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package render_test
import ( import (
@ -18,13 +8,13 @@ import (
"reflect" "reflect"
"testing" "testing"
_c "git.rob.mx/nidito/chinampa/internal/constants" "git.rob.mx/nidito/chinampa/pkg/env"
"git.rob.mx/nidito/chinampa/internal/render" "git.rob.mx/nidito/chinampa/pkg/render"
) )
func TestMarkdownUnstyled(t *testing.T) { func TestMarkdownUnstyled(t *testing.T) {
content := []byte("# hello") content := []byte("# hello")
os.Setenv(_c.EnvVarHelpUnstyled, "true") os.Setenv(env.HelpUnstyled, "true")
res, err := render.Markdown(content, false) res, err := render.Markdown(content, false)
if err != nil { if err != nil {
@ -38,7 +28,7 @@ func TestMarkdownUnstyled(t *testing.T) {
} }
func TestMarkdownNoColor(t *testing.T) { func TestMarkdownNoColor(t *testing.T) {
os.Unsetenv(_c.EnvVarHelpUnstyled) os.Unsetenv(env.HelpUnstyled)
content := []byte("# hello ﹅world﹅") content := []byte("# hello ﹅world﹅")
res, err := render.Markdown(content, false) res, err := render.Markdown(content, false)
@ -61,7 +51,7 @@ var autoStyleTestRender = "\n\x1b[38;5;228;48;5;63;1m\x1b[0m\x1b[38;5;228;48;5;6
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" 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) { func TestMarkdownColor(t *testing.T) {
os.Unsetenv(_c.EnvVarHelpUnstyled) os.Unsetenv(env.HelpUnstyled)
content := []byte("# hello") content := []byte("# hello")
styles := map[string][]byte{ styles := map[string][]byte{
@ -72,7 +62,7 @@ func TestMarkdownColor(t *testing.T) {
} }
for style, expected := range styles { for style, expected := range styles {
t.Run(fmt.Sprintf("style %s", style), func(t *testing.T) { t.Run(fmt.Sprintf("style %s", style), func(t *testing.T) {
os.Setenv(_c.EnvVarHelpStyle, style) os.Setenv(env.HelpStyle, style)
res, err := render.Markdown(content, true) res, err := render.Markdown(content, true)
if err != nil { if err != nil {

View File

@ -1,22 +1,12 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package runtime
import ( import (
"os" "os"
"strconv" "strconv"
_c "git.rob.mx/nidito/chinampa/internal/constants" "git.rob.mx/nidito/chinampa/pkg/env"
) )
var Executable = "chinampa" var Executable = "chinampa"
@ -63,45 +53,60 @@ func isTrueIsh(val string) bool {
} }
func DebugEnabled() bool { func DebugEnabled() bool {
return isTrueIsh(os.Getenv(_c.EnvVarDebug)) return isTrueIsh(os.Getenv(env.Debug))
} }
func ValidationEnabled() bool { func ValidationEnabled() bool {
return isFalseIsh(os.Getenv(_c.EnvVarValidationDisabled)) return isFalseIsh(os.Getenv(env.ValidationDisabled))
} }
func VerboseEnabled() bool { func VerboseEnabled() bool {
return isTrueIsh(os.Getenv(_c.EnvVarMilpaVerbose)) return isTrueIsh(os.Getenv(env.Verbose))
}
func SilenceEnabled() bool {
if isTrueIsh(os.Getenv(env.Silent)) {
return true
}
if VerboseEnabled() {
return false
}
for _, arg := range os.Args {
if arg == "--silent" {
return true
}
}
return false
} }
func ColorEnabled() bool { func ColorEnabled() bool {
return isFalseIsh(os.Getenv(_c.EnvVarMilpaUnstyled)) && !UnstyledHelpEnabled() return isFalseIsh(os.Getenv(env.NoColor)) && !UnstyledHelpEnabled()
} }
func UnstyledHelpEnabled() bool { func UnstyledHelpEnabled() bool {
return isTrueIsh(os.Getenv(_c.EnvVarHelpUnstyled)) return isTrueIsh(os.Getenv(env.HelpUnstyled))
} }
// EnvironmentMap returns the resolved environment map. // EnvironmentMap returns the resolved environment map.
func EnvironmentMap() map[string]string { func EnvironmentMap() map[string]string {
env := map[string]string{} res := map[string]string{}
trueString := strconv.FormatBool(true) trueString := strconv.FormatBool(true)
if !ColorEnabled() { if !ColorEnabled() {
env[_c.EnvVarMilpaUnstyled] = trueString res[env.NoColor] = trueString
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaForceColor)) { } else if isTrueIsh(os.Getenv(env.ForceColor)) {
env[_c.EnvVarMilpaForceColor] = "always" res[env.ForceColor] = "always"
} }
if DebugEnabled() { if DebugEnabled() {
env[_c.EnvVarDebug] = trueString res[env.Debug] = trueString
} }
if VerboseEnabled() { if VerboseEnabled() {
env[_c.EnvVarMilpaVerbose] = trueString res[env.Verbose] = trueString
} else if isTrueIsh(os.Getenv(_c.EnvVarMilpaSilent)) { } else if SilenceEnabled() {
env[_c.EnvVarMilpaSilent] = trueString res[env.Silent] = trueString
} }
return env return res
} }

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// // SPDX-License-Identifier: Apache-2.0
// 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 package runtime_test
import ( import (
@ -20,12 +10,12 @@ import (
"strconv" "strconv"
"testing" "testing"
_c "git.rob.mx/nidito/chinampa/internal/constants" "git.rob.mx/nidito/chinampa/pkg/env"
. "git.rob.mx/nidito/chinampa/pkg/runtime" . "git.rob.mx/nidito/chinampa/pkg/runtime"
) )
func TestEnabled(t *testing.T) { func TestEnabled(t *testing.T) {
defer func() { os.Setenv(_c.EnvVarMilpaVerbose, "") }() defer func() { os.Setenv(env.Verbose, "") }()
cases := []struct { cases := []struct {
Name string Name string
@ -33,29 +23,34 @@ func TestEnabled(t *testing.T) {
Expects bool Expects bool
}{ }{
{ {
Name: _c.EnvVarMilpaVerbose, Name: env.Verbose,
Func: VerboseEnabled, Func: VerboseEnabled,
Expects: true, Expects: true,
}, },
{ {
Name: _c.EnvVarValidationDisabled, Name: env.Verbose,
Func: SilenceEnabled,
Expects: false,
},
{
Name: env.ValidationDisabled,
Func: ValidationEnabled, Func: ValidationEnabled,
}, },
{ {
Name: _c.EnvVarMilpaUnstyled, Name: env.NoColor,
Func: ColorEnabled, Func: ColorEnabled,
}, },
{ {
Name: _c.EnvVarHelpUnstyled, Name: env.HelpUnstyled,
Func: ColorEnabled, Func: ColorEnabled,
}, },
{ {
Name: _c.EnvVarDebug, Name: env.Debug,
Func: DebugEnabled, Func: DebugEnabled,
Expects: true, Expects: true,
}, },
{ {
Name: _c.EnvVarHelpUnstyled, Name: env.HelpUnstyled,
Func: UnstyledHelpEnabled, Func: UnstyledHelpEnabled,
Expects: true, Expects: true,
}, },
@ -90,9 +85,9 @@ func TestEnabled(t *testing.T) {
func TestEnvironmentMapEnabled(t *testing.T) { func TestEnvironmentMapEnabled(t *testing.T) {
trueString := strconv.FormatBool(true) trueString := strconv.FormatBool(true)
os.Setenv(_c.EnvVarMilpaForceColor, trueString) os.Setenv(env.ForceColor, trueString)
os.Setenv(_c.EnvVarDebug, trueString) os.Setenv(env.Debug, trueString)
os.Setenv(_c.EnvVarMilpaVerbose, trueString) os.Setenv(env.Verbose, trueString)
res := EnvironmentMap() res := EnvironmentMap()
if res == nil { if res == nil {
@ -100,9 +95,9 @@ func TestEnvironmentMapEnabled(t *testing.T) {
} }
expected := map[string]string{ expected := map[string]string{
_c.EnvVarMilpaForceColor: "always", env.ForceColor: "always",
_c.EnvVarDebug: trueString, env.Debug: trueString,
_c.EnvVarMilpaVerbose: trueString, env.Verbose: trueString,
} }
if !reflect.DeepEqual(res, expected) { if !reflect.DeepEqual(res, expected) {
@ -113,12 +108,12 @@ func TestEnvironmentMapEnabled(t *testing.T) {
func TestEnvironmentMapDisabled(t *testing.T) { func TestEnvironmentMapDisabled(t *testing.T) {
trueString := strconv.FormatBool(true) trueString := strconv.FormatBool(true)
// clear COLOR // clear COLOR
os.Unsetenv(_c.EnvVarMilpaForceColor) os.Unsetenv(env.ForceColor)
// set NO_COLOR // set NO_COLOR
os.Setenv(_c.EnvVarMilpaUnstyled, trueString) os.Setenv(env.NoColor, trueString)
os.Unsetenv(_c.EnvVarDebug) os.Unsetenv(env.Debug)
os.Unsetenv(_c.EnvVarMilpaVerbose) os.Unsetenv(env.Verbose)
os.Setenv(_c.EnvVarMilpaSilent, trueString) os.Setenv(env.Silent, trueString)
res := EnvironmentMap() res := EnvironmentMap()
if res == nil { if res == nil {
@ -126,8 +121,8 @@ func TestEnvironmentMapDisabled(t *testing.T) {
} }
expected := map[string]string{ expected := map[string]string{
_c.EnvVarMilpaUnstyled: trueString, env.NoColor: trueString,
_c.EnvVarMilpaSilent: trueString, env.Silent: trueString,
} }
if !reflect.DeepEqual(res, expected) { if !reflect.DeepEqual(res, expected) {

View File

@ -0,0 +1,22 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package statuscode
// Exit statuses
// see man sysexits || grep "#define EX" /usr/include/sysexits.h
// and https://tldp.org/LDP/abs/html/exitcodes.html
const (
// 0 means everything is fine.
Ok = 0
// 42 provides answers to life, the universe and everything; also, renders help.
RenderHelp = 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.
Usage = 64
// EX_SOFTWARE An internal software error has been detected. This should be limited to non-operating system related errors as possible.
ProgrammerError = 70
// EX_CONFIG Something was found in an unconfigured or misconfigured state.
ConfigError = 78
// 127 command not found.
NotFound = 127
)

84
pkg/tree/tree.go Normal file
View File

@ -0,0 +1,84 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package tree
import (
"sort"
"git.rob.mx/nidito/chinampa/internal/registry"
"git.rob.mx/nidito/chinampa/pkg/command"
"github.com/spf13/cobra"
)
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
}
var tree *CommandTree
func Build(cc *cobra.Command, depth int) {
tree = &CommandTree{
Command: registry.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 := registry.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)
}
func Serialize(serializationFn func(any) ([]byte, error)) (string, error) {
bytes, err := serializationFn(tree)
if err != nil {
return "", err
}
return string(bytes), nil
}
func ChildrenNames() []string {
if tree == nil {
return []string{}
}
ret := make([]string, len(tree.Children))
for idx, cmd := range tree.Children {
ret[idx] = cmd.Command.Name()
}
sort.Strings(ret)
return ret
}
func CommandList() []*command.Command {
return registry.CommandList()
}