usage.go raw

   1  package env
   2  
   3  import (
   4  	"fmt"
   5  	"io"
   6  	"reflect"
   7  	"text/tabwriter"
   8  )
   9  
  10  // cache maps a struct type to the [Var] slice parsed from it.
  11  // It is needed to fix the following bug:
  12  //
  13  //	var cfg struct {
  14  //		Port int `env:"PORT"`
  15  //	}
  16  //	env.Load(&cfg, nil)             // 1. sets cfg.Port to 8080
  17  //	env.Usage(&cfg, os.Stdout, nil) // 2. prints cfg.Port's default == 8080 (instead of 0)
  18  //
  19  // It also speeds up [Usage] a bit, since a struct is only parsed once.
  20  var cache = make(map[reflect.Type][]Var)
  21  
  22  // Var holds the information about the environment variable parsed from a struct field.
  23  type Var struct {
  24  	Name     string       // The name of the variable.
  25  	Type     reflect.Type // The type of the variable.
  26  	Usage    string       // The usage string parsed from the `usage` tag (if exists).
  27  	Default  string       // The default value of the variable. Empty, if the variable is required.
  28  	Required bool         // True, if the variable is marked as required.
  29  	Expand   bool         // True, if the variable is marked to be expanded with [os.Expand].
  30  
  31  	structField   reflect.Value
  32  	hasDefaultTag bool
  33  }
  34  
  35  // Usage writes a usage message documenting all defined environment variables to the given [io.Writer].
  36  // The caller must pass the same [Options] to both [Load] and [Usage], or nil.
  37  // An optional usage string can be added to environment variables with the `usage:"STRING"` struct tag.
  38  // The format of the message can be customized by implementing the Usage([]env.Var, io.Writer, *env.Options) method on the cfg's type.
  39  func Usage(cfg any, w io.Writer, opts *Options) {
  40  	pv := reflect.ValueOf(cfg)
  41  	if !structPtr(pv) {
  42  		panic("env: cfg must be a non-nil struct pointer")
  43  	}
  44  
  45  	opts = setDefaultOptions(opts)
  46  
  47  	v := pv.Elem()
  48  	vars, ok := cache[v.Type()]
  49  	if !ok {
  50  		vars = parseVars(v, opts)
  51  	}
  52  
  53  	if u, ok := cfg.(interface {
  54  		Usage([]Var, io.Writer, *Options)
  55  	}); ok {
  56  		u.Usage(vars, w, opts)
  57  	} else {
  58  		defaultUsage(vars, w, opts)
  59  	}
  60  }
  61  
  62  func defaultUsage(vars []Var, w io.Writer, _ *Options) {
  63  	// TODO: use opts.SliceSep to parse slice values.
  64  
  65  	tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
  66  	defer tw.Flush()
  67  
  68  	for _, v := range vars {
  69  		fmt.Fprintf(tw, "\t%s\t%s", v.Name, v.Type)
  70  		if v.Required {
  71  			fmt.Fprintf(tw, "\trequired")
  72  		} else {
  73  			if v.Type.Kind() == reflect.String && v.Default == "" {
  74  				v.Default = "<empty>"
  75  			}
  76  			fmt.Fprintf(tw, "\tdefault %s", v.Default)
  77  		}
  78  		if v.Usage != "" {
  79  			fmt.Fprintf(tw, "\t%s", v.Usage)
  80  		}
  81  		fmt.Fprintf(tw, "\n")
  82  	}
  83  }
  84