Add attribute flag building and parsing (#1562)

This adds a few command sketches but that's not the main point of the PR
and filling those out will come in a different PR. This adds a new flag
type that can combine inputs to a single target slice, retaining the
ability to distinguish where it came from but also keep ordering across
different flag names. It also implements a function that does the
heuristic parsing with fallback.
This commit is contained in:
Jeff Mitchell authored and GitHub committed 2021-09-26 11:48:43 -04:00
1 parent 14cc97b645
commit a834a14785
16 files changed
+1166 -36

No files matched your search

+4
View File
@@ -83,6 +83,10 @@ type Command struct {
FlagRecursive bool
FlagFilter string
// Attribute values
FlagAttributes string
FlagAttrs []CombinedSliceFlagValue
client *api.Client
}
+118
View File
@@ -4,11 +4,14 @@ import (
"flag"
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/kr/pretty"
"github.com/posener/complete"
)
@@ -748,3 +751,118 @@ func (f *FlagSet) Var(value flag.Value, name, usage string) {
f.mainSet.Var(value, name, usage)
f.flagSet.Var(value, name, usage)
}
// CombinationSliceVar uses a wrapped value to allow storing values from
// different flags in one slice. This is useful if you need ordering to be
// maintained across flags. It does not currently support env vars.
//
// If KvSplit is set true, each value will be split on the first = into Key and
// Value parts so that validation can happen at parsing time. If you don't want
// this kind of behavior, simply combine them, or set KvSplit to false.
//
// If KeyDelimiter is non-nil (along with KvSplit being true), the string will
// be used to split the key. Otherwise, the Keys will be a single-element slice
// containing the full value.
//
// If ProtoCompat is true, the key will be validated against proto3 syntax
// requirements for identifiers. If the string is split via KeyDelimiter, each
// segment will be evaluated independently.
type CombinationSliceVar struct {
Name string
Aliases []string
Usage string
Hidden bool
Target *[]CombinedSliceFlagValue
Completion complete.Predictor
KvSplit bool
KeyDelimiter *string
ProtoCompatKey bool
}
func (f *FlagSet) CombinationSliceVar(i *CombinationSliceVar) {
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Value: newCombinedSliceValue(i.Name, i.Target, i.Hidden, i.KvSplit, i.KeyDelimiter, i.ProtoCompatKey),
Completion: i.Completion,
})
}
// CombinedSliceValue holds the raw value (as a string) and the name of the flag
// that added it.
type CombinedSliceFlagValue struct {
Name string
Keys []string
Value string
}
type combinedSliceValue struct {
name string
hidden bool
kvSplit bool
keyDelimiter *string
protoCompatKey bool
target *[]CombinedSliceFlagValue
}
func newCombinedSliceValue(name string, target *[]CombinedSliceFlagValue, hidden, kvSplit bool, keyDelimiter *string, protoCompatKey bool) *combinedSliceValue {
return &combinedSliceValue{
name: name,
hidden: hidden,
kvSplit: kvSplit,
keyDelimiter: keyDelimiter,
protoCompatKey: protoCompatKey,
target: target,
}
}
var protoIdentifierRegex = regexp.MustCompile("^[a-zA-Z][A-Za-z0-9_]*$")
func (c *combinedSliceValue) Set(val string) error {
ret := CombinedSliceFlagValue{
Name: c.name,
Value: strings.TrimSpace(val),
}
if c.kvSplit {
kv := strings.SplitN(ret.Value, "=", 2)
switch len(kv) {
case 0:
case 1:
ret.Value = strings.TrimSpace(kv[0])
default:
ret.Keys = []string{kv[0]}
if c.keyDelimiter != nil {
ret.Keys = strings.Split(kv[0], *c.keyDelimiter)
}
ret.Value = strings.TrimSpace(kv[1])
}
}
// Trim keys
for i, key := range ret.Keys {
ret.Keys[i] = strings.TrimSpace(key)
}
if c.protoCompatKey {
for _, key := range ret.Keys {
if !protoIdentifierRegex.Match([]byte(key)) {
return fmt.Errorf("key segment %q is invalid", key)
}
}
}
var err error
if ret.Value, err = parseutil.ParsePath(ret.Value); err != nil && err != parseutil.ErrNotAUrl {
return fmt.Errorf("error checking if value is a path: %w", err)
}
*c.target = append(*c.target, ret)
return nil
}
func (c *combinedSliceValue) Get() interface{} { return *c.target }
func (c *combinedSliceValue) String() string { return pretty.Sprint(*c.target) }
func (c *combinedSliceValue) Example() string { return "" }
func (c *combinedSliceValue) Hidden() bool { return c.hidden }