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
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/pflag v1.0.5
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/yaml.v3 v3.0.1
)
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/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.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
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/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/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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/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>
//
// 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.
// SPDX-License-Identifier: Apache-2.0
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 = "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
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.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
}
var HelpTemplate string

View File

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

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@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.
// SPDX-License-Identifier: Apache-2.0
package registry
import (
@ -18,14 +8,18 @@ import (
"sort"
"strings"
_c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/internal/errors"
"git.rob.mx/nidito/chinampa/internal/commands"
"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/sirupsen/logrus"
"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{
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) 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) {
@ -82,87 +57,54 @@ func CommandList() []*command.Command {
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 := newCobraRoot(command.Root)
ccRoot.Annotations["version"] = version
ccRoot.CompletionOptions.HiddenDefaultCmd = true
ccRoot.PersistentFlags().AddFlagSet(cmdRoot.FlagSet())
ccRoot.SetHelpCommand(commands.Help)
ccRoot.AddCommand(commands.Version)
ccRoot.AddCommand(commands.GenerateCompletions)
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(func(cc *cobra.Command, args []string) {
// cmdRoot.HelpRenderer(cmdRoot.Options)(cc, args)
// os.Exit(statuscode.RenderHelp)
// })
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 {
leaf := ToCobra(cmd, cmdRoot.Options)
logrus.Debugf("adding command %s to %s", leaf.Name(), container.Name())
container.AddCommand(leaf)
break
}
query := []string{cp}
if cc, _, err := container.Find(query); err == nil && cc != container {
container = cc
found := false
if cp == "help" && container == ccRoot {
container = commands.Help
} else {
for _, sub := range container.Commands() {
if sub.Name() == cp {
container = sub
found = true
}
}
}
if !found {
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{
Use: cp,
Short: fmt.Sprintf("%s subcommands", groupName),
@ -171,7 +113,7 @@ func Execute(version string) error {
SilenceUsage: true,
SilenceErrors: true,
Annotations: map[string]string{
_c.ContextKeyRuntimeIndex: strings.Join(groupPath, " "),
ContextKeyRuntimeIndex: strings.Join(groupPath, " "),
},
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.OnlyValidArgs(cmd, args); err == nil {
@ -196,13 +138,13 @@ func Execute(version string) error {
if len(args) == 0 {
return errors.NotFound{Msg: "No subcommand provided", Group: []string{}}
}
os.Exit(_c.ExitStatusNotFound)
os.Exit(statuscode.NotFound)
return nil
},
}
groupParent := &command.Command{
Path: cmd.Path[0 : len(cmd.Path)-1],
Path: groupPath,
Summary: fmt.Sprintf("%s subcommands", groupName),
Description: fmt.Sprintf("Runs subcommands within %s", groupName),
Arguments: command.Arguments{},
@ -219,12 +161,21 @@ func Execute(version string) error {
}
cmdRoot.SetCobra(ccRoot)
current, _, err := ccRoot.Find(os.Args[1:])
current, remaining, err := ccRoot.Find(os.Args[1:])
if err != nil {
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()
if err != nil {
logrus.Debugf("Chinampa found error calling command %s", current.Name())
errors.HandleCobraExit(current, err)
}

12
main.go
View File

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

View File

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

View File

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

View File

@ -1,22 +1,13 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@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.
// SPDX-License-Identifier: Apache-2.0
package command
import (
"fmt"
"strconv"
"strings"
"git.rob.mx/nidito/chinampa/internal/errors"
"git.rob.mx/nidito/chinampa/pkg/errors"
"git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -38,9 +29,12 @@ type Command struct {
Options Options `json:"options" yaml:"options" validate:"dive"`
HelpFunc HelpFunc `json:"-" yaml:"-"`
// The action to take upon running
Action Action
Action Action `json:"-" yaml:"-"`
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 {
@ -88,6 +82,22 @@ func (cmd *Command) FlagSet() *pflag.FlagSet {
}
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:
opt.Type = ValueTypeString
def := ""
@ -119,7 +129,7 @@ func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error {
logrus.Debug("Validating flags")
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,5 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@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.
// SPDX-License-Identifier: Apache-2.0
package command
import (
@ -21,9 +11,11 @@ import (
"text/template"
"time"
_c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/internal/exec"
"git.rob.mx/nidito/chinampa/pkg/exec"
"git.rob.mx/nidito/chinampa/pkg/render"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
// ValueType represent the kinds of or option.
@ -36,6 +28,8 @@ const (
ValueTypeString ValueType = "string"
// ValueTypeBoolean is a value treated like a boolean.
ValueTypeBoolean ValueType = "bool"
// ValueTypeBoolean is a value treated like an integer.
ValueTypeInt ValueType = "int"
)
type SourceCommand struct {
@ -43,7 +37,7 @@ type SourceCommand struct {
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.
type ValueSource struct {
@ -58,7 +52,7 @@ type ValueSource struct {
// 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"`
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 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
@ -68,6 +62,7 @@ type ValueSource struct {
command *Command `json:"-" yaml:"-" validate:"-"`
computed *[]string
flag cobra.ShellCompDirective
custom string // The app-defined key's value
}
// 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
timeout := time.Duration(vs.Timeout)
logrus.Errorf("resolving: %s", vs)
switch {
case vs.Static != nil:
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
}()
select {
@ -172,6 +168,8 @@ func (vs *ValueSource) Resolve(currentValue string) (values []string, flag cobra
if err != nil {
return nil, flag, err
}
default:
return nil, flag, fmt.Errorf("Empty value source")
}
vs.computed = &values
@ -215,7 +213,7 @@ func (cmd *Command) ResolveTemplate(templateString string, currentValue string)
"Current": func() string { return currentValue },
}
for k, v := range _c.TemplateFuncs {
for k, v := range render.TemplateFuncs {
fnMap[k] = v
}
@ -232,3 +230,91 @@ func (cmd *Command) ResolveTemplate(templateString string, currentValue string)
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>
//
// 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.
// SPDX-License-Identifier: Apache-2.0
package command_test
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>
//
// 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.
// SPDX-License-Identifier: Apache-2.0
package errors
import "fmt"

View File

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

View File

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

View File

@ -1,22 +1,15 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@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.
// SPDX-License-Identifier: Apache-2.0
package render
import (
"bytes"
"os"
"strings"
"text/template"
_c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/pkg/env"
"git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/charmbracelet/glamour"
"github.com/sirupsen/logrus"
@ -43,7 +36,7 @@ func Markdown(content []byte, withColor bool) ([]byte, error) {
var styleFunc glamour.TermRendererOption
if withColor {
style := os.Getenv(_c.EnvVarHelpStyle)
style := os.Getenv(env.HelpStyle)
switch style {
case "dark":
styleFunc = glamour.WithStandardStyle("dark")
@ -68,3 +61,27 @@ func Markdown(content []byte, withColor bool) ([]byte, error) {
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>
//
// 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.
// SPDX-License-Identifier: Apache-2.0
package render_test
import (
@ -18,13 +8,13 @@ import (
"reflect"
"testing"
_c "git.rob.mx/nidito/chinampa/internal/constants"
"git.rob.mx/nidito/chinampa/internal/render"
"git.rob.mx/nidito/chinampa/pkg/env"
"git.rob.mx/nidito/chinampa/pkg/render"
)
func TestMarkdownUnstyled(t *testing.T) {
content := []byte("# hello")
os.Setenv(_c.EnvVarHelpUnstyled, "true")
os.Setenv(env.HelpUnstyled, "true")
res, err := render.Markdown(content, false)
if err != nil {
@ -38,7 +28,7 @@ func TestMarkdownUnstyled(t *testing.T) {
}
func TestMarkdownNoColor(t *testing.T) {
os.Unsetenv(_c.EnvVarHelpUnstyled)
os.Unsetenv(env.HelpUnstyled)
content := []byte("# hello ﹅world﹅")
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"
func TestMarkdownColor(t *testing.T) {
os.Unsetenv(_c.EnvVarHelpUnstyled)
os.Unsetenv(env.HelpUnstyled)
content := []byte("# hello")
styles := map[string][]byte{
@ -72,7 +62,7 @@ func TestMarkdownColor(t *testing.T) {
}
for style, expected := range styles {
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)
if err != nil {

View File

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

View File

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