diff --git a/docs/README.md b/docs/README.md index b63752a9d6..c967f72ec5 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 --complete-arguments -- 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..3eea14b401 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,6 +66,7 @@ $ http example.org hello=world # => POST """, + completer=ChoicesCompleter(('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')) ) positional_arguments.add_argument( dest='url', @@ -79,6 +82,7 @@ $ http :/foo # => http://localhost/foo """, + completer=ChoicesCompleter(()), ) positional_arguments.add_argument( dest='request_items', @@ -136,6 +140,7 @@ field-name-with\:colon=value """, + completer=ChoicesCompleter(()), ) ####################################################################### @@ -189,7 +194,8 @@ short_help=( 'Specify a custom boundary string for multipart/form-data requests. ' 'Only has effect only together with --form.' - ) + ), + completer=ChoicesCompleter(()), ) content_types.add_argument( '--raw', @@ -351,6 +357,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): --response-charset=big5 """, + completer=ChoicesCompleter(()), ) output_processing.add_argument( '--response-mime', @@ -364,6 +371,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): --response-mime=text/xml """, + completer=ChoicesCompleter(()), ) output_processing.add_argument( '--format-options', @@ -389,6 +397,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): f' {option}' for option in DEFAULT_FORMAT_OPTIONS ).strip() ), + completer=ChoicesCompleter(()), ) ####################################################################### @@ -418,6 +427,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): response body is printed by default. """, + completer=ChoicesCompleter(()), ) output_options.add_argument( '--headers', @@ -492,6 +502,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', @@ -526,6 +537,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): printed to stderr. """, + completer=FilesCompleter(), ) output_options.add_argument( @@ -597,6 +609,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', @@ -608,6 +621,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): exchange. """, + completer=FilesCompleter(('json',)), ) ####################################################################### @@ -672,6 +686,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', @@ -717,6 +732,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', @@ -735,6 +751,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', @@ -743,7 +760,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): short_help=( 'The maximum number of response headers to be read before ' 'giving up (default 0, i.e., no limit).' - ) + ), + completer=ChoicesCompleter(()), ) network.add_argument( @@ -761,6 +779,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', @@ -811,6 +830,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', @@ -825,6 +845,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): are shown here). """, + completer=ChoicesCompleter(()), ) ssl.add_argument( '--ciphers', @@ -837,6 +858,7 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): {DEFAULT_SSL_CIPHERS} """, + completer=ChoicesCompleter(()), ) ssl.add_argument( '--cert', @@ -849,6 +871,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', @@ -860,6 +883,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( @@ -871,7 +895,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): The passphrase to be used to with the given private key. Only needed if --cert-key is given and the key file requires a passphrase. If not provided, you’ll be prompted interactively. - """ + """, + completer=ChoicesCompleter(()), ) ####################################################################### @@ -913,7 +938,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): troubleshooting.add_argument( '--default-scheme', default='http', - short_help='The default scheme to use if not specified in the URL.' + short_help='The default scheme to use if not specified in the URL.', + completer=ChoicesCompleter(('http', 'https')), ) troubleshooting.add_argument( '--debug', diff --git a/httpie/cli/options.py b/httpie/cli/options.py index c06a8ee615..3fe8ed7858 100644 --- a/httpie/cli/options.py +++ b/httpie/cli/options.py @@ -187,7 +187,7 @@ def __getattr__(self, attribute_name): Qualifiers.ZERO_OR_MORE: argparse.ZERO_OR_MORE, Qualifiers.ONE_OR_MORE: argparse.ONE_OR_MORE } -ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options') +ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options', 'completer') def to_argparse( @@ -211,12 +211,14 @@ def to_argparse( concrete_group = concrete_group.add_mutually_exclusive_group(required=False) for abstract_argument in abstract_group.arguments: - concrete_group.add_argument( + argument = concrete_group.add_argument( *abstract_argument.aliases, **drop_keys(map_qualifiers( abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP ), ARGPARSE_IGNORE_KEYS) ) + if 'completer' in abstract_argument.configuration: + argument.completer = abstract_argument.configuration['completer'] return concrete_parser 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',