Skip to content

Commit

Permalink
Switched to "==" a the separator for URL params.
Browse files Browse the repository at this point in the history
Also refactored item escaping.
  • Loading branch information
jkbrzt committed Jul 24, 2012
1 parent 728a1a1 commit 9944def
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 79 deletions.
19 changes: 10 additions & 9 deletions README.rst
Expand Up @@ -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
Expand Down Expand Up @@ -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::
Expand All @@ -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``).
Expand All @@ -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``
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions httpie/cli.py
Expand Up @@ -146,7 +146,7 @@ def _(text):

# ``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),
Expand Down Expand Up @@ -174,7 +174,7 @@ def _(text):
)
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).
Expand Down Expand Up @@ -221,7 +221,7 @@ def _(text):
parser.add_argument(
'items', nargs='*',
metavar='ITEM',
type=cliparse.KeyValueType(
type=cliparse.KeyValueArgType(
cliparse.SEP_COMMON,
cliparse.SEP_QUERY,
cliparse.SEP_DATA,
Expand All @@ -233,7 +233,7 @@ def _(text):
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.
Expand Down
138 changes: 90 additions & 48 deletions httpie/cliparse.py
Expand Up @@ -16,6 +16,7 @@
OrderedDict = dict

from requests.structures import CaseInsensitiveDict
from requests.compat import str

from . import __version__

Expand All @@ -25,7 +26,7 @@
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
SEP_QUERY = '=:'
SEP_QUERY = '=='
DATA_ITEM_SEPARATORS = [
SEP_DATA,
SEP_DATA_RAW_JSON,
Expand Down Expand Up @@ -61,7 +62,6 @@ def parse_args(self, env, args=None, namespace=None):

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.
Expand Down Expand Up @@ -99,7 +99,7 @@ def _guess_method(self, args, env):
# - 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,
Expand All @@ -119,20 +119,20 @@ def _guess_method(self, args, env):
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
Expand Down Expand Up @@ -195,49 +195,91 @@ def __eq__(self, other):
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

if not found:
"""
Parse `string` and return `self.key_value_class()` instance.
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):
Expand All @@ -260,13 +302,13 @@ def prompt_password(self):
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(
Expand All @@ -277,10 +319,10 @@ def __call__(self, string):
)


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:
Expand All @@ -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')
Expand All @@ -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
2 changes: 1 addition & 1 deletion httpie/core.py
Expand Up @@ -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):
Expand Down

0 comments on commit 9944def

Please sign in to comment.