Skip to content

Commit 00d8c81

Browse files
authored
Drop regexp for performance (#74)
* Update comment syntax to Go 1.19 * Drop regexp for performance Fixes GH-72
1 parent cbe83d6 commit 00d8c81

File tree

2 files changed

+139
-40
lines changed

2 files changed

+139
-40
lines changed

envconfig.go

+66-40
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,54 @@
1414

1515
// Package envconfig populates struct fields based on environment variable
1616
// values (or anything that responds to "Lookup"). Structs declare their
17-
// environment dependencies using the `env` tag with the key being the name of
17+
// environment dependencies using the "env" tag with the key being the name of
1818
// the environment variable, case sensitive.
1919
//
20-
// type MyStruct struct {
21-
// A string `env:"A"` // resolves A to $A
22-
// B string `env:"B,required"` // resolves B to $B, errors if $B is unset
23-
// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
20+
// type MyStruct struct {
21+
// A string `env:"A"` // resolves A to $A
22+
// B string `env:"B,required"` // resolves B to $B, errors if $B is unset
23+
// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
2424
//
25-
// D string `env:"D,required,default=foo"` // error, cannot be required and default
26-
// E string `env:""` // error, must specify key
27-
// }
25+
// D string `env:"D,required,default=foo"` // error, cannot be required and default
26+
// E string `env:""` // error, must specify key
27+
// }
2828
//
2929
// All built-in types are supported except Func and Chan. If you need to define
3030
// a custom decoder, implement Decoder:
3131
//
32-
// type MyStruct struct {
33-
// field string
34-
// }
32+
// type MyStruct struct {
33+
// field string
34+
// }
3535
//
36-
// func (v *MyStruct) EnvDecode(val string) error {
37-
// v.field = fmt.Sprintf("PREFIX-%s", val)
38-
// return nil
39-
// }
36+
// func (v *MyStruct) EnvDecode(val string) error {
37+
// v.field = fmt.Sprintf("PREFIX-%s", val)
38+
// return nil
39+
// }
4040
//
4141
// In the environment, slices are specified as comma-separated values:
4242
//
43-
// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
43+
// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
4444
//
4545
// In the environment, maps are specified as comma-separated key:value pairs:
4646
//
47-
// export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
47+
// export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
4848
//
4949
// If you need to modify environment variable values before processing, you can
5050
// specify a custom mutator:
5151
//
52-
// type Config struct {
53-
// Password `env:"PASSWORD_SECRET"`
54-
// }
52+
// type Config struct {
53+
// Password `env:"PASSWORD_SECRET"`
54+
// }
5555
//
56-
// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
57-
// if strings.HasPrefix(value, "secret://") {
58-
// return secretmanager.Resolve(ctx, value) // example
59-
// }
60-
// return value, nil
61-
// }
62-
//
63-
// var config Config
64-
// ProcessWith(&config, OsLookuper(), resolveSecretFunc)
56+
// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
57+
// if strings.HasPrefix(value, "secret://") {
58+
// return secretmanager.Resolve(ctx, value) // example
59+
// }
60+
// return value, nil
61+
// }
6562
//
63+
// var config Config
64+
// ProcessWith(&config, OsLookuper(), resolveSecretFunc)
6665
package envconfig
6766

6867
import (
@@ -74,7 +73,6 @@ import (
7473
"fmt"
7574
"os"
7675
"reflect"
77-
"regexp"
7876
"strconv"
7977
"strings"
8078
"time"
@@ -95,8 +93,6 @@ const (
9593
defaultSeparator = ":"
9694
)
9795

98-
var envvarNameRe = regexp.MustCompile(`\A[a-zA-Z_][a-zA-Z0-9_]*\z`)
99-
10096
// Error is a custom error type for errors returned by envconfig.
10197
type Error string
10298

@@ -138,7 +134,7 @@ func (o *osLookuper) Lookup(key string) (string, bool) {
138134
return os.LookupEnv(key)
139135
}
140136

141-
// OsLookuper returns a lookuper that uses the environment (os.LookupEnv) to
137+
// OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to
142138
// resolve values.
143139
func OsLookuper() Lookuper {
144140
return new(osLookuper)
@@ -203,12 +199,11 @@ func MultiLookuper(lookupers ...Lookuper) Lookuper {
203199
// Decoder is an interface that custom types/fields can implement to control how
204200
// decoding takes place. For example:
205201
//
206-
// type MyType string
207-
//
208-
// func (mt MyType) EnvDecode(val string) error {
209-
// return "CUSTOM-"+val
210-
// }
202+
// type MyType string
211203
//
204+
// func (mt MyType) EnvDecode(val string) error {
205+
// return "CUSTOM-"+val
206+
// }
212207
type Decoder interface {
213208
EnvDecode(val string) error
214209
}
@@ -229,7 +224,7 @@ type options struct {
229224
Required bool
230225
}
231226

232-
// Process processes the struct using the environment. See ProcessWith for a
227+
// Process processes the struct using the environment. See [ProcessWith] for a
233228
// more customizable version.
234229
func Process(ctx context.Context, i interface{}) error {
235230
return ProcessWith(ctx, i, OsLookuper())
@@ -427,7 +422,7 @@ func keyAndOpts(tag string) (string, *options, error) {
427422
parts := strings.Split(tag, ",")
428423
key, tagOpts := strings.TrimSpace(parts[0]), parts[1:]
429424

430-
if key != "" && !envvarNameRe.MatchString(key) {
425+
if key != "" && !validateEnvName(key) {
431426
return "", nil, fmt.Errorf("%q: %w ", key, ErrInvalidEnvvarName)
432427
}
433428

@@ -689,3 +684,34 @@ func processField(v string, ef reflect.Value, delimiter, separator string, noIni
689684

690685
return nil
691686
}
687+
688+
// validateEnvName validates the given string conforms to being a valid
689+
// environment variable.
690+
//
691+
// Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase
692+
// letters, digits, and _, and do not begin with a digit.
693+
func validateEnvName(s string) bool {
694+
if s == "" {
695+
return false
696+
}
697+
698+
for i, r := range s {
699+
if (i == 0 && !isLetter(r)) || (!isLetter(r) && !isNumber(r) && r != '_') {
700+
return false
701+
}
702+
}
703+
704+
return true
705+
}
706+
707+
// isLetter returns true if the given rune is a letter between a-z,A-Z. This is
708+
// different than unicode.IsLetter which includes all L character cases.
709+
func isLetter(r rune) bool {
710+
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
711+
}
712+
713+
// isNumber returns true if the given run is a number between 0-9. This is
714+
// different than unicode.IsNumber in that it only allows 0-9.
715+
func isNumber(r rune) bool {
716+
return r >= '0' && r <= '9'
717+
}

envconfig_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -2436,3 +2436,76 @@ func TestProcessWith(t *testing.T) {
24362436
})
24372437
}
24382438
}
2439+
2440+
func TestValidateEnvName(t *testing.T) {
2441+
t.Parallel()
2442+
2443+
cases := []struct {
2444+
name string
2445+
in string
2446+
exp bool
2447+
}{
2448+
{
2449+
name: "empty",
2450+
in: "",
2451+
exp: false,
2452+
},
2453+
{
2454+
name: "space",
2455+
in: " ",
2456+
exp: false,
2457+
},
2458+
{
2459+
name: "digit_start",
2460+
in: "1FOO",
2461+
exp: false,
2462+
},
2463+
{
2464+
name: "emoji_start",
2465+
in: "🚀",
2466+
exp: false,
2467+
},
2468+
{
2469+
name: "lowercase_start",
2470+
in: "f",
2471+
exp: true,
2472+
},
2473+
{
2474+
name: "lowercase",
2475+
in: "foo",
2476+
exp: true,
2477+
},
2478+
{
2479+
name: "uppercase_start",
2480+
in: "F",
2481+
exp: true,
2482+
},
2483+
{
2484+
name: "uppercase",
2485+
in: "FOO",
2486+
exp: true,
2487+
},
2488+
{
2489+
name: "emoji_middle",
2490+
in: "FOO🚀",
2491+
exp: false,
2492+
},
2493+
{
2494+
name: "space_middle",
2495+
in: "FOO BAR",
2496+
exp: false,
2497+
},
2498+
}
2499+
2500+
for _, tc := range cases {
2501+
tc := tc
2502+
2503+
t.Run(tc.name, func(t *testing.T) {
2504+
t.Parallel()
2505+
2506+
if got, want := validateEnvName(tc.in), tc.exp; got != want {
2507+
t.Errorf("expected %q to be %t (got %t)", tc.in, want, got)
2508+
}
2509+
})
2510+
}
2511+
}

0 commit comments

Comments
 (0)