-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathenvfmt.go
140 lines (132 loc) · 3.92 KB
/
envfmt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package modmake
import (
"bufio"
"bytes"
"io"
"os"
"strings"
)
// EnvMap is a specialized map for holding environment variables that are used to interpolate strings.
// Note that map keys are changed to a consistent case when merged with the environment values.
// So if multiple keys with the same characters but different cases are merged, then the eventual merged value is non-deterministic.
type EnvMap map[string]string
func (m EnvMap) merge(other EnvMap) EnvMap {
if other == nil {
return m
}
for k, v := range other {
if len(k) == 0 {
continue
}
m[strings.ToUpper(k)] = v
}
return m
}
// Environment returns the currently set environment values as an EnvMap.
func Environment() EnvMap {
m := EnvMap{}
for _, entry := range os.Environ() {
kv := strings.SplitN(entry, "=", 2)
if len(kv) != 2 {
m[strings.ToUpper(kv[0])] = ""
}
m[strings.ToUpper(kv[0])] = kv[1]
}
return m
}
// F will format a string, replacing variables with their value as found in the environment data.
// Additional values may be added as the second parameter, which will override values in the original environment.
//
// Variable placeholders may be specified like ${ENV_VAR_NAME}. The example below will replace ${BUILD_NUM} with a value from the environment, or an empty string.
// If a variable either doesn't exist in the environment, or has an empty value, then an empty string will replace the variable placeholder.
//
// str := F("My string that references build ${BUILD_NUM}")
//
// Note that the "${" prefix and "}" suffix are required, but the variable name may be space padded for readability if desired.
// Also, variable names are case insensitive.
//
// A default value for interpolation can be specified with a colon (":") separator after the key.
// In the example below, if BUILD_NUM was not defined, then the string "0" would be used instead.
//
// str := F("My string that references build ${BUILD_NUM:0}")
//
// Note that whitespace characters in default values will always be trimmed.
// The string "${" can still be expressed in F strings, but it must be formatted to use a default value, which means that interpolation cannot be recursive.
// The string output from the function call below will be "My string has a variable reference ${BUILD_NUM}".
//
// str := F("My string has a variable reference ${:$}{BUILD_NUM}")
func F(fmt string, data ...EnvMap) string {
return string(FReader(strings.NewReader(fmt), data...))
}
// FReader will do the same thing as F, but operates on an io.Reader expressing a stream of UTF-8 encoded bytes instead.
func FReader(in io.Reader, data ...EnvMap) []byte {
var rr io.RuneReader
if _rr, ok := in.(io.RuneReader); ok {
rr = _rr
} else {
rr = bufio.NewReader(in)
}
m := Environment()
if len(data) > 0 {
m.merge(data[0])
}
return parseString(rr, m)
}
func parseString(in io.RuneReader, data EnvMap) []byte {
const (
DOLLAR rune = '$'
BRACE rune = '{'
)
var (
outBuf bytes.Buffer
)
for {
r, _, err := in.ReadRune()
if err != nil {
return outBuf.Bytes()
}
switch r {
case DOLLAR:
maybeBrace, _, err := in.ReadRune()
if err != nil {
outBuf.WriteRune(r)
return outBuf.Bytes()
}
if maybeBrace == BRACE {
outBuf.Write(replaceIdentifier(in, data))
} else {
outBuf.WriteRune(r)
outBuf.WriteRune(maybeBrace)
}
default:
outBuf.WriteRune(r)
}
}
}
func replaceIdentifier(in io.RuneReader, data EnvMap) []byte {
const (
END_BRACE = '}'
)
var (
varBuf strings.Builder
defaultValue string
)
for {
r, _, err := in.ReadRune()
if err != nil {
return nil
}
if r == END_BRACE {
baseKey := strings.TrimSpace(varBuf.String())
if before, after, found := strings.Cut(baseKey, ":"); found {
baseKey, defaultValue = strings.TrimSpace(before), strings.TrimSpace(after)
}
baseKey = strings.ToUpper(baseKey)
if val, ok := data[baseKey]; ok {
return []byte(val)
}
return []byte(defaultValue)
}
varBuf.WriteRune(r)
}
}