View Source Dotenvy behaviour (Dotenvy v0.9.0)
Dotenvy
is an Elixir port of the original dotenv Ruby gem.
It is designed to help applications follow the principles of the 12-factor app and its recommendation to store configuration in the environment.
Unlike other configuration helpers, Dotenvy
enforces no convention for the naming
of your files: .env
is a common choice, you may name your configuration files whatever
you wish.
See the strategies for examples of various use cases.
Summary
Types
An input source may be either a path to an env file or a map with string keys,
e.g. "envs/.env"
or %{"FOO" => "bar"}
. This allows users to specify a list
of env files interspersed with other values from other sources, e.g. System.get_env()
or values fetched from a secure parameter store.
Callbacks
A parser implementation should receive the contents
read from a file,
a map of vars
(with string keys, as would come from System.get_env/0
),
and a keyword list of opts
.
Types
An input source may be either a path to an env file or a map with string keys,
e.g. "envs/.env"
or %{"FOO" => "bar"}
. This allows users to specify a list
of env files interspersed with other values from other sources, e.g. System.get_env()
or values fetched from a secure parameter store.
Callbacks
@callback parse(contents :: binary(), vars :: map(), opts :: keyword()) :: {:ok, map()} | {:error, any()}
A parser implementation should receive the contents
read from a file,
a map of vars
(with string keys, as would come from System.get_env/0
),
and a keyword list of opts
.
This callback is provided to help facilitate testing. See Dotenvy.Parser
for the default implementation.
Functions
@spec env!(variable :: binary(), type :: Dotenvy.Transformer.conversion_type()) :: any() | no_return()
Reads the given env variable
and converts its value to the given type
.
This function attempts to read a value from a local data store of sourced values.
This function may raise an error because type conversion is delegated to
Dotenvy.Transformer.to!/2
-- see its documentation for a list of supported types.
Examples
iex> env!("PORT", :integer)
5432
iex> env!("ENABLED", :boolean)
true
@spec env!( variable :: binary(), type :: Dotenvy.Transformer.conversion_type(), default :: any() ) :: any() | no_return()
Reads an env variable and converts its output or returns a default value.
Use
env!/2
when possibleUse of
env!/2
is recommended overenv!/3
because it creates a stronger contract with the environment: your app literally will not start when required env variables are missing.
If the given variable
is set, its value is converted to
the given type
. The provided default
value is only used when the
variable is not set; the default
value is returned as-is, without conversion.
This allows greater control of the output.
Conversion is delegated to Dotenvy.Transformer.to!/2
, which may raise an error.
See its documentation for a list of supported types.
This function attempts to read a value from a local data store of sourced values.
Examples
iex> env!("PORT", :integer, 5432)
5433
iex> env!("NOT_SET", :boolean, %{not: "converted"})
%{not: "converted"}
iex> System.put_env("HOST", "")
iex> env!("HOST", :string!, "localhost")
** (RuntimeError) Error converting HOST to string!: non-empty value required
@spec source(inputs :: input_source() | [input_source()], opts :: keyword()) :: {:ok, %{optional(String.t()) => String.t()}} | {:error, any()}
Like its Bash namesake command, source/2
accumulates values from the given input(s).
The accumulated values are stored via a side effect function to make them available
to the env!/2
and env!/3
functions.
Think of source/2
as a merging operation which can accept maps (like Map.merge/2
)
or paths to env files.
Inputs are processed from left to right so that values can be overridden by each
subsequent input. As with Map.merge/2
, the right-most input takes precedence.
Options
:parser
module that implementsDotenvy.parse/3
callback. Default:Dotenvy.Parser
:require_files
specifies which of the givenfiles
(if any) must be present. Whentrue
, all the listed files must exist. Whenfalse
, none of the listed files must exist. When some of the files are required and some are optional, provide a list specifying which files are required. If a file listed here is not included in the function'sfiles
argument, it is ignored. Default:false
:side_effect
an arity 1 function called after the successful parsing inputs. The default is an internal function that stores the values inside a process dictionary so the values are available to theenv!/2
andenv!/3
functions. This option is overridable to facilitate testing. Changing it is not recommended.
Examples
The simplest implementation is to parse a single file by including its path:
iex> Dotenvy.source(".env")
{:ok, %{
"TIMEOUT" => "5000",
"DATABASE_URL" => "postgres://postgres:postgres@localhost/myapp",
# ...etc...
}
}
More commonly, you will source multiple files (often based on the config_env()
)
and you will defer to pre-existing system variables. The most common pattern looks like this:
iex> Dotenvy.source([
"#{config_env()}.env",
"#{config_env()}.override.env",
System.get_env()
])
In the above example, the prod.env
, dev.env
, and test.env
files would be version-controlled,
but the *.override.env
variants would be ignored, giving developers the ability to override
values without needing to modify versioned files.
Give Precedence to System Envs!
Don't forget to include
System.get_env()
as the final input tosource/2
so that system environment variables take precedence over values sourced from.env
files.When you run a shell command like
❯ LOG_LEVEL=debug mix run
, your expectation is probably that theLOG_LEVEL
variable would be set todebug
, overriding whatever may have been defined in your sourced.env
files. Similarly, you may export env vars in your Bash profile. System env vars are not granted precedence automatically: you must explicitly includeSystem.get_env()
as the final input tosource/2
.
If your env files are making use of variable substitution based on system env vars,
e.g. ${PWD}
(see the Dotenv File Format), then you
would need to specify System.get_env()
as the first argument to source/2
.
For example, if your .env
references the system HOME
variable:
# .env
CACHE_DIR=${HOME}/cache
then your source/2
command would need to make the system env vars available
to the parser by including them as one of the inputs, e.g.
iex> Dotenvy.source([System.get_env(), ".env"])
Including the System.get_env()
before your files means that your files have final
say over the values, potentially overriding any pre-existing system env vars. In
some cases, you may wish to reference the system vars both before and after your own
.env files, e.g.
iex> Dotenvy.source([System.get_env(), ".env", System.get_env()])
or you may wish to cherry-pick which variables you need to make available for variable substitution:
iex> Dotenvy.source([
%{"HOME" => System.get_env("HOME")},
".env",
System.get_env()
])
This syntax favors explicitness so there is no confusion over what might have been "automagically" accumulated.
@spec source!(files :: binary() | [binary()], opts :: keyword()) :: %{optional(String.t()) => String.t()} | no_return()
As source/2
, but returns a map on success or raises on error.