From eaefca88c3a68d9b7fc65d2be9c014a765b43339 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Tue, 14 Apr 2020 16:31:13 +0200 Subject: [PATCH] Add support for Bash-like default values When interpolation is active, variable expansion can use a default value, like in Bash. --- CHANGELOG.md | 2 +- README.md | 15 ++++++++++----- src/dotenv/main.py | 24 ++++++++++++++++++------ tests/test_main.py | 11 +++++++++++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb9d58a..fbcbd6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -*No unreleased change at this time.* +- Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]). ## [0.12.0] - 2020-02-28 diff --git a/README.md b/README.md index 1ffa855d..7374d05c 100644 --- a/README.md +++ b/README.md @@ -39,18 +39,23 @@ export S3_BUCKET=YOURS3BUCKET export SECRET_KEY=YOURSECRETKEYGOESHERE ``` -`.env` can interpolate variables using POSIX variable expansion, -variables are replaced from the environment first or from other values -in the `.env` file if the variable is not present in the environment. +Python-dotenv can interpolate variables using POSIX variable expansion. + +The value of a variable is the first of the values defined in the following list: + +- Value of that variable in the environment. +- Value of that variable in the `.env` file. +- Default value, if provided. +- Empty string. + Ensure that variables are surrounded with `{}` like `${HOME}` as bare variables such as `$HOME` are not expanded. -(**Note**: Default Value Expansion is not supported as of yet, see -[\#30](https://github.com/theskumar/python-dotenv/pull/30#issuecomment-244036604).) ```shell CONFIG_PATH=${HOME}/.config/foo DOMAIN=example.org EMAIL=admin@${DOMAIN} +DEBUG=${DEBUG:-false} ``` ## Getting started diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 68ee02a0..7fbd24f8 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -30,7 +30,17 @@ else: _StringIO = StringIO[Text] -__posix_variable = re.compile(r'\$\{[^\}]*\}') # type: Pattern[Text] +__posix_variable = re.compile( + r""" + \$\{ + (?P[^\}:]*) + (?::- + (?P[^\}]*) + )? + \} + """, + re.VERBOSE, +) # type: Pattern[Text] def with_warn_for_invalid_lines(mappings): @@ -202,23 +212,25 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"): def resolve_nested_variables(values): # type: (Dict[Text, Optional[Text]]) -> Dict[Text, Optional[Text]] - def _replacement(name): - # type: (Text) -> Text + def _replacement(name, default): + # type: (Text, Optional[Text]) -> Text """ get appropriate value for a variable name. first search in environ, if not found, then look into the dotenv variables """ - ret = os.getenv(name, new_values.get(name, "")) + default = default if default is not None else "" + ret = os.getenv(name, new_values.get(name, default)) return ret # type: ignore - def _re_sub_callback(match_object): + def _re_sub_callback(match): # type: (Match[Text]) -> Text """ From a match object gets the variable name and returns the correct replacement """ - return _replacement(match_object.group()[2:-1]) + matches = match.groupdict() + return _replacement(name=matches["name"], default=matches["default"]) # type: ignore new_values = {} diff --git a/tests/test_main.py b/tests/test_main.py index d8678589..f877d21a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -304,20 +304,31 @@ def test_dotenv_values_file(dotenv_file): ({"b": "c"}, "a=$b", True, {"a": "$b"}), ({"b": "c"}, "a=${b}", False, {"a": "${b}"}), ({"b": "c"}, "a=${b}", True, {"a": "c"}), + ({"b": "c"}, "a=${b:-d}", False, {"a": "${b:-d}"}), + ({"b": "c"}, "a=${b:-d}", True, {"a": "c"}), # Defined in file ({}, "b=c\na=${b}", True, {"a": "c", "b": "c"}), # Undefined ({}, "a=${b}", True, {"a": ""}), + ({}, "a=${b:-d}", True, {"a": "d"}), # With quotes ({"b": "c"}, 'a="${b}"', True, {"a": "c"}), ({"b": "c"}, "a='${b}'", True, {"a": "c"}), + # With surrounding text + ({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}), + # Self-referential ({"a": "b"}, "a=${a}", True, {"a": "b"}), ({}, "a=${a}", True, {"a": ""}), + ({"a": "b"}, "a=${a:-c}", True, {"a": "b"}), + ({}, "a=${a:-c}", True, {"a": "c"}), + + # Reused + ({"b": "c"}, "a=${b}${b}", True, {"a": "cc"}), ], ) def test_dotenv_values_stream(env, string, interpolate, expected):