Repository: StackStorm/st2

Issue:
Error in alias format regex parsing once defaults are involved.
Based on some debugging to reduce problem reported in community by @tonybaloney.

Note : an extra print statement added for debugging purposes locally help point out the errant regex.

```
In [42]: format = "acl {{a}} {{b}} {{c}}"

In [43]: format = "acl {{a}} {{b}} {{c}} {{d=1}}"

In [44]: param_stream = 'acl "x" "y" "z"'

In [45]: p = action_alias_utils.ActionAliasFormatParser(format, param_stream)

In [46]: p.get_extracted_param_value()
^\s*acl\s*["\']?(?P<a}}\s*["\']?(?P<b>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']?(?:\s*["\']?(?P<c>(?:(?<=\').+?(?=\')|(?<=").+?(?>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']?\s+)?\s*\s*$
---------------------------------------------------------------------------
error                                     Traceback (most recent call last)
<ipython-input-46-b3174c03dc30> in <module>()
----> 1 p.get_extracted_param_value()

/mnt/st2repos/st2/st2common/st2common/models/utils/action_alias_utils.py in get_extracted_param_value(self)
     80         # 2. Matched parameters
     81         # 3. Extra parameters
---> 82         matched_stream = re.match(reg, self._param_stream, re.DOTALL)
     83         if matched_stream:
     84             values = matched_stream.groupdict()

/mnt/st2repos/st2/virtualenv/lib/python2.7/re.pyc in match(pattern, string, flags)
    135     """Try to apply the pattern at the start of the string, returning
    136     a match object, or None if no match was found."""
--> 137     return _compile(pattern, flags).match(string)
    138
    139 def search(pattern, string, flags=0):

/mnt/st2repos/st2/virtualenv/lib/python2.7/re.pyc in _compile(*key)
    242         p = sre_compile.compile(pattern, flags)
    243     except error, v:
--> 244         raise error, v # invalid expression
    245     if not bypass_cache:
    246         if len(_cache) >= _MAXCACHE:

error: bad character in group name 'a}}\\s*["\\\']?(?P<b'


In [47]: format = "acl {{a}} {{b}} {{c}}"

In [48]: p = action_alias_utils.ActionAliasFormatParser(format, param_stream)

In [49]: p.get_extracted_param_value()
^\s*acl\s*["\']?(?P<a>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']? \s*["\']?(?P<b>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']? \s*["\']?(?P<c>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']?\s*$
Out[49]: {'a': 'x', 'b': 'y', 'c': 'z'}
```



Discussion:

Resolving patch:
diff --git a/st2common/st2common/models/utils/action_alias_utils.py b/st2common/st2common/models/utils/action_alias_utils.py
--- a/st2common/st2common/models/utils/action_alias_utils.py
+++ b/st2common/st2common/models/utils/action_alias_utils.py
@@ -59,14 +59,14 @@ def get_extracted_param_value(self):
         # substituting {{ ... }} with regex named groups, so that param_stream
         # matched against this expression yields a dict of params with values.
         param_match = r'["\']?(?P<\2>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']?'
-        reg = re.sub(r'(\s*){{\s*([^=]+?)\s*}}(?=\s+{{[^}]+?=)',
-                     r'\s*' + param_match + r'\s+',
+        reg = re.sub(r'(\s*){{\s*([^=}]+?)\s*}}(?![\'"]?\s+}})',
+                     r'\1' + param_match,
                      self._format)
-        reg = re.sub(r'(\s*){{\s*(\S+)\s*=\s*(?:{.+?}|.+?)\s*}}(\s*)',
-                     r'(?:\s*' + param_match + r'\s+)?\s*',
+        reg = re.sub(r'(\s*){{\s*(\S+)\s*=\s*(?:{.+?}|.+?)\s*}}',
+                     r'(?:\1' + param_match + r')?',
                      reg)
-        reg = re.sub(r'(\s*){{\s*(.+?)\s*}}(\s*)',
-                     r'\s*' + param_match + r'\3',
+        reg = re.sub(r'(\s*){{\s*(.+?)\s*}}',
+                     r'\1' + param_match,
                      reg)
         reg = '^\s*' + reg + r'\s*$'
 


Test patch:
diff --git a/st2common/tests/unit/test_action_alias_utils.py b/st2common/tests/unit/test_action_alias_utils.py
--- a/st2common/tests/unit/test_action_alias_utils.py
+++ b/st2common/tests/unit/test_action_alias_utils.py
@@ -94,6 +94,21 @@ def test_spaced_parsing(self):
         extracted_values = parser.get_extracted_param_value()
         self.assertEqual(extracted_values, {'a': 'a1 a2', 'b': 'b1'})
 
+    def test_default_values(self):
+        alias_format = 'acl {{a}} {{b}} {{c}} {{d=1}}'
+        param_stream = 'acl "a1 a2" "b1" "c1"'
+        parser = ActionAliasFormatParser(alias_format, param_stream)
+        extracted_values = parser.get_extracted_param_value()
+        self.assertEqual(extracted_values, {'a': 'a1 a2', 'b': 'b1',
+                                            'c': 'c1', 'd': '1'})
+
+    def test_spacing(self):
+        alias_format = 'acl {{a}}'
+        param_stream = 'acl123'
+        parser = ActionAliasFormatParser(alias_format, param_stream)
+        extracted_values = parser.get_extracted_param_value()
+        self.assertEqual(extracted_values, {'a': ''})
+
     def test_json_parsing(self):
         alias_format = 'skip {{a}} more skip.'
         param_stream = 'skip {"a": "b", "c": "d"} more skip.'