diff --git a/awscli/argprocess.py b/awscli/argprocess.py index 816fa12de20c..a59ec727f45d 100644 --- a/awscli/argprocess.py +++ b/awscli/argprocess.py @@ -245,7 +245,7 @@ def _list_scalar_list_parse(self, param, value): current_value = unpack_scalar_cli_arg(args[current_key], current[1].strip()) if args[current_key].type == 'list': - current_parsed[current_key] = [current_value] + current_parsed[current_key] = current_value.split(',') else: current_parsed[current_key] = current_value elif current_key is not None: diff --git a/awscli/utils.py b/awscli/utils.py index 9fb6c6573aea..414474f1bc29 100644 --- a/awscli/utils.py +++ b/awscli/utils.py @@ -16,10 +16,10 @@ def split_on_commas(value): - if '"' not in value and '\\' not in value and "'" not in value: + if not any(char in value for char in ['"', '\\', "'", ']', '[']): # No quotes or escaping, just use a simple split. return value.split(',') - elif '"' not in value and "'" not in value: + elif not any(char in value for char in ['"', "'", '[', ']']): # Simple escaping, let the csv module handle it. return list(csv.reader(six.StringIO(value), escapechar='\\'))[0] else: @@ -36,8 +36,21 @@ def _split_with_quotes(value): iter_parts = iter(parts) new_parts = [] for part in iter_parts: + # Find the first quote quote_char = _find_quote_char_in_part(part) - if quote_char is None: + + # Find an opening list bracket + list_start = part.find('=[') + + if list_start >= 0 and value.find(']') != -1 and \ + (quote_char is None or part.find(quote_char) > list_start): + # This is a list, eat all the items until the end + new_chunk = _eat_items(value, iter_parts, part, ']') + list_items = _split_with_quotes(new_chunk[list_start + 2:-1]) + new_chunk = new_chunk[:list_start + 1] + ','.join(list_items) + new_parts.append(new_chunk) + continue + elif quote_char is None: new_parts.append(part) continue elif part.count(quote_char) == 2: @@ -49,21 +62,29 @@ def _split_with_quotes(value): continue # Now that we've found a starting quote char, we # need to combine the parts until we encounter an end quote. - current = part - chunks = [current.replace(quote_char, '')] - while True: - try: - current = six.advance_iterator(iter_parts) - except StopIteration: - raise ValueError(value) - chunks.append(current.replace(quote_char, '')) - if quote_char in current: - break - new_chunk = ','.join(chunks) + new_chunk = _eat_items(value, iter_parts, part, quote_char, quote_char) new_parts.append(new_chunk) return new_parts +def _eat_items(value, iter_parts, part, end_char, replace_char=''): + """ + Eat items from an iterator, optionally replacing characters with + a blank and stopping when the end_char has been reached. + """ + current = part + chunks = [current.replace(replace_char, '')] + while True: + try: + current = six.advance_iterator(iter_parts) + except StopIteration: + raise ValueError(value) + chunks.append(current.replace(replace_char, '')) + if end_char in current: + break + return ','.join(chunks) + + def _find_quote_char_in_part(part): if '"' not in part and "'" not in part: return diff --git a/tests/unit/test_argprocess.py b/tests/unit/test_argprocess.py index 5d6ef8a260af..3356006cc659 100644 --- a/tests/unit/test_argprocess.py +++ b/tests/unit/test_argprocess.py @@ -25,6 +25,7 @@ from awscli.argprocess import ParamError from awscli.argprocess import ParamUnknownKeyError from awscli.argprocess import uri_param +from awscli.arguments import CustomArgument MAPHELP = """--attributes key_name=string,key_name2=string @@ -194,6 +195,41 @@ def test_list_structure_list_scalar_2(self): self.assertEqual(simplified, expected) + def test_list_structure_list_scalar_3(self): + arg = CustomArgument('foo', schema={ + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'Name': { + 'type': 'string' + }, + 'Args': { + 'type': 'array', + 'items': { + 'type': 'string' + } + } + } + } + }) + arg.create_argument_object() + p = arg.argument_object + + expected = [ + {"Name": "foo", + "Args": ["a", "k1=v1", "b"]}, + {"Name": "bar", + "Args": ["baz"]} + ] + + simplified = self.simplify(p, [ + "Name=foo,Args=[a,k1=v1,b]", + "Name=bar,Args=baz" + ]) + + self.assertEqual(simplified, expected) + def test_list_structure_list_multiple_scalar(self): p = self.get_param_object('elastictranscoder.CreateJob.Playlists') returned = self.simplify( diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 37501a6d50af..16d3eeb20484 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -62,3 +62,23 @@ def test_trailing_commas(self): def test_escape_backslash(self): self.assertEqual(split_on_commas('foo,bar\\\\,baz\\\\,qux'), ['foo', 'bar\\', 'baz\\', 'qux']) + + def test_square_brackets(self): + self.assertEqual(split_on_commas('foo,bar=["a=b",\'2\',c=d],baz'), + ['foo', 'bar=a=b,2,c=d', 'baz']) + + def test_quoted_square_brackets(self): + self.assertEqual(split_on_commas('foo,bar="[blah]",c=d],baz'), + ['foo', 'bar=[blah]', 'c=d]', 'baz']) + + def test_missing_bracket(self): + self.assertEqual(split_on_commas('foo,bar=[a,baz'), + ['foo', 'bar=[a', 'baz']) + + def test_missing_bracket2(self): + self.assertEqual(split_on_commas('foo,bar=a],baz'), + ['foo', 'bar=a]', 'baz']) + + def test_bracket_in_middle(self): + self.assertEqual(split_on_commas('foo,bar=a[b][c],baz'), + ['foo', 'bar=a[b][c]', 'baz'])