Skip to content

Commit

Permalink
Support POSIX parameter expansion (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hugo Chinchilla Carbonell authored and theskumar committed Sep 8, 2016
1 parent 03a3851 commit 802496b
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 6 deletions.
19 changes: 19 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ ignored.
# I am a comment and that is OK
FOO="BAR"
``.env`` can interpolate variables using POSIX variable expansion, variables
are replaced from the environment first or from other values in the ``.env``
file if the variable is not present in the environment.

.. code:: shell
CONFIG_PATH=${HOME}/.config/foo
DOMAIN=example.org
EMAIL=admin@${DOMAIN}
Django
------

Expand Down Expand Up @@ -209,6 +220,13 @@ us a pull request.
This project is currently maintained by `Saurabh Kumar <https://saurabh-kumar.com>`__ and
would not have been possible without the support of these `awesome people <https://github.com/theskumar/python-dotenv/graphs/contributors>`__.

Executing the tests:

::

$ flake8
$ pytest

Changelog
=========

Expand All @@ -217,6 +235,7 @@ dev
- Drop support for Python 2.6
- Handle escaped charaters and newlines in quoted values. (Thanks `@iameugenejo`_)
- Remove any spaces around unquoted key/value. (Thanks `@paulochf`_)
- Added POSIX variable expansion.

0.5.1
----------
Expand Down
4 changes: 2 additions & 2 deletions dotenv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import click

from .main import get_key, parse_dotenv, set_key, unset_key
from .main import get_key, dotenv_values, set_key, unset_key


@click.group()
Expand Down Expand Up @@ -35,7 +35,7 @@ def cli(ctx, file, quote):
def list(ctx):
'''Display all the stored key/value.'''
file = ctx.obj['FILE']
dotenv_as_dict = parse_dotenv(file)
dotenv_as_dict = dotenv_values(file)
for k, v in dotenv_as_dict:
click.echo('%s="%s"' % (k, v))

Expand Down
37 changes: 34 additions & 3 deletions dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import os
import sys
import warnings
import re
from collections import OrderedDict

__escape_decoder = codecs.getdecoder('unicode_escape')
__posix_variable = re.compile('\$\{[^\}]*\}')


def decode_escaped(escaped):
Expand All @@ -21,7 +23,7 @@ def load_dotenv(dotenv_path):
if not os.path.exists(dotenv_path):
warnings.warn("Not loading %s - it doesn't exist." % dotenv_path)
return None
for k, v in parse_dotenv(dotenv_path):
for k, v in dotenv_values(dotenv_path).items():
os.environ.setdefault(k, v)
return True

Expand All @@ -36,7 +38,7 @@ def get_key(dotenv_path, key_to_get):
if not os.path.exists(dotenv_path):
warnings.warn("can't read %s - it doesn't exist." % dotenv_path)
return None
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
dotenv_as_dict = dotenv_values(dotenv_path)
if key_to_get in dotenv_as_dict:
return dotenv_as_dict[key_to_get]
else:
Expand Down Expand Up @@ -73,7 +75,7 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
if not os.path.exists(dotenv_path):
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path)
return None, key_to_unset
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
dotenv_as_dict = dotenv_values(dotenv_path)
if key_to_unset in dotenv_as_dict:
dotenv_as_dict.pop(key_to_unset, None)
else:
Expand All @@ -83,6 +85,12 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
return success, key_to_unset


def dotenv_values(dotenv_path):
values = OrderedDict(parse_dotenv(dotenv_path))
values = resolve_nested_variables(values)
return values


def parse_dotenv(dotenv_path):
with open(dotenv_path) as f:
for line in f:
Expand All @@ -103,6 +111,29 @@ def parse_dotenv(dotenv_path):
yield k, v


def resolve_nested_variables(values):
def _replacement(name):
"""
get appropiate value for a variable name.
first search in environ, if not found,
then look into the dotenv variables
"""
ret = os.getenv(name, values.get(name, ""))
return ret

def _re_sub_callback(match_object):
"""
From a match object gets the variable name and returns
the correct replacement
"""
return _replacement(match_object.group()[2:-1])

for k, v in values.items():
values[k] = __posix_variable.sub(_re_sub_callback, v)

return values


def flatten_and_write(dotenv_path, dotenv_as_dict, quote_mode="always"):
with open(dotenv_path, "w") as f:
for k, v in dotenv_as_dict.items():
Expand Down
35 changes: 35 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from os import environ
from os.path import dirname, join

import dotenv
Expand Down Expand Up @@ -83,3 +84,37 @@ def test_default_path(cli):
output = sh.dotenv('get', 'HELLO')
assert output == 'HELLO="WORLD"\n'
sh.rm(dotenv_path)


def test_get_key_with_interpolation(cli):
with cli.isolated_filesystem():
sh.touch(dotenv_path)
dotenv.set_key(dotenv_path, 'HELLO', 'WORLD')
dotenv.set_key(dotenv_path, 'FOO', '${HELLO}')
dotenv.set_key(dotenv_path, 'BAR', 'CONCATENATED_${HELLO}_POSIX_VAR')

# test replace from variable in file
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == 'WORLD'
stored_value = dotenv.get_key(dotenv_path, 'BAR')
assert stored_value == 'CONCATENATED_WORLD_POSIX_VAR'
# test replace from environ taking precedence over file
environ["HELLO"] = "TAKES_PRECEDENCE"
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == "TAKES_PRECEDENCE"
sh.rm(dotenv_path)


def test_get_key_with_interpolation_of_unset_variable(cli):
with cli.isolated_filesystem():
sh.touch(dotenv_path)
dotenv.set_key(dotenv_path, 'FOO', '${NOT_SET}')
# test unavailable replacement returns empty string
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == ''
# unless present in environment
environ['NOT_SET'] = 'BAR'
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == 'BAR'
del(environ['NOT_SET'])
sh.rm(dotenv_path)
16 changes: 15 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import pytest
import tempfile
import warnings
import sh

from dotenv import load_dotenv, find_dotenv
from dotenv import load_dotenv, find_dotenv, set_key


def test_warns_if_file_does_not_exist():
Expand Down Expand Up @@ -55,3 +56,16 @@ def test_find_dotenv():
with open(filename, 'w') as f:
f.write("TEST=test\n")
assert find_dotenv(usecwd=True) == filename


def test_load_dotenv(cli):
dotenv_path = '.test_load_dotenv'
with cli.isolated_filesystem():
sh.touch(dotenv_path)
set_key(dotenv_path, 'DOTENV', 'WORKS')
assert 'DOTENV' not in os.environ
success = load_dotenv(dotenv_path)
assert success
assert 'DOTENV' in os.environ
assert os.environ['DOTENV'] == 'WORKS'
sh.rm(dotenv_path)

0 comments on commit 802496b

Please sign in to comment.