diff --git a/README.rst b/README.rst
index 23235235..768963f9 100644
--- a/README.rst
+++ b/README.rst
@@ -83,8 +83,8 @@ File fields (``field@/path/to/file``)
``screenshot@/path/to/file.png``. The presence of a file field results into
a ``multipart/form-data`` request.
-Query string parameters (``name=:value``)
- Appends the given name/value pair as a query string parameter to the URL.
+Query string parameters (``name==value``)
+ Appends the given name/value pair as a query string parameter to the URL.
Examples
@@ -127,11 +127,12 @@ The above will send the same request as if the following HTML form were submitte
-Query string parameters can be added to any request::
+**Query string parameters** can be added to any request without having to quote
+the ``&`` characters::
- http GET example.com/ search=:donuts
+ http GET example.com/ search==donuts in==fridge
-Will GET the URL "example.com/?search=donuts".
+Will ``GET` the URL ``http://example.com/?search=donuts&in=fridge``.
A whole request body can be passed in via **``stdin``** instead, in which
case it will be used with no further processing::
@@ -151,7 +152,7 @@ the second one does via ``stdin``::
http https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
-Note that when the output is redirected (like the examples above), HTTPie
+Note that when the **output is redirected** (like the examples above), HTTPie
applies a different set of defaults then for console output. Namely colors
aren't used (can be forced with ``--pretty``) and only the response body
gets printed (can be overwritten with ``--print``).
@@ -165,7 +166,7 @@ request will send the verbatim contents of the file with
http PUT httpbin.org/put @/data/file.xml
-When using HTTPie from shell scripts you might want to use the
+When using HTTPie from **shell scripts**, you might want to use the
``--check-status`` flag. It instructs HTTPie to exit with an error if the
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5``
@@ -213,7 +214,7 @@ See ``http -h`` for more details::
separator used. It can be an HTTP header
(header:value), a data field to be used in the request
body (field_name=value), a raw JSON data field
- (field_name:=value), a query parameter (name=:value),
+ (field_name:=value), a query parameter (name=value),
or a file field (field_name@/path/to/file). You can
use a backslash to escape a colliding separator in the
field name.
@@ -333,7 +334,7 @@ Changelog
the new default behaviour is to only print the response body.
(It can still be overriden via the ``--print`` flag.)
* Improved highlighing of HTTP headers.
- * Added query string parameters (param=:value).
+ * Added query string parameters (param==value).
* Added support for terminal colors under Windows.
* `0.2.5 `_ (2012-07-17)
* Unicode characters in prettified JSON now don't get escaped for
diff --git a/httpie/cli.py b/httpie/cli.py
index fb77b8ca..e6932cfb 100644
--- a/httpie/cli.py
+++ b/httpie/cli.py
@@ -146,7 +146,7 @@ parser.add_argument(
# ``requests.request`` keyword arguments.
parser.add_argument(
- '--auth', '-a', type=cliparse.AuthCredentialsType(cliparse.SEP_COMMON),
+ '--auth', '-a', type=cliparse.AuthCredentialsArgType(cliparse.SEP_COMMON),
help=_('''
username:password.
If only the username is provided (-a username),
@@ -174,7 +174,7 @@ parser.add_argument(
)
parser.add_argument(
'--proxy', default=[], action='append',
- type=cliparse.KeyValueType(cliparse.SEP_COMMON),
+ type=cliparse.KeyValueArgType(cliparse.SEP_COMMON),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128).
@@ -221,7 +221,7 @@ parser.add_argument(
parser.add_argument(
'items', nargs='*',
metavar='ITEM',
- type=cliparse.KeyValueType(
+ type=cliparse.KeyValueArgType(
cliparse.SEP_COMMON,
cliparse.SEP_QUERY,
cliparse.SEP_DATA,
@@ -233,7 +233,7 @@ parser.add_argument(
separator used. It can be an HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value),
- a query parameter (name=:value),
+ a query parameter (name==value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding
separator in the field name.
diff --git a/httpie/cliparse.py b/httpie/cliparse.py
index ff8cf4e5..736b02e7 100644
--- a/httpie/cliparse.py
+++ b/httpie/cliparse.py
@@ -16,6 +16,7 @@ except ImportError:
OrderedDict = dict
from requests.structures import CaseInsensitiveDict
+from requests.compat import str
from . import __version__
@@ -25,7 +26,7 @@ SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
-SEP_QUERY = '=:'
+SEP_QUERY = '=='
DATA_ITEM_SEPARATORS = [
SEP_DATA,
SEP_DATA_RAW_JSON,
@@ -61,7 +62,6 @@ class Parser(argparse.ArgumentParser):
if not env.stdin_isatty:
self._body_from_file(args, env.stdin)
-
if args.auth and not args.auth.has_password():
# stdin has already been read (if not a tty) so
# it's save to prompt now.
@@ -99,7 +99,7 @@ class Parser(argparse.ArgumentParser):
# - Set `args.url` correctly.
# - Parse the first item and move it to `args.items[0]`.
- item = KeyValueType(
+ item = KeyValueArgType(
SEP_COMMON,
SEP_QUERY,
SEP_DATA,
@@ -119,20 +119,20 @@ class Parser(argparse.ArgumentParser):
def _parse_items(self, args):
"""
Parse `args.items` into `args.headers`,
- `args.data`, `args.queries`, and `args.files`.
+ `args.data`, `args.`, and `args.files`.
"""
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = OrderedDict()
args.files = OrderedDict()
- args.queries = CaseInsensitiveDict()
+ args.params = OrderedDict()
try:
parse_items(items=args.items,
headers=args.headers,
data=args.data,
files=args.files,
- queries=args.queries)
+ params=args.params)
except ParseError as e:
if args.traceback:
raise
@@ -195,49 +195,91 @@ class KeyValue(object):
return self.__dict__ == other.__dict__
-class KeyValueType(object):
- """A type used with `argparse`."""
+class KeyValueArgType(object):
+ """
+ A key-value pair argument type used with `argparse`.
+
+ Parses a key-value arg and constructs a `KeyValue` instance.
+ Used for headers, form data, and other key-value pair types.
+
+ """
key_value_class = KeyValue
def __init__(self, *separators):
self.separators = separators
- self.escapes = ['\\\\' + sep for sep in separators]
def __call__(self, string):
- found = {}
- found_escapes = []
- for esc in self.escapes:
- found_escapes += [m.span() for m in re.finditer(esc, string)]
- for sep in self.separators:
- matches = re.finditer(sep, string)
- for match in matches:
- start, end = match.span()
- inside_escape = False
- for estart, eend in found_escapes:
- if start >= estart and end <= eend:
- inside_escape = True
- break
- if start in found and len(found[start]) > len(sep):
- break
- if not inside_escape:
- found[start] = sep
+ """
+ Parse `string` and return `self.key_value_class()` instance.
- if not found:
+ The best of `self.separators` is determined (first found, longest).
+ Back slash escaped characters aren't considered as separators
+ (or parts thereof). Literal back slash characters have to be escaped
+ as well (r'\\').
+
+ """
+
+ class Escaped(str):
+ pass
+
+ def tokenize(s):
+ """
+ r'foo\=bar\\baz'
+ => ['foo', Escaped('='), 'bar', Escaped('\'), 'baz']
+
+ """
+ tokens = ['']
+ esc = False
+ for c in s:
+ if esc:
+ tokens.extend([Escaped(c), ''])
+ esc = False
+ else:
+ if c == '\\':
+ esc = True
+ else:
+ tokens[-1] += c
+ return tokens
+
+ tokens = tokenize(string)
+
+ # Sorting by length ensures that the longest one will be
+ # chosen as it will overwrite any shorter ones starting
+ # at the same position in the `found` dictionary.
+ separators = sorted(self.separators, key=len)
+
+ for i, token in enumerate(tokens):
+
+ if isinstance(token, Escaped):
+ continue
+
+ found = {}
+ for sep in separators:
+ pos = token.find(sep)
+ if pos != -1:
+ found[pos] = sep
+
+ if found:
+ # Starting first, longest separator found.
+ sep = found[min(found.keys())]
+
+ key, value = token.split(sep, 1)
+
+ # Any preceding tokens are part of the key.
+ key = ''.join(tokens[:i]) + key
+
+ # Any following tokens are part of the value.
+ value += ''.join(tokens[i + 1:])
+
+ break
+
+ else:
raise argparse.ArgumentTypeError(
'"%s" is not a valid value' % string)
- # split the string at the earliest non-escaped separator.
- seploc = min(found.keys())
- sep = found[seploc]
- key = string[:seploc]
- value = string[seploc + len(sep):]
-
- # remove escape chars
- for sepstr in self.separators:
- key = key.replace('\\' + sepstr, sepstr)
- value = value.replace('\\' + sepstr, sepstr)
- return self.key_value_class(key=key, value=value, sep=sep, orig=string)
+ return self.key_value_class(
+ key=key, value=value, sep=sep, orig=string)
class AuthCredentials(KeyValue):
@@ -260,13 +302,13 @@ class AuthCredentials(KeyValue):
sys.exit(0)
-class AuthCredentialsType(KeyValueType):
+class AuthCredentialsArgType(KeyValueArgType):
key_value_class = AuthCredentials
def __call__(self, string):
try:
- return super(AuthCredentialsType, self).__call__(string)
+ return super(AuthCredentialsArgType, self).__call__(string)
except argparse.ArgumentTypeError:
# No password provided, will prompt for it later.
return self.key_value_class(
@@ -277,10 +319,10 @@ class AuthCredentialsType(KeyValueType):
)
-def parse_items(items, data=None, headers=None, files=None, queries=None):
+def parse_items(items, data=None, headers=None, files=None, params=None):
"""
- Parse `KeyValueType` `items` into `data`, `headers`, `files`,
- and `queries`.
+ Parse `KeyValue` `items` into `data`, `headers`, `files`,
+ and `params`.
"""
if headers is None:
@@ -289,15 +331,15 @@ def parse_items(items, data=None, headers=None, files=None, queries=None):
data = {}
if files is None:
files = {}
- if queries is None:
- queries = {}
+ if params is None:
+ params = {}
for item in items:
value = item.value
key = item.key
if item.sep == SEP_HEADERS:
target = headers
elif item.sep == SEP_QUERY:
- target = queries
+ target = params
elif item.sep == SEP_FILES:
try:
value = open(os.path.expanduser(item.value), 'r')
@@ -322,4 +364,4 @@ def parse_items(items, data=None, headers=None, files=None, queries=None):
target[key] = value
- return headers, data, files, queries
+ return headers, data, files, params
diff --git a/httpie/core.py b/httpie/core.py
index b2b7c121..4cda53a8 100644
--- a/httpie/core.py
+++ b/httpie/core.py
@@ -53,7 +53,7 @@ def get_response(args):
proxies=dict((p.key, p.value) for p in args.proxy),
files=args.files,
allow_redirects=args.allow_redirects,
- params=args.queries,
+ params=args.params,
)
except (KeyboardInterrupt, SystemExit):
diff --git a/tests/tests.py b/tests/tests.py
index ad397897..0ef67e73 100755
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# coding=utf8
"""
@@ -598,7 +599,7 @@ class ExitStatusTest(BaseTestCase):
class ItemParsingTest(BaseTestCase):
def setUp(self):
- self.key_value_type = cliparse.KeyValueType(
+ self.key_value_type = cliparse.KeyValueArgType(
cliparse.SEP_HEADERS,
cliparse.SEP_QUERY,
cliparse.SEP_DATA,
@@ -613,7 +614,7 @@ class ItemParsingTest(BaseTestCase):
lambda: self.key_value_type(item))
def test_escape(self):
- headers, data, files, queries = cliparse.parse_items([
+ headers, data, files, params = cliparse.parse_items([
# headers
self.key_value_type('foo\\:bar:baz'),
self.key_value_type('jack\\@jill:hill'),
@@ -632,15 +633,15 @@ class ItemParsingTest(BaseTestCase):
self.assertIn('bar@baz', files)
def test_escape_longsep(self):
- headers, data, files, queries = cliparse.parse_items([
+ headers, data, files, params = cliparse.parse_items([
self.key_value_type('bob\\:==foo'),
])
- self.assertDictEqual(data, {
- 'bob:=': 'foo',
+ self.assertDictEqual(params, {
+ 'bob:': 'foo',
})
def test_valid_items(self):
- headers, data, files, queries = cliparse.parse_items([
+ headers, data, files, params = cliparse.parse_items([
self.key_value_type('string=value'),
self.key_value_type('header:value'),
self.key_value_type('list:=["a", 1, {}, false]'),
@@ -649,7 +650,7 @@ class ItemParsingTest(BaseTestCase):
self.key_value_type('ed='),
self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
- self.key_value_type('query=:value'),
+ self.key_value_type('query==value'),
])
self.assertDictEqual(headers, {
'header': 'value',
@@ -660,21 +661,13 @@ class ItemParsingTest(BaseTestCase):
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
- "obj": {"a": "b"}
+ "obj": {"a": "b"},
})
- self.assertDictEqual(queries, {
+ self.assertDictEqual(params, {
'query': 'value',
})
self.assertIn('test-file', files)
- def test_query_string(self):
- headers, data, files, queries = cliparse.parse_items([
- self.key_value_type('query=:value'),
- ])
- self.assertDictEqual(queries, {
- 'query': 'value',
- })
-
class ArgumentParserTestCase(unittest.TestCase):