diff --git a/README.rst b/README.rst index 9fac066..b1d2193 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,18 @@ The environmental variable can be embedded in a larger string, too: 'b': 2 } +More than one environmental variable can appear in a string: + +.. code-block:: python + + yamlenv.load(''' + a: foo ${A:-bar} ${B:-baz} + b: 2 + ''') == { + 'a': 'foo bar baz', + 'b': 2 + } + YaML files can include other YaML files, too. E.g. if ``b.yaml`` contains "2", then: diff --git a/tests/test_env.py b/tests/test_env.py index aef792b..342d945 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -41,6 +41,23 @@ def test_interpolate_string(self): b: 2 '''), {'a': 'password', 'b': 2}) + def test_interpolate_two_values(self): + os.environ['A'] = '1' + os.environ['B'] = 'foo' + self.assertEqual(yamlenv.load(''' +a: ${A} ${B} + '''), {'a': "1 foo"}) + + def test_interpolate_two_values_with_defaults(self): + self.assertEqual(yamlenv.load(''' +a: ${C:-foo} ${D:-bar} + '''), {'a': "foo bar"}) + + def test_interpolate_invalid_yaml_value(self): + self.assertEqual(yamlenv.load(''' +{'a': {'b': '{foo} ${C:-foo}'}} + '''), {'a': {'b': "{foo} foo"}}) + def test_interpolate_within_characters(self): os.environ['A'] = 'def' self.assertEqual(yamlenv.load(''' diff --git a/yamlenv/env.py b/yamlenv/env.py index c163692..e0377c7 100644 --- a/yamlenv/env.py +++ b/yamlenv/env.py @@ -1,6 +1,7 @@ import os import re import yaml +import yaml.parser try: from collections.abc import Mapping, Sequence, Set @@ -46,7 +47,7 @@ class EnvVar(object): __slots__ = ['name', 'separator', 'default', 'string'] RE = re.compile( - r'\$\{(?P[^:-]+)((?P:?)-(?P.*))?\}') + r'\$\{(?P[^:-]+?)((?P:?)-(?P.*?))?\}') def __init__(self, name, separator, default, string): # type: (str, str, str, str) -> None @@ -65,15 +66,19 @@ def value(self): # type: () -> str value = os.environ.get(self.name) if value: - return self.RE.sub(value, self.string) + return self.RE.sub(value, self.string, count=1) if self.allow_null_default or self.default: - return self.RE.sub(self.default, self.string) + return self.RE.sub(self.default, self.string, count=1) raise ValueError('Missing value and default for {}'.format(self.name)) @property - def yaml_value(self): + def maybe_yaml_value(self): # type: () -> T.Any - return yaml.safe_load(self.value) + v = self.value + try: + return yaml.safe_load(v) + except yaml.parser.ParserError: + return v @classmethod def from_string(cls, s): @@ -89,11 +94,16 @@ def from_string(cls, s): def interpolate(data): # type: (T.Any) -> Obj - for path, obj in objwalk(data): - e = EnvVar.from_string(obj) - if e is not None: - x = data - for k in path[:-1]: - x = x[k] - x[path[-1]] = e.yaml_value + while True: + more = False + for path, obj in objwalk(data): + e = EnvVar.from_string(obj) + if e is not None: + more = True + x = data + for k in path[:-1]: + x = x[k] + x[path[-1]] = e.maybe_yaml_value + if not more: + break return data