checking in before refactoring interpolation

This commit is contained in:
brent saner
2024-04-11 12:46:13 -04:00
parent 187ad868db
commit eed9c34ebf
17 changed files with 2014 additions and 48 deletions

View File

@@ -6,28 +6,16 @@ import (
`fmt`
`io/ioutil`
`os`
`reflect`
`strings`
`sync`
`r00t2.io/goutils/multierr`
`r00t2.io/sysutils/errs`
`r00t2.io/sysutils/internal`
`r00t2.io/sysutils/paths`
)
// 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
}
// GetEnvMap returns a map of all environment variables. All values are strings.
func GetEnvMap() (envVars map[string]string) {
@@ -114,6 +102,22 @@ func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
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.
@@ -185,3 +189,609 @@ func HasEnv(key string) (envIsSet bool) {
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
}