package envconf import ( "errors" "fmt" "os" "strings" "time" "unicode" ) type cEntry struct { value string parsed cValue dtype DataType unset bool empty bool defval string // The default value hasdef bool // Default value is defined } type Config struct { parsed bool env map[string]cEntry mapEnv map[string]map[string]cEntry } // NewConfig returns an envconf.Config that is used to read configuration from environment variables. // The environment variables are stored in envconf.Config, so changes to the environment after NewConfig has been called // will not be taken into account. func NewConfig() *Config { config := new(Config) config.parsed = false config.env = make(map[string]cEntry) config.mapEnv = make(map[string]map[string]cEntry) for _, v := range os.Environ() { splitted := strings.SplitN(v, "=", 2) if len(splitted) == 2 { key := cleanKey(splitted[0]) keysplit := strings.Split(key, "_") left := "" right := "" if len(keysplit) > 1 { left = strings.Join(keysplit[:len(keysplit)-1], "_") right = keysplit[len(keysplit)-1] } if unicode.IsLetter(getFirstRune(key)) { if len(config.mapEnv[left]) == 0 { config.mapEnv[left] = make(map[string]cEntry) } var entry cEntry entry.value = splitted[1] entry.dtype = TypeNone entry.unset = false entry.empty = false config.env[key] = entry config.mapEnv[left][right] = entry } } } return config } // Define the type of an environment variable. // Variables without a defined type will be ignored by Parse. func (c *Config) Define(key string, dtype DataType) { upper := strings.ToUpper(key) entry, ok := c.env[upper] if ok { entry.dtype = dtype c.env[upper] = entry } else { var entry cEntry entry.dtype = dtype entry.unset = true entry.empty = true c.env[upper] = entry } } // Define the type of an environment variable. // Variables without a defined type will be ignored by Parse. func (c *Config) DefineMap(key string, dtype DataType) { upper := strings.ToUpper(key) entries, ok := c.mapEnv[upper] if ok { for mapKey, entry := range entries { entry.dtype = dtype c.mapEnv[key][mapKey] = entry } } } // Define the type and default value of an environment variable. // Variables without a defined type will be ignored by Parse. func (c *Config) DefineDefault(key string, val string, dtype DataType) { upper := strings.ToUpper(key) entry, ok := c.env[upper] if ok { if entry.unset { entry.value = val } entry.dtype = dtype entry.empty = false entry.defval = val entry.hasdef = true c.env[upper] = entry } else { var entry cEntry entry.dtype = dtype entry.unset = true entry.empty = false entry.value = val entry.defval = val entry.hasdef = true c.env[upper] = entry } } // Parse parses the environment variables previously defined by Define and DefineDefault. // Parse should only be called once for a given envconf.Config. func (c *Config) Parse() { if c.parsed { return } c.parsed = true failed := false for k, v := range c.env { if v.empty { if v.unset { failed = true } } else { v.parsed = v.dtype.parse(k, v.value) c.env[k] = v } } for k, v := range c.mapEnv { for mk, mv := range v { mv.parsed = mv.dtype.parse(k+"_"+mk, mv.value) c.mapEnv[k][mk] = mv } } if failed { for k, v := range c.env { if (v.parsed.err == nil) && v.unset { if v.empty { v.parsed.err = errors.New(fmt.Sprintf(`Environment variable "%s" not found. It should have been of type %s.`, k, v.dtype)) c.env[k] = v } else { v.parsed.err = errors.New(fmt.Sprintf(`Environment variable "%s" not found. Default value "%s" of type %s used.`, k, v.value, v.dtype)) c.env[k] = v } } } } } func (c *Config) help() { max := make([]int, 2, 2) preq := false pdef := false for k, v := range c.env { if v.dtype != TypeNone { if len(k) > max[0] { max[0] = len(k) } if len(v.dtype.String()) > max[1] { max[1] = len(v.dtype.String()) } if v.hasdef { pdef = true } else { preq = true } } } if pdef { fmt.Println() for k, v := range c.env { if v.dtype != TypeNone { if v.hasdef { format := fmt.Sprintf("Variable %%-%ds| Type %%-%ds| Default %%s\n", max[0]+5, max[1]+3) fmt.Printf(format, fmt.Sprintf(`"%s"`, k), v.dtype, fmt.Sprintf(`"%s"`, v.defval)) } } } } if preq { fmt.Println() for k, v := range c.env { if v.dtype != TypeNone { if !v.hasdef { format := fmt.Sprintf("Variable %%-%ds| Type %%-%ds| Required\n", max[0]+5, max[1]+3) fmt.Printf(format, fmt.Sprintf(`"%s"`, k), v.dtype) } } } } fmt.Println() } func (c *Config) StatusHelp() bool { flags := make(map[string]bool) flags["--help"] = true flags["-help"] = true flags["-h"] = true if len(os.Args) > 1 { key := strings.ToLower(strings.TrimSpace(os.Args[1])) _, ok := flags[key] if ok { c.help() return false } } return c.Status() } // Status prints out failures that occured while parsing the environment to os.Stderr. // Variables that have been defined without a default value and are // missing from the environment will be considered a failure. // If parsing of any of the variables has failed Status will return false. func (c *Config) Status() (ok bool) { ok = c.parsed if ok { for _, v := range c.env { err := v.parsed.err if err != nil { ok = false if !v.empty { fmt.Fprintln(os.Stderr, err) } } } for _, v := range c.mapEnv { for _, mv := range v { err := mv.parsed.err if err != nil { ok = false if !mv.empty { fmt.Fprintln(os.Stderr, err) } } } } if !ok { fmt.Fprintln(os.Stderr, "") for _, v := range c.env { err := v.parsed.err if (err != nil) && v.empty { fmt.Fprintln(os.Stderr, err) } } } } return } func cleanKey(str string) string { fn := func(r rune) rune { if (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || r == '_' { return r } return -1 } str = strings.Map(fn, strings.ToUpper(str)) oldlen := len(str) + 1 newlen := len(str) for newlen < oldlen { oldlen = newlen str = strings.ReplaceAll(str, "__", "_") newlen = len(str) } return str } func (c *Config) getRaw(key string, dtype DataType) (val cValue) { val.dtype = TypeNone if c.parsed { upper := strings.ToUpper(key) entry, ok := c.env[upper] if ok && (entry.dtype == dtype) { return entry.parsed } } return } func (c *Config) getRawMap(key string, dtype DataType) (empty map[string]cValue) { empty = make(map[string]cValue) retval := make(map[string]cValue) if c.parsed { key = cleanKey(key) entries, ok := c.mapEnv[key] if ok { for k, v := range entries { if v.dtype == dtype { retval[k] = v.parsed } else { return } } return retval } } return } // GetInt returns the value of an environment variable. // If the variable is not defined as envconf.TypeInt the function will return 0. func (c *Config) GetInt(key string) int64 { val := c.getRaw(key, TypeInt) return val.intval } // GetMetric returns the value of an environment variable. // If the variable is not defined as envconf.TypeMetric the function will return 0. func (c *Config) GetMetric(key string) int64 { val := c.getRaw(key, TypeMetric) return val.intval } // GetDirectory returns the value of an environment variable. // If the variable is not defined as envconf.TypeDirectory the // function will return the empty string. func (c *Config) GetDirectory(key string) string { val := c.getRaw(key, TypeDirectory) return val.strval } // GetString returns the value of an environment variable. // If the variable is not defined as envconf.TypeString the // function will return the empty string. func (c *Config) GetString(key string) string { val := c.getRaw(key, TypeString) return val.strval } // GetDuration returns the value of an environment variable. // If the variable is not defined as envconf.TypeDuration the // function will return time.Duration(0). func (c *Config) GetDuration(key string) time.Duration { val := c.getRaw(key, TypeDuration) return val.durval } // GetBool returns the value of an environment variable. // If the variable is not defined as envconf.TypeBool the // function will return false. func (c *Config) GetBool(key string) bool { val := c.getRaw(key, TypeBool) return val.boolval } func (c *Config) GetMapInt(key string) (retval map[string]int64) { retval = make(map[string]int64) for k, v := range c.getRawMap(key, TypeInt) { retval[k] = v.intval } return } func (c *Config) GetMapDuration(key string) (retval map[string]time.Duration) { retval = make(map[string]time.Duration) for k, v := range c.getRawMap(key, TypeDuration) { retval[k] = v.durval } return } func (c *Config) GetMapString(key string) (retval map[string]string) { retval = make(map[string]string) for k, v := range c.getRawMap(key, TypeString) { retval[k] = v.strval } return } func (c *Config) GetMapBool(key string) (retval map[string]bool) { retval = make(map[string]bool) for k, v := range c.getRawMap(key, TypeBool) { retval[k] = v.boolval } return } func getFirstRune(str string) rune { for _, v := range str { return v } return rune(0) }