@@ -1,2 +1,109 @@ | |||
# 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 | |||
### 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. |
@@ -0,0 +1,5 @@ | |||
package env | |||
import ( | |||
_ "github.com/smartystreets/goconvey/convey" | |||
) |
@@ -0,0 +1,134 @@ | |||
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. | |||
*/ |
@@ -0,0 +1,243 @@ | |||
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 | |||
} |
@@ -0,0 +1,199 @@ | |||
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) | |||
}) | |||
} |