Skip to content

Commit c34e1a1

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

File tree

4 files changed

+62
-6
lines changed

4 files changed

+62
-6
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 existing pattern is used. The pattern set here is persistent.
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 existing pattern is used. The pattern set here is persistent.
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: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import re
22
from abc import ABCMeta, abstractmethod
3-
from typing import Iterator, Mapping, Optional, Pattern
3+
from typing import Iterator, Mapping, Optional
44

5-
_posix_variable: Pattern[str] = re.compile(
5+
DEFAULT_VARNAME_RE = r"""[^\}:]*"""
6+
def _pattern_builder(pattern: Optional[str] = None) -> re.Pattern[str]:
7+
"""Builds a regex pattern for ${xxx:-yyy} variable substitution."""
8+
return re.compile(
69
r"""
710
\$\{
8-
(?P<name>[^\}:]*)
11+
(?P<name>""" + (pattern if pattern else DEFAULT_VARNAME_RE) + r""")
912
(?::-
1013
(?P<default>[^\}]*)
1114
)?
@@ -15,6 +18,18 @@
1518
)
1619

1720

21+
_posix_variable = _pattern_builder()
22+
23+
24+
def set_variable_name_pattern(pattern: Optional[str] = None) -> None:
25+
"""Set the variable name pattern used by `parse_variables`.
26+
27+
If `pattern` is None, it resets to the default pattern.
28+
"""
29+
global _posix_variable
30+
_posix_variable = _pattern_builder(pattern)
31+
32+
1833
class Atom(metaclass=ABCMeta):
1934
def __ne__(self, other: object) -> bool:
2035
result = self.__eq__(other)

tests/test_variables.py

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

3-
from dotenv.variables import Literal, Variable, parse_variables
3+
from dotenv.variables import (
4+
Literal,
5+
Variable,
6+
parse_variables,
7+
set_variable_name_pattern,
8+
)
49

510

611
@pytest.mark.parametrize(
@@ -33,3 +38,19 @@ def test_parse_variables(value, expected):
3338
result = parse_variables(value)
3439

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

0 commit comments

Comments
 (0)