diff --git a/README.rst b/README.rst index f20d557f..6d37e8ae 100644 --- a/README.rst +++ b/README.rst @@ -1277,6 +1277,9 @@ Changelog * Added `CONTRIBUTING`_. * Fixed ``User-Agent`` overwriting when used within a session. * Fixed handling of empty passwords in URL credentials. + * To make it easier to deal with Windows paths in request items, ``\`` + now only special characters (the ones that are used as key-value + separators). * `0.8.0`_ (2014-01-25) * Added ``field=@file.txt`` and ``field:=@file.json`` for embedding the contents of text and JSON files into request data. diff --git a/httpie/input.py b/httpie/input.py index c05fa9b8..44a96fa6 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -398,6 +398,9 @@ class KeyValue(object): def __eq__(self, other): return self.__dict__ == other.__dict__ + def __repr__(self): + return repr(self.__dict__) + class SessionNameValidator(object): @@ -424,6 +427,9 @@ class KeyValueArgType(object): def __init__(self, *separators): self.separators = separators + self.special_characters = set('\\') + for separator in separators: + self.special_characters.update(separator) def __call__(self, string): """Parse `string` and return `self.key_value_class()` instance. @@ -446,17 +452,18 @@ class KeyValueArgType(object): => ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz'] """ + backslash = '\\' tokens = [''] - esc = False + s = iter(s) for c in s: - if esc: - tokens.extend([Escaped(c), '']) - esc = False - else: - if c == '\\': - esc = True + if c == backslash: + nc = next(s, '') + if nc in self.special_characters: + tokens.extend([Escaped(nc), '']) else: - tokens[-1] += c + tokens[-1] += c + nc + else: + tokens[-1] += c return tokens tokens = tokenize(string) diff --git a/tests/test_cli.py b/tests/test_cli.py index f8009153..d7e0c01e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -18,26 +18,28 @@ from fixtures import ( class TestItemParsing: - key_value_type = KeyValueArgType(*input.SEP_GROUP_ALL_ITEMS) + key_value = KeyValueArgType(*input.SEP_GROUP_ALL_ITEMS) def test_invalid_items(self): items = ['no-separator'] for item in items: - pytest.raises(argparse.ArgumentTypeError, - self.key_value_type, item) + pytest.raises(argparse.ArgumentTypeError, self.key_value, item) - def test_escape(self): + def test_escape_separator(self): items = input.parse_items([ # headers - self.key_value_type(r'foo\:bar:baz'), - self.key_value_type(r'jack\@jill:hill'), + self.key_value(r'foo\:bar:baz'), + self.key_value(r'jack\@jill:hill'), + # data - self.key_value_type(r'baz\=bar=foo'), + self.key_value(r'baz\=bar=foo'), + # files - self.key_value_type(r'bar\@baz@%s' % FILE_PATH_ARG) + self.key_value(r'bar\@baz@%s' % FILE_PATH_ARG), ]) # `requests.structures.CaseInsensitiveDict` => `dict` headers = dict(items.headers._store.values()) + assert headers == { 'foo:bar': 'baz', 'jack@jill': 'hill', @@ -45,25 +47,36 @@ class TestItemParsing: assert items.data == {'baz=bar': 'foo'} assert 'bar@baz' in items.files + @pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [ + ('path=c:\windows', 'path', '=', 'c:\windows'), + ('path=c:\windows\\', 'path', '=', 'c:\windows\\'), + ('path\==c:\windows', 'path=', '=', 'c:\windows'), + ]) + def test_backslash_before_non_special_character_does_not_escape( + self, string, key, sep, value): + expected = KeyValue(orig=string, key=key, sep=sep, value=value) + actual = self.key_value(string) + assert actual == expected + def test_escape_longsep(self): items = input.parse_items([ - self.key_value_type(r'bob\:==foo'), + self.key_value(r'bob\:==foo'), ]) assert items.params == {'bob:': 'foo'} def test_valid_items(self): items = input.parse_items([ - self.key_value_type('string=value'), - self.key_value_type('header:value'), - self.key_value_type('list:=["a", 1, {}, false]'), - self.key_value_type('obj:={"a": "b"}'), - self.key_value_type('eh:'), - self.key_value_type('ed='), - self.key_value_type('bool:=true'), - self.key_value_type('file@' + FILE_PATH_ARG), - self.key_value_type('query==value'), - self.key_value_type('string-embed=@' + FILE_PATH_ARG), - self.key_value_type('raw-json-embed:=@' + JSON_FILE_PATH_ARG), + self.key_value('string=value'), + self.key_value('header:value'), + self.key_value('list:=["a", 1, {}, false]'), + self.key_value('obj:={"a": "b"}'), + self.key_value('eh:'), + self.key_value('ed='), + self.key_value('bool:=true'), + self.key_value('file@' + FILE_PATH_ARG), + self.key_value('query==value'), + self.key_value('string-embed=@' + FILE_PATH_ARG), + self.key_value('raw-json-embed:=@' + JSON_FILE_PATH_ARG), ]) # Parsed headers