Skip to content

Commit d5d5fd8

Browse files
authored
Merge pull request #545 from tclose/rebased-shlex-split
Split command line using quote respecting shlex.split instead of ' '.split
2 parents fdbd3ae + 7de2b38 commit d5d5fd8

File tree

6 files changed

+170
-13
lines changed

6 files changed

+170
-13
lines changed

pydra/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import shutil
22
import os
3+
import pytest
34

45
os.environ["NO_ET"] = "true"
56

@@ -26,3 +27,30 @@ def pytest_generate_tests(metafunc):
2627
else:
2728
Plugins = ["cf"]
2829
metafunc.parametrize("plugin", Plugins)
30+
31+
32+
# For debugging in IDE's don't catch raised exceptions and let the IDE
33+
# break at it
34+
if os.getenv("_PYTEST_RAISE", "0") != "0":
35+
36+
@pytest.hookimpl(tryfirst=True)
37+
def pytest_exception_interact(call):
38+
raise call.excinfo.value
39+
40+
@pytest.hookimpl(tryfirst=True)
41+
def pytest_internalerror(excinfo):
42+
raise excinfo.value
43+
44+
45+
# Example VSCode launch configuration for debugging unittests
46+
# {
47+
# "name": "Test Config",
48+
# "type": "python",
49+
# "request": "launch",
50+
# "purpose": ["debug-test"],
51+
# "justMyCode": false,
52+
# "console": "internalConsole",
53+
# "env": {
54+
# "_PYTEST_RAISE": "1"
55+
# },
56+
# }

pydra/engine/core.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ def _run(self, rerun=False, **kwargs):
489489
lockfile = self.cache_dir / (checksum + ".lock")
490490
# Eagerly retrieve cached - see scenarios in __init__()
491491
self.hooks.pre_run(self)
492+
logger.debug(f"'%s' is attempting to acquire lock on %s", self.name, lockfile)
492493
with SoftFileLock(lockfile):
493494
if not (rerun or self.task_rerun):
494495
result = self.result()
@@ -1063,6 +1064,11 @@ async def _run(self, submitter=None, rerun=False, **kwargs):
10631064
output_dir = self.output_dir
10641065
lockfile = self.cache_dir / (checksum + ".lock")
10651066
self.hooks.pre_run(self)
1067+
logger.debug(
1068+
f"'%s' is attempting to acquire lock on %s with Pydra lock",
1069+
self.name,
1070+
lockfile,
1071+
)
10661072
async with PydraFileLock(lockfile):
10671073
if not (rerun or self.task_rerun):
10681074
result = self.result()

pydra/engine/task.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@
3838
<https://colab.research.google.com/drive/1RRV1gHbGJs49qQB1q1d5tQEycVRtuhw6>`__
3939
4040
"""
41+
import platform
42+
import re
4143
import attr
4244
import cloudpickle as cp
4345
import inspect
4446
import typing as ty
47+
import shlex
4548
from pathlib import Path
4649
import warnings
4750

@@ -447,7 +450,7 @@ def _command_pos_args(self, field):
447450
cmd_el_str = field.metadata["formatter"](**call_args_val)
448451
cmd_el_str = cmd_el_str.strip().replace(" ", " ")
449452
if cmd_el_str != "":
450-
cmd_add += cmd_el_str.split(" ")
453+
cmd_add += split_cmd(cmd_el_str)
451454
elif field.type is bool:
452455
# if value is simply True the original argstr is used,
453456
# if False, nothing is added to the command
@@ -483,10 +486,8 @@ def _command_pos_args(self, field):
483486
cmd_el_str = f"{argstr} {value}"
484487
else:
485488
cmd_el_str = ""
486-
# removing double spacing
487-
cmd_el_str = cmd_el_str.strip().replace(" ", " ")
488489
if cmd_el_str:
489-
cmd_add += cmd_el_str.split(" ")
490+
cmd_add += split_cmd(cmd_el_str)
490491
return pos, cmd_add
491492

492493
@property
@@ -501,9 +502,19 @@ def cmdline(self):
501502
if self.state:
502503
raise NotImplementedError
503504
if isinstance(self, ContainerTask):
504-
cmdline = " ".join(self.container_args + self.command_args)
505+
command_args = self.container_args + self.command_args
505506
else:
506-
cmdline = " ".join(self.command_args)
507+
command_args = self.command_args
508+
# Skip the executable, which can be a multi-part command, e.g. 'docker run'.
509+
cmdline = command_args[0]
510+
for arg in command_args[1:]:
511+
# If there are spaces in the arg and it is not enclosed by matching
512+
# quotes, add quotes to escape the space. Not sure if this should
513+
# be expanded to include other special characters apart from spaces
514+
if " " in arg:
515+
cmdline += " '" + arg + "'"
516+
else:
517+
cmdline += " " + arg
507518
return cmdline
508519

509520
def _run_task(self):
@@ -519,10 +530,12 @@ def _run_task(self):
519530
values = execute(args, strip=self.strip)
520531
self.output_ = dict(zip(keys, values))
521532
if self.output_["return_code"]:
533+
msg = f"Error running '{self.name}' task with {args}:"
522534
if self.output_["stderr"]:
523-
raise RuntimeError(self.output_["stderr"])
524-
else:
525-
raise RuntimeError(self.output_["stdout"])
535+
msg += "\n\nstderr:\n" + self.output_["stderr"]
536+
if self.output_["stdout"]:
537+
msg += "\n\nstdout:\n" + self.output_["stdout"]
538+
raise RuntimeError(msg)
526539

527540

528541
class ContainerTask(ShellCommandTask):
@@ -825,3 +838,29 @@ def container_args(self):
825838
cargs.extend(["--pwd", str(self.output_cpath)])
826839
cargs.append(self.inputs.image)
827840
return cargs
841+
842+
843+
def split_cmd(cmd: str):
844+
"""Splits a shell command line into separate arguments respecting quotes
845+
846+
Parameters
847+
----------
848+
cmd : str
849+
Command line string or part thereof
850+
851+
Returns
852+
-------
853+
str
854+
the command line string split into process args
855+
"""
856+
# Check whether running on posix or windows system
857+
on_posix = platform.system() != "Windows"
858+
args = shlex.split(cmd, posix=on_posix)
859+
cmd_args = []
860+
for arg in args:
861+
match = re.match("('|\")(.*)\\1$", arg)
862+
if match:
863+
cmd_args.append(match.group(2))
864+
else:
865+
cmd_args.append(arg)
866+
return cmd_args

pydra/engine/tests/test_nipype1_convert.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ class Interf_2(ShellCommandTask):
3434
executable = "testing command"
3535

3636

37+
class Interf_3(ShellCommandTask):
38+
"""class with customized input and executables"""
39+
40+
input_spec = SpecInfo(
41+
name="Input",
42+
fields=[
43+
(
44+
"in_file",
45+
str,
46+
{"help_string": "in_file", "argstr": "'{in_file}'"},
47+
)
48+
],
49+
bases=(ShellSpec,),
50+
)
51+
executable = "testing command"
52+
53+
3754
class TouchInterf(ShellCommandTask):
3855
"""class with customized input and executables"""
3956

@@ -96,6 +113,13 @@ def test_interface_executable_2():
96113
assert task.cmdline == "i want a different command"
97114

98115

116+
def test_interface_cmdline_with_spaces():
117+
task = Interf_3(in_file="/path/to/file/with spaces")
118+
assert task.executable == "testing command"
119+
assert task.inputs.executable == "testing command"
120+
assert task.cmdline == "testing command '/path/to/file/with spaces'"
121+
122+
99123
def test_interface_run_1():
100124
"""testing execution of a simple interf with customized input and executable"""
101125
task = TouchInterf(new_file="hello.txt")

pydra/engine/tests/test_shelltask.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55
from pathlib import Path
66
import re
7+
import stat
78

89
from ..task import ShellCommandTask
910
from ..submitter import Submitter
@@ -18,7 +19,7 @@
1819
MultiOutputFile,
1920
MultiInputObj,
2021
)
21-
from .utils import result_no_submitter, result_submitter, use_validator
22+
from .utils import result_no_submitter, result_submitter, use_validator, no_win
2223

2324
if sys.platform.startswith("win"):
2425
pytest.skip("SLURM not available in windows", allow_module_level=True)
@@ -307,7 +308,7 @@ def test_shell_cmd_inputspec_1(plugin, results_function, use_validator, tmpdir):
307308
)
308309
assert shelly.inputs.executable == cmd_exec
309310
assert shelly.inputs.args == cmd_args
310-
assert shelly.cmdline == "echo -n hello from pydra"
311+
assert shelly.cmdline == "echo -n 'hello from pydra'"
311312

312313
res = results_function(shelly, plugin)
313314
assert res.output.stdout == "hello from pydra"
@@ -356,7 +357,7 @@ def test_shell_cmd_inputspec_2(plugin, results_function, use_validator, tmpdir):
356357
)
357358
assert shelly.inputs.executable == cmd_exec
358359
assert shelly.inputs.args == cmd_args
359-
assert shelly.cmdline == "echo -n HELLO from pydra"
360+
assert shelly.cmdline == "echo -n HELLO 'from pydra'"
360361
res = results_function(shelly, plugin)
361362
assert res.output.stdout == "HELLO from pydra"
362363

@@ -4749,3 +4750,62 @@ def formatter_1(in1, in2):
47494750
# com_results = ["executable -t [in11 in2]", "executable -t [in12 in2]"]
47504751
# for i, cr in enumerate(com_results):
47514752
# assert results[i] == cr
4753+
4754+
4755+
@no_win
4756+
def test_shellcommand_error_msg(tmpdir):
4757+
4758+
script_path = Path(tmpdir) / "script.sh"
4759+
4760+
with open(script_path, "w") as f:
4761+
f.write(
4762+
"""#!/bin/bash
4763+
echo "first line is ok, it prints '$1'"
4764+
/command-that-doesnt-exist"""
4765+
)
4766+
4767+
os.chmod(
4768+
script_path,
4769+
mode=(
4770+
stat.S_IRUSR
4771+
| stat.S_IWUSR
4772+
| stat.S_IXUSR
4773+
| stat.S_IRGRP
4774+
| stat.S_IWGRP
4775+
| stat.S_IROTH
4776+
),
4777+
)
4778+
4779+
input_spec = SpecInfo(
4780+
name="Input",
4781+
fields=[
4782+
(
4783+
"in1",
4784+
str,
4785+
{"help_string": "a dummy string", "argstr": "", "mandatory": True},
4786+
),
4787+
],
4788+
bases=(ShellSpec,),
4789+
)
4790+
4791+
shelly = ShellCommandTask(
4792+
name="err_msg", executable=str(script_path), input_spec=input_spec, in1="hello"
4793+
)
4794+
4795+
with pytest.raises(RuntimeError) as excinfo:
4796+
shelly()
4797+
4798+
path_str = str(script_path)
4799+
4800+
assert (
4801+
str(excinfo.value)
4802+
== f"""Error running 'err_msg' task with ['{path_str}', 'hello']:
4803+
4804+
stderr:
4805+
{path_str}: line 3: /command-that-doesnt-exist: No such file or directory
4806+
4807+
4808+
stdout:
4809+
first line is ok, it prints 'hello'
4810+
"""
4811+
)

pydra/engine/tests/test_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ def test_docker_cmd(tmpdir):
11241124
docky.cmdline
11251125
== f"docker run --rm -v {docky.output_dir}:/output_pydra:rw -w /output_pydra busybox pwd"
11261126
)
1127-
docky.inputs.container_xargs = ["--rm -it"]
1127+
docky.inputs.container_xargs = ["--rm", "-it"]
11281128
assert (
11291129
docky.cmdline
11301130
== f"docker run --rm -it -v {docky.output_dir}:/output_pydra:rw -w /output_pydra busybox pwd"

0 commit comments

Comments
 (0)