mirror of
https://github.com/httpie/cli.git
synced 2024-11-28 08:38:44 +02:00
Switched to "==" a the separator for URL params.
Also refactored item escaping.
This commit is contained in:
parent
728a1a195b
commit
9944def703
19
README.rst
19
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
|
||||
<input type="file" name="cv" />
|
||||
</form>
|
||||
|
||||
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 <https://github.com/jkbr/httpie/compare/0.2.2...0.2.5>`_ (2012-07-17)
|
||||
* Unicode characters in prettified JSON now don't get escaped for
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user