From 8d18bad52670e298d169cfeb28f2bc83e9d66bae Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sat, 10 Jan 2026 18:49:12 +0100 Subject: [PATCH] Make `dotenv run` forward flags to given command Changes for users: - (BREAKING) Forward flags passed after `dotenv run` to the given command instead of interpreting them. - This means that an invocation such as `dotenv run ls --help` will show the help page of `ls` instead of that of `dotenv run`. - To pass flags to `dotenv run` itself, pass them right after `run`: `dotenv run --help` or `dotenv run --override ls`. - As usual, generic options should be passed right after `dotenv`: `dotenv --file path/to/env run ls` --- src/dotenv/cli.py | 13 ++++++++++--- tests/test_cli.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index c548aa39..7a4c7adc 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -156,7 +156,13 @@ def unset(ctx: click.Context, key: Any) -> None: sys.exit(1) -@cli.command(context_settings={"ignore_unknown_options": True}) +@cli.command( + context_settings={ + "allow_extra_args": True, + "allow_interspersed_args": False, + "ignore_unknown_options": True, + } +) @click.pass_context @click.option( "--override/--no-override", @@ -164,7 +170,7 @@ def unset(ctx: click.Context, key: Any) -> None: help="Override variables from the environment file with those from the .env file.", ) @click.argument("commandline", nargs=-1, type=click.UNPROCESSED) -def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: +def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None: """Run command with environment variables present.""" file = ctx.obj["FILE"] if not os.path.isfile(file): @@ -180,7 +186,8 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: if not commandline: click.echo("No command given.") sys.exit(1) - run_command(commandline, dotenv_as_dict) + + run_command([*commandline, *ctx.args], dotenv_as_dict) def run_command(command: List[str], env: Dict[str, str]) -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py index 343fdb23..7cc4533d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,7 @@ import os +import subprocess from pathlib import Path -from typing import Optional +from typing import Optional, Sequence import pytest import sh @@ -10,6 +11,21 @@ from dotenv.version import __version__ +def invoke_sub(args: Sequence[str]) -> subprocess.CompletedProcess: + """ + Invoke the `dotenv` CLI in a subprocess. + + This is necessary to test subcommands like `dotenv run` that replace the + current process. + """ + + return subprocess.run( + ["dotenv", *args], + capture_output=True, + text=True, + ) + + @pytest.mark.parametrize( "output_format,content,expected", ( @@ -249,3 +265,29 @@ def test_run_with_version(cli): assert result.exit_code == 0 assert result.output.strip().endswith(__version__) + + +def test_run_with_command_flags(dotenv_path): + """ + Check that command flags passed after `dotenv run` are not interpreted. + + Here, we want to run `printenv --version`, not `dotenv --version`. + """ + + result = invoke_sub(["--file", dotenv_path, "run", "printenv", "--version"]) + + assert result.returncode == 0 + assert result.stdout.strip().startswith("printenv ") + + +def test_run_with_dotenv_and_command_flags(cli, dotenv_path): + """ + Check that dotenv flags supersede command flags. + """ + + result = invoke_sub( + ["--version", "--file", dotenv_path, "run", "printenv", "--version"] + ) + + assert result.returncode == 0 + assert result.stdout.strip().startswith("dotenv, version")