go_sysutils/envs/funcs.go
2024-04-11 12:46:13 -04:00

798 lines
19 KiB
Go

package envs
import (
`bytes`
`errors`
`fmt`
`io/ioutil`
`os`
`reflect`
`strings`
`sync`
`r00t2.io/goutils/multierr`
`r00t2.io/sysutils/errs`
`r00t2.io/sysutils/internal`
`r00t2.io/sysutils/paths`
)
// 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
- 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, the default is to apply interpolation to both keys and values;
this can be changed with the `no_map_key` and `no_map_value` options (tag values).
Any other tag value(s) are ignored.
For maps and slices, Interpolate will recurse into values (e.g. [][]string will work as expected).
Supported struct tag options:
* `no_map_key` - Do not operate on map keys if they are strings or string pointers.
See also InterpolateOptNoMapKey.
* `no_map_value` - Do not operate on map values if they are strings or string pointers.
See also InterpolateOptNoMapValue.
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, opts ...optInterpolate) (err error) {
var sVal reflect.Value = reflect.ValueOf(s)
var sType reflect.Type = sVal.Type()
var kind reflect.Kind = sType.Kind()
var ptrVal reflect.Value
var ptrType reflect.Type
var ptrKind reflect.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, opts, nil)
} else {
// Otherwise, it should be a struct ptr.
if ptrKind != reflect.Struct {
return
}
err = interpolateStruct(ptrVal, opts, nil)
}
case reflect.Map:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateMap(sVal, opts, nil)
case reflect.Slice:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateSlice(sVal, opts, nil)
/*
case reflect.Struct:
if sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateStruct(sVal, opts, nil)
*/
}
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
}
/*
PopulateStruct takes (a pointer to) a struct and performs *population* on it.
Unlike the InterpolateStruct function, this *completely populates* (or *replaces*)
a field's value with the specified environment variable; no *substitution* is performed.
You can change the tag name used by changing the StructTagPopulate variable in this module;
the default is `envpop`.
Tag value format:
<tag>:"<VAR NAME>[,<option>,<option>...]"
e.g.
envpop:"SOMEVAR"
envpop:"OTHERVAR,force"
envpop:"OTHERVAR,allow_empty"
envpop:"OTHERVAR,force,allow_empty"
If the tag value is "-", or <VAR NAME> is not provided, the field will be explicitly skipped.
(This is the default behavior for struct fields not tagged with `envpop`.)
Recognized options:
* force - Existing field values that are non-empty strings or non-nil pointers are normally skipped; this option always replaces them.
* allow_empty - Normally no replacement will be performed if the specified variable is undefined/not found.
This option allows an empty string to be used instead.
Not very useful for string fields, but potentially useful for string pointer fields.
e.g.:
struct{
// If this is an empty string, it will be replaced with the value of $CWD.
CurrentDir string `envpop:"CWD"`
// This would only populate with $USER if the pointer is nil.
UserName *string `envpop:"USER"`
// This will *always* replace the field's value with the value of $DISPLAY,
// even if not an empty string.
// Note the `force` option.
Display string `envpop:"DISPLAY,force"`
// Likewise, even if not nil, this field's value would be replaced with the value of $SHELL.
Shell *string `envpop:"SHELL,force"`
// This field will be untouched if non-nil, otherwise it will be a pointer to an empty string
// if FOOBAR is undefined.
NonExistentVar *string `envpop:"FOOBAR,allow_empty"`
}
If s is nil, nothing will be done and err will be errs.ErrNilPtr.
If s is not a pointer to a struct, nothing will be done and err will be errs.ErrBadType.
*/
func PopulateStruct[T any](s T) (err error) {
var structVal reflect.Value
var structType reflect.Type
var field reflect.StructField
var fieldVal reflect.Value
var tagVal string
var valSplit []string
var varNm string
var varVal string
var optsMap map[string]bool
var force bool
var allowEmpty bool
var defined bool
if reflect.TypeOf(s).Kind() != reflect.Ptr {
err = errs.ErrBadType
return
}
structVal = reflect.ValueOf(s)
if structVal.IsNil() || structVal.IsZero() || !structVal.IsValid() {
err = errs.ErrNilPtr
return
}
structVal = reflect.ValueOf(s).Elem()
structType = structVal.Type()
if structType.Kind() != reflect.Struct {
err = errs.ErrBadType
return
}
for i := 0; i < structVal.NumField(); i++ {
field = structType.Field(i)
fieldVal = structVal.Field(i)
// Skip explicitly skipped or non-tagged fields.
tagVal = field.Tag.Get(StructTagPopulate)
if tagVal == "" || strings.TrimSpace(tagVal) == "-" || strings.HasPrefix(tagVal, "-,") {
continue
}
fieldVal = structVal.Field(i)
if fieldVal.Kind() != reflect.Ptr && fieldVal.Kind() != reflect.String {
continue
}
optsMap = make(map[string]bool)
valSplit = strings.Split(tagVal, ",")
if valSplit == nil || len(valSplit) == 0 {
continue
}
varNm = valSplit[0]
if strings.TrimSpace(varNm) == "" {
continue
}
if len(valSplit) >= 2 {
for _, o := range valSplit[1:] {
optsMap[o] = true
}
}
force = optsMap["force"]
allowEmpty = optsMap["allow_empty"]
// if !force && (!fieldVal.IsNil() && !fieldVal.IsZero()) {
if !force && !fieldVal.IsZero() {
continue
}
if fieldVal.Kind() == reflect.Ptr {
if field.Type.Elem().Kind() != reflect.String {
continue
}
}
if !fieldVal.CanSet() {
continue
}
varVal, defined = os.LookupEnv(varNm)
if !defined && !allowEmpty {
continue
}
switch fieldVal.Kind() {
case reflect.Ptr:
fieldVal.Set(reflect.ValueOf(&varVal))
case reflect.String:
fieldVal.SetString(varVal)
}
}
return
}
// interpolateMap is used by Interpolate and interpolateReflect for maps. v should be a reflect.Value of a map.
func interpolateMap(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
var kVal 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()
var valOpts *interpolateOpts = new(interpolateOpts)
if kind != reflect.Map {
err = errs.ErrBadType
return
}
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
*valOpts = defaultInterpolateOpts
if opts != nil && len(opts) > 0 {
for _, opt := range opts {
if err = opt(valOpts); err != nil {
return
}
}
}
if tagOpts != nil && len(tagOpts) > 0 {
for _, opt := range tagOpts {
if err = opt(valOpts); err != nil {
return
}
}
}
if valOpts.noMapKey && valOpts.noMapVal {
return
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
newMap = reflect.MakeMap(reflect.TypeOf(v.Interface()))
for _, e := range v.MapKeys() {
kVal = e
go func(mapK reflect.Value) {
var mapErr error
var newKey reflect.Value
var newVal reflect.Value
var vVal reflect.Value = v.MapIndex(mapK)
defer wg.Done()
if !valOpts.noMapKey {
newKey = reflect.New(reflect.TypeOf(mapK.Interface()))
newKey.Set(vVal)
if mapK.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newKey, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(newKey, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
}
} else {
newKey = mapK
}
if !valOpts.noMapVal {
newVal = reflect.New(vVal.Type())
newVal.Set(vVal)
if vVal.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newVal, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(newVal, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
}
} else {
newVal = vVal
}
newMap.SetMapIndex(reflect.ValueOf(newKey), reflect.ValueOf(newVal))
}(kVal)
}
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)
return
}
// interpolateSlice is used by Interpolate and interpolateReflect for slices. v should be a reflect.Value of a slice.
func interpolateSlice(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (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()
var valOpts *interpolateOpts = new(interpolateOpts)
return
if kind != reflect.Slice {
err = errs.ErrBadType
return
}
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
*valOpts = defaultInterpolateOpts
if opts != nil && len(opts) > 0 {
for _, opt := range opts {
if err = opt(valOpts); err != nil {
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
var newVal reflect.Value
defer wg.Done()
newVal = reflect.New(v.Index(idx).Type())
newVal.Set(v.Index(idx))
if v.Index(idx).Kind() == reflect.String {
if sErr = interpolateStringReflect(newVal, opts, tagOpts); sErr != nil {
errChan <- sErr
return
}
} else {
if sErr = interpolateValue(newVal, opts, tagOpts); sErr != nil {
errChan <- sErr
return
}
}
v.Index(idx).Set(reflect.ValueOf(newVal))
}(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, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
var strVal string
if strVal, err = interpolateString(v.String()); err != nil {
return
}
v.Set(reflect.ValueOf(strVal).Convert(v.Type()))
return
}
// interpolateStruct is used by Interpolate and interpolateReflect for structs. v should be a reflect.Value of a struct.
func interpolateStruct(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (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, opts, nil); 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, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
var tagVal string
// var ftKind reflect.Kind = field.Type.Kind()
var parsedTagOpts map[string]bool
var valOpts *interpolateOpts = new(interpolateOpts)
if !v.CanSet() {
return
}
*valOpts = defaultInterpolateOpts
// Skip if explicitly instructed to do so.
tagVal = field.Tag.Get(StructTagInterpolate)
parsedTagOpts = internal.StringToMapBool(tagVal)
if parsedTagOpts["-"] {
return
}
if opts != nil && len(opts) > 0 {
for _, opt := range opts {
if err = opt(valOpts); err != nil {
return
}
}
}
if v.Kind() == reflect.Ptr {
err = interpolateStructField(field, v.Elem(), opts, tagOpts)
} else {
err = interpolateValue(v, opts, tagOpts)
}
return
}
// interpolateValue is a dispatcher for a reflect value.
func interpolateValue(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (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, opts, tagOpts); err != nil {
return
}
case reflect.String:
if err = interpolateStringReflect(v, opts, tagOpts); err != nil {
return
}
return
case reflect.Slice:
if err = interpolateSlice(v, opts, tagOpts); err != nil {
}
case reflect.Map:
if err = interpolateMap(v, opts, tagOpts); err != nil {
return
}
case reflect.Struct:
if err = interpolateStruct(v, opts, tagOpts); err != nil {
return
}
}
return
}