From ee5fc59c5186bf7e9e50d063f758a890c9b06494 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 20 May 2022 10:42:38 +0300 Subject: [PATCH] Implement support for bash & completion flow generation --- extras/completion/completion.bash | 53 ++++++++++++ extras/completion/completion.zsh | 29 +++---- .../completion/templates/completion.bash.j2 | 40 +++++++++ extras/completion/templates/completion.zsh.j2 | 4 +- extras/httpie-completion.bash | 20 ----- extras/scripts/completion/bash.py | 83 +++++++++++++++++++ extras/scripts/completion/completion_flow.py | 2 +- .../scripts/completion/generate_completion.py | 22 +++-- extras/scripts/completion/zsh.py | 19 ++--- 9 files changed, 218 insertions(+), 54 deletions(-) create mode 100644 extras/completion/completion.bash create mode 100644 extras/completion/templates/completion.bash.j2 delete mode 100644 extras/httpie-completion.bash create mode 100644 extras/scripts/completion/bash.py diff --git a/extras/completion/completion.bash b/extras/completion/completion.bash new file mode 100644 index 00000000..50497f8d --- /dev/null +++ b/extras/completion/completion.bash @@ -0,0 +1,53 @@ +METHODS=("GET" "POST" "PUT" "DELETE" "HEAD" "OPTIONS" "PATCH" "TRACE" "CONNECT" ) +NORMARG=1 # TO-DO: dynamically calculate this? + +_http_complete() { + local cur_word=${COMP_WORDS[COMP_CWORD]} + local prev_word=${COMP_WORDS[COMP_CWORD - 1]} + + if [[ "$cur_word" == -* ]]; then + _http_complete_options "$cur_word" + else + if (( COMP_CWORD == NORMARG + 0 )); then + _http_complete_methods "$cur_word" + fi + if (( COMP_CWORD == NORMARG + 0 )); then + _http_complete_url "$cur_word" + fi + if (( COMP_CWORD == NORMARG + 1 )) && [[ " ${METHODS[*]} " =~ " ${prev_word} " ]]; then + _http_complete_url "$cur_word" + fi + if (( COMP_CWORD >= NORMARG + 2 )); then + _httpie_complete_request_item "$cur_word" + fi + if (( COMP_CWORD >= NORMARG + 1 )) && ! [[ " ${METHODS[*]} " =~ " ${prev_word} " ]]; then + _httpie_complete_request_item "$cur_word" + fi + + fi +} + +complete -o default -F _http_complete http httpie.http httpie.https https + +_http_complete_methods() { + local cur_word=$1 + local options="GET POST PUT DELETE HEAD OPTIONS PATCH TRACE CONNECT" + COMPREPLY+=( $( compgen -W "$options" -- "$cur_word" ) ) +} + +_http_complete_url() { + local cur_word=$1 + local options="http:// https://" + COMPREPLY+=( $( compgen -W "$options" -- "$cur_word" ) ) +} + +_httpie_complete_request_item() { + local cur_word=$1 + COMPREPLY+=("==" "=" ":=" ":=@") +} + +_http_complete_options() { + local cur_word=$1 + local options="--json -j --form -f --multipart --boundary --raw --compress -x --pretty --style -s --unsorted --sorted --response-charset --response-mime --format-options --print -p --headers -h --meta -m --body -b --verbose -v --all --stream -S --output -o --download -d --continue -c --quiet -q --session --session-read-only --auth -a --auth-type -A --ignore-netrc --offline --proxy --follow -F --max-redirects --max-headers --timeout --check-status --path-as-is --chunked --verify --ssl --ciphers --cert --cert-key --cert-key-pass --ignore-stdin -I --help --manual --version --traceback --default-scheme --debug " + COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) ) +} \ No newline at end of file diff --git a/extras/completion/completion.zsh b/extras/completion/completion.zsh index 02ca886c..a482668a 100644 --- a/extras/completion/completion.zsh +++ b/extras/completion/completion.zsh @@ -13,21 +13,22 @@ _httpie_params () { if ! [[ $current == -* ]]; then if (( CURRENT == NORMARG + 0 )); then - _httpie_method && ret=0 -fi - if (( CURRENT == NORMARG + 0 )); then - _httpie_url && ret=0 -fi - if (( CURRENT == NORMARG + 1 )) && [[ ${METHODS[(ie)$predecessor]} -le ${#METHODS} ]]; then - _httpie_url && ret=0 -fi - if (( CURRENT >= NORMARG + 2 )); then - _httpie_request_item && ret=0 -fi - if (( CURRENT >= NORMARG + 1 )) && ! [[ ${METHODS[(ie)$predecessor]} -le ${#METHODS} ]]; then - _httpie_request_item && ret=0 -fi + _httpie_method && ret=0 fi + if (( CURRENT == NORMARG + 0 )); then + _httpie_url && ret=0 + fi + if (( CURRENT == NORMARG + 1 )) && [[ ${METHODS[(ie)$predecessor]} -le ${#METHODS} ]]; then + _httpie_url && ret=0 + fi + if (( CURRENT >= NORMARG + 2 )); then + _httpie_request_item && ret=0 + fi + if (( CURRENT >= NORMARG + 1 )) && ! [[ ${METHODS[(ie)$predecessor]} -le ${#METHODS} ]]; then + _httpie_request_item && ret=0 + fi + + fi return $ret diff --git a/extras/completion/templates/completion.bash.j2 b/extras/completion/templates/completion.bash.j2 new file mode 100644 index 00000000..e2c52902 --- /dev/null +++ b/extras/completion/templates/completion.bash.j2 @@ -0,0 +1,40 @@ +METHODS=({% for method in methods -%} "{{ method }}" {% endfor -%}) +NORMARG=1 # TO-DO: dynamically calculate this? + +_http_complete() { + local cur_word=${COMP_WORDS[COMP_CWORD]} + local prev_word=${COMP_WORDS[COMP_CWORD - 1]} + + if [[ "$cur_word" == -* ]]; then + _http_complete_options "$cur_word" + else + {% for flow_item in generate_flow() -%} + {{ compile_bash(flow_item) | indent(width=8) }} + {% endfor %} + fi +} + +complete -o default -F _http_complete http httpie.http httpie.https https + +_http_complete_methods() { + local cur_word=$1 + local options="{{' '.join(methods)}}" + COMPREPLY+=( $( compgen -W "$options" -- "$cur_word" ) ) +} + +_http_complete_url() { + local cur_word=$1 + local options="http:// https://" + COMPREPLY+=( $( compgen -W "$options" -- "$cur_word" ) ) +} + +_httpie_complete_request_item() { + local cur_word=$1 + COMPREPLY+=("==" "=" ":=" ":=@") +} + +_http_complete_options() { + local cur_word=$1 + local options="{% for argument in arguments -%} {{ ' '.join(argument.aliases) }} {% endfor -%}" + COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) ) +} diff --git a/extras/completion/templates/completion.zsh.j2 b/extras/completion/templates/completion.zsh.j2 index 2fb5b241..fe7002e8 100755 --- a/extras/completion/templates/completion.zsh.j2 +++ b/extras/completion/templates/completion.zsh.j2 @@ -13,8 +13,8 @@ _httpie_params () { if ! [[ $current == -* ]]; then {% for flow_item in generate_flow() -%} - {{ compile_zsh(flow_item) }} - {% endfor -%} + {{ compile_zsh(flow_item) | indent(width=8) }} + {% endfor %} fi return $ret diff --git a/extras/httpie-completion.bash b/extras/httpie-completion.bash deleted file mode 100644 index 6abbc217..00000000 --- a/extras/httpie-completion.bash +++ /dev/null @@ -1,20 +0,0 @@ -_http_complete() { - local cur_word=${COMP_WORDS[COMP_CWORD]} - local prev_word=${COMP_WORDS[COMP_CWORD - 1]} - - if [[ "$cur_word" == -* ]]; then - _http_complete_options "$cur_word" - fi -} - -complete -o default -F _http_complete http httpie.http httpie.https https - -_http_complete_options() { - local cur_word=$1 - local options="-j --json -f --form --pretty -s --style -p --print - -v --verbose -h --headers -b --body -S --stream -o --output -d --download - -c --continue --session --session-read-only -a --auth --auth-type --proxy - --follow --verify --cert --cert-key --timeout --check-status --ignore-stdin - --help --version --traceback --debug --raw" - COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) ) -} diff --git a/extras/scripts/completion/bash.py b/extras/scripts/completion/bash.py new file mode 100644 index 00000000..444c6705 --- /dev/null +++ b/extras/scripts/completion/bash.py @@ -0,0 +1,83 @@ +from enum import Enum +from functools import singledispatch + +from completion_flow import ( + And, + Check, + Condition, + If, + Node, + Not, + Suggest, + Suggestion, + Variable, + generate_flow, +) + + +class BashVariable(str, Enum): + CURRENT = 'COMP_CWORD' + NORMARG = 'NORMARG' + CURRENT_WORD = 'cur_word' + PREDECESSOR = 'prev_word' + METHODS = 'METHODS' + + +SUGGESTION_TO_FUNCTION = { + Suggestion.METHOD: '_http_complete_methods', + Suggestion.URL: '_http_complete_url', + Suggestion.REQUEST_ITEM: '_httpie_complete_request_item', +} + + +@singledispatch +def compile_bash(node: Node) -> ...: + raise NotImplementedError(f'{type(node)} is not supported') + + +@compile_bash.register(If) +def compile_if(node: If) -> str: + check = compile_bash(node.check) + action = compile_bash(node.action) + return f'if {check}; then\n {action}\nfi' + + +@compile_bash.register(Check) +def compile_check(node: Check) -> str: + args = [ + BashVariable(arg.name) if isinstance(arg, Variable) else arg + for arg in node.args + ] + + if node.condition is Condition.POSITION_EQ: + return f'(( {BashVariable.CURRENT} == {BashVariable.NORMARG} + {args[0]} ))' + elif node.condition is Condition.POSITION_GE: + return f'(( {BashVariable.CURRENT} >= {BashVariable.NORMARG} + {args[0]} ))' + elif node.condition is Condition.CONTAINS_PREDECESSOR: + parts = [ + '[[ ', + '" ${', + BashVariable.METHODS, + '[*]} " =~ " ${', + BashVariable.PREDECESSOR, + '} " ]]', + ] + return ''.join(parts) + + +@compile_bash.register(And) +def compile_and(node: And) -> str: + return ' && '.join(compile_bash(check) for check in node.checks) + + +@compile_bash.register(Not) +def compile_not(node: Not) -> str: + return f'! {compile_bash(node.check)}' + + +@compile_bash.register(Suggest) +def compile_suggest(node: Suggest) -> str: + return ( + SUGGESTION_TO_FUNCTION[node.suggestion] + + f' "${BashVariable.CURRENT_WORD}"' + ) diff --git a/extras/scripts/completion/completion_flow.py b/extras/scripts/completion/completion_flow.py index f6d1faa4..9c98127e 100644 --- a/extras/scripts/completion/completion_flow.py +++ b/extras/scripts/completion/completion_flow.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from enum import Enum, auto -from typing import List, Iterator +from typing import Iterator, List class Condition(Enum): diff --git a/extras/scripts/completion/generate_completion.py b/extras/scripts/completion/generate_completion.py index fef64f72..8d848f38 100644 --- a/extras/scripts/completion/generate_completion.py +++ b/extras/scripts/completion/generate_completion.py @@ -1,18 +1,17 @@ -from atexit import register import functools import string import textwrap - -from jinja2 import Template +from atexit import register from pathlib import Path -from typing import Any, Dict, Callable, TypeVar +from typing import Any, Callable, Dict, TypeVar + +from completion_flow import generate_flow +from jinja2 import Template from httpie.cli.constants import SEPARATOR_FILE_UPLOAD from httpie.cli.definition import options from httpie.cli.options import Argument, ParserSpec -from completion_flow import generate_flow - T = TypeVar('T') EXTRAS_DIR = Path(__file__).parent.parent.parent @@ -202,7 +201,7 @@ def zsh_completer(spec: ParserSpec) -> Dict[str, Any]: return { 'escape_zsh': escape_zsh, 'serialize_argument_to_zsh': serialize_argument_to_zsh, - 'compile_zsh': compile_zsh + 'compile_zsh': compile_zsh, } @@ -213,6 +212,15 @@ def fish_completer(spec: ParserSpec) -> Dict[str, Any]: } +@use_template('bash') +def fish_completer(spec: ParserSpec) -> Dict[str, Any]: + from bash import compile_bash + + return { + 'compile_bash': compile_bash, + } + + def main(): for shell_type, completer in COMPLETERS.items(): print(f'Generating {shell_type} completer.') diff --git a/extras/scripts/completion/zsh.py b/extras/scripts/completion/zsh.py index 1a952e4f..ac18f86d 100644 --- a/extras/scripts/completion/zsh.py +++ b/extras/scripts/completion/zsh.py @@ -1,17 +1,16 @@ -from functools import singledispatch from enum import Enum -from lib2to3.pgen2.pgen import generate_grammar +from functools import singledispatch + from completion_flow import ( - Node, - Check, - Suggest, - Variable, - Condition, - Suggestion, - If, And, + Check, + Condition, + If, + Node, Not, - generate_flow, + Suggest, + Suggestion, + Variable, )