mirror of
https://github.com/httpie/cli.git
synced 2024-11-27 02:03:31 +01:00
Automate ZSH completion.
This commit is contained in:
parent
8abe47969e
commit
bd0b18489b
147
extras/completion/completion.zsh
Normal file
147
extras/completion/completion.zsh
Normal file
@ -0,0 +1,147 @@
|
||||
# compdef http
|
||||
# Copyright (c) 2015 Github zsh-users
|
||||
# Based on the initial work of http://github.com/zsh-users
|
||||
|
||||
|
||||
_httpie_params () {
|
||||
local ret=1 expl
|
||||
|
||||
if (( CURRENT == NORMARG )) && [[ $words[NORMARG] != *:* ]]; then
|
||||
# URL
|
||||
_httpie_urls && ret=0
|
||||
elif (( CURRENT > NORMARG )); then
|
||||
# regular param, if we already have a url
|
||||
# ignore all prefix stuff
|
||||
compset -P '(#b)([^:@=]#)'
|
||||
local name=$match[1]
|
||||
|
||||
if false; then
|
||||
false;
|
||||
elif compset -P ':'; then
|
||||
_message "$name HTTP Headers"
|
||||
|
||||
elif compset -P '=='; then
|
||||
_message "$name URL Parameters"
|
||||
|
||||
elif compset -P '='; then
|
||||
_message "$name Data Fields"
|
||||
|
||||
elif compset -P ':='; then
|
||||
_message "$name Raw JSON Fields"
|
||||
|
||||
elif compset -P '@'; then
|
||||
_files
|
||||
|
||||
else
|
||||
typeset -a ops
|
||||
ops=(
|
||||
"\::Arbitrary HTTP header, e.g X-API-Token:123"
|
||||
"==:Querystring parameter to the URL, e.g limit==50"
|
||||
"=:Data fields to be serialized as JSON (default) or Form Data (with --form)"
|
||||
"\:=:Data field for real JSON types."
|
||||
"@:Path field for uploading a file."
|
||||
)
|
||||
_describe -t httpparams 'parameter types' ops -Q -S ''
|
||||
fi
|
||||
ret=0
|
||||
fi
|
||||
|
||||
# first arg may be a request method
|
||||
(( CURRENT == NORMARG )) &&
|
||||
_wanted http_method expl 'Request Method' \
|
||||
compadd GET POST PUT DELETE HEAD OPTIONS PATCH TRACE CONNECT && ret=0
|
||||
|
||||
return $ret
|
||||
|
||||
}
|
||||
|
||||
_httpie_urls() {
|
||||
local ret=1
|
||||
|
||||
if ! [[ -prefix [-+.a-z0-9]#:// ]]; then
|
||||
local expl
|
||||
compset -S '[^:/]*' && compstate[to_end]=''
|
||||
_wanted url-schemas expl 'URL schema' compadd -S '' http:// https:// && ret=0
|
||||
else
|
||||
_urls && ret=0
|
||||
fi
|
||||
|
||||
return $ret
|
||||
}
|
||||
|
||||
_httpie_printflags () {
|
||||
local ret=1
|
||||
|
||||
# not sure why this is necessary, but it will complete "-pH" style without it
|
||||
[[ $IPREFIX == "-p" ]] && IPREFIX+=" "
|
||||
|
||||
compset -P '(#b)([a-zA-Z]#)'
|
||||
|
||||
local -a flags
|
||||
[[ $match[1] != *H* ]] && flags+=( "H:request headers" )
|
||||
[[ $match[1] != *B* ]] && flags+=( "B:request body" )
|
||||
[[ $match[1] != *h* ]] && flags+=( "h:response headers" )
|
||||
[[ $match[1] != *b* ]] && flags+=( "b:response body" )
|
||||
[[ $match[1] != *m* ]] && flags+=( "b:response meta" )
|
||||
|
||||
_describe -t printflags "print flags" flags -S '' && ret=0
|
||||
|
||||
return $ret
|
||||
}
|
||||
|
||||
integer NORMARG
|
||||
|
||||
_arguments -n -C -s \
|
||||
{--json,-j}'[(default) Serialize data items from the command line as a JSON object.]' \
|
||||
{--form,-f}'[Serialize data items from the command line as form field data.]' \
|
||||
'--multipart[Similar to --form, but always sends a multipart/form-data request (i.e., even without files).]' \
|
||||
'--boundary=[Specify a custom boundary string for multipart/form-data requests. Only has effect only together with --form.]' \
|
||||
'--raw=[Pass raw request data without extra processing.]' \
|
||||
{--compress,-x}'[Compress the content with Deflate algorithm.]' \
|
||||
'--pretty=[Control the processing of console outputs.]:PRETTY:(all colors format none)' \
|
||||
{--style,-s}'=[Output coloring style (default is "auto").]:STYLE:' \
|
||||
'--unsorted[Disables all sorting while formatting output.]' \
|
||||
'--sorted[Re-enables all sorting options while formatting output.]' \
|
||||
'--response-charset=[Override the response encoding for terminal display purposes.]:ENCODING:' \
|
||||
'--response-mime=[Override the response mime type for coloring and formatting for the terminal.]:MIME_TYPE:' \
|
||||
'--format-options=[Controls output formatting.]' \
|
||||
{--print,-p}'=[Options to specify what the console output should contain.]:WHAT:' \
|
||||
{--headers,-h}'[Print only the response headers.]' \
|
||||
{--meta,-m}'[Print only the response metadata.]' \
|
||||
{--body,-b}'[Print only the response body.]' \
|
||||
{--verbose,-v}'[Make output more verbose.]' \
|
||||
'--all[Show any intermediary requests/responses.]' \
|
||||
{--history-print,-P}'=[--print for intermediary requests/responses.]:WHAT:' \
|
||||
{--stream,-S}'[Always stream the response body by line, i.e., behave like `tail -f`.]' \
|
||||
{--output,-o}'=[Save output to FILE instead of stdout.]:FILE:' \
|
||||
{--download,-d}'[Download the body to a file instead of printing it to stdout.]' \
|
||||
{--continue,-c}'[Resume an interrupted download (--output needs to be specified).]' \
|
||||
{--quiet,-q}'[Do not print to stdout or stderr, except for errors and warnings when provided once.]' \
|
||||
'--session=[Create, or reuse and update a session.]:SESSION_NAME_OR_PATH:' \
|
||||
'--session-read-only=[Create or read a session without updating it]:SESSION_NAME_OR_PATH:' \
|
||||
{--auth,-a}'=[Credentials for the selected (-A) authentication method.]:USER[\:PASS] | TOKEN:' \
|
||||
{--auth-type,-A}'=[The authentication mechanism to be used.]' \
|
||||
'--ignore-netrc[Ignore credentials from .netrc.]' \
|
||||
'--offline[Build the request and print it but don’t actually send it.]' \
|
||||
'--proxy=[String mapping of protocol to the URL of the proxy.]:PROTOCOL\:PROXY_URL:' \
|
||||
{--follow,-F}'[Follow 30x Location redirects.]' \
|
||||
'--max-redirects=[The maximum number of redirects that should be followed (with --follow).]' \
|
||||
'--max-headers=[The maximum number of response headers to be read before giving up (default 0, i.e., no limit).]' \
|
||||
'--timeout=[The connection timeout of the request in seconds.]:SECONDS:' \
|
||||
'--check-status[Exit with an error status code if the server replies with an error.]' \
|
||||
'--path-as-is[Bypass dot segment (/../ or /./) URL squashing.]' \
|
||||
'--chunked[Enable streaming via chunked transfer encoding. The Transfer-Encoding header is set to chunked.]' \
|
||||
'--verify=[If "no", skip SSL verification. If a file path, use it as a CA bundle.]' \
|
||||
'--ssl=[The desired protocol version to used.]:SSL:(ssl2.3 tls1 tls1.1 tls1.2)' \
|
||||
'--ciphers=[A string in the OpenSSL cipher list format.]' \
|
||||
'--cert=[Specifys a local cert to use as client side SSL certificate.]' \
|
||||
'--cert-key=[The private key to use with SSL. Only needed if --cert is given.]' \
|
||||
'--cert-key-pass=[The passphrase to be used to with the given private key.]' \
|
||||
{--ignore-stdin,-I}'[Do not attempt to read stdin]' \
|
||||
'--help[Show this help message and exit.]' \
|
||||
'--manual[Show the full manual.]' \
|
||||
'--version[Show version and exit.]' \
|
||||
'--traceback[Prints the exception traceback should one occur.]' \
|
||||
'--default-scheme=[The default scheme to use if not specified in the URL.]' \
|
||||
'--debug[Print useful diagnostic information for bug reports.]' \
|
||||
'*:args:_httpie_params' && return 0
|
89
extras/completion/templates/completion.zsh.j2
Executable file
89
extras/completion/templates/completion.zsh.j2
Executable file
@ -0,0 +1,89 @@
|
||||
# compdef http
|
||||
# Copyright (c) 2015 Github zsh-users
|
||||
# Based on the initial work of http://github.com/zsh-users
|
||||
|
||||
|
||||
_httpie_params () {
|
||||
local ret=1 expl
|
||||
|
||||
if (( CURRENT == NORMARG )) && [[ $words[NORMARG] != *:* ]]; then
|
||||
# URL
|
||||
_httpie_urls && ret=0
|
||||
elif (( CURRENT > NORMARG )); then
|
||||
# regular param, if we already have a url
|
||||
# ignore all prefix stuff
|
||||
compset -P '(#b)([^:@=]#)'
|
||||
local name=$match[1]
|
||||
|
||||
if false; then
|
||||
false;
|
||||
{% for option_name, _, operator, desc in request_items.nested_options -%}
|
||||
elif compset -P '{{ operator }}'; then
|
||||
{% if is_file_based_operator(operator) -%}
|
||||
_files
|
||||
{% else -%}
|
||||
_message "$name {{ option_name }}"
|
||||
{% endif %}
|
||||
{% endfor -%}
|
||||
else
|
||||
typeset -a ops
|
||||
ops=(
|
||||
{% for option_name, _, operator, desc in request_items.nested_options -%}
|
||||
"{{ escape_zsh(operator) }}:{{ desc }}"
|
||||
{% endfor -%}
|
||||
)
|
||||
_describe -t httpparams 'parameter types' ops -Q -S ''
|
||||
fi
|
||||
ret=0
|
||||
fi
|
||||
|
||||
# first arg may be a request method
|
||||
(( CURRENT == NORMARG )) &&
|
||||
_wanted http_method expl 'Request Method' \
|
||||
compadd {% for method in methods -%} {{ method }} {% endfor -%} && ret=0
|
||||
|
||||
return $ret
|
||||
|
||||
}
|
||||
|
||||
_httpie_urls() {
|
||||
local ret=1
|
||||
|
||||
if ! [[ -prefix [-+.a-z0-9]#:// ]]; then
|
||||
local expl
|
||||
compset -S '[^:/]*' && compstate[to_end]=''
|
||||
_wanted url-schemas expl 'URL schema' compadd -S '' http:// https:// && ret=0
|
||||
else
|
||||
_urls && ret=0
|
||||
fi
|
||||
|
||||
return $ret
|
||||
}
|
||||
|
||||
_httpie_printflags () {
|
||||
local ret=1
|
||||
|
||||
# not sure why this is necessary, but it will complete "-pH" style without it
|
||||
[[ $IPREFIX == "-p" ]] && IPREFIX+=" "
|
||||
|
||||
compset -P '(#b)([a-zA-Z]#)'
|
||||
|
||||
local -a flags
|
||||
[[ $match[1] != *H* ]] && flags+=( "H:request headers" )
|
||||
[[ $match[1] != *B* ]] && flags+=( "B:request body" )
|
||||
[[ $match[1] != *h* ]] && flags+=( "h:response headers" )
|
||||
[[ $match[1] != *b* ]] && flags+=( "b:response body" )
|
||||
[[ $match[1] != *m* ]] && flags+=( "b:response meta" )
|
||||
|
||||
_describe -t printflags "print flags" flags -S '' && ret=0
|
||||
|
||||
return $ret
|
||||
}
|
||||
|
||||
integer NORMARG
|
||||
|
||||
_arguments -n -C -s \
|
||||
{% for argument in arguments -%}
|
||||
{{ serialize_argument_to_zsh(argument) }} \
|
||||
{% endfor -%}
|
||||
'*:args:_httpie_params' && return 0
|
166
extras/scripts/generate_completion.py
Normal file
166
extras/scripts/generate_completion.py
Normal file
@ -0,0 +1,166 @@
|
||||
from atexit import register
|
||||
import functools
|
||||
import string
|
||||
import textwrap
|
||||
|
||||
from jinja2 import Template
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Callable, TypeVar
|
||||
|
||||
from httpie.cli.constants import SEPARATOR_FILE_UPLOAD
|
||||
from httpie.cli.definition import options
|
||||
from httpie.cli.options import Argument, ParserSpec
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
EXTRAS_DIR = Path(__file__).parent.parent
|
||||
COMPLETION_DIR = EXTRAS_DIR / "completion"
|
||||
TEMPLATES_DIR = COMPLETION_DIR / "templates"
|
||||
|
||||
COMPLETION_TEMPLATE_BASE = TEMPLATES_DIR / "completion"
|
||||
COMPLETION_SCRIPT_BASE = COMPLETION_DIR / "completion"
|
||||
|
||||
COMMON_HTTP_METHODS = [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"TRACE",
|
||||
"CONNECT",
|
||||
]
|
||||
|
||||
|
||||
def use_template(shell_type):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(spec):
|
||||
template_file = COMPLETION_TEMPLATE_BASE.with_suffix(
|
||||
f".{shell_type}.j2"
|
||||
)
|
||||
compiletion_script_file = COMPLETION_SCRIPT_BASE.with_suffix(
|
||||
f".{shell_type}"
|
||||
)
|
||||
|
||||
jinja_template = Template(template_file.read_text())
|
||||
jinja_template.globals.update(prepare_objects(spec))
|
||||
extra_variables = func(spec)
|
||||
compiletion_script_file.write_text(
|
||||
jinja_template.render(**extra_variables)
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
BASE_FUNCTIONS = {}
|
||||
|
||||
|
||||
def prepare_objects(spec: ParserSpec) -> Dict[str, Any]:
|
||||
global_objects = {
|
||||
**BASE_FUNCTIONS,
|
||||
}
|
||||
global_objects["request_items"] = find_argument_by_target_name(
|
||||
spec, "REQUEST_ITEM"
|
||||
)
|
||||
global_objects["arguments"] = [
|
||||
argument
|
||||
for group in spec.groups
|
||||
for argument in group.arguments
|
||||
if not argument.is_hidden
|
||||
if not argument.is_positional
|
||||
]
|
||||
|
||||
return global_objects
|
||||
|
||||
|
||||
def register_function(func: T) -> T:
|
||||
BASE_FUNCTIONS[func.__name__] = func
|
||||
return func
|
||||
|
||||
|
||||
@register_function
|
||||
def is_file_based_operator(operator: str) -> bool:
|
||||
return operator in {SEPARATOR_FILE_UPLOAD}
|
||||
|
||||
|
||||
def escape_zsh(text: str) -> str:
|
||||
return text.replace(":", "\\:")
|
||||
|
||||
|
||||
def serialize_argument_to_zsh(argument):
|
||||
# The argument format is the followig:
|
||||
# $prefix'$alias$has_value[$short_desc]:$metavar$:($choice_1 $choice_2)'
|
||||
|
||||
prefix = ""
|
||||
declaration = []
|
||||
has_choices = "choices" in argument.configuration
|
||||
|
||||
# The format for the argument declaration canges depending on the
|
||||
# the number of aliases. For a single $alias, we'll embed it directly
|
||||
# in the declaration string, but for multiple of them, we'll use a
|
||||
# $prefix.
|
||||
if len(argument.aliases) > 1:
|
||||
prefix = "{" + ",".join(argument.aliases) + "}"
|
||||
else:
|
||||
declaration.append(argument.aliases[0])
|
||||
|
||||
if not argument.is_flag:
|
||||
declaration.append("=")
|
||||
|
||||
declaration.append("[" + argument.short_help + "]")
|
||||
|
||||
if "metavar" in argument.configuration:
|
||||
metavar = argument.metavar
|
||||
elif has_choices:
|
||||
# Choices always require a metavar, so even if we don't have one
|
||||
# we can generate it from the argument aliases.
|
||||
metavar = (
|
||||
max(argument.aliases, key=len)
|
||||
.lstrip("-")
|
||||
.replace("-", "_")
|
||||
.upper()
|
||||
)
|
||||
else:
|
||||
metavar = None
|
||||
|
||||
if metavar:
|
||||
# Strip out any whitespace, and escape any characters that would
|
||||
# conflict with the shell.
|
||||
metavar = escape_zsh(metavar.strip(" "))
|
||||
declaration.append(f":{metavar}:")
|
||||
|
||||
if has_choices:
|
||||
declaration.append("(" + " ".join(argument.choices) + ")")
|
||||
|
||||
return prefix + f"'{''.join(declaration)}'"
|
||||
|
||||
|
||||
def find_argument_by_target_name(spec: ParserSpec, name: str) -> Argument:
|
||||
for group in spec.groups:
|
||||
for argument in group.arguments:
|
||||
if argument.aliases:
|
||||
targets = argument.aliases
|
||||
else:
|
||||
targets = [argument.metavar]
|
||||
|
||||
if name in targets:
|
||||
return argument
|
||||
|
||||
raise ValueError(f"Could not find argument with name {name}")
|
||||
|
||||
|
||||
@use_template("zsh")
|
||||
def zsh_completer(spec: ParserSpec) -> Dict[str, Any]:
|
||||
return {
|
||||
"escape_zsh": escape_zsh,
|
||||
"serialize_argument_to_zsh": serialize_argument_to_zsh,
|
||||
"methods": COMMON_HTTP_METHODS,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
zsh_completer(options)
|
@ -13,6 +13,9 @@ from httpie.cli.constants import (BASE_OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
||||
PRETTY_STDOUT_TTY_ONLY,
|
||||
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
||||
SEPARATOR_HEADER, SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_DATA_STRING, SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
SORTED_FORMAT_OPTIONS_STRING,
|
||||
UNSORTED_FORMAT_OPTIONS_STRING, RequestType)
|
||||
from httpie.cli.options import ParserSpec, Qualifiers, to_argparse
|
||||
@ -91,11 +94,11 @@ positional_arguments.add_argument(
|
||||
'data, files, and URL parameters.'
|
||||
),
|
||||
nested_options=[
|
||||
('HTTP Headers', 'Name:Value', 'Arbitrary HTTP header, e.g X-API-Token:123'),
|
||||
('URL Parameters', 'name==value', 'Querystring parameter to the URL, e.g limit==50'),
|
||||
('Data Fields', 'field=value', 'Data fields to be serialized as JSON (default) or Form Data (with --form)'),
|
||||
('Raw JSON Fields', 'field:=json', 'Data field for real JSON types.'),
|
||||
('File upload Fields', 'field@/dir/file', 'Path field for uploading a file.'),
|
||||
('HTTP Headers', 'Name:Value', SEPARATOR_HEADER, 'Arbitrary HTTP header, e.g X-API-Token:123'),
|
||||
('URL Parameters', 'name==value', SEPARATOR_QUERY_PARAM, 'Querystring parameter to the URL, e.g limit==50'),
|
||||
('Data Fields', 'field=value', SEPARATOR_DATA_STRING, 'Data fields to be serialized as JSON (default) or Form Data (with --form)'),
|
||||
('Raw JSON Fields', 'field:=json', SEPARATOR_DATA_RAW_JSON, 'Data field for real JSON types.'),
|
||||
('File upload Fields', 'field@/dir/file', SEPARATOR_FILE_UPLOAD, 'Path field for uploading a file.'),
|
||||
],
|
||||
help=r"""
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
|
@ -172,6 +172,11 @@ class Argument(typing.NamedTuple):
|
||||
def is_hidden(self):
|
||||
return self.configuration.get('help') is Qualifiers.SUPPRESS
|
||||
|
||||
@property
|
||||
def is_flag(self):
|
||||
action = getattr(self, 'action', None)
|
||||
return action in ARGPARSE_FLAG_ACTIONS
|
||||
|
||||
def __getattr__(self, attribute_name):
|
||||
if attribute_name in self.configuration:
|
||||
return self.configuration[attribute_name]
|
||||
@ -188,6 +193,17 @@ ARGPARSE_QUALIFIER_MAP = {
|
||||
Qualifiers.ONE_OR_MORE: argparse.ONE_OR_MORE
|
||||
}
|
||||
ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options')
|
||||
ARGPARSE_FLAG_ACTIONS = [
|
||||
"store_true",
|
||||
"store_false",
|
||||
"count",
|
||||
"version",
|
||||
"help",
|
||||
"debug",
|
||||
"manual",
|
||||
"append_const",
|
||||
"store_const"
|
||||
]
|
||||
|
||||
|
||||
def to_argparse(
|
||||
|
@ -193,7 +193,7 @@ def to_help_message(
|
||||
value,
|
||||
dec,
|
||||
)
|
||||
for key, value, dec in argument.nested_options
|
||||
for key, value, _, dec in argument.nested_options
|
||||
]
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user