adding envs tagging/interpolation

This commit is contained in:
brent saner
2024-06-17 04:33:30 -04:00
parent eed9c34ebf
commit b64c318a4a
18 changed files with 734 additions and 1575 deletions

2
exec_extra/TODO Normal file
View File

@@ -0,0 +1,2 @@
- for GetCmdFromStruct, support []byte fields
-- support hex and base64 struct field opts (and others?) via `enc=` struct tag.

View File

@@ -1,34 +1,123 @@
package exec_extra
var (
StructTagCmdArgs string = "cmdarg"
import (
`r00t2.io/goutils/bitmask`
)
var (
CmdArgsTag string = "cmdarg"
/*
CmdArgsOptPreferShort, if specified, prefers the "short" argument over "long" if both are specified.
The default is to prefer long.
CmdArgsDictSep specifies the string to use to separate keys and values.
Can be specified per-field via the `prefer_short` option (no value/value ignored).
To override at the struct field level, use the tag value:
`<CmdArgsTag>:"dictsep=<str>"`
Where str is the string to use. e.g.:
`cmdarg:"short=d,long=data,dictsep=."`
Would render a map value of map[string]string{"foo": "bar"} as:
`-d foo.bar`
*/
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
}
CmdArgsDictSep string = ":"
)
// CmdArgOptNone is an "empty option" and does nothing.
const CmdArgOptNone bitmask.MaskBit = 0
const (
/*
CmdArgOptPreferShort prefers short options where possible.
Has no effect if Windows traditional syntax is used.
The default is to use long options.
See also CmdArgOptPreferLong.
Corresponding struct tag option: prefer_short
*/
CmdArgOptPreferShort cmdArgOpt = 1 << iota
/*
CmdArgOptPreferLong prefers long options where possible.
Has no effect if Windows traditional syntax is used.
This behavior is the default, but it can be used to
override a CmdArgOptPreferShort from a parent.
Corresponding struct tag option: prefer_long
*/
CmdArgOptPreferLong
/*
CmdArgOptShortEquals will use an equals separator
for short flags instead of a space (the default).
Has no effect if Windows traditional syntax is used.
Corresponding struct tag option: short_equals
*/
CmdArgOptShortEquals
/*
CmdArgOptShortNoEquals will use a space separator
for short flags instead of an equals.
Has no effect if Windows traditional syntax is used.
This behavior is the default, but it can be used to
override a CmdArgOptPreferShort from a parent.
Corresponding struct tag option: no_short_equals
*/
CmdArgOptShortNoEquals
/*
CmdArgOptLongEquals will use an equals separator
for long flags instead of a space.
Has no effect if Windows traditional syntax is used.
This behavior is the default, but it can be used to
override a CmdArgOptLongNoEquals from a parent.
Corresponding struct tag option: long_equals
*/
CmdArgOptLongEquals
/*
CmdArgOptLongNoEquals will use a space separator
for short flags instead of an equals.
Has no effect if Windows traditional syntax is used.
This behavior is the default, but it can be used to
override a CmdArgOptPreferShort from a parent.
Corresponding struct tag option: no_long_equals
*/
CmdArgOptLongNoEquals
/*
CmdArgOptForceNoPosix 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.
Has no effect if not running on Windows.
This behavior is the default, but it can be used to
override a CmdArgOptPreferShort from a parent.
See also the inverse of this option, CmdArgOptForcePosix.
Corresponding struct tag option: force_no_posix
*/
CmdArgOptForceNoPosix
/*
CmdArgOptForcePosix forces the resulting command string to use "POSIX" 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, then the POSIX flag syntax (-a/--arg) 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, CmdArgsOptForceNoPosix.
Corresponding struct tag option: force_posix
*/
CmdArgOptForcePosix
)

View File

@@ -1,45 +0,0 @@
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

@@ -19,9 +19,20 @@
package exec_extra
import (
"os/exec"
`fmt`
`os/exec`
`reflect`
`r00t2.io/goutils/bitmask`
`r00t2.io/goutils/structutils`
`r00t2.io/sysutils/errs`
)
/*
ExecCmdReturn runs cmd and alsom returns the exitStatus.
A non-zero exit status is not treated as an error.
*/
func ExecCmdReturn(cmd *exec.Cmd) (exitStatus int, err error) {
// https://stackoverflow.com/a/55055100/733214
err = cmd.Run()
@@ -31,14 +42,12 @@ func ExecCmdReturn(cmd *exec.Cmd) (exitStatus int, err error) {
}
/*
GetCmdFromStruct takes (a pointer to) a struct and returns a slice of
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 tag name used can be changed by setting the CmdArgsTag 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.
@@ -46,41 +55,301 @@ func ExecCmdReturn(cmd *exec.Cmd) (exitStatus int, err error) {
cmdarg:"short=l"
cmdarg:"long=list"
If the tag value is "-", or <VAR NAME> is not provided, the field will be explicitly skipped.
If the tag value is "-", the field will be explicitly skipped.
(This is the default behavior for struct fields not tagged with `cmdarg`.)
If the field is nil, it will be skipped.
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:
Aside from the 'short' and 'long' tag valued-options, see the comment for each CmdArgOpt* constant
for their corresponding tag option and the CmdArgs* variables as well for their corresponding tag option.
* short - A short flag for the argument
Each struct field can be one of the following types:
e.g.:
* string
* *string
* slice (with elements of supported types)
* array (with elements of supported types)
* map (with keys and values of supported types; see the CmdArgsDictSep variable for the separator to use)
* struct (with fields of supported types)
* int/int8/int16/int32/int64
* uint/uint8/uint16/uint32/uint64
* float32/float64
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"`
}
Struct fields, slice/array elements, etc. are processed in order.
Maps, because ordering is non-deterministic, may have unpredictable ordering.
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.
If s is nil, nothing will be done.
If s is not a pointer to a struct, nothing will be done.
*/
func GetCmdFromStruct[T any](s T, opts ...cmdArgOpt) (cmdSlice []string, err error) {
func GetCmdFromStruct[T any](s T, defaultOpts ...cmdArgOpt) (cmdSlice []string, err error) {
// TODO
var tmpSlice []string
var ptrVal reflect.Value
var ptrType reflect.Type
var ptrKind reflect.Kind
var argFlags *cmdArgFlag
var opts *bitmask.MaskBit = bitmask.NewMaskBit()
var sVal reflect.Value = reflect.ValueOf(s)
var sType reflect.Type = sVal.Type()
var kind reflect.Kind = sType.Kind()
if kind != reflect.Ptr {
return
}
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
ptrVal = sVal.Elem()
ptrType = ptrVal.Type()
ptrKind = ptrType.Kind()
if ptrKind != reflect.Struct {
return
}
tmpSlice = make([]string, 0)
if defaultOpts != nil && len(defaultOpts) != 0 {
for _, o := range defaultOpts {
opts.AddFlag(o.BitMask())
}
}
argFlags = &cmdArgFlag{
defaults: new(bitmask.MaskBit),
fieldOpts: new(bitmask.MaskBit),
boolMap: nil,
strMap: nil,
shortFlag: "",
longFlag: "",
field: nil,
value: &ptrVal,
argSlice: &tmpSlice,
}
*argFlags.defaults = *opts
*argFlags.fieldOpts = *opts
err = getCmdStruct(argFlags)
cmdSlice = tmpSlice
return
}
// getCmdStruct iterates over each field of reflect.Value struct v, and is called by GetCmdFromStruct.
func getCmdStruct(argFlags *cmdArgFlag) (err error) {
var t reflect.Type
var kind reflect.Kind
var fieldArgFlag *cmdArgFlag
if argFlags == nil {
return
}
if argFlags.value == nil {
return
}
t = argFlags.value.Type()
kind = t.Kind()
if kind != reflect.Struct {
err = errs.ErrBadType
return
}
for i := 0; i < argFlags.value.NumField(); i++ {
fieldArgFlag = new(cmdArgFlag)
*fieldArgFlag = *argFlags
fieldArgFlag.field = new(reflect.StructField)
fieldArgFlag.value = new(reflect.Value)
*fieldArgFlag.field = t.Field(i)
*fieldArgFlag.value = argFlags.value.Field(i)
if err = getCmdStructField(fieldArgFlag); err != nil {
return
}
}
return
}
// getCmdStructField parses an individual struct field.
func getCmdStructField(argFlags *cmdArgFlag) (err error) {
if argFlags == nil || argFlags.field == nil || argFlags.value == nil {
return
}
argFlags.boolMap = structutils.TagToBoolMap(*argFlags.field, CmdArgsTag, structutils.TagMapTrim)
if argFlags.boolMap["-"] {
return
}
argFlags.strMap = structutils.TagToStringMap(*argFlags.field, CmdArgsTag, structutils.TagMapTrim)
if argFlags.strMap == nil {
return
}
for key, val := range argFlags.strMap {
switch key {
case "short":
argFlags.shortFlag = val
case "long":
argFlags.longFlag = val
}
}
fmt.Println(argFlags.field.Name + ":")
fmt.Printf("BEFORE: %d\t%d\n", argFlags.defaults.Value(), argFlags.fieldOpts.Value())
argFlags.fieldOpts = parseCmdArgOpts(argFlags.fieldOpts, argFlags.defaults, *argFlags.field)
fmt.Printf("AFTER: %d\t%d\n\n", argFlags.defaults.Value(), argFlags.fieldOpts.Value())
/*
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return
}
err = getCmdStructField(field, v.Elem(), current, defaults, tmpSlice)
} else {
err = getCmdValue(v, opts, tagVals, tmpSlice)
}
*/
return
}
// getCmdValue is a dispatcher for a reflect value.
func getCmdValue(v reflect.Value, opts *bitmask.MaskBit, flagVals map[string]string, tmpSlice *[]string) (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 = getCmdValue(v, opts, tmpSlice); err != nil {
return
}
case reflect.String:
if err = getCmdString(v, opts, tmpSlice); err != nil {
return
}
return
case reflect.Slice, reflect.Array:
if err = getCmdSlice(v); err != nil {
}
case reflect.Map:
if err = getCmdMap(v); err != nil {
return
}
case reflect.Struct:
if err = getCmdStruct(v); err != nil {
return
}
}
*/
return
}
// parseCmdArgOpts returns a parsed, combined, etc. set of options into a single OR'd bitmask.
func parseCmdArgOpts(current *bitmask.MaskBit, defaults *bitmask.MaskBit, field reflect.StructField) (opts *bitmask.MaskBit) {
var tagOpts *bitmask.MaskBit = tagOptsToMask(field)
opts = defaults.Copy()
fmt.Printf(
"PARSE BEFORE:\n\tOPTS:\t%d\n\tCURRENT:\t%d\n\tDEFAULTS:\t%d\n\tTAGOPTS:\t%d\n",
opts.Value(),
)
for _, b := range []*bitmask.MaskBit{
current,
tagOpts,
} {
if b == nil {
continue
}
if b.HasFlag(CmdArgOptPreferShort.BitMask()) && !b.HasFlag(CmdArgOptPreferLong.BitMask()) {
opts.AddFlag(CmdArgOptPreferShort.BitMask())
opts.ClearFlag(CmdArgOptPreferLong.BitMask())
} else {
opts.AddFlag(CmdArgOptPreferLong.BitMask())
opts.ClearFlag(CmdArgOptPreferShort.BitMask())
}
if b.HasFlag(CmdArgOptShortEquals.BitMask()) && !b.HasFlag(CmdArgOptShortNoEquals.BitMask()) {
opts.AddFlag(CmdArgOptShortEquals.BitMask())
opts.ClearFlag(CmdArgOptShortNoEquals.BitMask())
} else {
opts.AddFlag(CmdArgOptShortNoEquals.BitMask())
opts.ClearFlag(CmdArgOptShortEquals.BitMask())
}
if b.HasFlag(CmdArgOptLongNoEquals.BitMask()) && !b.HasFlag(CmdArgOptLongEquals.BitMask()) {
opts.AddFlag(CmdArgOptLongNoEquals.BitMask())
opts.ClearFlag(CmdArgOptLongEquals.BitMask())
} else {
opts.AddFlag(CmdArgOptLongEquals.BitMask())
opts.ClearFlag(CmdArgOptLongNoEquals.BitMask())
}
if b.HasFlag(CmdArgOptForcePosix.BitMask()) && !b.HasFlag(CmdArgOptForceNoPosix.BitMask()) {
opts.AddFlag(CmdArgOptForcePosix.BitMask())
opts.ClearFlag(CmdArgOptForceNoPosix.BitMask())
} else {
opts.AddFlag(CmdArgOptForceNoPosix.BitMask())
opts.ClearFlag(CmdArgOptForcePosix.BitMask())
}
}
fmt.Printf("PARSE AFTER: %d\n", opts.Value())
return
}
// tagOptsToMask returns a bitmask.MaskBit from a struct field's tags.
func tagOptsToMask(field reflect.StructField) (b *bitmask.MaskBit) {
var o cmdArgOpt
var tagOpts map[string]bool = structutils.TagToBoolMap(field, CmdArgsTag, structutils.TagMapTrim)
b = bitmask.NewMaskBit()
// First round, these are normally disabled.
for k, v := range tagOpts {
switch k {
case "prefer_short":
o = CmdArgOptPreferShort
case "short_equals":
o = CmdArgOptShortEquals
case "no_long_equals":
o = CmdArgOptLongNoEquals
case "force_posix":
o = CmdArgOptForcePosix
}
if v {
b.AddFlag(o.BitMask())
} else {
b.ClearFlag(o.BitMask())
}
}
// Second round, these override the above.
for k, v := range tagOpts {
switch k {
case "prefer_long":
o = CmdArgOptPreferShort
case "no_short_equals":
o = CmdArgOptShortEquals
case "long_equals":
o = CmdArgOptLongNoEquals
case "force_no_posix":
o = CmdArgOptForcePosix
}
// Since these are meant to disable, we flip things around.
if v {
b.ClearFlag(o.BitMask())
} else {
b.AddFlag(o.BitMask())
}
}
return
}

View File

@@ -0,0 +1,13 @@
package exec_extra
import (
`r00t2.io/goutils/bitmask`
)
// BitMask returns the underlying bitmask.MaskBit representation of a cmdArgOpt.
func (c cmdArgOpt) BitMask() (b bitmask.MaskBit) {
b = bitmask.MaskBit(c)
return
}

27
exec_extra/funcs_test.go Normal file
View File

@@ -0,0 +1,27 @@
package exec_extra
import (
`testing`
)
type (
testStruct struct {
Foo string `cmdarg:"short=f,long=foo"`
Bar int `cmdarg:"short=b,long=bar,prefer_short"`
}
)
func TestGetCmdFromStruct(t *testing.T) {
var err error
var out []string
var v *testStruct = &testStruct{
Foo: "foo",
Bar: 123,
}
if out, err = GetCmdFromStruct(v); err != nil {
t.Fatalf("Received error getting command from struct: %v", err)
}
t.Logf("Got command args from struct:\n%#v", out)
}

View File

@@ -1,9 +1,25 @@
package exec_extra
type cmdArgsOpts struct {
preferShort bool
forcePosix bool
cmd *string
}
import (
`reflect`
type cmdArgOpt func(*cmdArgsOpts) (err error)
`r00t2.io/goutils/bitmask`
)
type (
cmdArgOpt bitmask.MaskBit
)
type (
cmdArgFlag struct {
defaults *bitmask.MaskBit
fieldOpts *bitmask.MaskBit
boolMap map[string]bool
strMap map[string]string
shortFlag string
longFlag string
field *reflect.StructField
value *reflect.Value
argSlice *[]string
}
)

View File

@@ -1,33 +0,0 @@
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
}