| @@ -1,2 +1,109 @@ | |||
| # env | |||
| env | |||
| === | |||
| [](https://travis-ci.org/goanywhere/env) [](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) | |||
| }) | |||
| } | |||