# env | |||||
env | |||||
=== | |||||
[![Build Status](https://travis-ci.org/goanywhere/env.svg?branch=master)](https://travis-ci.org/goanywhere/env) [![GoDoc](https://godoc.org/github.com/goanywhere/env?status.svg)](http://godoc.org/github.com/goanywhere/env) | |||||
Ease of Accessing Environment Varaibles in Golang | Ease of Accessing Environment Varaibles in Golang | ||||
### Installation | |||||
```shell | |||||
$ go get -v github.com/goanywhere/env | |||||
``` | |||||
### Usage | |||||
```shell | |||||
PORT=9394 | |||||
SECRET_KEY=YOURSECRETKEY | |||||
``` | |||||
You can double/single quote string values: | |||||
```shell | |||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----HkVN9…-----END DSA PRIVATE KEY-----" | |||||
``` | |||||
You can use `export` in front of each line just like your shell settings, so that you can `source` the file in your terminal directly: | |||||
```shell | |||||
export USERNAME=account@goanywhere.io | |||||
export PASSWORD=AccountPasswordGoesHere | |||||
``` | |||||
Export these values, you can then access them using env now. | |||||
``` go | |||||
package main | |||||
import ( | |||||
"github.com/goanywhere/env" | |||||
) | |||||
func main() { | |||||
env.Int("PORT") // 9394 | |||||
env.String("SECRET_KEY") // YOURSECERTKEY | |||||
....... | |||||
} | |||||
``` | |||||
`env` also supports custom struct for you to access the reflected values. | |||||
``` go | |||||
package main | |||||
import ( | |||||
"fmt" | |||||
"github.com/goanywhere/env" | |||||
) | |||||
type Spec struct { | |||||
App string | |||||
SecretKey string `env:"SECRET_KEY"` | |||||
} | |||||
func main() { | |||||
var spec Spec | |||||
env.Set("App", "myapplication") | |||||
env.Set("SECRET_KEY", "wGv7ELIx8P8qsUit9OuWw2zwPEF0nXtvjIKZQOioAVuI5GnHSwBAeWZ6l4-SpIPT") | |||||
env.Map(&spec) | |||||
fmt.Printf("App: %s", spec.App) // output: "App: myapplication" | |||||
fmt.Printf("Secret: %s", spec.SecretKey) // output: "Secret: wGv7ELIx8P8qsUit9OuWw2zwPEF0nXtvjIKZQOioAVuI5GnHSwBAeWZ6l4-SpIPT" | |||||
} | |||||
``` | |||||
We also includes dotenv supports, simply add the application settings to file `.env` right under the root of your project: | |||||
``` text | |||||
test1 = value1 | |||||
test2 = 'value2' | |||||
test3 = "value3" | |||||
export test4=value4 | |||||
``` | |||||
``` go | |||||
package main | |||||
import ( | |||||
"fmt" | |||||
"github.com/goanywhere/env" | |||||
) | |||||
func main() { | |||||
// Load '.env' from current working directory. | |||||
env.Load() | |||||
fmt.Printf("<test1: %s>", env.String("test1")) // output: "value1" | |||||
fmt.Printf("<test2: %s>", env.String("test2")) // output: "value2" | |||||
} | |||||
``` | |||||
### NOTES | |||||
Sensitive settings should **ONLY** be accessible on the machines that need access to them. **NEVER** commit them to a repository (even a private one) that is not needed by every development machine and server. |
package env | |||||
import ( | |||||
_ "github.com/smartystreets/goconvey/convey" | |||||
) |
package env | |||||
/* | |||||
env | |||||
=== | |||||
Ease of Accessing Environment Varaibles. | |||||
## Installation | |||||
```shell | |||||
$ go get -v github.com/goanywhere/env | |||||
``` | |||||
## Usage | |||||
Add the application settings to file `.env` right under the root of your project: | |||||
```shell | |||||
MY_SECRET_KEY=YOURSECRETKEY | |||||
Case_Will_Be_IgNoreD=YOURSECRETKEYGOESHERE | |||||
``` | |||||
You can double/single quote string values: | |||||
```shell | |||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----HkVN9…-----END DSA PRIVATE KEY-----" | |||||
``` | |||||
You can use `export` in front of each line just like your shell settings, so that you can `source` the file in your terminal directly: | |||||
```shell | |||||
export USERNAME=account@goanywhere.io | |||||
export PASSWORD=AccountPasswordGoesHere | |||||
``` | |||||
All set now, you are good to Go :-) | |||||
``` go | |||||
package main | |||||
import ( | |||||
"github.com/goanywhere/env" | |||||
"github.com/goanywhere/rex" | |||||
) | |||||
func index (ctx *rex.Context) { | |||||
ctx.HTML("index.html") | |||||
} | |||||
func main() { | |||||
// Override default 5000 port here. | |||||
env.Set("port", "9394") | |||||
app := rex.New() | |||||
app.Get("/", index) | |||||
app.Serve() | |||||
} | |||||
``` | |||||
You will now have the HTTP server running on `0.0.0.0:9394`. | |||||
`env` supports namespace (case-insensitive, same as the key). | |||||
``` go | |||||
import ( | |||||
"fmt" | |||||
"github.com/goanywhere/env" | |||||
) | |||||
func main() { | |||||
env.Set("debug", "false", "production") | |||||
fmt.Printf("debug: %s", env.Get("debug", "production")) | |||||
} | |||||
``` | |||||
`env` also supports custom struct for you to access the reflected values (the key is case-insensitive). | |||||
``` go | |||||
package main | |||||
import ( | |||||
"fmt" | |||||
"github.com/goanywhere/env" | |||||
) | |||||
type Spec struct { | |||||
App string | |||||
} | |||||
func main() { | |||||
var spec Spec | |||||
env.Set("app", "myapplication") | |||||
env.Map(&spec) | |||||
fmt.Printf("App: %s", spec.App) // output: "App: myapplication" | |||||
} | |||||
``` | |||||
We also includes dotenv supports: | |||||
``` text | |||||
test1 = value1 | |||||
test2 = 'value2' | |||||
test3 = "value3" | |||||
export test4=value4 | |||||
``` | |||||
``` go | |||||
package main | |||||
import ( | |||||
"fmt" | |||||
"github.com/goanywhere/env" | |||||
) | |||||
func main() { | |||||
// Load '.env' from current working directory. | |||||
env.Load(".env") | |||||
fmt.Printf("<test: %s>", env.Get("test")) // output: "value" | |||||
fmt.Printf("<test2: %s>", env.Get("test2")) // output: "value2" | |||||
} | |||||
``` | |||||
## NOTES | |||||
Sensitive settings should **ONLY** be accessible on the machines that need access to them. **NEVER** commit them to a repository (even a private one) that is not needed by every development machine and server. | |||||
*/ |
package env | |||||
import ( | |||||
"bufio" | |||||
"fmt" | |||||
"os" | |||||
"path/filepath" | |||||
"reflect" | |||||
"regexp" | |||||
"strconv" | |||||
"strings" | |||||
log "github.com/Sirupsen/logrus" | |||||
"github.com/goanywhere/x/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 | |||||
} |
package env | |||||
import ( | |||||
"bufio" | |||||
"fmt" | |||||
"os" | |||||
"testing" | |||||
"github.com/goanywhere/x/crypto" | |||||
. "github.com/smartystreets/goconvey/convey" | |||||
) | |||||
func TestFindKeyValue(t *testing.T) { | |||||
Convey("env.findKeyValue", t, func() { | |||||
k, v := findKeyValue(" test= value") | |||||
So(k, ShouldEqual, "test") | |||||
So(v, ShouldEqual, "value") | |||||
k, v = findKeyValue(" test= value") | |||||
So(k, ShouldEqual, "test") | |||||
So(v, ShouldEqual, "value") | |||||
k, v = findKeyValue("\ttest=\tvalue\t\n") | |||||
So(k, ShouldEqual, "test") | |||||
So(v, ShouldEqual, "value") | |||||
k, v = findKeyValue("export Test=\"Example\"") | |||||
So(k, ShouldEqual, "Test") | |||||
So(v, ShouldEqual, "Example") | |||||
k, v = findKeyValue(`export Secret_Keys=IK-vyX7OuiftwyasT6NXnEYyPMj8fEDssJZdppKOs8Y4hZTtWfUILer73RbsG78Q`) | |||||
So(k, ShouldEqual, "Secret_Keys") | |||||
So(v, ShouldEqual, "IK-vyX7OuiftwyasT6NXnEYyPMj8fEDssJZdppKOs8Y4hZTtWfUILer73RbsG78Q") | |||||
}) | |||||
} | |||||
func TestLoad(t *testing.T) { | |||||
filename := "/tmp/.env" | |||||
// plain value without quote | |||||
if dotenv, err := os.Create(filename); err == nil { | |||||
defer dotenv.Close() | |||||
defer os.Remove(filename) | |||||
secret := crypto.Random(64) | |||||
buffer := bufio.NewWriter(dotenv) | |||||
buffer.WriteString(fmt.Sprintf("secret=%s\n", secret)) | |||||
buffer.WriteString("app=myapp\n") | |||||
buffer.WriteString("export exportation=myexports") | |||||
buffer.Flush() | |||||
Convey("env.Load (without quoting)", t, func() { | |||||
Set("root", "/tmp") | |||||
Load("/tmp/.env") | |||||
So(String("secret"), ShouldEqual, secret) | |||||
So(String("app"), ShouldEqual, "myapp") | |||||
So(String("exportation"), ShouldEqual, "myexports") | |||||
}) | |||||
} | |||||
// value with `` quote | |||||
if dotenv, err := os.Create(filename); err == nil { | |||||
defer dotenv.Close() | |||||
defer os.Remove(filename) | |||||
secret := crypto.Random(64) | |||||
buffer := bufio.NewWriter(dotenv) | |||||
buffer.WriteString(fmt.Sprintf("secret='%s'\n", secret)) | |||||
buffer.WriteString("app='myapp'\n") | |||||
buffer.WriteString("export account='username'\n") | |||||
buffer.Flush() | |||||
Convey("env.Load (with quoting)", t, func() { | |||||
Set("root", "/tmp") | |||||
Load("/tmp/.env") | |||||
So(String("secret"), ShouldEqual, secret) | |||||
So(String("app"), ShouldEqual, "myapp") | |||||
So(String("account"), ShouldEqual, "username") | |||||
}) | |||||
} | |||||
// value with `"` quote | |||||
if dotenv, err := os.Create(filename); err == nil { | |||||
defer dotenv.Close() | |||||
defer os.Remove(filename) | |||||
secret := crypto.Random(64) | |||||
buffer := bufio.NewWriter(dotenv) | |||||
buffer.WriteString(fmt.Sprintf("secret=\"%s\"\n", secret)) | |||||
buffer.WriteString("app=\"myapp\"\n") | |||||
buffer.WriteString("export account=\"username\"\n") | |||||
buffer.Flush() | |||||
Convey("env.Load (with single quoting)", t, func() { | |||||
Set("root", "/tmp") | |||||
Load("/tmp/.env") | |||||
So(String("secret"), ShouldEqual, secret) | |||||
So(String("app"), ShouldEqual, "myapp") | |||||
So(String("account"), ShouldEqual, "username") | |||||
}) | |||||
} | |||||
} | |||||
func TestMap(t *testing.T) { | |||||
type Person struct { | |||||
Username string | |||||
Age uint | |||||
Kids int | |||||
Checked bool | |||||
Money float64 | |||||
FirstName string `env:"FIRST_NAME"` | |||||
Names []string | |||||
} | |||||
var person Person | |||||
Convey("env.Map", t, func() { | |||||
Set("Username", "abc") | |||||
Set("Age", 100) | |||||
Set("Kids", 2) | |||||
Set("Checked", true) | |||||
Set("Money", 1234567890.0987654321) | |||||
Set("FIRST_NAME", "abc") | |||||
Set("Names", "a,b,c,d,e,f,g") | |||||
Map(&person) | |||||
So(person.Username, ShouldEqual, "abc") | |||||
So(person.Age, ShouldEqual, 100) | |||||
So(person.Kids, ShouldEqual, 2) | |||||
So(person.Checked, ShouldBeTrue) | |||||
So(person.Money, ShouldEqual, 1234567890.0987654321) | |||||
So(person.FirstName, ShouldEqual, "abc") | |||||
So(person.Names, ShouldResemble, []string{"a", "b", "c", "d", "e", "f", "g"}) | |||||
}) | |||||
} | |||||
func TestString(t *testing.T) { | |||||
Convey("env.String", t, func() { | |||||
So(String("NotFound"), ShouldEqual, "") | |||||
Set("Found", "something") | |||||
So(String("Found"), ShouldEqual, "something") | |||||
So(String("NotFound", "default"), ShouldEqual, "default") | |||||
}) | |||||
} | |||||
func TestStrings(t *testing.T) { | |||||
Convey("env.Strings", t, func() { | |||||
So(Strings("StringList"), ShouldBeNil) | |||||
Set("StringList", "a,b,c") | |||||
So(Strings("StringList"), ShouldResemble, []string{"a", "b", "c"}) | |||||
So(Strings("NotFound", []string{"a", "b", "c"}), ShouldResemble, []string{"a", "b", "c"}) | |||||
}) | |||||
} | |||||
func TestInt(t *testing.T) { | |||||
Convey("env.Int", t, func() { | |||||
So(Int("integer"), ShouldEqual, 0) | |||||
Set("integer", 123) | |||||
So(Int("integer"), ShouldEqual, 123) | |||||
So(Int("NotFound", 123), ShouldEqual, 123) | |||||
}) | |||||
} | |||||
func TestInt64(t *testing.T) { | |||||
Convey("env.Int", t, func() { | |||||
So(Int64("int64"), ShouldEqual, 0) | |||||
Set("int64", 123) | |||||
So(Int64("int64"), ShouldEqual, 123) | |||||
So(Int64("NotFound", 123), ShouldEqual, 123) | |||||
}) | |||||
} | |||||
func TestUint(t *testing.T) { | |||||
Convey("env.Uint", t, func() { | |||||
So(Uint("uint"), ShouldEqual, 0) | |||||
Set("uint", 123) | |||||
So(Uint("uint"), ShouldEqual, 123) | |||||
So(Uint("NotFound", 123), ShouldEqual, 123) | |||||
}) | |||||
} | |||||
func TestUint64(t *testing.T) { | |||||
Convey("env.Uint64", t, func() { | |||||
So(Uint64("uint64"), ShouldEqual, 0) | |||||
Set("uint64", 123) | |||||
So(Uint64("uint64"), ShouldEqual, 123) | |||||
So(Uint64("NotFound", 123), ShouldEqual, 123) | |||||
}) | |||||
} | |||||
func TestBool(t *testing.T) { | |||||
Convey("env.Bool", t, func() { | |||||
So(Bool("bool"), ShouldBeFalse) | |||||
Set("bool", true) | |||||
So(Bool("bool"), ShouldBeTrue) | |||||
So(Bool("NotFound", true), ShouldBeTrue) | |||||
}) | |||||
} | |||||
func TestFloat(t *testing.T) { | |||||
Convey("env.Float", t, func() { | |||||
So(Float("float64"), ShouldEqual, 0.0) | |||||
Set("float64", 12345678990.0987654321) | |||||
So(Float("float64"), ShouldEqual, 12345678990.0987654321) | |||||
So(Float("NotFound", 12345678990.0987654321), ShouldEqual, 12345678990.0987654321) | |||||
}) | |||||
} |