304 lines
7.6 KiB
Go
304 lines
7.6 KiB
Go
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.rob.mx/nidito/chinampa/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func contains(haystack []string, needle string) bool {
|
|
for _, validValue := range haystack {
|
|
if needle == validValue {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Arguments is an ordered list of Argument.
|
|
type Arguments []*Argument
|
|
|
|
func (args *Arguments) AllKnown() map[string]any {
|
|
col := map[string]any{}
|
|
for _, arg := range *args {
|
|
col[arg.Name] = arg.ToValue()
|
|
}
|
|
return col
|
|
}
|
|
|
|
func (args *Arguments) AllKnownStr() map[string]string {
|
|
col := map[string]string{}
|
|
for _, arg := range *args {
|
|
col[arg.Name] = arg.ToString()
|
|
}
|
|
return col
|
|
}
|
|
|
|
func anySliceToStringSlice(src any) []string {
|
|
res := []string{}
|
|
switch d := src.(type) {
|
|
case []string:
|
|
res = d
|
|
case []any:
|
|
for _, valI := range d {
|
|
res = append(res, valI.(string))
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (args *Arguments) Parse(supplied []string) error {
|
|
parsed := []string{}
|
|
for idx, arg := range *args {
|
|
argumentProvided := idx < len(supplied)
|
|
|
|
if !argumentProvided {
|
|
if arg.Default != nil {
|
|
if arg.Variadic {
|
|
defaultSlice := anySliceToStringSlice(arg.Default)
|
|
arg.provided = &defaultSlice
|
|
} else {
|
|
defaultString := arg.Default.(string)
|
|
if defaultString != "" {
|
|
arg.provided = &[]string{defaultString}
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if arg.Variadic {
|
|
values := append([]string{}, supplied[idx:]...)
|
|
arg.SetValue(values)
|
|
} else {
|
|
arg.SetValue([]string{supplied[idx]})
|
|
}
|
|
parsed = append(parsed, *arg.provided...)
|
|
}
|
|
|
|
if len(parsed) != len(supplied) {
|
|
return errors.BadArguments{Msg: fmt.Sprintf("Unexpected arguments provided: %s", supplied)}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (args *Arguments) AreValid() error {
|
|
for _, arg := range *args {
|
|
if err := arg.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CompletionFunction is called by cobra when asked to complete arguments.
|
|
func (args *Arguments) CompletionFunction(cc *cobra.Command, provided []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
expectedArgLen := len(*args)
|
|
values := []string{}
|
|
directive := cobra.ShellCompDirectiveError
|
|
|
|
if cc.HasAvailableSubCommands() && len(provided) < 1 {
|
|
// if this is an "index" command and user is providing first argument
|
|
// allow completions instead of erroring by default
|
|
directive = cobra.ShellCompDirectiveDefault
|
|
}
|
|
|
|
if expectedArgLen > 0 {
|
|
argsCompleted := len(provided)
|
|
lastArg := (*args)[len(*args)-1]
|
|
hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic
|
|
lastArg.Command.Options.Parse(cc.Flags())
|
|
if err := args.Parse(provided); err != nil {
|
|
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
|
|
}
|
|
|
|
directive = cobra.ShellCompDirectiveDefault
|
|
if argsCompleted < expectedArgLen || hasVariadicArg {
|
|
var arg *Argument
|
|
if hasVariadicArg && argsCompleted >= expectedArgLen {
|
|
// completing a variadic argument
|
|
arg = lastArg
|
|
} else {
|
|
// completing regular argument (maybe variadic!)
|
|
arg = (*args)[argsCompleted]
|
|
}
|
|
|
|
if arg.Values != nil {
|
|
var err error
|
|
arg.Values.command = lastArg.Command
|
|
arg.Command = lastArg.Command
|
|
values, directive, err = arg.Resolve(toComplete)
|
|
if err != nil {
|
|
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
|
|
}
|
|
} else {
|
|
directive = cobra.ShellCompDirectiveError
|
|
}
|
|
values = cobra.AppendActiveHelp(values, arg.Description)
|
|
}
|
|
|
|
if toComplete != "" && directive != cobra.ShellCompDirectiveFilterFileExt && directive != cobra.ShellCompDirectiveFilterDirs {
|
|
filtered := []string{}
|
|
for _, value := range values {
|
|
if strings.HasPrefix(value, toComplete) {
|
|
filtered = append(filtered, value)
|
|
}
|
|
}
|
|
values = filtered
|
|
}
|
|
}
|
|
|
|
return values, directive
|
|
}
|
|
|
|
// Argument represents a single command-line argument.
|
|
type Argument struct {
|
|
// Name is how this variable will be exposed to the underlying command.
|
|
Name string `json:"name" yaml:"name" validate:"required,excludesall=!$\\/%^@#?:'\""`
|
|
// Description is what this argument is for.
|
|
Description string `json:"description" yaml:"description" validate:"required"`
|
|
// Default is the default value for this argument if none is provided.
|
|
Default any `json:"default,omitempty" yaml:"default,omitempty" validate:"excluded_with=Required"`
|
|
// Variadic makes an argument a list of all values from this one on.
|
|
Variadic bool `json:"variadic" yaml:"variadic"`
|
|
// Required raises an error if an argument is not provided.
|
|
Required bool `json:"required" yaml:"required" validate:"excluded_with=Default"`
|
|
// Values describes autocompletion and validation for an argument
|
|
Values *ValueSource `json:"values,omitempty" yaml:"values" validate:"omitempty"`
|
|
Command *Command `json:"-" yaml:"-" validate:"-"`
|
|
provided *[]string
|
|
}
|
|
|
|
func (arg *Argument) EnvName() string {
|
|
return strings.ToUpper(strings.ReplaceAll(arg.Name, "-", "_"))
|
|
}
|
|
|
|
func (arg *Argument) SetValue(value []string) {
|
|
arg.provided = &value
|
|
}
|
|
|
|
// Repeats tells if an argument should be presented as a list.
|
|
func (arg *Argument) Repeats() bool {
|
|
return arg.Variadic
|
|
}
|
|
|
|
func (arg *Argument) IsKnown() bool {
|
|
return arg.provided != nil && len(*arg.provided) > 0
|
|
}
|
|
|
|
func (arg *Argument) ToString() string {
|
|
val := arg.ToValue()
|
|
|
|
if arg.Variadic {
|
|
val := val.([]string)
|
|
return strings.Join(val, " ")
|
|
}
|
|
|
|
return val.(string)
|
|
}
|
|
|
|
func (arg *Argument) ToValue() any {
|
|
var value any
|
|
if arg.IsKnown() {
|
|
if arg.Variadic {
|
|
value = *arg.provided
|
|
} else {
|
|
vals := *arg.provided
|
|
value = vals[0]
|
|
}
|
|
} else {
|
|
if arg.Default != nil {
|
|
if arg.Variadic {
|
|
defaultSlice := anySliceToStringSlice(arg.Default)
|
|
// for _, valI := range arg.Default.([]any) {
|
|
// valStr := valI.(string)
|
|
// defaultSlice = append(defaultSlice, valStr)
|
|
// }
|
|
|
|
value = defaultSlice
|
|
} else {
|
|
value = arg.Default.(string)
|
|
}
|
|
} else {
|
|
if arg.Variadic {
|
|
value = []string{}
|
|
} else {
|
|
value = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func (arg *Argument) Validate() error {
|
|
if !arg.IsKnown() {
|
|
if arg.Required {
|
|
return errors.BadArguments{Msg: fmt.Sprintf("Missing argument for %s", strings.ToUpper(arg.Name))}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if !arg.Validates() {
|
|
return nil
|
|
}
|
|
|
|
validValues, _, err := arg.Resolve(strings.Join(*arg.provided, " "))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if arg.Variadic {
|
|
for _, current := range *arg.provided {
|
|
if !contains(validValues, current) {
|
|
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
|
|
}
|
|
}
|
|
} else {
|
|
current := arg.ToValue().(string)
|
|
if !contains(validValues, current) {
|
|
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for argument <%s>. Valid options are: %s", current, arg.Name, strings.Join(validValues, ", "))}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validates tells if the user-supplied value needs validation.
|
|
func (arg *Argument) Validates() bool {
|
|
return arg.Values != nil && arg.Values.Validates()
|
|
}
|
|
|
|
// ToDesc prints out the description of an argument for help and docs.
|
|
func (arg *Argument) ToDesc() string {
|
|
spec := arg.EnvName()
|
|
if arg.Variadic {
|
|
spec = fmt.Sprintf("%s...", spec)
|
|
}
|
|
|
|
if !arg.Required {
|
|
spec = fmt.Sprintf("[%s]", spec)
|
|
}
|
|
|
|
return spec
|
|
}
|
|
|
|
// Resolve returns autocomplete values for an argument.
|
|
func (arg *Argument) Resolve(current string) (values []string, flag cobra.ShellCompDirective, err error) {
|
|
if arg.Values != nil {
|
|
values, flag, err = arg.Values.Resolve(current)
|
|
if err != nil {
|
|
flag = cobra.ShellCompDirectiveError
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|