From aeb0a04a7fa74bd3d728fa3d6a49322d52c0f25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 23 Jul 2020 17:23:14 +0300 Subject: [PATCH] Add shell completion support via argcomplete --- docs/README.md | 14 ++++++++++- httpie/__main__.py | 1 + httpie/cli/definition.py | 53 +++++++++++++++++++++------------------- httpie/core.py | 3 +++ setup.py | 3 ++- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/docs/README.md b/docs/README.md index b63752a9d6..4cc40d2036 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2537,6 +2537,17 @@ The two modes, `--pretty=all` (default for terminal) and `--pretty=none` (defaul In the future, the command line syntax and some of the `--OPTIONS` may change slightly, as HTTPie improves and new features are added. All changes are recorded in the [change log](#change-log). +### Shell completion + +Shell completion is provided using the argcomplete library. It is suggested +to load the completion without falling back to the shell defaults in order +to avoid default completions in contexts where they do not apply. For example +for bash: + +```bash +$ eval "$(register-python-argcomplete --no-defaults http https)" +``` + ### Community and Support HTTPie has the following community channels: @@ -2549,10 +2560,11 @@ HTTPie has the following community channels: #### Dependencies -Under the hood, HTTPie uses these two amazing libraries: +Under the hood, HTTPie uses these three amazing libraries: - [Requests](https://requests.readthedocs.io/en/latest/) — Python HTTP library for humans - [Pygments](https://pygments.org/) — Python syntax highlighter +- [argcomplete](https://github.com/kislyuk/argcomplete) — Shell completion generator #### HTTPie friends diff --git a/httpie/__main__.py b/httpie/__main__.py index 7b5042b800..b045c0c684 100644 --- a/httpie/__main__.py +++ b/httpie/__main__.py @@ -1,3 +1,4 @@ +# PYTHON_ARGCOMPLETE_OK """The main entry point. Invoke as `http' or `python -m httpie'. """ diff --git a/httpie/cli/definition.py b/httpie/cli/definition.py index 0e5f91edf7..3d5650c007 100644 --- a/httpie/cli/definition.py +++ b/httpie/cli/definition.py @@ -3,6 +3,8 @@ import textwrap from argparse import FileType +from argcomplete.completers import ChoicesCompleter, FilesCompleter + from httpie import __doc__, __version__ from httpie.cli.argtypes import (KeyValueArgType, SessionNameValidator, SSLCredentials, readable_file_arg, @@ -64,7 +66,8 @@ $ http example.org hello=world # => POST """, -) +).completer = ChoicesCompleter( + ('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')) positional_arguments.add_argument( dest='url', metavar='URL', @@ -79,7 +82,7 @@ $ http :/foo # => http://localhost/foo """, -) +).completer = ChoicesCompleter(()) positional_arguments.add_argument( dest='request_items', metavar='REQUEST_ITEM', @@ -136,7 +139,7 @@ field-name-with\:colon=value """, -) +).completer = ChoicesCompleter(()) ####################################################################### # Content type. @@ -190,7 +193,7 @@ 'Specify a custom boundary string for multipart/form-data requests. ' 'Only has effect only together with --form.' ) -) +).completer = ChoicesCompleter(()) content_types.add_argument( '--raw', short_help='Pass raw request data without extra processing.', @@ -351,7 +354,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): --response-charset=big5 """, -) +).completer = ChoicesCompleter(()) output_processing.add_argument( '--response-mime', metavar='MIME_TYPE', @@ -364,7 +367,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): --response-mime=text/xml """, -) +).completer = ChoicesCompleter(()) output_processing.add_argument( '--format-options', action='append', @@ -389,7 +392,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): f' {option}' for option in DEFAULT_FORMAT_OPTIONS ).strip() ), -) +).completer = ChoicesCompleter(()) ####################################################################### # Output options @@ -418,7 +421,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): response body is printed by default. """, -) +).completer = ChoicesCompleter(()) output_options.add_argument( '--headers', '-h', @@ -492,7 +495,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): dest='output_options_history', metavar='WHAT', help=Qualifiers.SUPPRESS, -) +).completer = ChoicesCompleter(()) output_options.add_argument( '--stream', '-S', @@ -526,7 +529,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): printed to stderr. """, -) +).completer = FilesCompleter() output_options.add_argument( '--download', @@ -597,7 +600,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): https://httpie.io/docs/cli/config-file-directory """, -) +).completer = FilesCompleter(('json',)) sessions.add_argument( '--session-read-only', metavar='SESSION_NAME_OR_PATH', @@ -608,7 +611,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): exchange. """, -) +).completer = FilesCompleter(('json',)) ####################################################################### # Authentication @@ -672,7 +675,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): (-a username), HTTPie will prompt for the password. """, -) +).completer = ChoicesCompleter(()) authentication.add_argument( '--auth-type', '-A', @@ -683,7 +686,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): cache=False, short_help='The authentication mechanism to be used.', help_formatter=format_auth_help, -) +).completer = ChoicesCompleter(()) authentication.add_argument( '--ignore-netrc', default=False, @@ -717,7 +720,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): and $HTTPS_proxy are supported as well. """, -) +).completer = ChoicesCompleter(()) network.add_argument( '--follow', '-F', @@ -735,7 +738,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): By default, requests have a limit of 30 redirects (works with --follow). """, -) +).completer = ChoicesCompleter(()) network.add_argument( '--max-headers', type=int, @@ -744,7 +747,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): 'The maximum number of response headers to be read before ' 'giving up (default 0, i.e., no limit).' ) -) +).completer = ChoicesCompleter(()) network.add_argument( '--timeout', @@ -761,7 +764,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): the underlying socket for timeout seconds). """, -) +).completer = ChoicesCompleter(()) network.add_argument( '--check-status', default=False, @@ -811,7 +814,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment variable instead.) """, -) +).completer = ChoicesCompleter(('yes', 'no')) ssl.add_argument( '--ssl', dest='ssl_version', @@ -825,7 +828,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): are shown here). """, -) +).completer = ChoicesCompleter(()) ssl.add_argument( '--ciphers', short_help='A string in the OpenSSL cipher list format.', @@ -837,7 +840,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): {DEFAULT_SSL_CIPHERS} """, -) +).completer = ChoicesCompleter(()) ssl.add_argument( '--cert', default=None, @@ -849,7 +852,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): specify --cert-key separately. """, -) +).completer = FilesCompleter(('crt', 'cert', 'pem')) ssl.add_argument( '--cert-key', default=None, @@ -860,7 +863,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): certificate file does not contain the private key. """, -) +).completer = FilesCompleter(('key', 'pem')) ssl.add_argument( '--cert-key-pass', @@ -872,7 +875,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): is given and the key file requires a passphrase. If not provided, you’ll be prompted interactively. """ -) +).completer = ChoicesCompleter(()) ####################################################################### # Troubleshooting @@ -914,7 +917,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): '--default-scheme', default='http', short_help='The default scheme to use if not specified in the URL.' -) +).completer = ChoicesCompleter(('http', 'https')) troubleshooting.add_argument( '--debug', action='store_true', diff --git a/httpie/core.py b/httpie/core.py index d0c26dcbcc..4c6697abea 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -5,6 +5,7 @@ import socket from typing import List, Optional, Union, Callable +import argcomplete import requests from pygments import __version__ as pygments_version from requests import __version__ as requests_version @@ -73,6 +74,8 @@ def handle_generic_error(e, annotation=None): exit_status = ExitStatus.SUCCESS + argcomplete.autocomplete(parser) + try: parsed_args = parser.parse_args( args=args, diff --git a/setup.py b/setup.py index f506f2d0cd..f430a6a132 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,8 @@ 'multidict>=4.7.0', 'setuptools', 'importlib-metadata>=1.4.0; python_version < "3.8"', - 'rich>=9.10.0' + 'rich>=9.10.0', + 'argcomplete' ] install_requires_win_only = [ 'colorama>=0.2.4',