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 }