checking in before refactoring interpolation

This commit is contained in:
brent saner 2024-04-11 12:46:13 -04:00
parent 187ad868db
commit eed9c34ebf
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
17 changed files with 2014 additions and 48 deletions

View File

@ -9,3 +9,25 @@ var (
reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`) reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`)
reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`) reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`)
) )

var (
StructTagInterpolate string = "envsub"
StructTagPopulate string = "envpop"
)

var (
defaultInterpolateOpts interpolateOpts = interpolateOpts{
noMapKey: false,
noMapVal: false,
}
// InterpolateOptNoMapKey is the equivalent of the struct tag `no_map_key` for Interpolate.
InterpolateOptNoMapKey optInterpolate = func(o *interpolateOpts) (err error) {
o.noMapKey = true
return
}
// InterpolateOptNoMapValue is the equivalent of the struct tag `no_map_value` for Interpolate.
InterpolateOptNoMapValue optInterpolate = func(o *interpolateOpts) (err error) {
o.noMapVal = true
return
}
)

View File

@ -6,28 +6,16 @@ import (
`fmt` `fmt`
`io/ioutil` `io/ioutil`
`os` `os`
`reflect`
`strings` `strings`
`sync`


`r00t2.io/goutils/multierr`
`r00t2.io/sysutils/errs`
`r00t2.io/sysutils/internal` `r00t2.io/sysutils/internal`
`r00t2.io/sysutils/paths` `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. // GetEnvMap returns a map of all environment variables. All values are strings.
func GetEnvMap() (envVars map[string]string) { func GetEnvMap() (envVars map[string]string) {


@ -114,6 +102,22 @@ func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
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. GetPidEnvMap will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID. It gets the environment variables of a given process' PID.
@ -185,3 +189,609 @@ func HasEnv(key string) (envIsSet bool) {


return 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
}

825
envs/funcs.go.old Normal file
View File

@ -0,0 +1,825 @@
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 (comprised of the same requirements)
- a slice (comprised of the same requirements)

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 vVal reflect.Value
var mIter *reflect.MapIter
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()))

mIter = v.MapRange()
for mIter.Next() {
kVal = mIter.Key()
vVal = mIter.Value()
go func(mapK, mapV reflect.Value) {
var mapErr error
var newKey reflect.Value
var newVal reflect.Value

defer wg.Done()

if !valOpts.noMapKey {
/*
if mapK.Kind() == reflect.String {
if mapErr = interpolateStringReflect(mapK, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(mapK, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
}
*/
newKey = reflect.New(mapK.Type())
newKey.Set(mapK)
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 {
/*
if mapV.Kind() == reflect.String {
if mapErr = interpolateStringReflect(mapV, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(mapV, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
}
*/
newVal = reflect.New(mapV.Type())
newVal.Set(mapV)
if mapV.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 = mapV
}
newMap.SetMapIndex(reflect.ValueOf(newKey), reflect.ValueOf(newVal))
}(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)

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
}

182
envs/funcs_test.go Normal file
View File

@ -0,0 +1,182 @@
package envs

import (
`errors`
`os`
`testing`
`time`

`github.com/davecgh/go-spew/spew`
`r00t2.io/sysutils/errs`
)

type (
testCustom string
testStruct struct {
Hello string `envpop:"USER"`
HelloPtr *string `envpop:"USER"`
HelloForce string `envpop:"USER,force"`
HelloPtrForce *string `envpop:"USER,force"`
HelloNo string `envsub:"-" envpop:"-"`
HelloNoPtr *string `envsub:"-" envpop:"-"`
BadType int
NilField *string `envpop:"NONEXISTENTBADVAR,allow_empty"`
PtrInt *int
Custom testCustom
MapStr map[string]string
SliceStr []string
SliceSlice [][]string
SliceMap []map[string]string
SliceStruct []testStruct
}
)

func TestInterpolateString(t *testing.T) {

var err error
var s string = "My username is ${USER}; hello!"
var sp *string = &s

if err = InterpolateString(&s); err != nil {
t.Fatalf("Failed interpolation: %v", err)
}
t.Logf("String test passed:\n%v", s)

if err = InterpolateString(sp); err != nil {
t.Fatalf("Failed interpolation: %v", err)
}
t.Logf("String pointer test passed:\n%v", *sp)
}

func TestInterpolateStruct(t *testing.T) {

var err error

for _, i := range []interface{}{
"i am ${USER}, it is ${CURDATETIME}",
new(string),
/*
testStruct{
Hello: "i am ${USER}, it is ${CURDATETIME}",
HelloPtr: new(string),
HelloForce: "i am ${USER}, it is ${CURDATETIME}",
HelloPtrForce: new(string),
HelloNo: "i am ${USER}, it is ${CURDATETIME}",
HelloNoPtr: new(string),
BadType: 4,
NilField: nil,
PtrInt: new(int),
Custom: testCustom("i am ${USER}, it is ${CURDATETIME}"),
SliceStr: []string{"i am ${USER}, it is ${CURDATETIME}"},
SliceSlice: [][]string{[]string{"i am ${USER}, it is ${CURDATETIME}"}},
SliceMap: []map[string]string{map[string]string{"i am ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
SliceStruct: []testStruct{
{
Hello: "i am nested ${USER}, it is ${CURDATETIME}",
HelloPtr: nil,
HelloForce: "i am nested ${USER}, it is ${CURDATETIME}",
HelloPtrForce: nil,
HelloNo: "i am nested ${USER}, it is ${CURDATETIME}",
HelloNoPtr: nil,
BadType: 0,
NilField: nil,
PtrInt: nil,
Custom: testCustom("i am nested ${USER}, it is ${CURDATETIME}"),
SliceStr: []string{"i am nested ${USER}, it is ${CURDATETIME}"},
SliceSlice: [][]string{[]string{"i am nested ${USER}, it is ${CURDATETIME}"}},
SliceMap: []map[string]string{map[string]string{"i am nested ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
SliceStruct: nil,
},
},
},
*/
&testStruct{
Hello: "i am ${USER}, it is ${CURDATETIME}",
HelloPtr: new(string),
HelloForce: "i am ${USER}, it is ${CURDATETIME}",
HelloPtrForce: new(string),
HelloNo: "i am ${USER}, it is ${CURDATETIME}",
HelloNoPtr: new(string),
BadType: 4,
NilField: nil,
PtrInt: new(int),
Custom: testCustom("i am ${USER}, it is ${CURDATETIME}"),
MapStr: map[string]string{"i am ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"},
SliceStr: []string{"i am ${USER}, it is ${CURDATETIME}"},
SliceSlice: [][]string{[]string{"i am ${USER}, it is ${CURDATETIME}"}},
SliceMap: []map[string]string{map[string]string{"i am ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
SliceStruct: []testStruct{
{
Hello: "i am nested ${USER}, it is ${CURDATETIME}",
HelloPtr: nil,
HelloForce: "i am nested ${USER}, it is ${CURDATETIME}",
HelloPtrForce: nil,
HelloNo: "i am nested ${USER}, it is ${CURDATETIME}",
HelloNoPtr: nil,
BadType: 0,
NilField: nil,
PtrInt: nil,
Custom: testCustom("i am nested ${USER}, it is ${CURDATETIME}"),
SliceStr: []string{"i am nested ${USER}, it is ${CURDATETIME}"},
SliceSlice: [][]string{[]string{"i am nested ${USER}, it is ${CURDATETIME}"}},
SliceMap: []map[string]string{map[string]string{"i am nested ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
SliceStruct: nil,
},
},
},
} {
if err = os.Setenv("CURDATETIME", time.Now().String()); err != nil {
t.Fatalf("Received error setting CURDATETIME: %v", err)
}
switch x := i.(type) {
case *string:
*x = "i am ${USER}, it is ${CURDATETIME}"
case testStruct:
*x.HelloPtr = x.Hello
*x.HelloPtrForce = x.HelloForce
*x.HelloNoPtr = x.HelloNo
*x.PtrInt = x.BadType
case *testStruct:
*x.HelloPtr = x.Hello
*x.HelloPtrForce = x.HelloForce
*x.HelloNoPtr = x.HelloNo
*x.PtrInt = x.BadType
}
t.Logf("Before (%T):\n%v", i, spew.Sdump(i))
if err = Interpolate(i); err != nil {
t.Fatalf("Failed interpolation: %v", err)
}
t.Logf("After (%T):\n%v\n", i, spew.Sdump(i))
}
}

func TestPopulateStruct(t *testing.T) {

var err error
var greet string = "My username is ${USER}; hello!"
var num int = 4
var sp *testStruct = &testStruct{
Hello: greet,
HelloPtr: &greet,
HelloForce: greet,
HelloPtrForce: &greet,
HelloNo: greet,
HelloNoPtr: &greet,
BadType: 4,
PtrInt: &num,
}

if err = PopulateStruct(sp); err != nil {
if errors.Is(err, errs.ErrNilPtr) {
err = nil
t.Logf("Detected nil.")
} else if errors.Is(err, errs.ErrBadType) {
err = nil
t.Log("Detected bad type.")
} else {
t.Fatalf("Failed interpolation: %v", err)
}
}
t.Logf("Evaluated:\n%v", spew.Sdump(sp))

}

10
envs/types.go Normal file
View File

@ -0,0 +1,10 @@
package envs

type (
interpolateOpts struct {
noMapKey bool
noMapVal bool
isTagged bool
}
optInterpolate func(o *interpolateOpts) (err error)
)

18
envs/utils_nonwindows.go Normal file
View File

@ -0,0 +1,18 @@
//go:build !windows

package envs

import (
`os`
)

/*
interpolateString takes string s and performs environment variable interpolation/substitution on it.
err will always be nil; it's here for compat with the Windows equivalent.
*/
func interpolateString(s string) (subbed string, err error) {

subbed = os.ExpandEnv(s)

return
}

15
envs/utils_windows.go Normal file
View File

@ -0,0 +1,15 @@
//go:build windows

package envs

import (
"golang.org/x/sys/windows/registry"
)

// interpolateString takes string s and performs environment variable interpolation/substitution on it.
func interpolateString(s string) (subbed string, err error) {

subbed, err = registry.ExpandString(os.ExpandEnv(s))

return
}

10
errs/errs.go Normal file
View File

@ -0,0 +1,10 @@
package errs

import (
`errors`
)

var (
ErrBadType error = errors.New("a bad type was passed")
ErrNilPtr error = errors.New("a nil pointer was passed")
)

34
exec_extra/consts.go Normal file
View File

@ -0,0 +1,34 @@
package exec_extra

var (
StructTagCmdArgs string = "cmdarg"
)

var (
/*
CmdArgsOptPreferShort, if specified, prefers the "short" argument over "long" if both are specified.
The default is to prefer long.

Can be specified per-field via the `prefer_short` option (no value/value ignored).
*/
CmdArgsOptPreferShort cmdArgOpt = func(opts *cmdArgsOpts) (err error) {
opts.preferShort = true
return
}

/*
CmdArgsOptShortEquals, if specified, renders short flags *with* an equals sign
(if using POSIX args).

Has no effect if using Windows traditional syntax or if there is no value for the field.
*/
CmdArgsOptShortEquals cmdArgOpt = func(opts *cmdArgsOpts) (err error) {
opts.preferShort = true
return
}

CmdArgsOptLongNoEquals cmdArgOpt = func(opts *cmdArgsOpts) (err error) {
opts.preferShort = true
return
}
)

View File

@ -0,0 +1,45 @@
package exec_extra

var (
/*
CmdArgsOptForcePosix forces the resulting command string to use "POSIX-style" flag notation.

Traditionally, Windows used flags like `/f` instead of POSIX `-f`, `/c:value` instead of `-c value`
or `-c=value`, etc.

If this option is passed, either to GetCmdFromStruct() or for a specific field via the
tag defined by StructTagCmdArgs (option `force_posix`, no value/value ignored), then the
POSIX-style flag syntax will be used instead.

Note that on Windows runtime, the default is to use the traditional slash-based syntax.
If you are generating command strings for Powershell or third-party software, you probably
want to use this option.

See also the inverse of this option, CmdArgsOptForceNoPosix.
*/
CmdArgsOptForcePosix cmdArgOpt = func(opts *cmdArgsOpts) (err error) {
opts.forcePosix = true
return
}

/*
CmdArgsOptForceNoPosix forces the resulting command string to use "traditional Windows" flag notation.

Traditionally, Windows used flags like `/f` instead of POSIX `-f`, `/c:value` instead of `-c value`
or `-c=value`, etc.

If this option is passed, either to GetCmdFromStruct() or for a specific field via the
tag defined by StructTagCmdArgs (option `force_no_posix`, no value/value ignored), then the
Windows-style flag syntax will be used instead.

Note that on Windows runtime, the default is to use the traditional slash-based syntax.
If you are generating command strings for Powershell or third-party software, you probably
want to use CmdArgsOptForcePosix instead.

See also the inverse of this option, CmdArgsOptForcePosix.
*/
CmdArgsOptForceNoPosix cmdArgOpt = func(opts *cmdArgsOpts) (err error) {
opts.forcePosix = false
return
}
)

View File

@ -1,31 +0,0 @@
/*
SysUtils - a library to assist with various system-related functions
Copyright (C) 2020 Brent Saner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package exec_extra

import (
"os/exec"
)

func ExecCmdReturn(cmd *exec.Cmd) (exitStatus int, err error) {
// https://stackoverflow.com/a/55055100/733214
err = cmd.Run()
exitErr, _ := err.(*exec.ExitError)
exitStatus = exitErr.ExitCode()
return
}

86
exec_extra/funcs.go Normal file
View File

@ -0,0 +1,86 @@
/*
SysUtils - a library to assist with various system-related functions
Copyright (C) 2020 Brent Saner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package exec_extra

import (
"os/exec"
)

func ExecCmdReturn(cmd *exec.Cmd) (exitStatus int, err error) {
// https://stackoverflow.com/a/55055100/733214
err = cmd.Run()
exitErr, _ := err.(*exec.ExitError)
exitStatus = exitErr.ExitCode()
return
}

/*
GetCmdFromStruct takes (a pointer to) a struct and returns a slice of
strings compatible with os/exec.Cmd.

The tag name used can be changed by setting the StructTagCmdArgs variable in this module;
the default is `cmdarg`.

If the tag value is "-", the field will be skipped. Any other tag value(s) are ignored.

Tag value format:
<tag>:"<option>=<value>[,<option>[=<value>],<option>[=<value>]...]"
e.g.
cmdarg:"short=l,long=list"
cmdarg:"short=l"
cmdarg:"long=list"

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 `cmdarg`.)

If a cmdarg tag is specified but has no `short` or `long` option value, the field will be skipped entirely.
If a field's value is nil, it will be skipped.
Otherwise if a field's value is the zero-value, it will be skipped.

Recognized options:

* short - A short flag for the argument

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 GetCmdFromStruct[T any](s T, opts ...cmdArgOpt) (cmdSlice []string, err error) {

// TODO

return
}

9
exec_extra/types.go Normal file
View File

@ -0,0 +1,9 @@
package exec_extra

type cmdArgsOpts struct {
preferShort bool
forcePosix bool
cmd *string
}

type cmdArgOpt func(*cmdArgsOpts) (err error)

33
exec_extra/utils.go Normal file
View File

@ -0,0 +1,33 @@
package exec_extra

import (
`r00t2.io/sysutils/paths`
)

/*
CmdArgsWithBin returns a cmdArgsOpt that specifies program/executable/binary path `bin`,
ensuring that the resulting cmdSlice from GetCmdFromStruct() will return a ready-to-use slice.
(Otherwise the executable would need to be prepended to the resulting slice.)

Path normalization/canonziation can be enabled/disabled via normalizePath.
*/
func CmdArgsWithBin(bin string, normalizePath bool) (opt cmdArgOpt, err error) {

if normalizePath {
if err = paths.RealPath(&bin); err != nil {
return
}
}

opt = func(opts *cmdArgsOpts) (err error) {
/*
if opts.cmd == nil {
opts.cmd = new(string)
}
*/
*opts.cmd = bin
return
}

return
}

7
go.mod
View File

@ -2,7 +2,12 @@ module r00t2.io/sysutils


go 1.21 go 1.21


require github.com/g0rbe/go-chattr v1.0.1 require (
github.com/davecgh/go-spew v1.1.1
github.com/g0rbe/go-chattr v1.0.1
golang.org/x/sys v0.19.0
r00t2.io/goutils v1.4.0
)


// Pending https://github.com/g0rbe/go-chattr/pull/3 // Pending https://github.com/g0rbe/go-chattr/pull/3
replace github.com/g0rbe/go-chattr => github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 replace github.com/g0rbe/go-chattr => github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13

10
go.sum
View File

@ -1,2 +1,12 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 h1:tgEbuE4bNVjaCWWIB1u9lDzGqH/ZdBTg33+4vNW2rjg= github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 h1:tgEbuE4bNVjaCWWIB1u9lDzGqH/ZdBTg33+4vNW2rjg=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
r00t2.io/goutils v1.4.0 h1:/x/etLpMFv3+j1aPtT7KK2G0uOk+gQkGvXIYBCdjn3E=
r00t2.io/goutils v1.4.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=

View File

@ -2,6 +2,7 @@ package internal


import ( import (
`runtime` `runtime`
`strings`
) )


// GetPathEnvName gets the OS-specific path environment variable name. // GetPathEnvName gets the OS-specific path environment variable name.
@ -16,3 +17,85 @@ func GetPathEnvName() (envVarName string) {


return return
} }

/*
StringToMap takes string s, assumed to be in the form of
key=value[,key=value,key=value...]
and returns a map[string]string (map[key]value).
It is proccessed in order; later duplicate keys overwrite previous ones.

If s is an empty string or comprised only of whitespace, m will be nil.
If only a key is provided with no value, the value in the map will be an empty string.
(e.g. "foo,bar=baz" => map[string]string{"foo": "", "bar: "baz"}

Surrounding whitespace is trimmed.
*/
func StringToMap(s string) (m map[string]string) {

var kvSplit []string
var valSplit []string
var k string
var v string

if strings.TrimSpace(s) == "" {
return
}

kvSplit = strings.Split(s, ",")
if kvSplit == nil || len(kvSplit) == 0 {
return
}
for _, kv := range kvSplit {
valSplit = strings.SplitN(kv, "=", 2)
if valSplit == nil || len(valSplit) == 0 {
continue
}
k = valSplit[0]
switch len(valSplit) {
case 1:
v = ""
case 2:
v = valSplit[1]
// It's not possible to have more than 2.
}
if m == nil {
m = make(map[string]string)
}
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
m[k] = v
}

return
}

/*
StringToMapBool is like StringToMap but designed for a map of booleans.
It takes string s, assumed to be in the form of
option[,option,option...]
and returns a map[string]bool (map[option]true).

If s is an empty string or comprised only of whitespace, m will be nil.

Surrounding whitespace is trimmed.
*/
func StringToMapBool(s string) (m map[string]bool) {

var optSplit []string

if strings.TrimSpace(s) == "" {
return
}

optSplit = strings.Split(s, ",")
if optSplit == nil || len(optSplit) == 0 {
return
}
m = make(map[string]bool)
for _, o := range optSplit {
o = strings.TrimSpace(o)
m[o] = true
}

return
}