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  }