Skip to content

Commit

Permalink
✨ feat: varexpr - add new internal package varexpr for parse ENV var
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Oct 13, 2023
1 parent da9fd86 commit d829299
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 72 deletions.
4 changes: 2 additions & 2 deletions cliutil/cmdline/parser.go
Expand Up @@ -6,7 +6,7 @@ import (
"strings"

"github.com/gookit/goutil/comdef"
"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/internal/varexpr"
"github.com/gookit/goutil/strutil"
)

Expand Down Expand Up @@ -86,7 +86,7 @@ func (p *LineParser) Parse() []string {

// enable parse Env var
if p.ParseEnv {
p.Line = comfunc.ParseEnvVar(p.Line, nil)
p.Line = varexpr.SafeParse(p.Line)
}

p.nodes = strings.Split(p.Line, " ")
Expand Down
31 changes: 20 additions & 11 deletions envutil/envutil.go
Expand Up @@ -4,7 +4,7 @@ package envutil
import (
"os"

"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/internal/varexpr"
)

// ValueGetter Env value provider func.
Expand All @@ -17,14 +17,13 @@ var ValueGetter = os.Getenv
// is alias of the os.ExpandEnv()
func VarReplace(s string) string { return os.ExpandEnv(s) }

// VarParse alias of the ParseValue
func VarParse(val string) string {
return comfunc.ParseEnvVar(val, ValueGetter)
}

// ParseEnvValue alias of the ParseValue
func ParseEnvValue(val string) string {
return comfunc.ParseEnvVar(val, ValueGetter)
// ParseOrErr parse ENV var value from input string, support default value.
//
// Diff with the ParseValue, this support return error.
//
// With error format: ${VAR_NAME | ?error}
func ParseOrErr(val string) (string, error) {
return varexpr.Parse(val)
}

// ParseValue parse ENV var value from input string, support default value.
Expand All @@ -38,8 +37,18 @@ func ParseEnvValue(val string) string {
//
// envutil.ParseValue("${ APP_NAME }")
// envutil.ParseValue("${ APP_ENV | dev }")
func ParseValue(val string) (newVal string) {
return comfunc.ParseEnvVar(val, ValueGetter)
func ParseValue(val string) string {
return varexpr.SafeParse(val)
}

// VarParse alias of the ParseValue
func VarParse(val string) string {
return varexpr.SafeParse(val)
}

// ParseEnvValue alias of the ParseValue
func ParseEnvValue(val string) string {
return varexpr.SafeParse(val)
}

// SetEnvMap set multi ENV(string-map) to os
Expand Down
16 changes: 16 additions & 0 deletions envutil/envutil_test.go
Expand Up @@ -65,6 +65,22 @@ func TestParseEnvValue(t *testing.T) {
})
}

func TestParseOrErr(t *testing.T) {
val, err := ParseOrErr("${NotExist | ?error msg}")
assert.ErrMsg(t, err, "error msg")
assert.Eq(t, "", val)

val, err = ParseOrErr("${NotExist | ?}")
assert.ErrMsg(t, err, "value is required for var: NotExist")
assert.Eq(t, "", val)

testutil.MockEnvValue("NotExist", "val", func(eVal string) {
val, err = ParseOrErr("${NotExist | ?}")
assert.NoErr(t, err)
assert.Eq(t, "val", val)
})
}

func TestSetEnvs(t *testing.T) {
envMp := map[string]string{
"FirstEnv": "abc",
Expand Down
54 changes: 0 additions & 54 deletions internal/comfunc/comfunc.go
Expand Up @@ -26,60 +26,6 @@ func Environ() map[string]string {
return envMap
}

// parse env value, allow:
//
// only key - "${SHELL}"
// with default - "${NotExist | defValue}"
// multi key - "${GOPATH}/${APP_ENV | prod}/dir"
//
// Notice:
//
// must add "?" - To ensure that there is no greedy match
// var envRegex = regexp.MustCompile(`\${[\w-| ]+}`)
var envRegex = regexp.MustCompile(`\${.+?}`)

// ParseEnvVar parse ENV var value from input string, support default value.
//
// Format:
//
// ${var_name} Only var name
// ${var_name | default} With default value
//
// Usage:
//
// comfunc.ParseEnvVar("${ APP_NAME }")
// comfunc.ParseEnvVar("${ APP_ENV | dev }")
func ParseEnvVar(val string, getFn func(string) string) (newVal string) {
if !strings.Contains(val, "${") {
return val
}

// default use os.Getenv
if getFn == nil {
getFn = os.Getenv
}

var name, def string
return envRegex.ReplaceAllStringFunc(val, func(eVar string) string {
// eVar like "${NotExist|defValue}", first remove "${" and "}", then split it
ss := strings.SplitN(eVar[2:len(eVar)-1], "|", 2)

// with default value. ${NotExist|defValue}
if len(ss) == 2 {
name, def = strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1])
} else {
name = strings.TrimSpace(ss[0])
}

// get ENV value by name
eVal := getFn(name)
if eVal == "" {
eVal = def
}
return eVal
})
}

var (
// TIP: extend unit d,w. eg: "1d", "2w"
// time.ParseDuration() is max support hour "h".
Expand Down
145 changes: 145 additions & 0 deletions internal/varexpr/varexpr.go
@@ -0,0 +1,145 @@
// Package varexpr provides some commonly ENV var parse functions.
//
// parse env value, allow expressions:
//
// ${VAR_NAME} Only var name
// ${VAR_NAME | default} With default value, if value is empty.
// ${VAR_NAME | ?error} With error on value is empty.
//
// Examples:
//
// only key - "${SHELL}"
// with default - "${NotExist | defValue}"
// multi key - "${GOPATH}/${APP_ENV | prod}/dir"
package varexpr

import (
"errors"
"os"
"regexp"
"strings"
)

// SepChar separator char
const SepChar = "|"

// ParseOptFn option func
type ParseOptFn func(o *ParseOpts)

// ParseOpts parse options for ParseValue
type ParseOpts struct {
// Getter Env value provider func.
Getter func(string) string
// ParseFn custom parse expr func. expr like "${SHELL}" "${NotExist|defValue}"
ParseFn func(string) (string, error)
// Regexp custom expression regex.
Regexp *regexp.Regexp
// Keyword check chars for expression. default is "${"
Keyword string
}

// must add "?" - To ensure that there is no greedy match
var envRegex = regexp.MustCompile(`\${.+?}`)
var std = New()

// Parse parse ENV var value from input string, support default value.
//
// Format:
//
// ${var_name} Only var name
// ${var_name | default} With default value
// ${var_name | ?error} With error on value is empty.
func Parse(val string) (string, error) {
return std.Parse(val)
}

// SafeParse parse ENV var value from input string, support default value.
func SafeParse(val string) string {
s, _ := std.Parse(val)
return s
}

// ParseWith parse ENV var value from input string, support default value.
func ParseWith(val string, optFns ...ParseOptFn) (string, error) {
return New(optFns...).Parse(val)
}

// Parser parse ENV var value from input string, support default value.
type Parser struct {
ParseOpts
}

// New create a new Parser
func New(optFns ...ParseOptFn) *Parser {
opts := &ParseOpts{
Getter: os.Getenv,
Regexp: envRegex,
Keyword: "${",
}
for _, fn := range optFns {
fn(opts)
}

return &Parser{ParseOpts: *opts}
}

// Parse parse ENV var value from input string, support default value.
//
// Format:
//
// ${var_name} Only var name
// ${var_name | default} With default value
// ${var_name | ?error} With error on value is empty.
func (p *Parser) Parse(val string) (newVal string, err error) {
if p.Regexp == nil {
p.Regexp = envRegex
}

if p.Keyword != "" && !strings.Contains(val, p.Keyword) {
return val, nil
}

// parse expression
newVal = p.Regexp.ReplaceAllStringFunc(val, func(s string) string {
if err != nil {
return s
}
s, err = p.parseOne(s)
return s
})
return
}

// parse one node expression.
func (p *Parser) parseOne(eVar string) (val string, err error) {
if p.ParseFn != nil {
return p.ParseFn(eVar)
}

// eVar like "${NotExist|defValue}", first remove "${" and "}", then split it
ss := strings.SplitN(eVar[2:len(eVar)-1], SepChar, 2)
var name, def string

// with default value. ${NotExist|defValue}
if len(ss) == 2 {
name, def = strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1])
} else {
name = strings.TrimSpace(ss[0])
}

// get ENV value by name
val = p.Getter(name)
if val == "" && def != "" {
// check def is "?error"
if def[0] == '?' {
msg := "value is required for var: " + name
if len(def) > 1 {
msg = def[1:]
}
err = errors.New(msg)
} else {
val = def
}
}
return
}
4 changes: 2 additions & 2 deletions structs/init.go
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"reflect"

"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/internal/varexpr"
"github.com/gookit/goutil/reflects"
"github.com/gookit/goutil/strutil"
)
Expand Down Expand Up @@ -174,7 +174,7 @@ func initDefaultValue(fv reflect.Value, val string, parseEnv bool) error {

// parse env var
if parseEnv {
val = comfunc.ParseEnvVar(val, nil)
val = varexpr.SafeParse(val)
}

var anyVal any = val
Expand Down
6 changes: 3 additions & 3 deletions strutil/textutil/var_replacer.go
Expand Up @@ -6,7 +6,7 @@ import (
"strings"

"github.com/gookit/goutil/arrutil"
"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/internal/varexpr"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/strutil"
)
Expand Down Expand Up @@ -146,7 +146,7 @@ func (r *VarReplacer) Render(s string, tplVars map[string]any) string {
maputil.FlatWithFunc(tplVars, func(path string, val reflect.Value) {
if val.Kind() == reflect.String {
if r.parseEnv {
varMap[path] = comfunc.ParseEnvVar(val.String(), nil)
varMap[path] = varexpr.SafeParse(val.String())
} else {
varMap[path] = val.String()
}
Expand Down Expand Up @@ -174,7 +174,7 @@ func (r *VarReplacer) RenderSimple(s string, varMap map[string]string) string {

if r.parseEnv {
for name, val := range varMap {
varMap[name] = comfunc.ParseEnvVar(val, nil)
varMap[name] = varexpr.SafeParse(val)
}
}

Expand Down

0 comments on commit d829299

Please sign in to comment.