diff --git a/oslo_utils/strutils.py b/oslo_utils/strutils.py index 10ebac0c..9a9b8664 100644 --- a/oslo_utils/strutils.py +++ b/oslo_utils/strutils.py @@ -22,6 +22,7 @@ import math import re import unicodedata +import pyparsing as pp import six from six.moves import urllib @@ -468,3 +469,20 @@ def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): segs = segs[1:maxsegs] segs.extend([None] * (maxsegs - 1 - len(segs))) return segs + + +def split_by_commas(value): + """Split values by commas and quotes according to api-wg + + :param value: value to be split + + .. versionadded:: 3.17 + """ + word = (pp.QuotedString(quoteChar='"', escChar='\\') + | pp.Word(pp.printables, excludeChars='",')) + grammar = pp.stringStart + pp.delimitedList(word) + pp.stringEnd + + try: + return list(grammar.parseString(value)) + except pp.ParseException: + raise ValueError("Invalid value: %s" % value) diff --git a/oslo_utils/tests/test_strutils.py b/oslo_utils/tests/test_strutils.py index 138ff648..a2fddd08 100644 --- a/oslo_utils/tests/test_strutils.py +++ b/oslo_utils/tests/test_strutils.py @@ -762,3 +762,30 @@ class SplitPathTestCase(test_base.BaseTestCase): strutils.split_path('o\nn e', 2, 3, True) except ValueError as err: self.assertEqual(str(err), 'Invalid path: o%0An%20e') + + +class SplitByCommas(test_base.BaseTestCase): + def test_not_closed_quotes(self): + self.assertRaises(ValueError, strutils.split_by_commas, '"ab","b""') + + def test_no_comma_before_opening_quotes(self): + self.assertRaises(ValueError, strutils.split_by_commas, '"ab""b"') + + def test_quote_inside_unquoted(self): + self.assertRaises(ValueError, strutils.split_by_commas, 'a"b,cd') + + def check(self, expect, input): + self.assertEqual(expect, strutils.split_by_commas(input)) + + def test_plain(self): + self.check(["a,b", "ac"], '"a,b",ac') + + def test_with_backslash_inside_quoted(self): + self.check(['abc"', 'de', 'fg,h', 'klm\\', '"nop'], + r'"abc\"","de","fg,h","klm\\","\"nop"') + + def test_with_backslash_inside_unquoted(self): + self.check([r'a\bc', 'de'], r'a\bc,de') + + def test_with_escaped_quotes_in_row_inside_quoted(self): + self.check(['a"b""c', 'd'], r'"a\"b\"\"c",d')