diff --git a/httpie/cli.py b/httpie/cli.py index 1e05e272..bfeb2d12 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -3,7 +3,10 @@ NOTE: the CLI interface may change before reaching v1.0. """ -from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS +from textwrap import dedent, wrap +#noinspection PyCompatibility +from argparse import (RawDescriptionHelpFormatter, FileType, + OPTIONAL, ZERO_OR_MORE, SUPPRESS) from . import __doc__ from . import __version__ @@ -12,77 +15,120 @@ from .output import AVAILABLE_STYLES, DEFAULT_STYLE from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS, OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, - OUT_RESP_BODY, OUTPUT_OPTIONS, + OUT_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator) -def _(text): - """Normalize whitespace.""" - return ' '.join(text.strip().split()) +class HTTPieHelpFormatter(RawDescriptionHelpFormatter): + """A nicer help formatter. + Help for arguments can be indented and contain new lines. + It will be de-dented and arguments in the help + will be separated by a blank line for better readability. + + + """ + def __init__(self, max_help_position=6, *args, **kwargs): + # A smaller indent for args help. + kwargs['max_help_position'] = max_help_position + super(HTTPieHelpFormatter, self).__init__(*args, **kwargs) + + def _split_lines(self, text, width): + text = dedent(text).strip() + '\n\n' + return text.splitlines() parser = Parser( + formatter_class=HTTPieHelpFormatter, description='%s ' % __doc__.strip(), - epilog='For every --option there is a --no-option' - ' that reverts the option to its default value.\n\n' - 'Suggestions and bug reports are greatly appreciated:\n' - 'https://github.com/jkbr/httpie/issues' + epilog=dedent(""" + For every --OPTION there is also a --no-OPTION that reverts OPTION + to its default value. + + Suggestions and bug reports are greatly appreciated: + + https://github.com/jkbr/httpie/issues + + """) ) -############################################################################### +####################################################################### # Positional arguments. -############################################################################### +####################################################################### positional = parser.add_argument_group( title='Positional arguments', - description=_(''' - These arguments come after any flags and in the - order they are listed here. Only URL is required. - ''') + description=dedent(""" + These arguments come after any flags and in the order they are listed here. + Only URL is required. + + """) ) positional.add_argument( 'method', metavar='METHOD', nargs=OPTIONAL, default=None, - help=_(''' - The HTTP method to be used for the request - (GET, POST, PUT, DELETE, PATCH, ...). - If this argument is omitted, then HTTPie - will guess the HTTP method. If there is some - data to be sent, then it will be POST, otherwise GET. - ''') + help=""" + The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...). + + This argument can be omitted in which case HTTPie will use POST if there + is some data to be sent, otherwise GET: + + $ http example.org # => GET + $ http example.org hello=world # => POST + + """ ) positional.add_argument( 'url', metavar='URL', - help=_(''' - The protocol defaults to http:// if the - URL does not include one. - ''') + help=""" + The scheme defaults to 'http://' if the URL does not include one. + + """ ) positional.add_argument( 'items', metavar='REQUEST ITEM', nargs=ZERO_OR_MORE, type=KeyValueArgType(*SEP_GROUP_ITEMS), - help=_(''' - A key-value pair whose type is defined by the - 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), - or a file field (field_name@/path/to/file). - You can use a backslash to escape a colliding - separator in the field name. - ''') + help=r""" + Optional key-value pairs to be included in the request. The separator used + determines the type: + + ':' HTTP headers: + + Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0 + + '==' URL parameters to be appended to the request URI: + + search==httpie + + '=' Data fields to be serialized into a JSON object (with --json, -j) + or form data (with --form, -f): + + name=HTTPie language=Python description='CLI HTTP client' + + '@' Form file fields (only with --form, -f): + + cs@~/Documents/CV.pdf + + ':=' Non-string JSON data fields (only with --json, -j): + + awesome:=true amount:=42 colors:='["red", "green", "blue"]' + + You can use a backslash to escape a colliding separator in the field name: + + field-name-with\:colon=value + + """ ) -############################################################################### +####################################################################### # Content type. -############################################################################### +####################################################################### content_type = parser.add_argument_group( title='Predefined content types', @@ -92,29 +138,30 @@ content_type = parser.add_argument_group( content_type.add_argument( '--json', '-j', action='store_true', - help=_(''' - (default) Data items from the command - line are serialized as a JSON object. - The Content-Type and Accept headers - are set to application/json (if not specified). - ''') + help=""" + (default) Data items from the command line are serialized as a JSON object. + The Content-Type and Accept headers are set to application/json + (if not specified). + + """ ) content_type.add_argument( '--form', '-f', action='store_true', - help=_(''' - Data items from the command line are serialized as form fields. - The Content-Type is set to application/x-www-form-urlencoded - (if not specified). - The presence of any file fields results - in a multipart/form-data request. - ''') + help=""" + Data items from the command line are serialized as form fields. + + The Content-Type is set to application/x-www-form-urlencoded (if not + specified). The presence of any file fields results in a + multipart/form-data request. + + """ ) -############################################################################### +####################################################################### # Output processing -############################################################################### +####################################################################### output_processing = parser.add_argument_group(title='Output processing') @@ -123,12 +170,12 @@ output_processing.add_argument( dest='prettify', default=PRETTY_STDOUT_TTY_ONLY, choices=sorted(PRETTY_MAP.keys()), - help=_(''' - Controls output processing. The value can be "none" to not prettify - the output (default for redirected output), "all" to apply both colors - and formatting - (default for terminal output), "colors", or "format". - ''') + help=""" + Controls output processing. The value can be "none" to not prettify + the output (default for redirected output), "all" to apply both colors + and formatting (default for terminal output), "colors", or "format". + + """ ) output_processing.add_argument( '--style', '-s', @@ -136,75 +183,97 @@ output_processing.add_argument( metavar='STYLE', default=DEFAULT_STYLE, choices=AVAILABLE_STYLES, - help=_(''' - Output coloring style. One of %s. Defaults to "%s". - For this option to work properly, please make sure that the - $TERM environment variable is set to "xterm-256color" or similar - (e.g., via `export TERM=xterm-256color' in your ~/.bashrc). - ''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE) + help=""" + Output coloring style (default is "{default}"). On of: + +{available} + + For this option to work properly, please make sure that the $TERM + environment variable is set to "xterm-256color" or similar + (e.g., via `export TERM=xterm-256color' in your ~/.bashrc). + + """ + .format( + default=DEFAULT_STYLE, + available='\n'.join( + '{0: >20}'.format(line.strip()) + for line in + wrap(' '.join(sorted(AVAILABLE_STYLES)), 60) + ), + ) ) -############################################################################### +####################################################################### # Output options -############################################################################### +####################################################################### output_options = parser.add_argument_group(title='Output options') output_options.add_argument( '--print', '-p', dest='output_options', metavar='WHAT', - help=_(''' - String specifying what the output should contain: - "{request_headers}" stands for the request headers, and - "{request_body}" for the request body. - "{response_headers}" stands for the response headers and - "{response_body}" for response the body. - The default behaviour is "hb" (i.e., the response - headers and body is printed), if standard output is not redirected. - If the output is piped to another program or to a file, - then only the body is printed by default. - '''.format(request_headers=OUT_REQ_HEAD, - request_body=OUT_REQ_BODY, - response_headers=OUT_RESP_HEAD, - response_body=OUT_RESP_BODY,)) + help=""" + String specifying what the output should contain: + + '{req_head}' request headers + '{req_body}' request body + '{res_head}' response headers + '{res_body}' response body + + The default behaviour is '{default}' (i.e., the response headers and body + is printed), if standard output is not redirected. If the output is piped + to another program or to a file, then only the response body is printed + by default. + + """ + .format( + req_head=OUT_REQ_HEAD, + req_body=OUT_REQ_BODY, + res_head=OUT_RESP_HEAD, + res_body=OUT_RESP_BODY, + default=OUTPUT_OPTIONS_DEFAULT, + ) ) output_options.add_argument( '--verbose', '-v', dest='output_options', action='store_const', const=''.join(OUTPUT_OPTIONS), - help=_(''' - Print the whole request as well as the response. - Shortcut for --print={0}. - '''.format(''.join(OUTPUT_OPTIONS))) + help=""" + Print the whole request as well as the response. Shortcut for --print={0}. + + """ + .format(''.join(OUTPUT_OPTIONS)) ) output_options.add_argument( '--headers', '-h', dest='output_options', action='store_const', const=OUT_RESP_HEAD, - help=_(''' - Print only the response headers. - Shortcut for --print={0}. - '''.format(OUT_RESP_HEAD)) + help=""" + Print only the response headers. Shortcut for --print={0}. + + """ + .format(OUT_RESP_HEAD) ) output_options.add_argument( '--body', '-b', dest='output_options', action='store_const', const=OUT_RESP_BODY, - help=_(''' - Print only the response body. - Shortcut for --print={0}. - '''.format(OUT_RESP_BODY)) + help=""" + Print only the response body. Shortcut for --print={0}. + + """ + .format(OUT_RESP_BODY) ) output_options.add_argument( '--stream', '-S', action='store_true', default=False, - help=_(''' + help=""" Always stream the output by line, i.e., behave like `tail -f'. Without --stream and with --pretty (either set or implied), @@ -216,33 +285,31 @@ output_options.add_argument( It is useful also without --pretty: It ensures that the output is flushed more often and in smaller chunks. - ''') + """ ) -output_processing.add_argument( +output_options.add_argument( '--output', '-o', type=FileType('a+b'), dest='output_file', metavar='FILE', - help=_( - ''' - Save output to FILE. If --download is set, then only the response - body is saved to the file. Other parts of the HTTP exchange are - printed to stderr. + help=""" + Save output to FILE. If --download is set, then only the response body is + saved to the file. Other parts of the HTTP exchange are printed to stderr. + + """ - ''' - ) ) output_options.add_argument( '--download', '-d', action='store_true', default=False, - help=_(''' + help=""" Do not print the response body to stdout. Rather, download it and store it in a file. The filename is guessed unless specified with --output [filename]. This action is similar to the default behaviour of wget. - ''') + """ ) output_options.add_argument( @@ -250,48 +317,56 @@ output_options.add_argument( dest='download_resume', action='store_true', default=False, - help=_(''' - Resume an interrupted download. - The --output option needs to be specified as well. - ''') + help=""" + Resume an interrupted download. Note that the --output option needs to be + specified as well. + + """ ) -############################################################################### +####################################################################### # Sessions -############################################################################### +####################################################################### sessions = parser.add_argument_group(title='Sessions')\ .add_mutually_exclusive_group(required=False) session_name_validator = SessionNameValidator( - 'Session name contains invalid characters.') + 'Session name contains invalid characters.' +) sessions.add_argument( '--session', metavar='SESSION_NAME_OR_PATH', type=session_name_validator, - help=_(''' - Create, or reuse and update a session. - Within a session, custom headers, auth credential, as well as any - cookies sent by the server persist between requests. - Session files are stored in %s//.json. - ''' % DEFAULT_SESSIONS_DIR) + help=""" + Create, or reuse and update a session. Within a session, custom headers, + auth credential, as well as any cookies sent by the server persist between + requests. + + Session files are stored in: + + {session_dir}//.json. + + """ + .format(session_dir=DEFAULT_SESSIONS_DIR) ) sessions.add_argument( '--session-read-only', metavar='SESSION_NAME_OR_PATH', type=session_name_validator, - help=_(''' - Create or read a session without updating it form the - request/response exchange. - ''') + help=""" + Create or read a session without updating it form the request/response + exchange. + + """ ) -############################################################################### +####################################################################### # Authentication -############################################################################### +####################################################################### # ``requests.request`` keyword arguments. auth = parser.add_argument_group(title='Authentication') @@ -299,26 +374,27 @@ auth.add_argument( '--auth', '-a', metavar='USER[:PASS]', type=AuthCredentialsArgType(SEP_CREDENTIALS), - help=_(''' - If only the username is provided (-a username), - HTTPie will prompt for the password. - '''), + help=""" + If only the username is provided (-a username), HTTPie will prompt + for the password. + + """, ) auth.add_argument( '--auth-type', choices=['basic', 'digest'], default='basic', - help=_(''' - The authentication mechanism to be used. - Defaults to "basic". - ''') + help=""" + The authentication mechanism to be used. Defaults to "basic". + + """ ) -############################################################################### +####################################################################### # Network -############################################################################### +####################################################################### network = parser.add_argument_group(title='Network') @@ -328,31 +404,31 @@ network.add_argument( action='append', metavar='PROTOCOL:HOST', type=KeyValueArgType(SEP_PROXY), - help=_(''' - String mapping protocol to the URL of the proxy - (e.g. http:foo.bar:3128). You can specify multiple - proxies with different protocols. - ''') + help=""" + String mapping protocol to the URL of the proxy (e.g. http:foo.bar:3128). + You can specify multiple proxies with different protocols. + + """ ) network.add_argument( '--follow', default=False, action='store_true', - help=_(''' - Set this flag if full redirects are allowed - (e.g. re-POST-ing of data at new ``Location``) - ''') + help=""" + Set this flag if full redirects are allowed (e.g. re-POST-ing of data at + new Location). + + """ ) network.add_argument( '--verify', default='yes', - help=_(''' - Set to "no" to skip checking the host\'s SSL certificate. - You can also pass the path to a CA_BUNDLE - file for private certs. You can also set - the REQUESTS_CA_BUNDLE environment variable. - Defaults to "yes". - ''') + help=""" + Set to "no" to skip checking the host's SSL certificate. You can also pass + the path to a CA_BUNDLE file for private certs. You can also set the + REQUESTS_CA_BUNDLE environment variable. Defaults to "yes". + + """ ) network.add_argument( @@ -360,36 +436,33 @@ network.add_argument( type=float, default=30, metavar='SECONDS', - help=_(''' - The connection timeout of the request in seconds. - The default value is 30 seconds. - ''') + help=""" + The connection timeout of the request in seconds. The default value is + 30 seconds. + + """ ) network.add_argument( '--check-status', default=False, action='store_true', - help=_(''' - By default, HTTPie exits with 0 when no network or other fatal - errors occur. + help=""" + By default, HTTPie exits with 0 when no network or other fatal errors + occur. This flag instructs HTTPie to also check the HTTP status code and + exit with an error if the status indicates one. - This flag instructs HTTPie to also check the HTTP status code and - exit with an error if the status indicates one. + When the server replies with a 4xx (Client Error) or 5xx (Server Error) + status code, HTTPie exits with 4 or 5 respectively. If the response is a + 3xx (Redirect) and --follow hasn't been set, then the exit status is 3. + Also an error message is written to stderr if stdout is redirected. - When the server replies with a 4xx (Client Error) or 5xx - (Server Error) status code, HTTPie exits with 4 or 5 respectively. - If the response is a 3xx (Redirect) and --follow - hasn't been set, then the exit status is 3. - - Also an error message is written to stderr if stdout is redirected. - - ''') + """ ) -############################################################################### +####################################################################### # Troubleshooting -############################################################################### +####################################################################### troubleshooting = parser.add_argument_group(title='Troubleshooting') @@ -397,26 +470,36 @@ troubleshooting.add_argument( '--help', action='help', default=SUPPRESS, - help='Show this help message and exit' + help=""" + Show this help message and exit. + + """ ) troubleshooting.add_argument( '--version', action='version', - version=__version__ + version=__version__, + help=""" + Show version and exit. + + """ ) troubleshooting.add_argument( '--traceback', action='store_true', default=False, - help='Prints exception traceback should one occur.' + help=""" + Prints exception traceback should one occur. + + """ ) troubleshooting.add_argument( '--debug', action='store_true', default=False, - help=_(''' - Prints exception traceback should one occur, and also other - information that is useful for debugging HTTPie itself and - for bug reports. - ''') + help=""" + Prints exception traceback should one occur, and also other information + that is useful for debugging HTTPie itself and for reporting bugs. + + """ ) diff --git a/httpie/input.py b/httpie/input.py index 5e087bca..07ecb8d2 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -8,6 +8,7 @@ import json import mimetypes import getpass from io import BytesIO +#noinspection PyCompatibility from argparse import ArgumentParser, ArgumentTypeError, ArgumentError try: @@ -417,8 +418,8 @@ class KeyValueArgType(object): """Tokenize `s`. There are only two token types - strings and escaped characters: - >>> tokenize(r'foo\=bar\\baz') - ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz'] + tokenize(r'foo\=bar\\baz') + => ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz'] """ tokens = ['']