Skip to content

Commit d3d55a3

Browse files
fix: fix quoting of string arguments that are passed to spawned jobs (#60)
Co-authored-by: Christian Meesters <meesters@uni-mainz.de>
1 parent cc2e3f5 commit d3d55a3

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

snakemake_interface_executor_plugins/utils.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import asyncio
77
from collections import UserDict
88
from pathlib import Path
9+
import re
910
import shlex
1011
import threading
1112
from typing import Any, List
@@ -39,16 +40,25 @@ def format_cli_pos_arg(value, quote=True):
3940
elif not_iterable(value):
4041
return format_cli_value(value)
4142
else:
42-
return join_cli_args(format_cli_value(v) for v in value)
43+
return join_cli_args(
44+
format_cli_value(v, quote_if_contains_whitespace=True) for v in value
45+
)
4346

4447

45-
def format_cli_value(value: Any) -> str:
48+
def format_cli_value(value: Any, quote_if_contains_whitespace: bool = False) -> str:
4649
if isinstance(value, SettingsEnumBase):
4750
return value.item_to_choice()
4851
elif isinstance(value, Path):
4952
return shlex.quote(str(value))
5053
elif isinstance(value, str):
51-
return shlex.quote(value)
54+
if is_quoted(value):
55+
# the value is already quoted, do not quote again
56+
return value
57+
elif quote_if_contains_whitespace and " " in value:
58+
# may be expression
59+
return repr(value)
60+
else:
61+
return value
5262
else:
5363
return repr(value)
5464

@@ -99,3 +109,10 @@ async def async_lock(_lock: threading.Lock):
99109
yield # the lock is held
100110
finally:
101111
_lock.release()
112+
113+
114+
_is_quoted_re = re.compile(r"^['\"].+['\"]")
115+
116+
117+
def is_quoted(value: str) -> bool:
118+
return _is_quoted_re.match(value) is not None

tests/tests.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from snakemake_interface_common.plugin_registry.tests import TestRegistryBase
44
from snakemake_interface_common.plugin_registry.plugin import PluginBase, SettingsBase
55
from snakemake_interface_common.plugin_registry import PluginRegistryBase
6+
from snakemake_interface_executor_plugins.utils import format_cli_arg
67

78

89
class TestRegistry(TestRegistryBase):
@@ -26,3 +27,30 @@ def validate_settings(self, settings: SettingsBase, plugin: PluginBase):
2627

2728
def get_example_args(self) -> List[str]:
2829
return ["--cluster-generic-submit-cmd", "qsub"]
30+
31+
32+
def test_format_cli_arg_single_quote():
33+
fmt = format_cli_arg("--default-resources", {"slurm_extra": "'--gres=gpu:1'"})
34+
assert fmt == "--default-resources \"slurm_extra='--gres=gpu:1'\""
35+
36+
37+
def test_format_cli_arg_double_quote():
38+
fmt = format_cli_arg("--default-resources", {"slurm_extra": '"--gres=gpu:1"'})
39+
assert fmt == "--default-resources 'slurm_extra=\"--gres=gpu:1\"'"
40+
41+
42+
def test_format_cli_arg_int():
43+
fmt = format_cli_arg("--default-resources", {"mem_mb": 200})
44+
assert fmt == "--default-resources 'mem_mb=200'"
45+
46+
47+
def test_format_cli_arg_expr():
48+
fmt = format_cli_arg(
49+
"--default-resources", {"mem_mb": "min(2 * input.size_mb, 2000)"}
50+
)
51+
assert fmt == "--default-resources 'mem_mb=min(2 * input.size_mb, 2000)'"
52+
53+
54+
def test_format_cli_arg_list():
55+
fmt = format_cli_arg("--config", ["foo={'bar': 1}"])
56+
assert fmt == "--config \"foo={'bar': 1}\""

0 commit comments

Comments
 (0)