lib/livebook_env.ex

defmodule LivebookEnv do
  @moduledoc """
  A tiny utility for importing .env files into Livebooks.
  """

  @doc """
  Import a .env file into your current Livebook.

  Assuming you have a .env file containing `FOOBAR=123`:

  ## Examples

      iex> LivebookEnv.import_dotenv("/Users/alex/myproject/.env)
      {:ok, "Imported 1 variable(s): FOOBAR"}

  """
  def import_dotenv(envfile_path) do
    env = envfile_path
    |> File.read!()
    |> parse_contents()

    System.put_env(env)

    {
      :ok,
      "Imported #{Enum.count(env)}: #{Map.keys(env) |> Enum.join(", ")}"
    }
  end

  # Implementation

  @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

  defp parse_contents(contents) do
    values = String.split(contents, "\n")

    values
    |> Enum.flat_map(&Regex.scan(@pattern, &1))
    |> trim_quotes_from_values
    |> Enum.reduce(Map.new(), &collect_into_map/2)
  end

  defp collect_into_map([_whole, k, v], env), do: Map.put(env, k, v)

  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
end