/
env.go
243 lines (208 loc) · 6.02 KB
/
env.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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
package env
import (
"bufio"
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/goanywhere/fs"
)
const tag string = "env"
var pattern = regexp.MustCompile(`(export\s*)?(?P<key>\w+)\s*(=)\s*(?P<value>("|')?[\w\s-$,:/\.\+]+)("|')?`)
// findKeyValue finds '=' separated key/value pair from the given string.
func findKeyValue(line string) (key, value string) {
// Intentionally insert a linebreak here to avoid non-stop characters on [[:graph:]].
line = fmt.Sprintf("%s\n", line)
match := pattern.FindStringSubmatch(line)
if len(match) != 0 {
result := make(map[string]string)
for index, name := range pattern.SubexpNames() {
result[name] = match[index]
}
// preserve those quoted spaces for value.
key, value = result["key"], strings.TrimSpace(result["value"])
value = strings.Trim(value, "'")
value = strings.Trim(value, "\"")
}
return
}
// Load parses & set the values in the given files into os environment.
func Load(dotenv string) {
if fs.Exists(dotenv) {
dotenv, _ = filepath.Abs(dotenv)
if file, err := os.Open(dotenv); err == nil {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
k, v := findKeyValue(scanner.Text())
if k != "" && v != "" {
Set(k, v)
} else {
continue
}
}
}
}
}
// Get retrieves the string value of the environment variable named by the key.
// It returns the value, which will be empty if the variable is not present.
func Get(key string) (value string, exists bool) {
if v := os.Getenv(key); v != "" {
value, exists = v, true
}
return
}
// Set stores the value of the environment variable named by the key. It returns an error, if any.
func Set(key string, value interface{}) error {
var sv string
switch T := value.(type) {
case bool:
sv = strconv.FormatBool(T)
case float32, float64:
sv = strconv.FormatFloat(reflect.ValueOf(value).Float(), 'g', -1, 64)
case int, int8, int16, int32, int64:
sv = strconv.FormatInt(reflect.ValueOf(value).Int(), 10)
case uint, uint8, uint16, uint32, uint64:
sv = strconv.FormatUint(reflect.ValueOf(value).Uint(), 10)
case string:
sv = value.(string)
case []string:
sv = strings.Join(value.([]string), ",")
default:
return fmt.Errorf("Unsupported type: %v", T)
}
return os.Setenv(key, sv)
}
// String retrieves the string value from environment named by the key.
func String(key string, fallback ...string) (value string) {
if str, exists := Get(key); exists {
value = str
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Strings retrieves the string values separated by comma from the environment.
func Strings(key string, fallback ...[]string) (value []string) {
if v, exists := Get(key); exists {
for _, item := range strings.Split(strings.TrimSpace(v), ",") {
value = append(value, strings.TrimSpace(item))
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Int retrieves the integer values separated by comma from the environment.
func Int(key string, fallback ...int) (value int) {
if str, exists := Get(key); exists {
if v, e := strconv.ParseInt(str, 10, 0); e == nil {
value = int(v)
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Int retrieves the 64-bit integer values separated by comma from the environment.
func Int64(key string, fallback ...int64) (value int64) {
if str, exists := Get(key); exists {
if v, e := strconv.ParseInt(str, 10, 64); e == nil {
value = v
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Uint retrieves the unsigned integer values separated by comma from the environment.
func Uint(key string, fallback ...uint) (value uint) {
if str, exists := Get(key); exists {
if v, e := strconv.ParseUint(str, 10, 0); e == nil {
value = uint(v)
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Uint64 retrieves the 64-bit unsigned integer values separated by comma from the environment.
func Uint64(key string, fallback ...uint64) (value uint64) {
if str, exists := Get(key); exists {
if v, e := strconv.ParseUint(str, 10, 64); e == nil {
value = v
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Bool retrieves boolean value from the environment.
func Bool(key string, fallback ...bool) (value bool) {
if str, exists := Get(key); exists {
if v, e := strconv.ParseBool(str); e == nil {
value = v
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Float retrieves float (64) value from the environment.
func Float(key string, fallback ...float64) (value float64) {
if str, exists := Get(key); exists {
if v, e := strconv.ParseFloat(str, 64); e == nil {
value = v
}
} else if len(fallback) > 0 {
value = fallback[0]
}
return
}
// Map fetches the key/value pair from os.Environ into the given spec.
// Tags are supported via `env:ACTUAL_OS_KEY`.
func Map(spec interface{}) error {
value := reflect.ValueOf(spec)
s := value.Elem()
var stype reflect.Type = s.Type()
var field reflect.Value
for index := 0; index < s.NumField(); index++ {
field = s.Field(index)
if field.CanSet() {
key := stype.Field(index).Tag.Get(tag)
if key == "" {
key = stype.Field(index).Name
}
value, exists := Get(key)
if !exists {
continue
}
// converts the environmental value from string to its real type.
// Supports: String | Strings | Bool | Float | Integer | Unsiged Integer
switch field.Kind() {
case reflect.String:
field.SetString(value)
case reflect.Bool:
field.SetBool(Bool(key))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
field.SetInt(Int64(key))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
field.SetUint(Uint64(key))
case reflect.Float32, reflect.Float64:
field.SetFloat(Float(key))
case reflect.Slice:
switch field.Interface().(type) {
case []string:
field.Set(reflect.ValueOf(Strings(key)))
default:
log.Fatalf("Only string slice is supported")
}
}
}
}
return nil
}