From 089a90b0820a097d7924a63b7e5320ed214b3bd4 Mon Sep 17 00:00:00 2001 From: Hugo Chinchilla Carbonell Date: Wed, 31 Aug 2016 16:58:09 +0200 Subject: [PATCH] Support POSIX parameter expansion --- README.rst | 19 +++++++++++++++++++ dotenv/cli.py | 4 ++-- dotenv/main.py | 37 ++++++++++++++++++++++++++++++++++--- tests/test_cli.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_core.py | 16 +++++++++++++++- 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 778d9e8a..c71c70a7 100644 --- a/README.rst +++ b/README.rst @@ -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 ------ @@ -209,6 +220,13 @@ us a pull request. This project is currently maintained by `Saurabh Kumar `__ and would not have been possible without the support of these `awesome people `__. +Executing the tests: + +:: + + $ flake8 + $ pytest + Changelog ========= @@ -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 ---------- diff --git a/dotenv/cli.py b/dotenv/cli.py index 4d04dacd..fa70ff49 100644 --- a/dotenv/cli.py +++ b/dotenv/cli.py @@ -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() @@ -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)) diff --git a/dotenv/main.py b/dotenv/main.py index 607a3538..2fe1a831 100644 --- a/dotenv/main.py +++ b/dotenv/main.py @@ -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): @@ -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 @@ -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: @@ -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: @@ -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: @@ -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(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 4a5118ca..c755d6f3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from os import environ from os.path import dirname, join import dotenv @@ -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) diff --git a/tests/test_core.py b/tests/test_core.py index 37c6a0d1..29f0f9a3 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -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(): @@ -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)