236 lines
6.4 KiB
Elixir
236 lines
6.4 KiB
Elixir
defmodule Dotenv do
|
|
@moduledoc """
|
|
This module implements both an OTP application API and a "serverless" API.
|
|
|
|
Server API
|
|
==========
|
|
|
|
Start the application with `start/2` On starting, it will automatically export
|
|
the environment variables in the default path (`.env`).
|
|
|
|
The environment can then be reloaded with `reload!/0` or a specific path
|
|
or list of paths can be provided to `reload!/1`.
|
|
|
|
Serverless API
|
|
==============
|
|
|
|
To use the serverless API, you can either load the environment variables with
|
|
`load!` (again, optionally passing in a path or list of paths), or you
|
|
can retrieve the variables without exporting them using `load`.
|
|
"""
|
|
|
|
use Application
|
|
alias Dotenv.Env
|
|
|
|
def start(_type, env_path \\ :automatic) do
|
|
Dotenv.Supervisor.start_link(env_path)
|
|
end
|
|
|
|
@quotes_pattern ~r/^(['"])(.*)\1$/
|
|
@pattern ~r/
|
|
\A
|
|
(?:export\s+)? # optional export
|
|
([\w\.]+) # key
|
|
(?:\s*=\s*|:\s+?) # separator
|
|
( # optional value begin
|
|
'(?:\'|[^'])*?' # single quoted value
|
|
| # or
|
|
"(?:\"|[^"])*?" # double quoted value
|
|
| # or
|
|
[^#\n]+? # unquoted value
|
|
)? # value end
|
|
(?:\s*\#.*)? # optional comment
|
|
\z
|
|
/x
|
|
|
|
# https://regex101.com/r/XrvCwE/1
|
|
@env_expand_pattern ~r/
|
|
(?:^|[^\\]) # prevent to expand \\$
|
|
( # get variable key pattern
|
|
\$ #
|
|
(?: #
|
|
([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*) # get variable key
|
|
| #
|
|
(?: #
|
|
{([A-Z0-9_]*[A-Z_]+[A-Z0-9_]*)} # get variable key between {}
|
|
) #
|
|
) #
|
|
) #
|
|
/x
|
|
|
|
##############################################################################
|
|
# Server API
|
|
##############################################################################
|
|
|
|
@doc """
|
|
Calls the server to reload the values in the `.env` file into the
|
|
system environment.
|
|
|
|
This call is asynchronous (`cast`).
|
|
"""
|
|
@spec reload!() :: :ok
|
|
def reload! do
|
|
:gen_server.cast(:dotenv, :reload!)
|
|
end
|
|
|
|
@doc """
|
|
Calls the server to reload the values in the file located at `env_path` into
|
|
the system environment.
|
|
|
|
This call is asynchronous (`cast`).
|
|
"""
|
|
@spec reload!(any) :: :ok
|
|
def reload!(env_path) do
|
|
:gen_server.cast(:dotenv, {:reload!, env_path})
|
|
end
|
|
|
|
@doc """
|
|
Returns the current state of the server as a `Dotenv.Env` struct.
|
|
"""
|
|
@spec env() :: Env.t()
|
|
def env do
|
|
:gen_server.call(:dotenv, :env)
|
|
end
|
|
|
|
@doc """
|
|
Retrieves the value of the given `key` from the server, or `fallback` if the
|
|
value is not found.
|
|
"""
|
|
@spec get(String.t(), String.t() | nil) :: String.t()
|
|
def get(key, fallback \\ nil) do
|
|
:gen_server.call(:dotenv, {:get, key, fallback})
|
|
end
|
|
|
|
##############################################################################
|
|
# Serverless API
|
|
##############################################################################
|
|
|
|
@doc """
|
|
Reads the env files at the provided `env_path` path(s), exports the values into
|
|
the system environment, and returns them in a `Dotenv.Env` struct.
|
|
"""
|
|
def load!(env_path \\ :automatic) do
|
|
env = load(env_path)
|
|
System.put_env(env.values)
|
|
env
|
|
end
|
|
|
|
@doc """
|
|
Reads the env files at the provided `env_path` path(s) and returns the values in a `Dotenv.Env` struct.
|
|
"""
|
|
@spec load(String.t() | :automatic | [String.t()]) :: Env.t()
|
|
def load(env_path \\ :automatic)
|
|
|
|
def load([env_path | env_paths]) do
|
|
first_env = load(env_path)
|
|
rest_env = load(env_paths)
|
|
|
|
%Env{paths: [env_path | rest_env.paths], values: Map.merge(first_env.values, rest_env.values)}
|
|
end
|
|
|
|
def load([]) do
|
|
%Env{paths: [], values: Map.new()}
|
|
end
|
|
|
|
def load(env_path) do
|
|
{env_path, contents} = read_env_file(env_path)
|
|
values = contents |> parse_contents()
|
|
%Env{paths: [env_path], values: values}
|
|
end
|
|
|
|
def parse_contents(contents) do
|
|
values = String.split(contents, "\n")
|
|
|
|
values
|
|
|> Enum.flat_map(&Regex.scan(@pattern, &1))
|
|
|> trim_quotes_from_values
|
|
|> Enum.reduce([], &expand_env/2)
|
|
|> Enum.reduce(Map.new(), &collect_into_map/2)
|
|
end
|
|
|
|
defp collect_into_map([_whole, k, v], env), do: Map.put(env, k, v)
|
|
defp collect_into_map([_whole, _k], env), do: env
|
|
|
|
defp trim_quotes_from_values(values) do
|
|
values
|
|
|> Enum.map(fn values ->
|
|
Enum.map(values, &trim_quotes/1)
|
|
end)
|
|
end
|
|
|
|
defp trim_quotes(value) do
|
|
String.replace(value, @quotes_pattern, "\\2")
|
|
end
|
|
|
|
# without value
|
|
defp expand_env([_whole, _k], acc), do: acc
|
|
|
|
defp expand_env([whole, k, v], acc) do
|
|
matchs = Regex.scan(@env_expand_pattern, v)
|
|
|
|
new_value =
|
|
case Enum.empty?(matchs) do
|
|
true ->
|
|
v
|
|
|
|
false ->
|
|
matchs
|
|
|> Enum.reduce(v, fn [_whole, pattern | keys], v ->
|
|
v |> replace_env(pattern, keys, acc)
|
|
end)
|
|
end
|
|
|
|
acc ++ [[whole, k, new_value]]
|
|
end
|
|
|
|
defp replace_env(value, pattern, ["" | keys], env), do: replace_env(value, pattern, keys, env)
|
|
defp replace_env(value, pattern, [key | _], env), do: replace_env(value, pattern, key, env)
|
|
|
|
defp replace_env(value, pattern, key, %Env{} = env) do
|
|
new_value = env |> Env.get(key) || ""
|
|
|
|
pattern
|
|
|> Regex.escape()
|
|
|> Regex.compile!()
|
|
|> Regex.replace(value, new_value)
|
|
end
|
|
|
|
defp replace_env(value, pattern, key, acc) when is_list(acc) do
|
|
values = acc |> Enum.reduce(Map.new(), &collect_into_map/2)
|
|
replace_env(value, pattern, key, %Env{values: values})
|
|
end
|
|
|
|
defp replace_env(value, pattern, key, %{} = values) do
|
|
replace_env(value, pattern, key, %Env{values: values})
|
|
end
|
|
|
|
defp read_env_file(:automatic) do
|
|
case find_env_path() do
|
|
{:ok, env_path} -> {env_path, File.read!(env_path)}
|
|
{:error, _} -> {:none, ""}
|
|
end
|
|
end
|
|
|
|
defp read_env_file(:none) do
|
|
{:none, ""}
|
|
end
|
|
|
|
defp read_env_file(env_path) do
|
|
{env_path, File.read!(env_path)}
|
|
end
|
|
|
|
defp find_env_path do
|
|
find_env_path(File.cwd!())
|
|
end
|
|
|
|
defp find_env_path(dir) do
|
|
candidate = Path.join(dir, ".env")
|
|
|
|
cond do
|
|
File.exists?(candidate) -> {:ok, candidate}
|
|
dir == "/" -> {:error, "No .env found"}
|
|
true -> find_env_path(Path.dirname(dir))
|
|
end
|
|
end
|
|
end
|