Skip to content

Commit d0049ea

Browse files
committed
Add option to restrict POSIX variable name regex
1 parent 16f2bda commit d0049ea

File tree

4 files changed

+62
-17
lines changed

4 files changed

+62
-17
lines changed

src/dotenv/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
from typing import Any, Optional
22

3-
from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key
3+
from .main import (
4+
dotenv_values,
5+
find_dotenv,
6+
get_key,
7+
load_dotenv,
8+
set_key,
9+
set_variable_name_pattern,
10+
unset_key,
11+
)
412

513

614
def load_ipython_extension(ipython: Any) -> None:
@@ -48,4 +56,5 @@ def get_cli_string(
4856
"unset_key",
4957
"find_dotenv",
5058
"load_ipython_extension",
59+
"set_variable_name_pattern",
5160
]

src/dotenv/main.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union
1111

1212
from .parser import Binding, parse_stream
13-
from .variables import parse_variables
13+
from .variables import parse_variables, set_variable_name_pattern
1414

1515
# A type alias for a string path to be used for the paths in this file.
1616
# These paths may flow to `open()` and `shutil.move()`; `shutil.move()`
@@ -341,6 +341,7 @@ def load_dotenv(
341341
override: bool = False,
342342
interpolate: bool = True,
343343
encoding: Optional[str] = "utf-8",
344+
varname_pattern: Optional[str] = None,
344345
) -> bool:
345346
"""Parse a .env file and then load all the variables found as environment variables.
346347
@@ -352,6 +353,8 @@ def load_dotenv(
352353
override: Whether to override the system environment variables with the variables
353354
from the `.env` file.
354355
encoding: Encoding to be used to read the file.
356+
varname_pattern: Optional regex pattern to restrict variable names.
357+
If `None`, the default pattern is used, allow characters except \\, :, }
355358
Returns:
356359
Bool: True if at least one environment variable is set else False
357360
@@ -380,6 +383,9 @@ def load_dotenv(
380383
override=override,
381384
encoding=encoding,
382385
)
386+
387+
if varname_pattern is not None:
388+
set_variable_name_pattern(varname_pattern)
383389
return dotenv.set_as_environment_variables()
384390

385391

@@ -389,6 +395,7 @@ def dotenv_values(
389395
verbose: bool = False,
390396
interpolate: bool = True,
391397
encoding: Optional[str] = "utf-8",
398+
varname_pattern: Optional[str] = None,
392399
) -> Dict[str, Optional[str]]:
393400
"""
394401
Parse a .env file and return its content as a dict.
@@ -402,13 +409,17 @@ def dotenv_values(
402409
stream: `StringIO` object with .env content, used if `dotenv_path` is `None`.
403410
verbose: Whether to output a warning if the .env file is missing.
404411
encoding: Encoding to be used to read the file.
412+
varname_pattern: Optional regex pattern to restrict variable names.
413+
If `None`, the default pattern is used, allow characters except \\, :, }
405414
406415
If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
407416
.env file.
408417
"""
409418
if dotenv_path is None and stream is None:
410419
dotenv_path = find_dotenv()
411420

421+
if varname_pattern is not None:
422+
set_variable_name_pattern(varname_pattern)
412423
return DotEnv(
413424
dotenv_path=dotenv_path,
414425
stream=stream,

src/dotenv/variables.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import re
22
from abc import ABCMeta, abstractmethod
3-
from typing import Iterator, Mapping, Optional, Pattern
4-
5-
_posix_variable: Pattern[str] = re.compile(
6-
r"""
7-
\$\{
8-
(?P<name>[^\}:]*)
9-
(?::-
10-
(?P<default>[^\}]*)
11-
)?
12-
\}
13-
""",
14-
re.VERBOSE,
15-
)
16-
3+
from typing import Iterator, Mapping, Optional
4+
5+
DEFAULT_VARNAME_RE = r"""[^\}:]*"""
6+
7+
def set_variable_name_pattern(pattern: Optional[str] = None) -> None:
8+
"""Set the variable name pattern used by `parse_variables`.
9+
10+
If `pattern` is None, it resets to the default pattern.
11+
"""
12+
global _posix_variable
13+
_posix_variable = re.compile(
14+
r"""
15+
\$\{
16+
(?P<name>""" + (pattern if pattern else DEFAULT_VARNAME_RE) + r""")
17+
(?::-
18+
(?P<default>[^\}]*)
19+
)?
20+
\}
21+
""",
22+
re.VERBOSE,
23+
)
24+
25+
set_variable_name_pattern(DEFAULT_VARNAME_RE)
1726

1827
class Atom(metaclass=ABCMeta):
1928
def __ne__(self, other: object) -> bool:

tests/test_variables.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from dotenv.variables import Literal, Variable, parse_variables
3+
from dotenv.variables import Literal, Variable, parse_variables, set_variable_name_pattern
44

55

66
@pytest.mark.parametrize(
@@ -33,3 +33,19 @@ def test_parse_variables(value, expected):
3333
result = parse_variables(value)
3434

3535
assert list(result) == expected
36+
37+
@pytest.mark.parametrize(
38+
"value,expected",
39+
[
40+
("", []),
41+
("${AB_CD}", [Variable(name="AB_CD", default=None)]),
42+
("${A.B.C.D}", [Literal(value="${A.B.C.D}")]),
43+
("${a}", [Literal(value="${a}")]),
44+
],
45+
)
46+
def test_parse_variables_re(value, expected):
47+
set_variable_name_pattern(r"""[A-Z0-9_]+""")
48+
result = parse_variables(value)
49+
50+
assert list(result) == expected
51+
set_variable_name_pattern(None)

0 commit comments

Comments
 (0)