config.py: Allow escaping commas in parametrized scenario names

comma character ',' is used in the command line to split between
parameters, which means a parameter value itself couldn't contain it.
This commit allows passing a comma inside a character value by escaping
it with '\,'.

Change-Id: Ic0bd9a029137a59e8c4a32b807eba7a64fcfa51f
diff --git a/src/osmo_gsm_tester/config.py b/src/osmo_gsm_tester/config.py
index 5016018..9333601 100644
--- a/src/osmo_gsm_tester/config.py
+++ b/src/osmo_gsm_tester/config.py
@@ -201,6 +201,49 @@
         self.path = path
         self.param_list = param_list
 
+    @classmethod
+    def count_cont_char_backward(cls, str, before_pos, c):
+        n = 0
+        i = before_pos - 1
+        while i >= 0:
+            if str[i] != c:
+                break
+            n += 1
+            i -= 1
+        return n
+
+    @classmethod
+    def split_scenario_parameters(cls, str):
+        cur_pos = 0
+        param_li = []
+        saved = ''
+        # Split into a list, but we want to escape '\,' to avoid splitting parameters containing commas.
+        while True:
+            prev_pos = cur_pos
+            cur_pos = str.find(',', prev_pos)
+            if cur_pos == -1:
+                param_li.append(str[prev_pos:])
+                break
+            if cur_pos == 0:
+                param_li.append('')
+            elif cur_pos != 0 and str[cur_pos - 1] == '\\' and cls.count_cont_char_backward(str, cur_pos, '\\') % 2 == 1:
+                saved += str[prev_pos:cur_pos - 1] + ','
+            else:
+                param_li.append(saved + str[prev_pos:cur_pos])
+                saved = ''
+            cur_pos += 1
+        i = 0
+        # Also escape '\\' -> '\'
+        while i < len(param_li):
+            param_li[i] = param_li[i].replace('\\\\', '\\')
+            i += 1
+        return param_li
+
+    @classmethod
+    def from_param_list_str(cls, name, path, param_list_str):
+        param_list = cls.split_scenario_parameters(param_list_str)
+        return cls(name, path, param_list)
+
     def read_from_file(self, validation_schema):
         with open(self.path, 'r') as f:
             config_str = f.read()
@@ -228,6 +271,7 @@
     if not is_parametrized_file:
         if not os.path.isfile(path):
             raise RuntimeError('No such scenario file: %r' % path)
+        sc = Scenario(name, path)
     else: # parametrized scenario:
         # Allow first matching complete matching names (eg: scenario@param1,param2.conf),
         # this allows setting specific content in different files for specific values.
@@ -240,8 +284,7 @@
         # At this point, we have existing file path. Let's now scrap the parameter(s):
         # get param1,param2 str from scenario@param1,param2.conf
         param_list_str = name.split('@', 1)[1][:-len('.conf')]
-        param_list = param_list_str.split(',')
-    sc = Scenario(name, path, param_list)
+        sc = Scenario.from_param_list_str(name, path, param_list_str)
     sc.read_from_file(validation_schema)
     return sc