Skip to content

Commit 9d30d64

Browse files
committed
feat: allow nested variable expansion in bash-like default value syntax
1 parent b6fe193 commit 9d30d64

File tree

2 files changed

+56
-25
lines changed

2 files changed

+56
-25
lines changed

src/dotenv/variables.py

+40-24
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,6 @@
22
from abc import ABCMeta
33
from typing import Iterator, Mapping, Optional, Pattern
44

5-
_posix_variable = re.compile(
6-
r"""
7-
\$\{
8-
(?P<name>[^\}:]*)
9-
(?::-
10-
(?P<default>[^\}]*)
11-
)?
12-
\}
13-
""",
14-
re.VERBOSE,
15-
) # type: Pattern[str]
16-
175

186
class Atom():
197
__metaclass__ = ABCMeta
@@ -48,7 +36,7 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str:
4836

4937

5038
class Variable(Atom):
51-
def __init__(self, name: str, default: Optional[str]) -> None:
39+
def __init__(self, name: str, default: Optional[Iterator[Atom]]) -> None:
5240
self.name = name
5341
self.default = default
5442

@@ -64,24 +52,52 @@ def __hash__(self) -> int:
6452
return hash((self.__class__, self.name, self.default))
6553

6654
def resolve(self, env: Mapping[str, Optional[str]]) -> str:
67-
default = self.default if self.default is not None else ""
55+
# default = self.default if self.default is not None else ""
56+
default = "".join(atom.resolve(env) for atom in self.default) if self.default is not None else ""
6857
result = env.get(self.name, default)
6958
return result if result is not None else ""
7059

7160

72-
def parse_variables(value: str) -> Iterator[Atom]:
73-
cursor = 0
61+
_variable_re = re.compile(
62+
r"""
63+
^
64+
(?P<name>[^\}:]*?)
65+
(?::[-=]
66+
(?P<default>.*)
67+
)?
68+
$
69+
""",
70+
re.VERBOSE,
71+
) # type: Pattern[str]
72+
73+
ESC_CHAR = '\\'
7474

75-
for match in _posix_variable.finditer(value):
76-
(start, end) = match.span()
77-
name = match.groupdict()["name"]
78-
default = match.groupdict()["default"]
7975

80-
if start > cursor:
81-
yield Literal(value=value[cursor:start])
76+
def parse_variables(value: str) -> Iterator[Atom]:
77+
cursor = 0
8278

83-
yield Variable(name=name, default=default)
84-
cursor = end
79+
starts = []
80+
esc = False
81+
for i in range(len(value)):
82+
if esc:
83+
esc = False
84+
elif ESC_CHAR == value[i]:
85+
esc = True
86+
elif i < len(value) - 1 and '$' == value[i] and '{' == value[i+1]:
87+
if len(starts) == 0 and cursor < i:
88+
yield Literal(value=value[cursor:i])
89+
starts.append(i + 2)
90+
elif '}' == value[i]:
91+
start = starts.pop()
92+
end = i
93+
cursor = i+1
94+
if len(starts) == 0:
95+
print(value[start:end])
96+
for match in _variable_re.finditer(value[start:end]):
97+
name = match.groupdict()["name"]
98+
default = match.groupdict()["default"]
99+
default = None if default is None else list(parse_variables(default))
100+
yield Variable(name=name, default=default)
85101

86102
length = len(value)
87103
if cursor < length:

tests/test_variables.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,22 @@
99
("", []),
1010
("a", [Literal(value="a")]),
1111
("${a}", [Variable(name="a", default=None)]),
12-
("${a:-b}", [Variable(name="a", default="b")]),
12+
("${a:-b}", [Variable(name="a", default=[Literal(value="b")])]),
13+
("${a:=b}", [Variable(name="a", default=[Literal(value="b")])]),
14+
(
15+
"${a:-a${b:-c${d}e}f}",
16+
[
17+
Variable(name="a", default=[
18+
Literal(value="a"),
19+
Variable(name="b", default=[
20+
Literal(value="c"),
21+
Variable(name="d", default=None),
22+
Literal(value="e")
23+
]),
24+
Literal(value="f")
25+
])
26+
]
27+
),
1328
(
1429
"${a}${b}",
1530
[

0 commit comments

Comments
 (0)