api-v2/deps/dotenv/lib/dotenv.ex
2025-04-16 10:03:13 -03:00

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