1a93d5d9f3
ADDED: - env/DefEnv(), env/DefEnvBlank()
632 lines
14 KiB
Go
632 lines
14 KiB
Go
package envs
|
|
|
|
import (
|
|
`bytes`
|
|
`errors`
|
|
`fmt`
|
|
`io/ioutil`
|
|
`os`
|
|
`reflect`
|
|
`strings`
|
|
`sync`
|
|
|
|
`r00t2.io/goutils/multierr`
|
|
`r00t2.io/goutils/structutils`
|
|
`r00t2.io/sysutils/errs`
|
|
`r00t2.io/sysutils/internal`
|
|
`r00t2.io/sysutils/paths`
|
|
)
|
|
|
|
/*
|
|
DefEnv operates like Python's .get() method on dicts (maps);
|
|
if the environment variable specified by key does not exist/is not specified,
|
|
then the value specified by fallback will be returned instead
|
|
otherwise key's value is returned.
|
|
*/
|
|
func DefEnv(key, fallback string) (value string) {
|
|
|
|
var exists bool
|
|
|
|
if value, exists = os.LookupEnv(key); !exists {
|
|
value = fallback
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// DefEnvBlank is like DefEnv but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
|
|
func DefEnvBlank(key, fallback string) (value string) {
|
|
|
|
value = DefEnv(key, fallback)
|
|
if value == "" {
|
|
value = fallback
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetEnvMap returns a map of all environment variables. All values are strings.
|
|
func GetEnvMap() (envVars map[string]string) {
|
|
|
|
var envList []string = os.Environ()
|
|
|
|
envVars = envListToMap(envList)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
GetEnvMapNative returns a map of all environment variables, but attempts to "nativize" them.
|
|
All values are interfaces. It is up to the caller to typeswitch them to proper types.
|
|
|
|
Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
|
|
a []string (as per GetPathEnv). No other env vars, even if they contain os.PathListSeparator,
|
|
will be transformed to a slice or the like.
|
|
If an error occurs during parsing the path env var, it will be rendered as a string.
|
|
|
|
All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).
|
|
|
|
If a type cannot be determined for a value, its string form will be used
|
|
(as it would be found in GetEnvMap).
|
|
*/
|
|
func GetEnvMapNative() (envMap map[string]interface{}) {
|
|
|
|
var stringMap map[string]string = GetEnvMap()
|
|
|
|
envMap = nativizeEnvMap(stringMap)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
GetFirst gets the first instance if populated/set occurrence of varNames.
|
|
|
|
For example, if you have three potential env vars, FOO, FOOBAR, FOOBARBAZ,
|
|
and want to follow the logic flow of:
|
|
|
|
1.) Check if FOO is set. If not,
|
|
2.) Check if FOOBAR is set. If not,
|
|
3.) Check if FOOBARBAZ is set.
|
|
|
|
Then this would be specified as:
|
|
|
|
GetFirst([]string{"FOO", "FOOBAR", "FOOBARBAZ"})
|
|
|
|
If val is "" and ok is true, this means that one of the specified variable names IS
|
|
set but is set to an empty value. If ok is false, none of the specified variables
|
|
are set.
|
|
|
|
It is a thin wrapper around GetFirstWithRef.
|
|
*/
|
|
func GetFirst(varNames []string) (val string, ok bool) {
|
|
|
|
val, ok, _ = GetFirstWithRef(varNames)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
GetFirstWithRef behaves exactly like GetFirst, but with an additional returned value, idx,
|
|
which specifies the index in varNames in which a set variable was found. e.g. if:
|
|
|
|
GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})
|
|
|
|
is called and FOO is not set but FOOBAR is, idx will be 1.
|
|
|
|
If ok is false, idx will always be -1 and should be ignored.
|
|
*/
|
|
func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
|
|
|
|
idx = -1
|
|
|
|
for i, vn := range varNames {
|
|
if HasEnv(vn) {
|
|
ok = true
|
|
idx = i
|
|
val = os.Getenv(vn)
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetPathEnv returns a slice of the PATH variable's items.
|
|
func GetPathEnv() (pathList []string, err error) {
|
|
|
|
var pathVar string = internal.GetPathEnvName()
|
|
|
|
pathList = make([]string, 0)
|
|
|
|
for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
|
|
if err = paths.RealPath(&p); err != nil {
|
|
return
|
|
}
|
|
pathList = append(pathList, p)
|
|
}
|
|
return
|
|
}
|
|
|
|
/*
|
|
GetPidEnvMap will only work on *NIX-like systems with procfs.
|
|
It gets the environment variables of a given process' PID.
|
|
*/
|
|
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
|
|
|
|
var envBytes []byte
|
|
var envList []string
|
|
var envArr [][]byte
|
|
var procPath string
|
|
var exists bool
|
|
|
|
envMap = make(map[string]string, 0)
|
|
|
|
procPath = fmt.Sprintf("/proc/%v/environ", pid)
|
|
|
|
if exists, err = paths.RealPathExists(&procPath); err != nil {
|
|
return
|
|
}
|
|
if !exists {
|
|
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
|
|
return
|
|
}
|
|
|
|
if envBytes, err = ioutil.ReadFile(procPath); err != nil {
|
|
return
|
|
}
|
|
|
|
envArr = bytes.Split(envBytes, []byte{0x0})
|
|
envList = make([]string, len(envArr))
|
|
for idx, b := range envArr {
|
|
envList[idx] = string(b)
|
|
}
|
|
|
|
envMap = envListToMap(envList)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
GetPidEnvMapNative, like GetEnvMapNative, returns a map of all environment variables, but attempts to "nativize" them.
|
|
All values are interfaces. It is up to the caller to typeswitch them to proper types.
|
|
|
|
See the documentation for GetEnvMapNative for details.
|
|
*/
|
|
func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {
|
|
|
|
var stringMap map[string]string
|
|
|
|
if stringMap, err = GetPidEnvMap(pid); err != nil {
|
|
return
|
|
}
|
|
|
|
envMap = nativizeEnvMap(stringMap)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
HasEnv is much like os.LookupEnv, but only returns a boolean for
|
|
if the environment variable key exists or not.
|
|
|
|
This is useful anywhere you may need to set a boolean in a func call
|
|
depending on the *presence* of an env var or not.
|
|
*/
|
|
func HasEnv(key string) (envIsSet bool) {
|
|
|
|
_, envIsSet = os.LookupEnv(key)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
Interpolate takes one of:
|
|
|
|
- a string (pointer only)
|
|
- a struct (pointer only)
|
|
- a map (applied to both keys *and* values)
|
|
- a slice
|
|
|
|
and performs variable substitution on strings from environment variables.
|
|
|
|
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
|
|
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
|
|
|
|
For structs, the tag name used can be changed by setting the StructTagInterpolate
|
|
variable in this submodule; the default is `envsub`.
|
|
If the tag value is "-", the field will be skipped.
|
|
For map fields within structs etc., the default is to apply interpolation to both keys and values.
|
|
All other tag value(s) are ignored.
|
|
|
|
For maps and slices, Interpolate will recurse into values (e.g. [][]string will work as expected).
|
|
|
|
If s is nil, no interpolation will be performed. No error will be returned.
|
|
If s is not a valid/supported type, no interpolation will be performed. No error will be returned.
|
|
*/
|
|
func Interpolate[T any](s T) (err error) {
|
|
|
|
var ptrVal reflect.Value
|
|
var ptrType reflect.Type
|
|
var ptrKind reflect.Kind
|
|
var sVal reflect.Value = reflect.ValueOf(s)
|
|
var sType reflect.Type = sVal.Type()
|
|
var kind reflect.Kind = sType.Kind()
|
|
|
|
switch kind {
|
|
case reflect.Ptr:
|
|
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
|
|
return
|
|
}
|
|
ptrVal = sVal.Elem()
|
|
ptrType = ptrVal.Type()
|
|
ptrKind = ptrType.Kind()
|
|
if ptrKind == reflect.String {
|
|
err = interpolateStringReflect(ptrVal)
|
|
} else {
|
|
// Otherwise, it should be a struct ptr.
|
|
if ptrKind != reflect.Struct {
|
|
return
|
|
}
|
|
err = interpolateStruct(ptrVal)
|
|
}
|
|
case reflect.Map:
|
|
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
|
|
return
|
|
}
|
|
err = interpolateMap(sVal)
|
|
case reflect.Slice:
|
|
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
|
|
return
|
|
}
|
|
err = interpolateSlice(sVal)
|
|
/*
|
|
case reflect.Struct:
|
|
if sVal.IsZero() || !sVal.IsValid() {
|
|
return
|
|
}
|
|
err = interpolateStruct(sVal)
|
|
|
|
*/
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
InterpolateString takes (a pointer to) a struct or string and performs variable substitution on it
|
|
from environment variables.
|
|
|
|
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
|
|
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
|
|
|
|
If s is nil, nothing will be done and err will be ErrNilPtr.
|
|
|
|
This is a standalone function that is much more performant than Interpolate
|
|
at the cost of rigidity.
|
|
*/
|
|
func InterpolateString(s *string) (err error) {
|
|
|
|
var newStr string
|
|
|
|
if s == nil {
|
|
err = errs.ErrNilPtr
|
|
return
|
|
}
|
|
|
|
if newStr, err = interpolateString(*s); err != nil {
|
|
return
|
|
}
|
|
*s = newStr
|
|
|
|
return
|
|
}
|
|
|
|
// interpolateMap is used by Interpolate for maps. v should be a reflect.Value of a map.
|
|
func interpolateMap(v reflect.Value) (err error) {
|
|
|
|
var kVal reflect.Value
|
|
var vVal reflect.Value
|
|
var newMap reflect.Value
|
|
var wg sync.WaitGroup
|
|
var numJobs int
|
|
var errChan chan error
|
|
var doneChan chan bool = make(chan bool, 1)
|
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
|
var t reflect.Type = v.Type()
|
|
var kind reflect.Kind = t.Kind()
|
|
|
|
if kind != reflect.Map {
|
|
err = errs.ErrBadType
|
|
return
|
|
}
|
|
|
|
if v.IsNil() || v.IsZero() || !v.IsValid() {
|
|
return
|
|
}
|
|
|
|
numJobs = v.Len()
|
|
errChan = make(chan error, numJobs)
|
|
wg.Add(numJobs)
|
|
|
|
newMap = reflect.MakeMap(v.Type())
|
|
|
|
for _, kVal = range v.MapKeys() {
|
|
vVal = v.MapIndex(kVal)
|
|
go func(key, val reflect.Value) {
|
|
var mapErr error
|
|
var newKey reflect.Value
|
|
var newVal reflect.Value
|
|
|
|
newKey = reflect.New(key.Type()).Elem()
|
|
newVal = reflect.New(val.Type()).Elem()
|
|
|
|
newKey.Set(key.Convert(newKey.Type()))
|
|
newVal.Set(val.Convert(newVal.Type()))
|
|
|
|
defer wg.Done()
|
|
|
|
// key
|
|
if key.Kind() == reflect.String {
|
|
if mapErr = interpolateStringReflect(newKey); mapErr != nil {
|
|
errChan <- mapErr
|
|
return
|
|
}
|
|
} else {
|
|
if mapErr = interpolateValue(newKey); mapErr != nil {
|
|
errChan <- mapErr
|
|
return
|
|
}
|
|
}
|
|
// value
|
|
if val.Kind() == reflect.String {
|
|
if mapErr = interpolateStringReflect(newVal); mapErr != nil {
|
|
errChan <- mapErr
|
|
return
|
|
}
|
|
} else {
|
|
if mapErr = interpolateValue(newVal); mapErr != nil {
|
|
errChan <- mapErr
|
|
return
|
|
}
|
|
}
|
|
|
|
newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type()))
|
|
}(kVal, vVal)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(errChan)
|
|
doneChan <- true
|
|
}()
|
|
|
|
<-doneChan
|
|
|
|
for i := 0; i < numJobs; i++ {
|
|
if err = <-errChan; err != nil {
|
|
mErr.AddError(err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
if !mErr.IsEmpty() {
|
|
err = mErr
|
|
return
|
|
}
|
|
|
|
v.Set(newMap.Convert(v.Type()))
|
|
|
|
return
|
|
}
|
|
|
|
// interpolateSlice is used by Interpolate for slices and arrays. v should be a reflect.Value of a slice/array.
|
|
func interpolateSlice(v reflect.Value) (err error) {
|
|
|
|
var wg sync.WaitGroup
|
|
var errChan chan error
|
|
var numJobs int
|
|
var doneChan chan bool = make(chan bool, 1)
|
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
|
var t reflect.Type = v.Type()
|
|
var kind reflect.Kind = t.Kind()
|
|
|
|
switch kind {
|
|
case reflect.Slice:
|
|
if v.IsNil() || v.IsZero() || !v.IsValid() {
|
|
return
|
|
}
|
|
case reflect.Array:
|
|
if v.IsZero() || !v.IsValid() {
|
|
return
|
|
}
|
|
default:
|
|
err = errs.ErrBadType
|
|
return
|
|
}
|
|
|
|
numJobs = v.Len()
|
|
errChan = make(chan error, numJobs)
|
|
wg.Add(numJobs)
|
|
|
|
for i := 0; i < v.Len(); i++ {
|
|
go func(idx int) {
|
|
var sErr error
|
|
|
|
defer wg.Done()
|
|
|
|
if v.Index(idx).Kind() == reflect.String {
|
|
if sErr = interpolateStringReflect(v.Index(idx)); sErr != nil {
|
|
errChan <- sErr
|
|
return
|
|
}
|
|
} else {
|
|
if sErr = interpolateValue(v.Index(idx)); sErr != nil {
|
|
errChan <- sErr
|
|
return
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(errChan)
|
|
doneChan <- true
|
|
}()
|
|
|
|
<-doneChan
|
|
|
|
for i := 0; i < numJobs; i++ {
|
|
if err = <-errChan; err != nil {
|
|
mErr.AddError(err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
if !mErr.IsEmpty() {
|
|
err = mErr
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// interpolateStringReflect is used for structs/nested strings using reflection.
|
|
func interpolateStringReflect(v reflect.Value) (err error) {
|
|
|
|
var strVal string
|
|
|
|
if v.Kind() != reflect.String {
|
|
err = errs.ErrBadType
|
|
return
|
|
}
|
|
|
|
if strVal, err = interpolateString(v.String()); err != nil {
|
|
return
|
|
}
|
|
|
|
v.Set(reflect.ValueOf(strVal).Convert(v.Type()))
|
|
|
|
return
|
|
}
|
|
|
|
// interpolateStruct is used by Interpolate for structs. v should be a reflect.Value of a struct.
|
|
func interpolateStruct(v reflect.Value) (err error) {
|
|
|
|
var field reflect.StructField
|
|
var fieldVal reflect.Value
|
|
var wg sync.WaitGroup
|
|
var errChan chan error
|
|
var numJobs int
|
|
var doneChan chan bool = make(chan bool, 1)
|
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
|
var t reflect.Type = v.Type()
|
|
var kind reflect.Kind = t.Kind()
|
|
|
|
if kind != reflect.Struct {
|
|
err = errs.ErrBadType
|
|
return
|
|
}
|
|
|
|
numJobs = v.NumField()
|
|
wg.Add(numJobs)
|
|
errChan = make(chan error, numJobs)
|
|
|
|
for i := 0; i < v.NumField(); i++ {
|
|
field = t.Field(i)
|
|
fieldVal = v.Field(i)
|
|
|
|
go func(f reflect.StructField, fv reflect.Value) {
|
|
var fErr error
|
|
|
|
defer wg.Done()
|
|
|
|
if fErr = interpolateStructField(f, fv); fErr != nil {
|
|
errChan <- fErr
|
|
return
|
|
}
|
|
}(field, fieldVal)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(errChan)
|
|
doneChan <- true
|
|
}()
|
|
|
|
<-doneChan
|
|
|
|
for i := 0; i < numJobs; i++ {
|
|
if err = <-errChan; err != nil {
|
|
mErr.AddError(err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
if !mErr.IsEmpty() {
|
|
err = mErr
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// interpolateStructField interpolates a struct field.
|
|
func interpolateStructField(field reflect.StructField, v reflect.Value) (err error) {
|
|
|
|
var parsedTagOpts map[string]bool
|
|
|
|
if !v.CanSet() {
|
|
return
|
|
}
|
|
|
|
// Skip if explicitly instructed to do so.
|
|
parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim)
|
|
if parsedTagOpts["-"] {
|
|
return
|
|
}
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
err = interpolateStructField(field, v.Elem())
|
|
} else {
|
|
err = interpolateValue(v)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// interpolateValue is a dispatcher for a reflect value.
|
|
func interpolateValue(v reflect.Value) (err error) {
|
|
|
|
var kind reflect.Kind = v.Kind()
|
|
|
|
switch kind {
|
|
case reflect.Ptr:
|
|
if v.IsNil() || v.IsZero() || !v.IsValid() {
|
|
return
|
|
}
|
|
v = v.Elem()
|
|
if err = interpolateValue(v); err != nil {
|
|
return
|
|
}
|
|
case reflect.String:
|
|
if err = interpolateStringReflect(v); err != nil {
|
|
return
|
|
}
|
|
return
|
|
case reflect.Slice, reflect.Array:
|
|
if err = interpolateSlice(v); err != nil {
|
|
}
|
|
case reflect.Map:
|
|
if err = interpolateMap(v); err != nil {
|
|
return
|
|
}
|
|
case reflect.Struct:
|
|
if err = interpolateStruct(v); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|