Current section
8 Versions
Jump to
Current section
8 Versions
Compare versions
8
files changed
+154
additions
-59
deletions
| @@ -0,0 +1,3 @@ | |
| 1 | + [ |
| 2 | + inputs: ["mix.exs", "{lib,test}/**/*.{ex,exs}"], |
| 3 | + ] |
| @@ -1,22 +1,20 @@ | |
| 1 | - # WARNING: This isn't the Elixir way. |
| 1 | + # Dotenv for Elixir [](https://hex.pm/packages/dotenv)  |
| 2 | + |
| 3 | + |
| 4 | + This is a port of @bkeepers' [dotenv](https://github.com/bkeepers/dotenv) project to Elixir. You can read more about dotenv on that project's page. The short version is that it simplifies developing projects where configuration is stored in environment variables (e.g. projects intended to be deployed to Heroku). |
| 5 | + |
| 6 | + ## WARNING: Not compatible with Elixir releases |
| 2 7 | |
| 3 8 | Elixir has an excellent configuration system and this dotenv implementation has |
| 4 9 | a serious limitation in that it isn't available at compile time. It fits very |
| 5 | - poorly into a typical deployment setup using exrm or similar. Configuration |
| 6 | - management should be built around Elixir's existing not system, not environment |
| 7 | - variables. |
| 10 | + poorly into a deployment setup using Elixir releases, distillery, or similar. |
| 8 11 | |
| 9 | - A good example is [Phoenix](http://www.phoenixframework.org/) which generates a |
| 12 | + Configuration management should be built around Elixir's existing configuration system. A good example is [Phoenix](http://www.phoenixframework.org/) which generates a |
| 10 13 | project where the production config imports the "secrets" from a file stored |
| 11 14 | outside of version control. Even if you're using this for development, the same |
| 12 15 | approach could be taken. |
| 13 16 | |
| 14 | - If you're sure this is the correct solution to a problem in your |
| 15 | - development/deployment workflow, read on! |
| 16 | - |
| 17 | - ## Dotenv for Elixir |
| 18 | - |
| 19 | - This is a port of @bkeepers' [dotenv](https://github.com/bkeepers/dotenv) project to Elixir. You can read more about [dotenv](https://github.com/bkeepers/dotenv) on that project's page. The short version is that it simplifies developing projects where configuration is stored in environment variables (e.g. projects intended to be deployed to Heroku). |
| 17 | + However, if you are using Heroku, Dokku, or another deployment process that does *not* use releases, read on! |
| 20 18 | |
| 21 19 | ### Quick Start |
| 22 20 | |
| @@ -27,7 +25,13 @@ First add `dotenv` to your dependencies. | |
| 27 25 | For the latest release: |
| 28 26 | |
| 29 27 | ```elixir |
| 30 | - {:dotenv, "~> 2.0.0"} |
| 28 | + {:dotenv, "~> 3.0.0"} |
| 29 | + ``` |
| 30 | + |
| 31 | + Most likely, if you are deploying in a Heroku-like environment, you'll want to only load the package in a non-production environment: |
| 32 | + |
| 33 | + ```elixir |
| 34 | + {:dotenv, "~> 3.0.0", only: [:dev, :test]} |
| 31 35 | ``` |
| 32 36 | |
| 33 37 | For master: |
| @@ -38,7 +42,30 @@ For master: | |
| 38 42 | |
| 39 43 | Fetch your dependencies with `mix deps.get`. |
| 40 44 | |
| 41 | - Now, add the `:dotenv` application to your applications list when running in the `:dev` environment: |
| 45 | + Now, when you load your app in a console with `iex -S mix`, your environment variables will be set automatically. |
| 46 | + |
| 47 | + #### Using Environment Variables in Configuration |
| 48 | + |
| 49 | + [Mix loads configuration before loading any application code.](https://github.com/elixir-lang/elixir/blob/52141f2a3fa69906397017883242948dd93d91b5/lib/mix/lib/mix/tasks/run.ex#L123) If you want to use `.env` variables in your application configuration, you'll need to load dotenv manually on application start and reload your application config: |
| 50 | + |
| 51 | + ```elixir |
| 52 | + defmodule App.Application do |
| 53 | + use Application |
| 54 | + |
| 55 | + def start(_type, _args) do |
| 56 | + unless Mix.env == :prod do |
| 57 | + Dotenv.load |
| 58 | + Mix.Task.run("loadconfig") |
| 59 | + end |
| 60 | + |
| 61 | + # ... the rest of your application startup |
| 62 | + end |
| 63 | + end |
| 64 | + ``` |
| 65 | + |
| 66 | + #### Elixir 1.9 and older |
| 67 | + |
| 68 | + If you are running an old version of Elixir, you'll need to add the `:dotenv` application to your applications list when running in the `:dev` environment: |
| 42 69 | |
| 43 70 | ```elixir |
| 44 71 | # Configuration for the OTP application |
| @@ -54,8 +81,6 @@ defp app_list(_), do: app_list | |
| 54 81 | defp app_list, do: [...] |
| 55 82 | ``` |
| 56 83 | |
| 57 | - Now, when you load your app in a console with `iex -S mix`, your environment variables will be set automatically. |
| 58 | - |
| 59 84 | #### Reloading the `.env` file |
| 60 85 | |
| 61 86 | The `Dotenv.reload!/0` function will reload the variables defined in the `.env` file. |
| @@ -70,7 +95,7 @@ The `load!/1` function loads variables into the process environment, and can be | |
| 70 95 | |
| 71 96 | Alternately, `load/1` will return a data structure of the variables read from the `.env` file: |
| 72 97 | |
| 73 | - ``` |
| 98 | + ```elixir |
| 74 99 | iex(1)> Dotenv.load |
| 75 100 | %Dotenv.Env{paths: ["/elixir/dotenv_elixir/.env"], |
| 76 101 | values: %{"APP_TEST_VAR" => "HELLO"}} |
| @@ -3,11 +3,11 @@ | |
| 3 3 | {<<"description">>,<<"A port of dotenv to Elixir">>}. |
| 4 4 | {<<"elixir">>,<<"~> 1.0">>}. |
| 5 5 | {<<"files">>, |
| 6 | - [<<"lib/dotenv.ex">>,<<"lib/dotenv/env.ex">>,<<"lib/dotenv/server.ex">>, |
| 7 | - <<"lib/dotenv/supervisor.ex">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}. |
| 6 | + [<<"lib">>,<<"lib/dotenv">>,<<"lib/dotenv/supervisor.ex">>, |
| 7 | + <<"lib/dotenv/server.ex">>,<<"lib/dotenv/env.ex">>,<<"lib/dotenv.ex">>, |
| 8 | + <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}. |
| 8 9 | {<<"licenses">>,[<<"MIT">>]}. |
| 9 10 | {<<"links">>,[{<<"github">>,<<"https://github.com/avdi/dotenv_elixir">>}]}. |
| 10 | - {<<"maintainers">>,[<<"Jared Norman">>]}. |
| 11 11 | {<<"name">>,<<"dotenv">>}. |
| 12 12 | {<<"requirements">>,[]}. |
| 13 | - {<<"version">>,<<"3.0.0">>}. |
| 13 | + {<<"version">>,<<"3.1.0">>}. |
| @@ -43,6 +43,21 @@ defmodule Dotenv do | |
| 43 43 | \z |
| 44 44 | /x |
| 45 45 | |
| 46 | + # https://regex101.com/r/XrvCwE/1 |
| 47 | + @env_expand_pattern ~r/ |
| 48 | + (?:^|[^\\]) # prevent to expand \\$ |
| 49 | + ( # get variable key pattern |
| 50 | + \$ # |
| 51 | + (?: # |
| 52 | + ([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*) # get variable key |
| 53 | + | # |
| 54 | + (?: # |
| 55 | + {([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*)} # get variable key between {} |
| 56 | + ) # |
| 57 | + ) # |
| 58 | + ) # |
| 59 | + /x |
| 60 | + |
| 46 61 | ############################################################################## |
| 47 62 | # Server API |
| 48 63 | ############################################################################## |
| @@ -55,7 +70,7 @@ defmodule Dotenv do | |
| 55 70 | """ |
| 56 71 | @spec reload!() :: :ok |
| 57 72 | def reload! do |
| 58 | - :gen_server.cast :dotenv, :reload! |
| 73 | + :gen_server.cast(:dotenv, :reload!) |
| 59 74 | end |
| 60 75 | |
| 61 76 | @doc """ |
| @@ -66,24 +81,24 @@ defmodule Dotenv do | |
| 66 81 | """ |
| 67 82 | @spec reload!(any) :: :ok |
| 68 83 | def reload!(env_path) do |
| 69 | - :gen_server.cast :dotenv, {:reload!, env_path} |
| 84 | + :gen_server.cast(:dotenv, {:reload!, env_path}) |
| 70 85 | end |
| 71 86 | |
| 72 87 | @doc """ |
| 73 88 | Returns the current state of the server as a `Dotenv.Env` struct. |
| 74 89 | """ |
| 75 | - @spec env() :: Env.t |
| 90 | + @spec env() :: Env.t() |
| 76 91 | def env do |
| 77 | - :gen_server.call :dotenv, :env |
| 92 | + :gen_server.call(:dotenv, :env) |
| 78 93 | end |
| 79 94 | |
| 80 95 | @doc """ |
| 81 96 | Retrieves the value of the given `key` from the server, or `fallback` if the |
| 82 97 | value is not found. |
| 83 98 | """ |
| 84 | - @spec get(String.t, String.t) :: String.t |
| 99 | + @spec get(String.t(), String.t() | nil) :: String.t() |
| 85 100 | def get(key, fallback \\ nil) do |
| 86 | - :gen_server.call :dotenv, {:get, key, fallback} |
| 101 | + :gen_server.call(:dotenv, {:get, key, fallback}) |
| 87 102 | end |
| 88 103 | |
| 89 104 | ############################################################################## |
| @@ -103,36 +118,42 @@ defmodule Dotenv do | |
| 103 118 | @doc """ |
| 104 119 | Reads the env files at the provided `env_path` path(s) and returns the values in a `Dotenv.Env` struct. |
| 105 120 | """ |
| 106 | - @spec load([String.t]) :: Env.t |
| 107 | - @spec load(String.t) :: Env.t |
| 121 | + @spec load(String.t() | :automatic | [String.t()]) :: Env.t() |
| 108 122 | def load(env_path \\ :automatic) |
| 109 123 | |
| 110 | - def load([env_path|env_paths]) do |
| 124 | + def load([env_path | env_paths]) do |
| 111 125 | first_env = load(env_path) |
| 112 | - rest_env = load(env_paths) |
| 113 | - %Env{paths: [env_path|rest_env.paths], |
| 114 | - values: Map.merge(first_env.values, rest_env.values)} |
| 126 | + rest_env = load(env_paths) |
| 127 | + |
| 128 | + %Env{paths: [env_path | rest_env.paths], values: Map.merge(first_env.values, rest_env.values)} |
| 115 129 | end |
| 116 130 | |
| 117 131 | def load([]) do |
| 118 | - %Env{paths: [], values: Map.new} |
| 132 | + %Env{paths: [], values: Map.new()} |
| 119 133 | end |
| 120 134 | |
| 121 135 | def load(env_path) do |
| 122 136 | {env_path, contents} = read_env_file(env_path) |
| 137 | + values = contents |> parse_contents() |
| 138 | + %Env{paths: [env_path], values: values} |
| 139 | + end |
| 123 140 | |
| 141 | + def parse_contents(contents) do |
| 124 142 | values = String.split(contents, "\n") |
| 125 | - |> Enum.flat_map(&Regex.scan(@pattern,&1)) |
| 126 | - |> trim_quotes_from_values |
| 127 | - |> Enum.reduce(Map.new, &collect_into_map/2) |
| 128 | - %Env{paths: [env_path], values: values} |
| 143 | + |
| 144 | + values |
| 145 | + |> Enum.flat_map(&Regex.scan(@pattern, &1)) |
| 146 | + |> trim_quotes_from_values |
| 147 | + |> Enum.reduce([], &expand_env/2) |
| 148 | + |> Enum.reduce(Map.new(), &collect_into_map/2) |
| 129 149 | end |
| 130 150 | |
| 131 151 | defp collect_into_map([_whole, k, v], env), do: Map.put(env, k, v) |
| 132 | - defp collect_into_map([_whole, _k], env), do: env |
| 152 | + defp collect_into_map([_whole, _k], env), do: env |
| 133 153 | |
| 134 154 | defp trim_quotes_from_values(values) do |
| 135 | - values |> Enum.map(fn(values)-> |
| 155 | + values |
| 156 | + |> Enum.map(fn values -> |
| 136 157 | Enum.map(values, &trim_quotes/1) |
| 137 158 | end) |
| 138 159 | end |
| @@ -141,10 +162,52 @@ defmodule Dotenv do | |
| 141 162 | String.replace(value, @quotes_pattern, "\\2") |
| 142 163 | end |
| 143 164 | |
| 165 | + # without value |
| 166 | + defp expand_env([_whole, _k], acc), do: acc |
| 167 | + |
| 168 | + defp expand_env([whole, k, v], acc) do |
| 169 | + matchs = Regex.scan(@env_expand_pattern, v) |
| 170 | + |
| 171 | + new_value = |
| 172 | + case Enum.empty?(matchs) do |
| 173 | + true -> |
| 174 | + v |
| 175 | + |
| 176 | + false -> |
| 177 | + matchs |
| 178 | + |> Enum.reduce(v, fn [_whole, pattern | keys], v -> |
| 179 | + v |> replace_env(pattern, keys, acc) |
| 180 | + end) |
| 181 | + end |
| 182 | + |
| 183 | + acc ++ [[whole, k, new_value]] |
| 184 | + end |
| 185 | + |
| 186 | + defp replace_env(value, pattern, ["" | keys], env), do: replace_env(value, pattern, keys, env) |
| 187 | + defp replace_env(value, pattern, [key | _], env), do: replace_env(value, pattern, key, env) |
| 188 | + |
| 189 | + defp replace_env(value, pattern, key, %Env{} = env) do |
| 190 | + new_value = env |> Env.get(key) || "" |
| 191 | + |
| 192 | + pattern |
| 193 | + |> Regex.escape() |
| 194 | + |> Regex.compile!() |
| 195 | + |> Regex.replace(value, new_value) |
| 196 | + end |
| 197 | + |
| 198 | + defp replace_env(value, pattern, key, acc) when is_list(acc) do |
| 199 | + values = acc |> Enum.reduce(Map.new(), &collect_into_map/2) |
| 200 | + replace_env(value, pattern, key, %Env{values: values}) |
| 201 | + end |
| 202 | + |
| 203 | + defp replace_env(value, pattern, key, %{} = values) do |
| 204 | + replace_env(value, pattern, key, %Env{values: values}) |
| 205 | + end |
| 206 | + |
| 144 207 | defp read_env_file(:automatic) do |
| 145 208 | case find_env_path() do |
| 146 209 | {:ok, env_path} -> {env_path, File.read!(env_path)} |
| 147 | - {:error, _} -> {:none, ""} |
| 210 | + {:error, _} -> {:none, ""} |
| 148 211 | end |
| 149 212 | end |
| 150 213 | |
| @@ -157,15 +220,16 @@ defmodule Dotenv do | |
| 157 220 | end |
| 158 221 | |
| 159 222 | defp find_env_path do |
| 160 | - find_env_path(File.cwd!) |
| 223 | + find_env_path(File.cwd!()) |
| 161 224 | end |
| 162 225 | |
| 163 226 | defp find_env_path(dir) do |
| 164 227 | candidate = Path.join(dir, ".env") |
| 228 | + |
| 165 229 | cond do |
| 166 230 | File.exists?(candidate) -> {:ok, candidate} |
| 167 | - dir == "/" -> {:error, "No .env found"} |
| 168 | - true -> find_env_path(Path.dirname(dir)) |
| 231 | + dir == "/" -> {:error, "No .env found"} |
| 232 | + true -> find_env_path(Path.dirname(dir)) |
| 169 233 | end |
| 170 234 | end |
| 171 235 | end |
| @@ -1,6 +1,6 @@ | |
| 1 1 | defmodule Dotenv.Env do |
| 2 | - @type t :: %Dotenv.Env{paths: [String.t], values: %{String.t => String.t}} |
| 3 | - defstruct paths: [], values: Map.new |
| 2 | + @type t :: %Dotenv.Env{paths: [String.t()], values: %{String.t() => String.t()}} |
| 3 | + defstruct paths: [], values: Map.new() |
| 4 4 | |
| 5 5 | def path(%Dotenv.Env{paths: paths}) do |
| 6 6 | Enum.join(paths, ":") |
Loading more files…