Skip to content

Commit 4f6638b

Browse files
Pavel Minaevint19h
authored andcommitted
Fix #1001: Enable controlling shell expansion via "argsCanBeInterpretedByShell"
Fix #357: "argsExpansion" does not do what it says in VSCode Treat non-array "args" in debug config as a request to prevent shell argument escaping and allow shell expansion. Remove "argsExpansion".
1 parent 6b276e3 commit 4f6638b

File tree

8 files changed

+68
-46
lines changed

8 files changed

+68
-46
lines changed

src/debugpy/adapter/clients.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Capabilities(components.Capabilities):
2929
"supportsVariablePaging": False,
3030
"supportsRunInTerminalRequest": False,
3131
"supportsMemoryReferences": False,
32+
"supportsArgsCanBeInterpretedByShell": False,
3233
}
3334

3435
class Expectations(components.Capabilities):
@@ -364,20 +365,6 @@ def property_or_debug_option(prop_name, flag_name):
364365
'"program", "module", and "code" are mutually exclusive'
365366
)
366367

367-
# Propagate "args" via CLI if and only if shell expansion is requested.
368-
args_expansion = request(
369-
"argsExpansion", json.enum("shell", "none", optional=True)
370-
)
371-
if args_expansion == "shell":
372-
args += request("args", json.array(str))
373-
request.arguments.pop("args", None)
374-
375-
cwd = request("cwd", str, optional=True)
376-
if cwd == ():
377-
# If it's not specified, but we're launching a file rather than a module,
378-
# and the specified path has a directory in it, use that.
379-
cwd = None if program == () else (os.path.dirname(program) or None)
380-
381368
console = request(
382369
"console",
383370
json.enum(
@@ -389,6 +376,30 @@ def property_or_debug_option(prop_name, flag_name):
389376
)
390377
console_title = request("consoleTitle", json.default("Python Debug Console"))
391378

379+
# Propagate "args" via CLI so that shell expansion can be applied if requested.
380+
target_args = request("args", json.array(str, vectorize=True))
381+
args += target_args
382+
383+
# If "args" was a single string rather than an array, shell expansion must be applied.
384+
shell_expand_args = len(target_args) > 0 and isinstance(
385+
request.arguments["args"], str
386+
)
387+
if shell_expand_args:
388+
if not self.capabilities["supportsArgsCanBeInterpretedByShell"]:
389+
raise request.isnt_valid(
390+
'Shell expansion in "args" is not supported by the client'
391+
)
392+
if console == "internalConsole":
393+
raise request.isnt_valid(
394+
'Shell expansion in "args" is not available for "console":"internalConsole"'
395+
)
396+
397+
cwd = request("cwd", str, optional=True)
398+
if cwd == ():
399+
# If it's not specified, but we're launching a file rather than a module,
400+
# and the specified path has a directory in it, use that.
401+
cwd = None if program == () else (os.path.dirname(program) or None)
402+
392403
sudo = bool(property_or_debug_option("sudo", "Sudo"))
393404
if sudo and sys.platform == "win32":
394405
raise request.cant_handle('"sudo":true is not supported on Windows.')
@@ -412,6 +423,7 @@ def property_or_debug_option(prop_name, flag_name):
412423
launcher_path,
413424
adapter_host,
414425
args,
426+
shell_expand_args,
415427
cwd,
416428
console,
417429
console_title,

src/debugpy/adapter/launchers.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sys
88

99
from debugpy import adapter, common
10-
from debugpy.common import json, log, messaging, sockets
10+
from debugpy.common import log, messaging, sockets
1111
from debugpy.adapter import components, servers
1212

1313

@@ -70,6 +70,7 @@ def spawn_debuggee(
7070
launcher_path,
7171
adapter_host,
7272
args,
73+
shell_expand_args,
7374
cwd,
7475
console,
7576
console_title,
@@ -119,16 +120,6 @@ def on_launcher_connected(sock):
119120
if console == "internalConsole":
120121
log.info("{0} spawning launcher: {1!r}", session, cmdline)
121122
try:
122-
for i, arg in enumerate(cmdline):
123-
try:
124-
cmdline[i] = arg
125-
except UnicodeEncodeError as exc:
126-
raise start_request.cant_handle(
127-
"Invalid command line argument {0}: {1}",
128-
json.repr(arg),
129-
exc,
130-
)
131-
132123
# If we are talking to the client over stdio, sys.stdin and sys.stdout
133124
# are redirected to avoid mangling the DAP message stream. Make sure
134125
# the launcher also respects that.
@@ -154,6 +145,8 @@ def on_launcher_connected(sock):
154145
}
155146
if cwd is not None:
156147
request_args["cwd"] = cwd
148+
if shell_expand_args:
149+
request_args["argsCanBeInterpretedByShell"] = True
157150
try:
158151
# It is unspecified whether this request receives a response immediately, or only
159152
# after the spawned command has completed running, so do not block waiting for it.

src/debugpy/launcher/handlers.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from debugpy.common import json
1111
from debugpy.launcher import debuggee
1212

13+
1314
def launch_request(request):
1415
debug_options = set(request("debugOptions", json.array(str)))
1516

@@ -67,12 +68,9 @@ def property_or_debug_option(prop_name, flag_name):
6768
debugpy_args = request("debugpyArgs", json.array(str))
6869
cmdline += debugpy_args
6970

70-
# Further arguments can come via two channels: the launcher's own command line, or
71-
# "args" in the request; effective arguments are concatenation of these two in order.
72-
# Arguments for debugpy (such as -m) always come via CLI, but those specified by the
73-
# user via "args" are passed differently by the adapter depending on "argsExpansion".
71+
# Use the copy of arguments that was propagated via the command line rather than
72+
# "args" in the request itself, to allow for shell expansion.
7473
cmdline += sys.argv[1:]
75-
cmdline += request("args", json.array(str))
7674

7775
process_name = request("processName", sys.executable)
7876

tests/debug/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ class DebugConfig(MutableMapping):
4949
"type": (),
5050
# Launch
5151
"args": [],
52-
"argsExpansion": "shell",
5352
"code": (),
5453
"console": "internal",
5554
"cwd": (),

tests/debug/runners.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,12 @@ def spawn_debuggee(occ):
325325
attach_listen.host = "127.0.0.1"
326326
attach_listen.port = net.get_test_server_port(5478, 5600)
327327

328-
all_launch_terminal = [launch["integratedTerminal"], launch["externalTerminal"]]
328+
all_launch_terminal = [
329+
launch.with_options(console="integratedTerminal"),
330+
launch.with_options(console="externalTerminal"),
331+
]
329332

330-
all_launch = [launch["internalConsole"]] + all_launch_terminal
333+
all_launch = [launch.with_options(console="internalConsole")] + all_launch_terminal
331334

332335
all_attach_listen = [attach_listen["api"], attach_listen["cli"]]
333336

tests/debug/session.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,11 @@ def run_in_terminal(self, args, cwd, env):
490490
def _process_request(self, request):
491491
self.timeline.record_request(request, block=False)
492492
if request.command == "runInTerminal":
493-
args = request("args", json.array(str))
493+
args = request("args", json.array(str, vectorize=True))
494+
if len(args) > 0 and request("argsCanBeInterpretedByShell", False):
495+
# The final arg is a string that contains multiple actual arguments.
496+
last_arg = args.pop()
497+
args += last_arg.split()
494498
cwd = request("cwd", ".")
495499
env = request("env", json.object(str))
496500
try:
@@ -557,6 +561,7 @@ def _start_channel(self, stream):
557561
"columnsStartAt1": True,
558562
"supportsVariableType": True,
559563
"supportsRunInTerminalRequest": True,
564+
"supportsArgsCanBeInterpretedByShell": True,
560565
},
561566
)
562567

tests/debug/targets.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ def cli(self, env):
4545
"""
4646
raise NotImplementedError
4747

48+
@property
49+
def argslist(self):
50+
args = self.args
51+
if isinstance(args, str):
52+
return [args]
53+
else:
54+
return list(args)
55+
4856
@property
4957
def co_filename(self):
5058
"""co_filename of code objects created at runtime from the source that this
@@ -121,9 +129,11 @@ def configure(self, session):
121129

122130
def cli(self, env):
123131
if self._cwd:
124-
return [self._get_relative_program()] + list(self.args)
132+
cli = [self._get_relative_program()]
125133
else:
126-
return [self.filename.strpath] + list(self.args)
134+
cli = [self.filename.strpath]
135+
cli += self.argslist
136+
return cli
127137

128138

129139
class Module(Target):
@@ -150,7 +160,7 @@ def configure(self, session):
150160
def cli(self, env):
151161
if self.filename is not None:
152162
env.prepend_to("PYTHONPATH", self.filename.dirname)
153-
return ["-m", self.name] + list(self.args)
163+
return ["-m", self.name] + self.argslist
154164

155165

156166
class Code(Target):
@@ -176,7 +186,7 @@ def configure(self, session):
176186
session.config["args"] = self.args
177187

178188
def cli(self, env):
179-
return ["-c", self.code] + list(self.args)
189+
return ["-c", self.code] + self.argslist
180190

181191
@property
182192
def co_filename(self):

tests/debugpy/test_args.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ def code_to_debug():
3434

3535
@pytest.mark.parametrize("target", targets.all)
3636
@pytest.mark.parametrize("run", runners.all_launch)
37-
@pytest.mark.parametrize("expansion", ["", "none", "shell"])
37+
@pytest.mark.parametrize("expansion", ["preserve", "expand"])
3838
def test_shell_expansion(pyfile, target, run, expansion):
39+
if expansion == "expand" and run.console == "internalConsole":
40+
pytest.skip('Shell expansion is not supported for "internalConsole"')
41+
3942
@pyfile
4043
def code_to_debug():
4144
import sys
@@ -46,6 +49,8 @@ def code_to_debug():
4649
backchannel.send(sys.argv)
4750

4851
def expand(args):
52+
if expansion != "expand":
53+
return
4954
log.info("Before expansion: {0}", args)
5055
for i, arg in enumerate(args):
5156
if arg.startswith("$"):
@@ -57,17 +62,14 @@ def run_in_terminal(self, args, cwd, env):
5762
expand(args)
5863
return super().run_in_terminal(args, cwd, env)
5964

60-
args = ["0", "$1", "2"]
65+
argslist = ["0", "$1", "2"]
66+
args = argslist if expansion == "preserve" else " ".join(argslist)
6167
with Session() as session:
62-
if expansion:
63-
session.config["argsExpansion"] = expansion
64-
6568
backchannel = session.open_backchannel()
6669
with run(session, target(code_to_debug, args=args)):
6770
pass
6871

6972
argv = backchannel.receive()
7073

71-
if session.config["console"] != "internalConsole" and expansion != "none":
72-
expand(args)
73-
assert argv == [some.str] + args
74+
expand(argslist)
75+
assert argv == [some.str] + argslist

0 commit comments

Comments
 (0)