Files
threshold/internal/cmd/base/flags.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

1038 lines
25 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package base
import (
"errors"
"flag"
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/kr/pretty"
"github.com/posener/complete"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// FlagExample is an interface which declares an example value.
type FlagExample interface {
Example() string
}
// FlagVisibility is an interface which declares whether a flag should be
// hidden from help and completions. This is usually used for deprecations
// on "internal-only" flags.
type FlagVisibility interface {
Hidden() bool
}
// FlagBool is an interface which boolean flags implement.
type FlagBool interface {
IsBoolFlag() bool
}
// -- BoolVar and boolValue
type BoolVar struct {
Name string
Aliases []string
Usage string
Default bool
Hidden bool
EnvVar string
Target *bool
Completion complete.Predictor
}
func (f *FlagSet) BoolVar(i *BoolVar) {
def := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if b, err := strconv.ParseBool(v); err == nil {
def = b
}
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: strconv.FormatBool(i.Default),
EnvVar: i.EnvVar,
Value: newBoolValue(def, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type boolValue struct {
hidden bool
target *bool
}
func newBoolValue(def bool, target *bool, hidden bool) *boolValue {
*target = def
return &boolValue{
hidden: hidden,
target: target,
}
}
func (b *boolValue) Set(s string) error {
v, err := strconv.ParseBool(s)
if err != nil {
return err
}
*b.target = v
return nil
}
func (b *boolValue) Get() any { return *b.target }
func (b *boolValue) String() string { return strconv.FormatBool(*b.target) }
func (b *boolValue) Example() string { return "" }
func (b *boolValue) Hidden() bool { return b.hidden }
func (b *boolValue) IsBoolFlag() bool { return true }
// -- IntVar and intValue
type IntVar struct {
Name string
Aliases []string
Usage string
Default int
Hidden bool
EnvVar string
Target *int
Completion complete.Predictor
}
func (f *FlagSet) IntVar(i *IntVar) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if i, err := strconv.ParseInt(v, 0, 64); err == nil {
initial = int(i)
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatInt(int64(i.Default), 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newIntValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type intValue struct {
hidden bool
target *int
}
func newIntValue(def int, target *int, hidden bool) *intValue {
*target = def
return &intValue{
hidden: hidden,
target: target,
}
}
func (i *intValue) Set(s string) error {
v, err := strconv.ParseInt(s, 0, 64)
if err != nil {
return err
}
*i.target = int(v)
return nil
}
func (i *intValue) Get() any { return int(*i.target) }
func (i *intValue) String() string { return strconv.Itoa(int(*i.target)) }
func (i *intValue) Example() string { return "int" }
func (i *intValue) Hidden() bool { return i.hidden }
// -- Int64Var and int64Value
type Int64Var struct {
Name string
Aliases []string
Usage string
Default int64
Hidden bool
EnvVar string
Target *int64
Completion complete.Predictor
}
func (f *FlagSet) Int64Var(i *Int64Var) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if i, err := strconv.ParseInt(v, 0, 64); err == nil {
initial = i
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatInt(int64(i.Default), 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newInt64Value(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type int64Value struct {
hidden bool
target *int64
}
func newInt64Value(def int64, target *int64, hidden bool) *int64Value {
*target = def
return &int64Value{
hidden: hidden,
target: target,
}
}
func (i *int64Value) Set(s string) error {
v, err := strconv.ParseInt(s, 0, 64)
if err != nil {
return err
}
*i.target = v
return nil
}
func (i *int64Value) Get() any { return int64(*i.target) }
func (i *int64Value) String() string { return strconv.FormatInt(int64(*i.target), 10) }
func (i *int64Value) Example() string { return "int" }
func (i *int64Value) Hidden() bool { return i.hidden }
// -- UintVar && uintValue
type UintVar struct {
Name string
Aliases []string
Usage string
Default uint
Hidden bool
EnvVar string
Target *uint
Completion complete.Predictor
}
func (f *FlagSet) UintVar(i *UintVar) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if i, err := strconv.ParseUint(v, 0, 64); err == nil {
initial = uint(i)
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatUint(uint64(i.Default), 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newUintValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type uintValue struct {
hidden bool
target *uint
}
func newUintValue(def uint, target *uint, hidden bool) *uintValue {
*target = def
return &uintValue{
hidden: hidden,
target: target,
}
}
func (i *uintValue) Set(s string) error {
v, err := strconv.ParseUint(s, 0, 64)
if err != nil {
return err
}
*i.target = uint(v)
return nil
}
func (i *uintValue) Get() any { return uint(*i.target) }
func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.target), 10) }
func (i *uintValue) Example() string { return "uint" }
func (i *uintValue) Hidden() bool { return i.hidden }
// -- Uint64Var and uint64Value
type Uint64Var struct {
Name string
Aliases []string
Usage string
Default uint64
Hidden bool
EnvVar string
Target *uint64
Completion complete.Predictor
}
func (f *FlagSet) Uint64Var(i *Uint64Var) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if i, err := strconv.ParseUint(v, 0, 64); err == nil {
initial = i
}
}
def := ""
if i.Default != 0 {
strconv.FormatUint(i.Default, 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newUint64Value(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type uint64Value struct {
hidden bool
target *uint64
}
func newUint64Value(def uint64, target *uint64, hidden bool) *uint64Value {
*target = def
return &uint64Value{
hidden: hidden,
target: target,
}
}
func (i *uint64Value) Set(s string) error {
v, err := strconv.ParseUint(s, 0, 64)
if err != nil {
return err
}
*i.target = v
return nil
}
func (i *uint64Value) Get() any { return uint64(*i.target) }
func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i.target), 10) }
func (i *uint64Value) Example() string { return "uint" }
func (i *uint64Value) Hidden() bool { return i.hidden }
// -- StringVar and stringValue
type StringVar struct {
Name string
Aliases []string
Usage string
Default string
Hidden bool
EnvVar string
Target *string
Completion complete.Predictor
}
func (f *FlagSet) StringVar(i *StringVar) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
initial = v
}
def := ""
if i.Default != "" {
def = i.Default
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newStringValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type stringValue struct {
hidden bool
target *string
}
func newStringValue(def string, target *string, hidden bool) *stringValue {
*target = def
return &stringValue{
hidden: hidden,
target: target,
}
}
func (s *stringValue) Set(val string) error {
*s.target = val
return nil
}
func (s *stringValue) Get() any { return *s.target }
func (s *stringValue) String() string { return *s.target }
func (s *stringValue) Example() string { return "string" }
func (s *stringValue) Hidden() bool { return s.hidden }
// -- Float64Var and float64Value
type Float64Var struct {
Name string
Aliases []string
Usage string
Default float64
Hidden bool
EnvVar string
Target *float64
Completion complete.Predictor
}
func (f *FlagSet) Float64Var(i *Float64Var) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if i, err := strconv.ParseFloat(v, 64); err == nil {
initial = i
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatFloat(i.Default, 'e', -1, 64)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newFloat64Value(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type float64Value struct {
hidden bool
target *float64
}
func newFloat64Value(def float64, target *float64, hidden bool) *float64Value {
*target = def
return &float64Value{
hidden: hidden,
target: target,
}
}
func (f *float64Value) Set(s string) error {
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
*f.target = v
return nil
}
func (f *float64Value) Get() any { return float64(*f.target) }
func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f.target), 'g', -1, 64) }
func (f *float64Value) Example() string { return "float" }
func (f *float64Value) Hidden() bool { return f.hidden }
// -- DurationVar and durationValue
type DurationVar struct {
Name string
Aliases []string
Usage string
Default time.Duration
Hidden bool
EnvVar string
Target *time.Duration
Completion complete.Predictor
}
func (f *FlagSet) DurationVar(i *DurationVar) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if d, err := time.ParseDuration(appendDurationSuffix(v)); err == nil {
initial = d
}
}
def := ""
if i.Default != 0 {
def = i.Default.String()
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newDurationValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type durationValue struct {
hidden bool
target *time.Duration
}
func newDurationValue(def time.Duration, target *time.Duration, hidden bool) *durationValue {
*target = def
return &durationValue{
hidden: hidden,
target: target,
}
}
func (d *durationValue) Set(s string) error {
// Maintain bc for people specifying "system" as the value.
if s == "system" {
s = "-1"
}
v, err := time.ParseDuration(appendDurationSuffix(s))
if err != nil {
return err
}
*d.target = v
return nil
}
func (d *durationValue) Get() any { return time.Duration(*d.target) }
func (d *durationValue) String() string { return (*d.target).String() }
func (d *durationValue) Example() string { return "duration" }
func (d *durationValue) Hidden() bool { return d.hidden }
// appendDurationSuffix is used as a backwards-compat tool for assuming users
// meant "seconds" when they do not provide a suffixed duration value.
func appendDurationSuffix(s string) string {
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "m") || strings.HasSuffix(s, "h") {
return s
}
return s + "s"
}
// StringSliceVar reads in parameters from the same flag into a string array.
// Setting NullCheck enables the following behavior: the function will be run
// whenever a "null" is seen in the input to determine whether or not it is
// allowed (and erroring if not); and even if allowed generally, the flag will
// ensure that "null" is the only value passed.
type StringSliceVar struct {
Name string
Aliases []string
Usage string
Default []string
Hidden bool
EnvVar string
Target *[]string
NullCheck func() bool
Completion complete.Predictor
}
func (f *FlagSet) StringSliceVar(i *StringSliceVar) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
parts := strings.Split(v, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
initial = parts
}
def := ""
if i.Default != nil {
def = strings.Join(i.Default, ",")
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newStringSliceValue(initial, i.Target, i.Hidden, i.NullCheck),
Completion: i.Completion,
})
}
type stringSliceValue struct {
hidden bool
nullCheck func() bool
target *[]string
}
func newStringSliceValue(def []string, target *[]string, hidden bool, nullCheck func() bool) *stringSliceValue {
*target = def
return &stringSliceValue{
hidden: hidden,
nullCheck: nullCheck,
target: target,
}
}
func (s *stringSliceValue) Set(val string) error {
trimmedVal := strings.TrimSpace(val)
// Don't enable this behavior if nullCheck is not defined
if s.nullCheck != nil {
// If we got null in, go through some checks
if trimmedVal == "null" {
// Ensure null is allowed here
if !s.nullCheck() {
return fmt.Errorf(`"null" is not an allowed value`)
}
// If we have at least one value already and it's not "null" then
// error; we don't check if _all_ are "null" since presumably this
// function prevents there being more values than just "null" if
// that's specified
if len(*s.target) > 0 && (*s.target)[0] != "null" {
return fmt.Errorf(`"null" cannot be combined with other values`)
}
// Set the target to only contain "null" and return
*s.target = []string{"null"}
return nil
} else if len(*s.target) == 1 && (*s.target)[0] == "null" {
// Something came in that isn't "null" but we already have "null"
return fmt.Errorf(`"null" cannot be combined with other values`)
}
}
// Append in all other cases, or if we didn't error above or return just
// "null"
*s.target = append(*s.target, strings.TrimSpace(val))
return nil
}
func (s *stringSliceValue) Get() any { return *s.target }
func (s *stringSliceValue) String() string { return strings.Join(*s.target, ",") }
func (s *stringSliceValue) Example() string { return "string" }
func (s *stringSliceValue) Hidden() bool { return s.hidden }
// -- StringMapVar and stringMapValue
type StringMapVar struct {
Name string
Aliases []string
Usage string
Default map[string]string
Hidden bool
Target *map[string]string
Completion complete.Predictor
}
func (f *FlagSet) StringMapVar(i *StringMapVar) {
def := ""
if i.Default != nil {
def = mapToKV(i.Default)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
Value: newStringMapValue(i.Default, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type stringMapValue struct {
hidden bool
target *map[string]string
}
func newStringMapValue(def map[string]string, target *map[string]string, hidden bool) *stringMapValue {
*target = def
return &stringMapValue{
hidden: hidden,
target: target,
}
}
func (s *stringMapValue) Set(val string) error {
idx := strings.Index(val, "=")
if idx == -1 {
return fmt.Errorf("missing = in KV pair: %q", val)
}
if *s.target == nil {
*s.target = make(map[string]string)
}
k, v := val[0:idx], val[idx+1:]
(*s.target)[k] = v
return nil
}
func (s *stringMapValue) Get() any { return *s.target }
func (s *stringMapValue) String() string { return mapToKV(*s.target) }
func (s *stringMapValue) Example() string { return "key=value" }
func (s *stringMapValue) Hidden() bool { return s.hidden }
func mapToKV(m map[string]string) string {
list := make([]string, 0, len(m))
for k := range m {
list = append(list, k)
}
sort.Strings(list)
for i, k := range list {
list[i] = k + "=" + m[k]
}
return strings.Join(list, ",")
}
// StringSliceMapVar maps a key string to a slice of string values.
// This is useful for cases such as modifying Worker tags, which can have
// multiple values associated per key.
// Setting NullCheck to return true enables the string input value "null"
// to be accepted without throwing an error. This is useful for add/set/remove
// patterns where we want to enable "set null".
type StringSliceMapVar struct {
Name string
Aliases []string
Usage string
Default map[string][]string
Hidden bool
Target *map[string][]string
NullCheck func() bool
Completion complete.Predictor
}
func (f *FlagSet) StringSliceMapVar(i *StringSliceMapVar) {
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Value: newStringSliceMapValue(i.Default, i.Target, i.NullCheck, i.Hidden),
Completion: i.Completion,
})
}
type stringSliceMapValue struct {
hidden bool
target *map[string][]string
nullCheck func() bool
isNull bool
}
func newStringSliceMapValue(def map[string][]string, target *map[string][]string, nullCheck func() bool, hidden bool) *stringSliceMapValue {
*target = def
return &stringSliceMapValue{
hidden: hidden,
target: target,
nullCheck: nullCheck,
isNull: false,
}
}
func (s *stringSliceMapValue) Set(val string) error {
kv := strings.TrimSpace(val)
// Don't enable this behavior if nullCheck is not defined.
if s.nullCheck != nil {
if kv == "null" {
if !s.nullCheck() {
return fmt.Errorf(`"null" is not an allowed value`)
}
// Ensure "null" cannot be entered as a consecutive entry in a chain of valid entries
if s.isNull == true || len(*s.target) > 0 {
return fmt.Errorf(`"null" cannot be combined with other values`)
}
// Set target to nil and return
if kv == "null" {
*s.target = map[string][]string{"null": nil}
s.isNull = true
return nil
}
} else if s.isNull == true {
// Ensure consecutive "non-null" entries cannot be entered after a "null"
return fmt.Errorf(`"null" cannot be combined with other values`)
}
}
// Return a better error message if "null" is not enabled
if kv == "null" {
return fmt.Errorf(`"null" is not an allowed value`)
}
if *s.target == nil {
*s.target = make(map[string][]string)
}
split := strings.SplitN(kv, "=", 2)
if len(split) != 2 {
return fmt.Errorf("missing = in KV pair: %q", val)
}
// trim space, check proto, and assign to map
key := strings.TrimSpace(split[0])
if !protoIdentifierRegex.Match([]byte(key)) {
return fmt.Errorf("key %q is invalid", key)
}
vals := strings.Split(split[1], ",")
for i, v := range vals {
vals[i] = strings.TrimSpace(v)
if !protoIdentifierRegex.Match([]byte(vals[i])) {
return fmt.Errorf("value %q is invalid", vals[i])
}
}
for _, v := range vals {
(*s.target)[key] = append((*s.target)[key], v)
}
return nil
}
func sliceMapToKV(m map[string][]string) string {
list := make([]string, 0, len(m))
for k, vals := range m {
vv := strings.Join(vals, ",")
list = append(list, k+"="+vv)
}
return strings.Join(list, ", ")
}
func (c *stringSliceMapValue) Get() any { return *c.target }
func (c *stringSliceMapValue) String() string { return sliceMapToKV(*c.target) }
func (c *stringSliceMapValue) Example() string { return "key1=val-a, key2=val-b,val-c" }
func (c *stringSliceMapValue) Hidden() bool { return c.hidden }
// -- VarFlag
type VarFlag struct {
Name string
Aliases []string
Usage string
Default string
EnvVar string
Value flag.Value
Completion complete.Predictor
}
func (f *FlagSet) VarFlag(i *VarFlag) {
// If the flag is marked as hidden, just add it to the set and return to
// avoid unnecessary computations here. We do not want to add completions or
// generate help output for hidden flags.
if v, ok := i.Value.(FlagVisibility); ok && v.Hidden() {
f.Var(i.Value, i.Name, "")
return
}
// Calculate the full usage
usage := i.Usage
if len(i.Aliases) > 0 {
sentence := make([]string, len(i.Aliases))
for i, a := range i.Aliases {
sentence[i] = fmt.Sprintf(`"-%s"`, a)
}
aliases := ""
switch len(sentence) {
case 0:
// impossible...
case 1:
aliases = sentence[0]
case 2:
aliases = sentence[0] + " and " + sentence[1]
default:
sentence[len(sentence)-1] = "and " + sentence[len(sentence)-1]
aliases = strings.Join(sentence, ", ")
}
usage += fmt.Sprintf(" This is aliased as %s.", aliases)
}
if i.Default != "" {
usage += fmt.Sprintf(" The default is %s.", i.Default)
}
if i.EnvVar != "" {
usage += fmt.Sprintf(" This can also be specified via the %s "+
"environment variable.", i.EnvVar)
}
// Add aliases to the main set
for _, a := range i.Aliases {
f.mainSet.Var(i.Value, a, "")
}
f.Var(i.Value, i.Name, usage)
f.completions["-"+i.Name] = i.Completion
}
// Var is a lower-level API for adding something to the flags. It should be used
// with caution, since it bypasses all validation. Consider VarFlag instead.
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 KeyOnlyAllowed is true then it is valid to parse an input with only a key
// segment and no value.
//
// 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
KeyOnlyAllowed 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.KeyOnlyAllowed, 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 *wrapperspb.StringValue
}
type combinedSliceValue struct {
name string
hidden bool
kvSplit bool
keyOnlyAllowed bool
keyDelimiter *string
protoCompatKey bool
target *[]CombinedSliceFlagValue
}
func newCombinedSliceValue(name string, target *[]CombinedSliceFlagValue, hidden, kvSplit, keyOnlyAllowed bool, keyDelimiter *string, protoCompatKey bool) *combinedSliceValue {
return &combinedSliceValue{
name: name,
hidden: hidden,
kvSplit: kvSplit,
keyDelimiter: keyDelimiter,
keyOnlyAllowed: keyOnlyAllowed,
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: wrapperspb.String(strings.TrimSpace(val)),
}
if c.kvSplit {
kv := strings.SplitN(ret.Value.GetValue(), "=", 2)
switch len(kv) {
case 0:
// This shouldn't happen
return fmt.Errorf("unexpected length of string after splitting")
case 1:
if !c.keyOnlyAllowed {
return fmt.Errorf("key-only value provided but not supported for this flag")
}
ret.Keys = []string{kv[0]}
ret.Value = nil
default:
ret.Keys = []string{kv[0]}
if c.keyDelimiter != nil {
ret.Keys = strings.Split(kv[0], *c.keyDelimiter)
}
ret.Value = wrapperspb.String(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)
}
}
}
if ret.Value != nil {
pathParsedValue, err := parseutil.ParsePath(ret.Value.GetValue())
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return fmt.Errorf("error checking if value is a path: %w", err)
}
// This will either be the round-tripped value or the substituted value
ret.Value = wrapperspb.String(pathParsedValue)
}
*c.target = append(*c.target, ret)
return nil
}
func (c *combinedSliceValue) Get() any { 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 }