From 9d30d64564aab9847cad627aba0bc211ab6778df Mon Sep 17 00:00:00 2001 From: Jia Huang Date: Thu, 25 Aug 2022 00:37:17 +0800 Subject: [PATCH 1/3] feat: allow nested variable expansion in bash-like default value syntax --- src/dotenv/variables.py | 64 +++++++++++++++++++++++++---------------- tests/test_variables.py | 17 ++++++++++- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index d77b700c..fbc10a79 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -2,18 +2,6 @@ from abc import ABCMeta from typing import Iterator, Mapping, Optional, Pattern -_posix_variable = re.compile( - r""" - \$\{ - (?P[^\}:]*) - (?::- - (?P[^\}]*) - )? - \} - """, - re.VERBOSE, -) # type: Pattern[str] - class Atom(): __metaclass__ = ABCMeta @@ -48,7 +36,7 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str: class Variable(Atom): - def __init__(self, name: str, default: Optional[str]) -> None: + def __init__(self, name: str, default: Optional[Iterator[Atom]]) -> None: self.name = name self.default = default @@ -64,24 +52,52 @@ def __hash__(self) -> int: return hash((self.__class__, self.name, self.default)) def resolve(self, env: Mapping[str, Optional[str]]) -> str: - default = self.default if self.default is not None else "" + # default = self.default if self.default is not None else "" + default = "".join(atom.resolve(env) for atom in self.default) if self.default is not None else "" result = env.get(self.name, default) return result if result is not None else "" -def parse_variables(value: str) -> Iterator[Atom]: - cursor = 0 +_variable_re = re.compile( + r""" + ^ + (?P[^\}:]*?) + (?::[-=] + (?P.*) + )? + $ + """, + re.VERBOSE, +) # type: Pattern[str] + +ESC_CHAR = '\\' - for match in _posix_variable.finditer(value): - (start, end) = match.span() - name = match.groupdict()["name"] - default = match.groupdict()["default"] - if start > cursor: - yield Literal(value=value[cursor:start]) +def parse_variables(value: str) -> Iterator[Atom]: + cursor = 0 - yield Variable(name=name, default=default) - cursor = end + starts = [] + esc = False + for i in range(len(value)): + if esc: + esc = False + elif ESC_CHAR == value[i]: + esc = True + elif i < len(value) - 1 and '$' == value[i] and '{' == value[i+1]: + if len(starts) == 0 and cursor < i: + yield Literal(value=value[cursor:i]) + starts.append(i + 2) + elif '}' == value[i]: + start = starts.pop() + end = i + cursor = i+1 + if len(starts) == 0: + print(value[start:end]) + for match in _variable_re.finditer(value[start:end]): + name = match.groupdict()["name"] + default = match.groupdict()["default"] + default = None if default is None else list(parse_variables(default)) + yield Variable(name=name, default=default) length = len(value) if cursor < length: diff --git a/tests/test_variables.py b/tests/test_variables.py index 86b06466..28e16677 100644 --- a/tests/test_variables.py +++ b/tests/test_variables.py @@ -9,7 +9,22 @@ ("", []), ("a", [Literal(value="a")]), ("${a}", [Variable(name="a", default=None)]), - ("${a:-b}", [Variable(name="a", default="b")]), + ("${a:-b}", [Variable(name="a", default=[Literal(value="b")])]), + ("${a:=b}", [Variable(name="a", default=[Literal(value="b")])]), + ( + "${a:-a${b:-c${d}e}f}", + [ + Variable(name="a", default=[ + Literal(value="a"), + Variable(name="b", default=[ + Literal(value="c"), + Variable(name="d", default=None), + Literal(value="e") + ]), + Literal(value="f") + ]) + ] + ), ( "${a}${b}", [ From 7d2c82ad9823fb39d72c80cd16a3e2465f4c7c51 Mon Sep 17 00:00:00 2001 From: Jia Huang Date: Thu, 25 Aug 2022 04:06:21 +0800 Subject: [PATCH 2/3] fix: remove debug code --- src/dotenv/variables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index fbc10a79..e9745eac 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -92,7 +92,6 @@ def parse_variables(value: str) -> Iterator[Atom]: end = i cursor = i+1 if len(starts) == 0: - print(value[start:end]) for match in _variable_re.finditer(value[start:end]): name = match.groupdict()["name"] default = match.groupdict()["default"] From d3478e8c3b8ac5fa4d92bfc5c973b3e51f5a3c89 Mon Sep 17 00:00:00 2001 From: Jia Huang Date: Sun, 4 Sep 2022 07:41:02 +0800 Subject: [PATCH 3/3] fix: typing error in variables.py and its test --- src/dotenv/variables.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index e9745eac..cad22b9f 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -1,6 +1,6 @@ import re from abc import ABCMeta -from typing import Iterator, Mapping, Optional, Pattern +from typing import Iterator, Mapping, Optional, Pattern, List class Atom(): @@ -36,7 +36,7 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str: class Variable(Atom): - def __init__(self, name: str, default: Optional[Iterator[Atom]]) -> None: + def __init__(self, name: str, default: Optional[List[Atom]]) -> None: self.name = name self.default = default @@ -52,7 +52,6 @@ def __hash__(self) -> int: return hash((self.__class__, self.name, self.default)) def resolve(self, env: Mapping[str, Optional[str]]) -> str: - # default = self.default if self.default is not None else "" default = "".join(atom.resolve(env) for atom in self.default) if self.default is not None else "" result = env.get(self.name, default) return result if result is not None else "" @@ -76,7 +75,7 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str: def parse_variables(value: str) -> Iterator[Atom]: cursor = 0 - starts = [] + starts: List[int] = [] esc = False for i in range(len(value)): if esc: