Skip to content

Commit d3cff26

Browse files
committed
Use regex to unfold arguments.
1 parent bbb48ec commit d3cff26

File tree

5 files changed

+108
-60
lines changed

5 files changed

+108
-60
lines changed

scripts/smart_dispatch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ def main():
3636
commands = smartdispatch.get_commands_from_file(args.commandsFile)
3737
else:
3838
# Commands that needs to be parsed and unfolded.
39-
arguments = map(smartdispatch.unfold_argument, args.commandAndOptions)
39+
arguments = smartdispatch.unfold_arguments(args.commandAndOptions)
4040
jobname = smartdispatch.generate_name_from_arguments(arguments, max_length=235)
41-
commands = smartdispatch.get_commands_from_arguments(" ".join(args.commandAndOptions))
41+
commands = smartdispatch.get_commands_from_arguments(arguments)
4242

4343
commands = smartdispatch.replace_uid_tag(commands)
4444

smartdispatch/argument.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import re
2+
3+
4+
class Argument(object):
5+
def __init__(self):
6+
self.name = ""
7+
self.regex = ""
8+
9+
def unfold(self, match):
10+
raise NotImplementedError("Subclass must implement this method!")
11+
12+
13+
class EnumerationArgument(object):
14+
def __init__(self):
15+
self.name = "enumeration"
16+
self.regex = "\[[^]]*\]"
17+
18+
def unfold(self, match):
19+
return match[1:-1].split(',')
20+
21+
22+
class RangeArgument(object):
23+
def __init__(self):
24+
self.name = "range"
25+
self.regex = "\[\d+:\d+(:\d+)?\]"
26+
27+
def unfold(self, match):
28+
groups = re.search(self.regex, match).groups()
29+
start = int(groups[0])
30+
end = int(groups[1])
31+
step = 1 if groups[2] is None else int(groups[2])
32+
return range(start, end, step)

smartdispatch/smartdispatch.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import absolute_import
22

33
import os
4+
import re
5+
import itertools
46
from datetime import datetime
57

68
import smartdispatch
79
from smartdispatch import utils
10+
from smartdispatch.argument import EnumerationArgument, RangeArgument
811

912
UID_TAG = "{UID}"
1013

@@ -98,48 +101,29 @@ def get_commands_from_file(fileobj):
98101

99102

100103
def get_commands_from_arguments(arguments):
101-
unfolded_commands = [arguments]
102-
unfolded_commands_stard_idx = [0]
103-
104-
while True:
105-
new_unfolded_commands = []
106-
new_unfolded_commands_stard_idx = []
107-
108-
for idx in range(len(unfolded_commands)):
109-
start_bracket_idx = unfolded_commands[idx].find("[", unfolded_commands_stard_idx[idx])
110-
111-
if start_bracket_idx == -1:
112-
new_unfolded_commands_stard_idx = [-1]
113-
break
114-
115-
while unfolded_commands[idx][start_bracket_idx + 1] == "[":
116-
start_bracket_idx += 1
117-
118-
stop_bracket_idx = unfolded_commands[idx].find("]", start_bracket_idx)
119-
120-
for argument in unfolded_commands[idx][start_bracket_idx + 1:stop_bracket_idx].split(" "):
121-
new_unfolded_commands_stard_idx += [start_bracket_idx + len(argument)]
122-
new_unfolded_commands += [unfolded_commands[idx][0:start_bracket_idx] + argument + unfolded_commands[idx][stop_bracket_idx + 1:]]
123-
124-
if -1 in new_unfolded_commands_stard_idx:
125-
break
126-
127-
unfolded_commands = new_unfolded_commands
128-
unfolded_commands_stard_idx = new_unfolded_commands_stard_idx
129-
130-
return unfolded_commands
104+
''' Obtains commands from the product of every unfolded arguments.
105+
Parameters
106+
----------
107+
arguments : list of list of str
108+
list of unfolded arguments
109+
Returns
110+
-------
111+
commands : list of str
112+
commands resulting from the product of every unfolded arguments
113+
'''
114+
return ["".join(argvalues) for argvalues in itertools.product(*arguments)]
131115

132116

133-
def unfold_argument(argument):
134-
''' Unfolds a folded argument into a list of unfolded arguments.
117+
def unfold_arguments(arguments):
118+
''' Unfolds folded arguments into a list of unfolded arguments.
135119
136-
An argument can be folded e.g. a list of unfolded arguments separated by spaces.
120+
An argument can be folded e.g. a list of unfolded arguments separated by commas.
137121
An unfolded argument unfolds to itself.
138122
139123
Parameters
140124
----------
141-
argument : str
142-
argument to unfold
125+
arguments : list of str
126+
arguments to unfold
143127
144128
Returns
145129
-------
@@ -148,11 +132,34 @@ def unfold_argument(argument):
148132
149133
Complex arguments
150134
-----------------
151-
*list (space)*: "item1 item2 ... itemN"
135+
*enumeration*: "[item1,item2,...,itemN]"
136+
*range*: "[start:end]" or "[start:end:step]"
152137
'''
138+
text = utils.escape(" ".join(arguments))
139+
140+
# Order matter, if some regex is more greedy than another, the it should go after
141+
arguments = [EnumerationArgument(), RangeArgument()]
142+
143+
# Build the master regex with all argument's regex
144+
regex = "(" + "|".join(["(?P<{0}>{1})".format(arg.name, arg.regex) for arg in arguments]) + ")"
145+
146+
pos = 0
147+
unfolded_arguments = []
148+
for match in re.finditer(regex, text):
149+
# Add already unfolded argument
150+
unfolded_arguments.append([text[pos:match.start()]])
151+
152+
# Unfold argument
153+
groupdict = match.groupdict()
154+
for argument in arguments:
155+
if groupdict[argument.name] is not None:
156+
unfolded_arguments.append(argument.unfold(groupdict[argument.name]))
157+
158+
pos = match.end()
153159

154-
# Suppose `argument`is a space separated list
155-
return argument.split(" ")
160+
unfolded_arguments.append([text[pos:]]) # Add remaining unfolded arguments
161+
unfolded_arguments = [map(utils.hex2str, argvalues) for argvalues in unfolded_arguments]
162+
return unfolded_arguments
156163

157164

158165
def replace_uid_tag(commands):

smartdispatch/tests/test_smartdispatch.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,28 @@ def test_get_commands_from_file():
7878

7979

8080
def test_get_commands_from_arguments():
81-
# Test single unfolded arguments
82-
args = "ls"
81+
# Test with one argument
82+
args = [["ls"]]
8383
assert_equal(smartdispatch.get_commands_from_arguments(args), ["ls"])
8484

85-
args = "echo 1"
85+
args = [["echo 1"]]
8686
assert_equal(smartdispatch.get_commands_from_arguments(args), ["echo 1"])
8787

88-
args = "echo [1 2]"
88+
# Test two arguments
89+
args = smartdispatch.unfold_arguments(["echo [1,2]"])
8990
assert_equal(smartdispatch.get_commands_from_arguments(args), ["echo 1", "echo 2"])
9091

91-
args = "echo test [1 2] yay"
92+
args = smartdispatch.unfold_arguments(["echo test [1,2] yay"])
9293
assert_equal(smartdispatch.get_commands_from_arguments(args), ["echo test 1 yay", "echo test 2 yay"])
9394

94-
args = "echo test[1 2]"
95+
args = smartdispatch.unfold_arguments(["echo test[1,2]"])
9596
assert_equal(smartdispatch.get_commands_from_arguments(args), ["echo test1", "echo test2"])
9697

97-
args = "echo test[1 2]yay"
98+
args = smartdispatch.unfold_arguments(["echo test[1,2]yay"])
9899
assert_equal(smartdispatch.get_commands_from_arguments(args), ["echo test1yay", "echo test2yay"])
99100

100-
# Test multiple unfolded arguments
101-
args = "python my_command.py [0.01 0.000001 0.00000000001] -1 [omicron mu]"
101+
# Test multiple folded arguments
102+
args = smartdispatch.unfold_arguments(["python my_command.py", "[0.01,0.000001,0.00000000001]", "-1", "[omicron,mu]"])
102103
assert_equal(smartdispatch.get_commands_from_arguments(args), ["python my_command.py 0.01 -1 omicron",
103104
"python my_command.py 0.01 -1 mu",
104105
"python my_command.py 0.000001 -1 omicron",
@@ -107,7 +108,7 @@ def test_get_commands_from_arguments():
107108
"python my_command.py 0.00000000001 -1 mu"])
108109

109110
# Test multiple unfolded arguments and not unfoldable brackets
110-
args = "python my_command.py [0.01 0.000001 0.00000000001] -1 [[42 133,666]] slow [omicron mu]"
111+
args = smartdispatch.unfold_arguments(["python my_command.py [0.01,0.000001,0.00000000001] -1 \[[42,133\,666]\] slow [omicron,mu]"])
111112
assert_equal(smartdispatch.get_commands_from_arguments(args), ["python my_command.py 0.01 -1 [42] slow omicron",
112113
"python my_command.py 0.01 -1 [42] slow mu",
113114
"python my_command.py 0.01 -1 [133,666] slow omicron",
@@ -122,16 +123,6 @@ def test_get_commands_from_arguments():
122123
"python my_command.py 0.00000000001 -1 [133,666] slow mu"])
123124

124125

125-
def test_unfold_argument():
126-
# Test simple argument
127-
for arg in ["arg1", "[arg1"]:
128-
assert_array_equal(smartdispatch.unfold_argument(arg), [arg])
129-
130-
# Test list (space)
131-
for arg in ["arg1 arg2", "arg1 ", " arg1"]:
132-
assert_array_equal(smartdispatch.unfold_argument(arg), arg.split(" "))
133-
134-
135126
def test_replace_uid_tag():
136127
command = "command without uid tag"
137128
assert_array_equal(smartdispatch.replace_uid_tag([command]), [command])

smartdispatch/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ def slugify(value):
3535
return str(re.sub('[-\s]+', '_', value))
3636

3737

38+
def escape(text, escaping_character="\\"):
39+
""" Escape the escaped character using its hex representation """
40+
def hexify(match):
41+
return "\\x{0}".format(match.group()[-1].encode("hex"))
42+
43+
return re.sub(r"\\.", hexify, text)
44+
45+
46+
def hex2str(text):
47+
""" Convert hex representation to the character it represents """
48+
if len(text) == 0:
49+
return ''
50+
51+
def unhexify(match):
52+
return match.group()[2:].decode("hex")
53+
54+
return re.sub(r"\\x..", unhexify, text)
55+
3856
@contextmanager
3957
def open_with_lock(*args, **kwargs):
4058
""" Context manager for opening file with an exclusive lock. """

0 commit comments

Comments
 (0)