cuelang.org/go@v0.10.1/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 string may contain a comma-separated list of name=value pairs values 28 // representing the boolean fields in the struct type T. If the value is omitted 29 // entirely, the value is assumed to be name=true. 30 // 31 // Names are treated case insensitively. Value strings are parsed as Go booleans 32 // via [strconv.ParseBool], meaning that they accept "true" and "false" but also 33 // the shorter "1" and "0". 34 func Parse[T any](flags *T, env string) error { 35 // Collect the field indices and set the default values. 36 indexByName := make(map[string]int) 37 fv := reflect.ValueOf(flags).Elem() 38 ft := fv.Type() 39 for i := 0; i < ft.NumField(); i++ { 40 field := ft.Field(i) 41 defaultValue := false 42 if tagStr, ok := field.Tag.Lookup("envflag"); ok { 43 defaultStr, ok := strings.CutPrefix(tagStr, "default:") 44 // TODO: consider panicking for these error types. 45 if !ok { 46 return fmt.Errorf("expected tag like `envflag:\"default:true\"`: %s", tagStr) 47 } 48 v, err := strconv.ParseBool(defaultStr) 49 if err != nil { 50 return fmt.Errorf("invalid default bool value for %s: %v", field.Name, err) 51 } 52 defaultValue = v 53 fv.Field(i).SetBool(defaultValue) 54 } 55 indexByName[strings.ToLower(field.Name)] = i 56 } 57 58 if env == "" { 59 return nil 60 } 61 var errs []error 62 for _, elem := range strings.Split(env, ",") { 63 name, valueStr, ok := strings.Cut(elem, "=") 64 // "somename" is short for "somename=true" or "somename=1". 65 value := true 66 if ok { 67 v, err := strconv.ParseBool(valueStr) 68 if err != nil { 69 // Invalid format, return an error immediately. 70 return invalidError{ 71 fmt.Errorf("invalid bool value for %s: %v", name, err), 72 } 73 } 74 value = v 75 } 76 index, ok := indexByName[name] 77 if !ok { 78 // Unknown option, proceed processing options as long as the format 79 // is valid. 80 errs = append(errs, fmt.Errorf("unknown %s", elem)) 81 continue 82 } 83 fv.Field(index).SetBool(value) 84 } 85 return errors.Join(errs...) 86 } 87 88 // An InvalidError indicates a malformed input string. 89 var InvalidError = errors.New("invalid value") 90 91 type invalidError struct{ error } 92 93 func (invalidError) Is(err error) bool { 94 return err == InvalidError 95 }