Skip to content

Commit

Permalink
Fix #7 Support multiple env vars in same string
Browse files Browse the repository at this point in the history
Use non-greed regex to avoid capturing multiple env-like strings.
Keep parsing until all env-like strings are substituted.
  • Loading branch information
lbolla committed Mar 11, 2019
1 parent 26352a5 commit 27a506d
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 12 deletions.
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
17 changes: 17 additions & 0 deletions tests/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('''
Expand Down
34 changes: 22 additions & 12 deletions yamlenv/env.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import re
import yaml
import yaml.parser

try:
from collections.abc import Mapping, Sequence, Set
Expand Down Expand Up @@ -46,7 +47,7 @@ class EnvVar(object):
__slots__ = ['name', 'separator', 'default', 'string']

RE = re.compile(
r'\$\{(?P<name>[^:-]+)((?P<separator>:?)-(?P<default>.*))?\}')
r'\$\{(?P<name>[^:-]+?)((?P<separator>:?)-(?P<default>.*?))?\}')

def __init__(self, name, separator, default, string):
# type: (str, str, str, str) -> None
Expand All @@ -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):
Expand All @@ -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

0 comments on commit 27a506d

Please sign in to comment.