From 28d11b24e059fa967af0745d822c58e6da493479 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 1/2] 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..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..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', From 84976926e856110edd9788f4099485c7478c345d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 7 Apr 2023 13:43:29 +0300 Subject: [PATCH 2/2] Add shell completion support via argcomplete --- httpie/cli/definition.py | 83 +++++++++++++++++++++++++--------------- httpie/cli/options.py | 6 ++- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/httpie/cli/definition.py b/httpie/cli/definition.py index 3d5650c007..3eea14b401 100644 --- a/httpie/cli/definition.py +++ b/httpie/cli/definition.py @@ -66,8 +66,8 @@ $ http example.org hello=world # => POST """, -).completer = ChoicesCompleter( - ('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')) + completer=ChoicesCompleter(('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')) +) positional_arguments.add_argument( dest='url', metavar='URL', @@ -82,7 +82,8 @@ $ http :/foo # => http://localhost/foo """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) positional_arguments.add_argument( dest='request_items', metavar='REQUEST_ITEM', @@ -139,7 +140,8 @@ field-name-with\:colon=value """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) ####################################################################### # Content type. @@ -192,8 +194,9 @@ short_help=( 'Specify a custom boundary string for multipart/form-data requests. ' 'Only has effect only together with --form.' - ) -).completer = ChoicesCompleter(()) + ), + completer=ChoicesCompleter(()), +) content_types.add_argument( '--raw', short_help='Pass raw request data without extra processing.', @@ -354,7 +357,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): --response-charset=big5 """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) output_processing.add_argument( '--response-mime', metavar='MIME_TYPE', @@ -367,7 +371,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): --response-mime=text/xml """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) output_processing.add_argument( '--format-options', action='append', @@ -392,7 +397,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): f' {option}' for option in DEFAULT_FORMAT_OPTIONS ).strip() ), -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) ####################################################################### # Output options @@ -421,7 +427,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): response body is printed by default. """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) output_options.add_argument( '--headers', '-h', @@ -495,7 +502,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): dest='output_options_history', metavar='WHAT', help=Qualifiers.SUPPRESS, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) output_options.add_argument( '--stream', '-S', @@ -529,7 +537,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): printed to stderr. """, -).completer = FilesCompleter() + completer=FilesCompleter(), +) output_options.add_argument( '--download', @@ -600,7 +609,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): https://httpie.io/docs/cli/config-file-directory """, -).completer = FilesCompleter(('json',)) + completer=FilesCompleter(('json',)), +) sessions.add_argument( '--session-read-only', metavar='SESSION_NAME_OR_PATH', @@ -611,7 +621,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False): exchange. """, -).completer = FilesCompleter(('json',)) + completer=FilesCompleter(('json',)), +) ####################################################################### # Authentication @@ -675,7 +686,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): (-a username), HTTPie will prompt for the password. """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) authentication.add_argument( '--auth-type', '-A', @@ -686,7 +698,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, @@ -720,7 +732,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): and $HTTPS_proxy are supported as well. """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) network.add_argument( '--follow', '-F', @@ -738,7 +751,8 @@ 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(()) + completer=ChoicesCompleter(()), +) network.add_argument( '--max-headers', type=int, @@ -746,8 +760,9 @@ 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(()) + ), + completer=ChoicesCompleter(()), +) network.add_argument( '--timeout', @@ -764,7 +779,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): the underlying socket for timeout seconds). """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) network.add_argument( '--check-status', default=False, @@ -814,7 +830,8 @@ 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')) + completer=ChoicesCompleter(('yes', 'no')), +) ssl.add_argument( '--ssl', dest='ssl_version', @@ -828,7 +845,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): are shown here). """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) ssl.add_argument( '--ciphers', short_help='A string in the OpenSSL cipher list format.', @@ -840,7 +858,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): {DEFAULT_SSL_CIPHERS} """, -).completer = ChoicesCompleter(()) + completer=ChoicesCompleter(()), +) ssl.add_argument( '--cert', default=None, @@ -852,7 +871,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): specify --cert-key separately. """, -).completer = FilesCompleter(('crt', 'cert', 'pem')) + completer=FilesCompleter(('crt', 'cert', 'pem')), +) ssl.add_argument( '--cert-key', default=None, @@ -863,7 +883,8 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False): certificate file does not contain the private key. """, -).completer = FilesCompleter(('key', 'pem')) + completer=FilesCompleter(('key', 'pem')), +) ssl.add_argument( '--cert-key-pass', @@ -874,8 +895,9 @@ 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(()) + """, + completer=ChoicesCompleter(()), +) ####################################################################### # Troubleshooting @@ -916,8 +938,9 @@ 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.' -).completer = ChoicesCompleter(('http', 'https')) + 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/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