cuelang.org/go@v0.13.0/internal/envflag/flag.go (about)

     1  package envflag
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"reflect"
     8  	"strconv"
     9  	"strings"
    10  )
    11  
    12  // Init uses Parse with the contents of the given environment variable as input.
    13  func Init[T any](flags *T, envVar string) error {
    14  	err := Parse(flags, os.Getenv(envVar))
    15  	if err != nil {
    16  		return fmt.Errorf("cannot parse %s: %w", envVar, err)
    17  	}
    18  	return nil
    19  }
    20  
    21  // Parse initializes the fields in flags from the attached struct field tags as
    22  // well as the contents of the given string.
    23  //
    24  // The struct field tag may contain a default value other than the zero value,
    25  // such as `envflag:"default:true"` to set a boolean field to true by default.
    26  //
    27  // The tag may be marked as deprecated with `envflag:"deprecated"`
    28  // which will cause Parse to return an error if the user attempts to set
    29  // its value to anything but the default value.
    30  //
    31  // The string may contain a comma-separated list of name=value pairs values
    32  // representing the boolean fields in the struct type T. If the value is omitted
    33  // entirely, the value is assumed to be name=true.
    34  //
    35  // Names are treated case insensitively. Boolean values are parsed via [strconv.ParseBool],
    36  // integers via [strconv.Atoi], and strings are accepted as-is.
    37  func Parse[T any](flags *T, env string) error {
    38  	// Collect the field indices and set the default values.
    39  	indexByName := make(map[string]int)
    40  	deprecated := make(map[string]bool)
    41  	fv := reflect.ValueOf(flags).Elem()
    42  	ft := fv.Type()
    43  	for i := 0; i < ft.NumField(); i++ {
    44  		field := ft.Field(i)
    45  		name := strings.ToLower(field.Name)
    46  		if tagStr, ok := field.Tag.Lookup("envflag"); ok {
    47  			for _, f := range strings.Split(tagStr, ",") {
    48  				key, rest, hasRest := strings.Cut(f, ":")
    49  				switch key {
    50  				case "default":
    51  					val, err := parseValue(name, field.Type.Kind(), rest)
    52  					if err != nil {
    53  						return err
    54  					}
    55  					fv.Field(i).Set(reflect.ValueOf(val))
    56  				case "deprecated":
    57  					if hasRest {
    58  						return fmt.Errorf("cannot have a value for deprecated tag")
    59  					}
    60  					deprecated[name] = true
    61  				default:
    62  					return fmt.Errorf("unknown envflag tag %q", f)
    63  				}
    64  			}
    65  		}
    66  		indexByName[name] = i
    67  	}
    68  
    69  	var errs []error
    70  	for _, elem := range strings.Split(env, ",") {
    71  		if elem == "" {
    72  			// Allow empty elements such as `,somename=true` so that env vars
    73  			// can be joined together like
    74  			//
    75  			//     os.Setenv("CUE_EXPERIMENT", os.Getenv("CUE_EXPERIMENT")+",extra")
    76  			//
    77  			// even when the previous env var is empty.
    78  			continue
    79  		}
    80  		name, valueStr, hasValue := strings.Cut(elem, "=")
    81  
    82  		index, knownFlag := indexByName[name]
    83  		if !knownFlag {
    84  			errs = append(errs, fmt.Errorf("unknown flag %q", elem))
    85  			continue
    86  		}
    87  		field := fv.Field(index)
    88  		var val any
    89  		if hasValue {
    90  			var err error
    91  			val, err = parseValue(name, field.Kind(), valueStr)
    92  			if err != nil {
    93  				errs = append(errs, err)
    94  				continue
    95  			}
    96  		} else if field.Kind() == reflect.Bool {
    97  			// For bools, "somename" is short for "somename=true" or "somename=1".
    98  			// This mimicks how Go flags work, e.g. -knob is short for -knob=true.
    99  			val = true
   100  		} else {
   101  			// For any other type, a value must be specified.
   102  			// This mimicks how Go flags work, e.g. -output=path does not allow -output.
   103  			errs = append(errs, fmt.Errorf("value needed for %s flag %q", field.Kind(), name))
   104  			continue
   105  		}
   106  
   107  		if deprecated[name] {
   108  			// We allow setting deprecated flags to their default value so that
   109  			// bold explorers will not be penalised for their experimentation.
   110  			if field.Interface() != val {
   111  				errs = append(errs, fmt.Errorf("cannot change default value of deprecated flag %q", name))
   112  			}
   113  			continue
   114  		}
   115  
   116  		field.Set(reflect.ValueOf(val))
   117  	}
   118  	return errors.Join(errs...)
   119  }
   120  
   121  func parseValue(name string, kind reflect.Kind, str string) (val any, err error) {
   122  	switch kind {
   123  	case reflect.Bool:
   124  		val, err = strconv.ParseBool(str)
   125  	case reflect.Int:
   126  		val, err = strconv.Atoi(str)
   127  	case reflect.String:
   128  		val = str
   129  	default:
   130  		return nil, errInvalid{fmt.Errorf("unsupported kind %s", kind)}
   131  	}
   132  	if err != nil {
   133  		return nil, errInvalid{fmt.Errorf("invalid %s value for %s: %v", kind, name, err)}
   134  	}
   135  	return val, nil
   136  }
   137  
   138  // An ErrInvalid indicates a malformed input string.
   139  var ErrInvalid = errors.New("invalid value")
   140  
   141  type errInvalid struct{ error }
   142  
   143  func (errInvalid) Is(err error) bool {
   144  	return err == ErrInvalid
   145  }