chinampa/pkg/command/options.go

235 lines
6.0 KiB
Go

// 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.
package command
import (
"fmt"
"strconv"
"strings"
"git.rob.mx/nidito/chinampa/internal/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Options is a map of name to Option.
type Options map[string]*Option
func (opts *Options) AllKnown() map[string]any {
col := map[string]any{}
for name, opt := range *opts {
col[name] = opt.ToValue()
}
return col
}
func (opts *Options) AllKnownStr() map[string]string {
col := map[string]string{}
for name, opt := range *opts {
col[name] = opt.ToString()
}
return col
}
// func envValue(opts Options, f *pflag.Flag) (*string, *string) {
// name := f.Name
// if name == _c.HelpCommandName {
// return nil, nil
// }
// envName := ""
// value := f.Value.String()
// if cname, ok := _c.EnvFlagNames[name]; ok {
// if value == "false" {
// return nil, nil
// }
// envName = cname
// } else {
// envName = fmt.Sprintf("%s%s", _c.OutputPrefixOpt, strings.ToUpper(strings.ReplaceAll(name, "-", "_")))
// opt := opts[name]
// if opt != nil {
// value = opt.ToString(true)
// }
// if value == "false" && opt.Type == ValueTypeBoolean {
// // makes dealing with false flags in shell easier
// value = ""
// }
// }
// return &envName, &value
// }
// // ToEnv writes shell variables to dst.
// func (opts *Options) ToEnv(command *Command, dst *[]string, prefix string) {
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
// envName, value := envValue(*opts, f)
// if envName != nil && value != nil {
// *dst = append(*dst, fmt.Sprintf("%s%s=%s", prefix, *envName, *value))
// }
// })
// }
// func (opts *Options) EnvMap(command *Command, dst *map[string]string) {
// command.cc.Flags().VisitAll(func(f *pflag.Flag) {
// envName, value := envValue(*opts, f)
// if envName != nil && value != nil {
// (*dst)[*envName] = *value
// }
// })
// }
func (opts *Options) Parse(supplied *pflag.FlagSet) {
// logrus.Debugf("Parsing supplied flags, %v", supplied)
for name, opt := range *opts {
switch opt.Type {
case ValueTypeBoolean:
if val, err := supplied.GetBool(name); err == nil {
opt.provided = val
continue
}
default:
opt.Type = ValueTypeString
if val, err := supplied.GetString(name); err == nil {
opt.provided = val
continue
}
}
}
}
func (opts *Options) AreValid() error {
for name, opt := range *opts {
if err := opt.Validate(name); err != nil {
return err
}
}
return nil
}
// Option represents a command line flag.
type Option struct {
ShortName string `json:"short-name,omitempty" yaml:"short-name,omitempty"` // nolint:tagliatelle
Type ValueType `json:"type" yaml:"type" validate:"omitempty,oneof=string bool"`
Description string `json:"description" yaml:"description" validate:"required"`
Default any `json:"default,omitempty" yaml:"default,omitempty"`
Values *ValueSource `json:"values,omitempty" yaml:"values,omitempty" validate:"omitempty"`
Repeated bool `json:"repeated" yaml:"repeated" validate:"omitempty"`
Command *Command `json:"-" yaml:"-" validate:"-"`
provided any
}
func (opt *Option) IsKnown() bool {
return opt.provided != nil
}
func (opt *Option) ToValue() any {
if opt.IsKnown() {
return opt.provided
}
return opt.Default
}
func (opt *Option) ToString() string {
value := opt.ToValue()
stringValue := ""
if opt.Type == "bool" {
if value == nil {
stringValue = ""
} else {
stringValue = strconv.FormatBool(value.(bool))
}
} else {
if value != nil {
stringValue = value.(string)
}
}
return stringValue
}
func (opt *Option) Validate(name string) error {
if !opt.Validates() {
return nil
}
current := opt.ToString() // nolint:ifshort
if current == "" {
return nil
}
validValues, _, err := opt.Resolve(current)
if err != nil {
return err
}
if !contains(validValues, current) {
return errors.BadArguments{Msg: fmt.Sprintf("%s is not a valid value for option <%s>. Valid options are: %s", current, name, strings.Join(validValues, ", "))}
}
return nil
}
// Validates tells if the user-supplied value needs validation.
func (opt *Option) Validates() bool {
return opt.Values != nil && opt.Values.Validates()
}
// providesAutocomplete tells if this option provides autocomplete values.
func (opt *Option) providesAutocomplete() bool {
return opt.Values != nil
}
// Resolve returns autocomplete values for an option.
func (opt *Option) Resolve(currentValue string) (values []string, flag cobra.ShellCompDirective, err error) {
if opt.Values != nil {
if opt.Values.command == nil {
opt.Values.command = opt.Command
}
return opt.Values.Resolve(currentValue)
}
return
}
// CompletionFunction is called by cobra when asked to complete an option.
func (opt *Option) CompletionFunction(cmd *cobra.Command, args []string, toComplete string) (values []string, flag cobra.ShellCompDirective) {
if !opt.providesAutocomplete() {
flag = cobra.ShellCompDirectiveNoFileComp
return
}
opt.Command.Arguments.Parse(args)
opt.Command.Options.Parse(cmd.Flags())
var err error
values, flag, err = opt.Resolve(toComplete)
if err != nil {
return values, cobra.ShellCompDirectiveError
}
if toComplete != "" {
filtered := []string{}
for _, value := range values {
if strings.HasPrefix(value, toComplete) {
filtered = append(filtered, value)
}
}
values = filtered
}
return cobra.AppendActiveHelp(values, opt.Description), flag
}