Files
go_sysutils/envs/funcs_staticenv.go
2025-12-06 19:19:53 -05:00

698 lines
15 KiB
Go

package envs
import (
"os"
"reflect"
"strings"
"sync"
"r00t2.io/goutils/multierr"
"r00t2.io/goutils/structutils"
"r00t2.io/sysutils/errs"
"r00t2.io/sysutils/internal"
)
/*
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 (s *StaticEnv) 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 (s *StaticEnv) DefEnvBlank(key, fallback string) (value string) {
value = DefEnv(key, fallback)
if value == "" {
value = fallback
}
return
}
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an [EnvErrNoVal].
func (s *StaticEnv) GetEnvErr(key string) (value string, err error) {
var exists bool
if value, exists = os.LookupEnv(key); !exists {
err = &EnvErrNoVal{
VarName: key,
}
return
}
return
}
/*
GetEnvErrNoBlank behaves exactly like [GetEnvErr] with the
additional stipulation that the value must not be empty.
An error for a value that is non-empty but whitespace only (e.g. VARNM="\t")
can be returned if ignoreWhitespace == true.
(If it is, an [EnvErrNoVal] will also be returned.)
*/
func (s *StaticEnv) GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err error) {
var exists bool
var e *EnvErrNoVal = &EnvErrNoVal{
VarName: key,
WasRequiredNonEmpty: true,
IgnoreWhitespace: ignoreWhitespace,
}
if value, exists = os.LookupEnv(key); !exists {
err = e
return
} else {
e.WasFound = true
e.WasWhitespace = (strings.TrimSpace(value) == "") && (value != "")
if ignoreWhitespace && e.WasWhitespace {
err = e
return
}
}
return
}
// GetEnvMap returns a map of all environment variables. All values are strings.
func (s *StaticEnv) GetEnvMap() (envVars map[string]string) {
var envList []string = os.Environ()
envVars = internal.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 (s *StaticEnv) GetEnvMapNative() (envMap map[string]interface{}) {
var stringMap map[string]string = GetEnvMap()
envMap = internal.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 (s *StaticEnv) 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 (s *StaticEnv) 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 (s *StaticEnv) GetPathEnv() (pathList []string, err error) {
if pathList, err = internal.GetPathEnv(); err != nil {
return
}
return
}
/*
GetPidEnvMap will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID.
*/
func (s *StaticEnv) GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
if envMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
return
}
/*
GetPidEnvMapNative returns a map of all environment variables (like [GetEnvMapNative]), 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 (s *StaticEnv) GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {
var stringMap map[string]string
if stringMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
envMap = internal.NativizeEnvMap(stringMap)
return
}
/*
HasEnv is much like [os.LookupEnv], but only returns a boolean indicating
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 (s *StaticEnv) 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 (s *StaticEnv) Interpolate(inStr any) (err error) {
var ptrVal reflect.Value
var ptrType reflect.Type
var ptrKind reflect.Kind
var sVal reflect.Value = reflect.ValueOf(inStr)
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 = s.interpolateStringReflect(ptrVal)
} else {
// Otherwise, it should be a struct ptr.
if ptrKind != reflect.Struct {
return
}
err = s.interpolateStruct(ptrVal)
}
case reflect.Map:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = s.interpolateMap(sVal)
case reflect.Slice:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = s.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 [errs.ErrNilPtr].
This is a standalone function that is much more performant than [Interpolate]
at the cost of rigidity.
*/
func (s *StaticEnv) InterpolateString(inStr *string) (err error) {
var newStr string
if inStr == nil {
err = errs.ErrNilPtr
return
}
if newStr, err = interpolateString(*inStr); err != nil {
return
}
*inStr = newStr
return
}
/*
Refresh refreshes the current environment.
This is called automatically on all other methods if
the [StaticEnv] was allocated with `dynamic` set to true.
*/
func (s *StaticEnv) Refresh() (err error) {
var ev []string
if !s.self && s.proc == nil {
// NO-OP; static mapping
return
}
if s.self {
ev = os.Environ()
} else if s.proc != nil {
if ev, err = s.proc.Environ(); err != nil {
return
}
}
s.envVars = internal.EnvListToMap(ev)
return
}
/*
Set sets variable key to value.
This only works under the following conditions:
- [SetEnv] was created from [NewEnvFromMap]
- [SetEnv] is from [Current]
- [SetEnv] is from [NewEnvFromPid] *and*
this process has PTRACE permissions/capabilities on/for
the target PID's process.
*/
func (s *StaticEnv) Set(key string, value string) {
var err error
s.lock.Lock()
defer s.lock.Unlock()
s.envVars[key] = value
if s.proc != nil {
if err = s.setProcVal(key, value); err != nil && s.strict {
panic(err)
}
}
return
}
// interpolateMap is used by [Interpolate] for maps. v should be a [reflect.Value] of a map.
func (s *StaticEnv) 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 = s.interpolateStringReflect(newKey); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = s.interpolateValue(newKey); mapErr != nil {
errChan <- mapErr
return
}
}
// value
if val.Kind() == reflect.String {
if mapErr = s.interpolateStringReflect(newVal); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = s.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 (s *StaticEnv) 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 = s.interpolateStringReflect(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
} else {
if sErr = s.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 (s *StaticEnv) 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 (s *StaticEnv) 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 = s.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 (s *StaticEnv) 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 = s.interpolateStructField(field, v.Elem())
} else {
err = s.interpolateValue(v)
}
return
}
// interpolateValue is a dispatcher for a reflect value.
func (s *StaticEnv) 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 = s.interpolateValue(v); err != nil {
return
}
case reflect.String:
if err = s.interpolateStringReflect(v); err != nil {
return
}
return
case reflect.Slice, reflect.Array:
if err = s.interpolateSlice(v); err != nil {
}
case reflect.Map:
if err = s.interpolateMap(v); err != nil {
return
}
case reflect.Struct:
if err = s.interpolateStruct(v); err != nil {
return
}
}
return
}