Repository: aws/aws-cli

Issue:
Error parsing parameter in elb configure-health-check shorthand
You're unable to specify a heathcheck url that uses an '=' sign because it's treated as a parse error.

aws elb configure-health-check  --load-balancer-name mylb --health-check target=HTTP:80/?a=b,...

Error parsing parameter --health-check



Discussion:

Resolving patch:
diff --git a/awscli/argprocess.py b/awscli/argprocess.py
--- a/awscli/argprocess.py
+++ b/awscli/argprocess.py
@@ -237,7 +237,7 @@ def _key_value_parse(self, param, value):
         valid_names = self._create_name_to_params(param)
         for part in parts:
             try:
-                key, value = part.split('=')
+                key, value = part.split('=', 1)
             except ValueError:
                 raise ParamSyntaxError(part)
             key = key.strip()
diff --git a/awscli/utils.py b/awscli/utils.py
--- a/awscli/utils.py
+++ b/awscli/utils.py
@@ -36,13 +36,17 @@ def _split_with_quotes(value):
     iter_parts = iter(parts)
     new_parts = []
     for part in iter_parts:
-        if part.count('"') == 1:
-            quote_char = '"'
-        elif part.count("'") == 1:
-            quote_char = "'"
-        else:
+        quote_char = _find_quote_char_in_part(part)
+        if quote_char is None:
             new_parts.append(part)
             continue
+        elif part.count(quote_char) == 2:
+            # Starting and ending quote are in this part.
+            # While it's not needed right now, this will
+            # break down if we ever need to escape quotes while
+            # quoting a value.
+            new_parts.append(part.replace(quote_char, ''))
+            continue
         # Now that we've found a starting quote char, we
         # need to combine the parts until we encounter an end quote.
         current = part
@@ -51,7 +55,6 @@ def _split_with_quotes(value):
             try:
                 current = six.advance_iterator(iter_parts)
             except StopIteration:
-                #raise ParamSyntaxError(value)
                 raise ValueError(value)
             chunks.append(current.replace(quote_char, ''))
             if quote_char in current:
@@ -59,3 +62,20 @@ def _split_with_quotes(value):
         new_chunk = ','.join(chunks)
         new_parts.append(new_chunk)
     return new_parts
+
+
+def _find_quote_char_in_part(part):
+    if '"' not in part and "'" not in part:
+        return
+    quote_char = None
+    double_quote = part.find('"')
+    single_quote = part.find("'")
+    if double_quote >= 0 and single_quote == -1:
+        quote_char = '"'
+    elif single_quote >= 0 and double_quote == -1:
+        quote_char = "'"
+    elif double_quote < single_quote:
+        quote_char = '"'
+    elif single_quote < double_quote:
+        quote_char = "'"
+    return quote_char


Test patch:
diff --git a/tests/unit/elb/test_configure_health_check.py b/tests/unit/elb/test_configure_health_check.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/elb/test_configure_health_check.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+#     http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+import os
+from tests.unit import BaseAWSCommandParamsTest
+
+
+class TestConfigureHealthCheck(BaseAWSCommandParamsTest):
+
+    prefix = 'elb configure-health-check'
+
+    def test_shorthand_basic(self):
+        cmdline = self.prefix
+        cmdline += ' --load-balancer-name my-lb'
+        cmdline += (' --health-check Target=HTTP:80/weather/us/wa/seattle,'
+                    'Interval=300,Timeout=60,UnhealthyThreshold=5,'
+                    'HealthyThreshold=9')
+        result = {'HealthCheck.HealthyThreshold': '9',
+                  'HealthCheck.Interval': '300',
+                  'HealthCheck.Target': 'HTTP:80/weather/us/wa/seattle',
+                  'HealthCheck.Timeout': '60',
+                  'HealthCheck.UnhealthyThreshold': '5',
+                  'LoadBalancerName': 'my-lb'}
+        self.assert_params_for_cmd(cmdline, result)
+
+    def test_json(self):
+        cmdline = self.prefix
+        cmdline += ' --load-balancer-name my-lb '
+        cmdline += ('--health-check {"Target":"HTTP:80/weather/us/wa/seattle'
+                    '?a=b","Interval":300,"Timeout":60,'
+                    '"UnhealthyThreshold":5,"HealthyThreshold":9}')
+        result = {'HealthCheck.HealthyThreshold': '9',
+                  'HealthCheck.Interval': '300',
+                  'HealthCheck.Target': 'HTTP:80/weather/us/wa/seattle?a=b',
+                  'HealthCheck.Timeout': '60',
+                  'HealthCheck.UnhealthyThreshold': '5',
+                  'LoadBalancerName': 'my-lb'}
+        self.assert_params_for_cmd(cmdline, result)
+
+    def test_shorthand_with_multiple_equals_for_value(self):
+        cmdline = self.prefix
+        cmdline += ' --load-balancer-name my-lb'
+        cmdline += (' --health-check Target="HTTP:80/weather/us/wa/seattle?a=b"'
+                    ',Interval=300,Timeout=60,UnhealthyThreshold=5,'
+                    'HealthyThreshold=9')
+        result = {'HealthCheck.HealthyThreshold': '9',
+                  'HealthCheck.Interval': '300',
+                  'HealthCheck.Target': 'HTTP:80/weather/us/wa/seattle?a=b',
+                  'HealthCheck.Timeout': '60',
+                  'HealthCheck.UnhealthyThreshold': '5',
+                  'LoadBalancerName': 'my-lb'}
+        self.assert_params_for_cmd(cmdline, result)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/unit/test_argprocess.py b/tests/unit/test_argprocess.py
--- a/tests/unit/test_argprocess.py
+++ b/tests/unit/test_argprocess.py
@@ -199,7 +199,7 @@ def test_keyval_with_long_values(self):
     def test_error_messages_for_structure_scalar(self):
         p = self.get_param_object(
             'elasticbeanstalk.CreateConfigurationTemplate.SourceConfiguration')
-        value = 'ApplicationName==foo,TemplateName=bar'
+        value = 'ApplicationName:foo,TemplateName=bar'
         error_msg = "Error parsing parameter --source-configuration.*should be"
         with self.assertRaisesRegexp(ParamError, error_msg):
             self.simplify(p, value)
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -29,6 +29,26 @@ def test_inner_quote_split(self):
         self.assertEqual(split_on_commas('foo,bar="1,2,3",baz'),
                          ['foo', 'bar=1,2,3', 'baz'])
 
+    def test_single_quote(self):
+        self.assertEqual(split_on_commas("foo,bar='1,2,3',baz"),
+                         ['foo', 'bar=1,2,3', 'baz'])
+
+    def test_mixing_double_single_quotes(self):
+        self.assertEqual(split_on_commas("""foo,bar="1,'2',3",baz"""),
+                         ['foo', "bar=1,'2',3", 'baz'])
+
+    def test_mixing_double_single_quotes_before_first_comma(self):
+        self.assertEqual(split_on_commas("""foo,bar="1','2',3",baz"""),
+                         ['foo', "bar=1','2',3", 'baz'])
+
+    def test_inner_quote_split_with_equals(self):
+        self.assertEqual(split_on_commas('foo,bar="Foo:80/bar?a=b",baz'),
+                         ['foo', 'bar=Foo:80/bar?a=b', 'baz'])
+
+    def test_single_quoted_inner_value_with_no_commas(self):
+        self.assertEqual(split_on_commas("foo,bar='BAR',baz"),
+                         ['foo', 'bar=BAR', 'baz'])
+
     def test_escape_quotes(self):
         self.assertEqual(split_on_commas('foo,bar=1\,2\,3,baz'),
                          ['foo', 'bar=1,2,3', 'baz'])