Current section

8 Versions

Jump to

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 [![Hex pm](http://img.shields.io/hexpm/v/dotenv.svg?style=flat)](https://hex.pm/packages/dotenv) ![.github/workflows/main.yaml](https://github.com/iloveitaly/dotenv_elixir/workflows/.github/workflows/main.yaml/badge.svg)
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…