From 4b515d98ba83252a9acecb1bc9b02d9b1df599df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:35:25 +0000 Subject: [PATCH 1/3] Initial plan From 9a8f682802a11b9f26ee23b9d0ed7d5218879498 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:38:18 +0000 Subject: [PATCH 2/3] Add .env support for mounted directory Co-authored-by: dmalan <788678+dmalan@users.noreply.github.com> --- cli50/__main__.py | 30 ++++++++++++++++++++++++++++++ tests/test_env.py | 22 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/test_env.py diff --git a/cli50/__main__.py b/cli50/__main__.py index ee300c5..b462845 100644 --- a/cli50/__main__.py +++ b/cli50/__main__.py @@ -221,6 +221,12 @@ def main(): if lang := os.getenv("LANG"): options += ["--env", f"LANG={lang}"] + # Check for .env + try: + options += env_options(directory) + except RuntimeError as e: + sys.exit(_("{}: unable to source").format(e)) + # Validate ports if not args["port"]: args["port"] = PORTS @@ -321,6 +327,30 @@ def ports(container): return ", ".join(mappings) +def env_options(directory): + """Return Docker options for .env in directory.""" + dotenv = os.path.join(directory, ".env") + if not os.path.isfile(dotenv): + return [] + try: + before = subprocess.check_output([ + "bash", "-c", "env -0" + ], env={}, cwd=directory).decode("utf-8") + after = subprocess.check_output([ + "bash", "-c", 'set -a && source "$1" && env -0', "_", dotenv + ], env={}, cwd=directory).decode("utf-8") + except subprocess.CalledProcessError: + raise RuntimeError(dotenv) from None + + before = dict(entry.split("=", 1) for entry in before.rstrip("\0").split("\0") if entry) + after = dict(entry.split("=", 1) for entry in after.rstrip("\0").split("\0") if entry) + options = [] + for key, value in after.items(): + if before.get(key) != value: + options += ["--env", f"{key}={value}"] + return options + + def pull(image, tag): """Pull image as needed.""" import json diff --git a/tests/test_env.py b/tests/test_env.py new file mode 100644 index 0000000..4ffff29 --- /dev/null +++ b/tests/test_env.py @@ -0,0 +1,22 @@ +import os +import tempfile +import unittest + +from cli50.__main__ import env_options + + +def env_dict(options): + return dict(option.split("=", 1) for option in options[1::2]) + + +class EnvOptionsTestCase(unittest.TestCase): + + def test_env_options_sources_dotenv(self): + with tempfile.TemporaryDirectory() as directory: + with open(os.path.join(directory, ".env"), "w") as file: + file.write("FOO=bar\nexport BAZ='qux quux'\n") + self.assertEqual(env_dict(env_options(directory)), {"FOO": "bar", "BAZ": "qux quux"}) + + def test_env_options_ignores_missing_dotenv(self): + with tempfile.TemporaryDirectory() as directory: + self.assertEqual(env_options(directory), []) From ca00d09042e4be921633f7b4c6ad1fc6a6be8557 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:53:48 +0000 Subject: [PATCH 3/3] Address PR feedback on .env change Co-authored-by: dmalan <788678+dmalan@users.noreply.github.com> --- cli50/__main__.py | 5 +++++ tests/test_env.py | 22 ---------------------- 2 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 tests/test_env.py diff --git a/cli50/__main__.py b/cli50/__main__.py index b462845..46587d5 100644 --- a/cli50/__main__.py +++ b/cli50/__main__.py @@ -223,6 +223,7 @@ def main(): # Check for .env try: + # Source and export any variables from DIRECTORY/.env. options += env_options(directory) except RuntimeError as e: sys.exit(_("{}: unable to source").format(e)) @@ -333,9 +334,13 @@ def env_options(directory): if not os.path.isfile(dotenv): return [] try: + # Capture bash's baseline environment so shell defaults like PWD + # are not forwarded unless DIRECTORY/.env actually changes them. before = subprocess.check_output([ "bash", "-c", "env -0" ], env={}, cwd=directory).decode("utf-8") + + # Source and export DIRECTORY/.env, then emit the resulting environment. after = subprocess.check_output([ "bash", "-c", 'set -a && source "$1" && env -0', "_", dotenv ], env={}, cwd=directory).decode("utf-8") diff --git a/tests/test_env.py b/tests/test_env.py deleted file mode 100644 index 4ffff29..0000000 --- a/tests/test_env.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -import tempfile -import unittest - -from cli50.__main__ import env_options - - -def env_dict(options): - return dict(option.split("=", 1) for option in options[1::2]) - - -class EnvOptionsTestCase(unittest.TestCase): - - def test_env_options_sources_dotenv(self): - with tempfile.TemporaryDirectory() as directory: - with open(os.path.join(directory, ".env"), "w") as file: - file.write("FOO=bar\nexport BAZ='qux quux'\n") - self.assertEqual(env_dict(env_options(directory)), {"FOO": "bar", "BAZ": "qux quux"}) - - def test_env_options_ignores_missing_dotenv(self): - with tempfile.TemporaryDirectory() as directory: - self.assertEqual(env_options(directory), [])