@@ -57,13 +57,14 @@ def _write_echo_batch(tmp_path: Path, suffix: str = ".bat") -> Path:
5757
5858def test_windows_batch_command () -> None :
5959 command = nox .popen ._windows_batch_command (
60- [r"C:\Program Files\tool.cmd" , "plain" , "has space" , "requests<99" ]
60+ [r"C:\Program Files\tool.cmd" , "plain" , "has space" , "requests<99" , "py(3)" ]
6161 )
6262
63- assert command == r'"C:\Program Files\tool.cmd" plain "has space" "requests<99"'
63+ expected = r'"C:\Program Files\tool.cmd" plain "has space" "requests<99" "py(3)"'
64+ assert command == expected
6465
6566
66- @pytest .mark .parametrize ("argument" , ['"urllib3<1.25" ' , 'a"< b' , 'a">b ' ])
67+ @pytest .mark .parametrize ("argument" , ['a"<b ' , 'a"> b' , '"urllib3<1.25' , 'urllib3<1.25" ' ])
6768def test_windows_batch_command_rejects_quoted_metacharacters (argument : str ) -> None :
6869 # cmd.exe only counts quote characters, so an argument mixing quotes and
6970 # metacharacters cannot be escaped reliably; 'a">b' would even run
@@ -72,10 +73,62 @@ def test_windows_batch_command_rejects_quoted_metacharacters(argument: str) -> N
7273 nox .popen ._windows_batch_command ([r"C:\tool.bat" , argument ])
7374
7475
76+ def test_windows_batch_command_accepts_pre_quoted_argument () -> None :
77+ # the historical workaround from #312: the caller quotes the constraint
78+ # themselves, and the outer quotes already protect the metacharacters.
79+ command = nox .popen ._windows_batch_command ([r"C:\tool.bat" , '"urllib3<1.25"' ])
80+
81+ assert command == r'C:\tool.bat "urllib3<1.25"'
82+
83+
84+ @pytest .mark .parametrize (
85+ "argument" ,
86+ ["%NOX_TEST_VAR%" , "pre%NOX_TEST_VAR%post" , "%%NOX_TEST_VAR%%" , '"%NOX_TEST_VAR%"' ],
87+ )
88+ def test_windows_batch_command_rejects_percent_expansion (argument : str ) -> None :
89+ # cmd.exe expands %VAR% references even inside double quotes, so an
90+ # argument referencing a defined variable cannot be passed through
91+ # faithfully; a variable value could even inject extra commands.
92+ with pytest .raises (ValueError , match = "would expand %NOX_TEST_VAR%" ):
93+ nox .popen ._windows_batch_command (
94+ [r"C:\tool.bat" , argument ], {"nox_test_var" : "value" }
95+ )
96+
97+
98+ @pytest .mark .parametrize ("argument" , ["%RANDOM%" , "%__CD__%" , "%=C:%" , "%PATH:;=,%" ])
99+ def test_windows_batch_command_rejects_dynamic_percent_expansion (
100+ argument : str ,
101+ ) -> None :
102+ # these expand even when the variable is missing from the child
103+ # environment: cmd.exe resolves them dynamically or defines them itself.
104+ with pytest .raises (ValueError , match = "would expand" ):
105+ nox .popen ._windows_batch_command ([r"C:\tool.bat" , argument ], {"PATH" : "C:;D:" })
106+
107+
108+ @pytest .mark .parametrize (
109+ "argument" , ["100%" , "%%" , "%undefined_variable%" , "https://x.test/%20a%20b.whl" ]
110+ )
111+ def test_windows_batch_command_allows_literal_percents (argument : str ) -> None :
112+ # cmd.exe leaves references to undefined variables alone, so these
113+ # arguments survive verbatim and are safe to pass through.
114+ command = nox .popen ._windows_batch_command ([r"C:\tool.bat" , argument ], {})
115+
116+ assert command == f"C:\\ tool.bat { argument } "
117+
118+
75119@only_on_windows
76120@pytest .mark .parametrize ("suffix" , [".bat" , ".cmd" ])
77121@pytest .mark .parametrize (
78- "argument" , ["name&value" , "name<value" , "name>value" , "name^value" , "name|value" ]
122+ "argument" ,
123+ [
124+ "name&value" ,
125+ "name<value" ,
126+ "name>value" ,
127+ "name^value" ,
128+ "name|value" ,
129+ "name(value" ,
130+ "name)value" ,
131+ ],
79132)
80133def test_run_windows_batch_metacharacter_arg (
81134 tmp_path : Path , suffix : str , argument : str
@@ -87,6 +140,34 @@ def test_run_windows_batch_metacharacter_arg(
87140 assert result .strip () == argument
88141
89142
143+ @only_on_windows
144+ def test_run_windows_batch_paren_arg_in_block (tmp_path : Path ) -> None :
145+ # conda.bat-style scripts wrap commands in if (...) blocks, where an
146+ # unquoted ")" in an argument ends the block early and silently corrupts
147+ # the argument (or aborts with "... was unexpected at this time").
148+ batch = tmp_path / "block.bat"
149+ batch .write_text (
150+ f'@echo off\n if "1"=="1" (\n '
151+ f' "{ PYTHON } " -c "import sys; print(sys.argv[1])" %*\n )\n ' ,
152+ encoding = "utf-8" ,
153+ )
154+
155+ result = nox .command .run ([batch , "(pkg)" ], silent = True )
156+
157+ assert result .strip () == "(pkg)"
158+
159+
160+ @only_on_windows
161+ def test_run_windows_batch_pre_quoted_arg (tmp_path : Path ) -> None :
162+ # the pre-quoted idiom from #312 keeps working; the child process sees
163+ # the argument without the caller's quotes, which is what conda expects.
164+ batch = _write_echo_batch (tmp_path )
165+
166+ result = nox .command .run ([batch , '"urllib3<1.25"' ], silent = True )
167+
168+ assert result .strip () == "urllib3<1.25"
169+
170+
90171@only_on_windows
91172def test_run_windows_batch_quoted_metacharacter_arg (
92173 tmp_path : Path , monkeypatch : pytest .MonkeyPatch
0 commit comments