diff --git a/Gemfile b/Gemfile index ac1eeba8..77c2e8e7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source 'https://rubygems.org', cooldown: 7 +source 'https://rubygems.org' # gem 'debug' gem 'rspec' diff --git a/bashly.gemspec b/bashly.gemspec index a45fabc9..aec0f84d 100644 --- a/bashly.gemspec +++ b/bashly.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 3.2' s.add_dependency 'colsole', '~> 1.0' - s.add_dependency 'completely', '~> 0.7.0' + s.add_dependency 'completely', '~> 0.8.0.rc3' s.add_dependency 'gtx', '~> 0.1.1' s.add_dependency 'listen', '~> 3.9' s.add_dependency 'lp', '~> 0.2.0' diff --git a/examples/colors-usage/src/lib/colors.sh b/examples/colors-usage/src/lib/colors.sh index 0653bb63..e03cb478 100644 --- a/examples/colors-usage/src/lib/colors.sh +++ b/examples/colors-usage/src/lib/colors.sh @@ -42,6 +42,7 @@ white() { print_in_color "\e[37m" "$*"; } bold() { print_in_color "\e[1m" "$*"; } underlined() { print_in_color "\e[4m" "$*"; } +bold_underlined() { print_in_color "\e[1;4m" "$*"; } red_bold() { print_in_color "\e[1;31m" "$*"; } green_bold() { print_in_color "\e[1;32m" "$*"; } diff --git a/examples/completions/src/lib/send_completions.sh b/examples/completions/src/lib/send_completions.sh index b5800e2a..18954f92 100644 --- a/examples/completions/src/lib/send_completions.sh +++ b/examples/completions/src/lib/send_completions.sh @@ -6,104 +6,231 @@ send_completions() { echo $'# completely (https://github.com/bashly-framework/completely)' echo $'# Modifying it manually is not recommended' echo $'' - echo $'_cli_completions_filter() {' - echo $' local words="$1"' - echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local result=()' - echo $'' - echo $' # words the user already typed (excluding the command itself)' - echo $' local used=()' - echo $' if ((COMP_CWORD > 1)); then' - echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' - echo $' fi' - echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' # Completing an option: offer everything (including options)' - echo $' echo "$words"' - echo $'' - echo $' else' - echo $' # Completing a non-option: offer only non-options,' - echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in $words; do' - echo $' [[ "${word:0:1}" == "-" ]] && continue' - echo $'' - echo $' local seen=0' - echo $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$word" ]]; then' - echo $' seen=1' - echo $' break' - echo $' fi' - echo $' done' - echo $' ((!seen)) && result+=("$word")' - echo $' done' + echo $'_cli_completions_flag_expects_value() {' + echo $' case "$1" in' + echo $' --handler) return 0 ;;' + echo $' --user|-u) return 0 ;;' + echo $' --password|-p) return 0 ;;' + echo $' esac' echo $'' - echo $' echo "${result[*]}"' - echo $' fi' + echo $' return 1' echo $'}' echo $'' echo $'_cli_completions() {' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local compwords=()' + echo $' local prev=' echo $' if ((COMP_CWORD > 0)); then' - echo $' compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' prev=${COMP_WORDS[$((COMP_CWORD - 1))]}' echo $' fi' - echo $' local compline="${compwords[*]}"' echo $'' - echo $' COMPREPLY=()' + echo $' local completed=()' + echo $' if ((COMP_CWORD > 1)); then' + echo $' completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' fi' echo $'' - echo $' case "$compline" in' - echo $' \'download\'*\'--handler\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "curl wget")" -- "$cur")' - echo $' ;;' + echo $' local non_options=()' + echo $' local completed_options=()' + echo $' local skip_next=0' + echo $' for word in "${completed[@]}"; do' + echo $' if ((skip_next)); then' + echo $' skip_next=0' + echo $' continue' + echo $' fi' + echo $'' + echo $' if [[ "${word:0:1}" == "-" ]]; then' + echo $' completed_options+=("$word")' + echo $' if _cli_completions_flag_expects_value "$word"; then' + echo $' skip_next=1' + echo $' fi' + echo $' continue' + echo $' fi' + echo $'' + echo $' non_options+=("$word")' + echo $' done' + echo $'' + echo $' local route_id=' + echo $' local route_word_count=-1' + echo $' local route_has_positionals=0' + echo $' local positional_index=0' + echo $' if (( ${#non_options[@]} >= 0 )) &&' + echo $' (( 0 > route_word_count ))' + echo $' then' + echo $' route_id=0' + echo $' route_word_count=0' + echo $' route_has_positionals=0' + echo $' positional_index=$((${#non_options[@]} - 0))' + echo $' fi' echo $'' - echo $' \'upload\'*\'--user\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur")' - echo $' ;;' + echo $' if (( ${#non_options[@]} >= 1 )) &&' + echo $' (( 1 > route_word_count )) &&' + echo $' [[ "${non_options[0]}" == "completions" ]]' + echo $' then' + echo $' route_id=1' + echo $' route_word_count=1' + echo $' route_has_positionals=0' + echo $' positional_index=$((${#non_options[@]} - 1))' + echo $' fi' echo $'' - echo $' \'completions\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help -h")" -- "$cur")' - echo $' ;;' + echo $' if (( ${#non_options[@]} >= 1 )) &&' + echo $' (( 1 > route_word_count )) &&' + echo $' [[ "${non_options[0]}" == "download" || "${non_options[0]}" == "d" ]]' + echo $' then' + echo $' route_id=2' + echo $' route_word_count=1' + echo $' route_has_positionals=1' + echo $' positional_index=$((${#non_options[@]} - 1))' + echo $' fi' echo $'' - echo $' \'d\'*\'--handler\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "curl wget")" -- "$cur")' - echo $' ;;' + echo $' if (( ${#non_options[@]} >= 1 )) &&' + echo $' (( 1 > route_word_count )) &&' + echo $' [[ "${non_options[0]}" == "upload" || "${non_options[0]}" == "u" ]]' + echo $' then' + echo $' route_id=3' + echo $' route_word_count=1' + echo $' route_has_positionals=1' + echo $' positional_index=$((${#non_options[@]} - 1))' + echo $' fi' echo $'' - echo $' \'upload\'*\'-u\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur")' - echo $' ;;' + echo $' COMPREPLY=()' echo $'' - echo $' \'download\'*)' - echo $' compopt -o filenames 2>/dev/null' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_cli_completions_filter "--force --handler --help -f -h")" -- "$cur")' - echo $' ;;' + echo $' if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "completions download d upload u" -- "$cur")' + echo $' return' + echo $' fi' echo $'' - echo $' \'u\'*\'--user\')' + echo $' case "$route_id:$prev" in' + echo $' 2:--handler)' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "curl wget" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' 3:--user|3:-u)' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur")' + echo $' return' echo $' ;;' - echo $'' - echo $' \'upload\'*)' - echo $' compopt -o filenames 2>/dev/null' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -A user -W "$(_cli_completions_filter "--help --password --user -h -p -u CHANGELOG.md README.md")" -- "$cur")' + echo $' 3:--password|3:-p)' + echo $' return' echo $' ;;' + echo $' esac' echo $'' - echo $' \'u\'*\'-u\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur")' - echo $' ;;' + echo $' if [[ "${cur:0:1}" == "-" ]]; then' + echo $' case "$route_id" in' + echo $' 0)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --version|-v) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--version" "-v")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' 1)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' 2)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --force|-f) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--force" "-f")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --handler) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--handler")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' 3)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --user|-u) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--user" "-u")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --password|-p) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--password" "-p")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' esac' + echo $' fi' echo $'' - echo $' \'d\'*)' - echo $' compopt -o filenames 2>/dev/null' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_cli_completions_filter "--force --handler --help -f -h")" -- "$cur")' + echo $' case "$route_id:$positional_index" in' + echo $' 2:0)' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur")' + echo $' return' echo $' ;;' - echo $'' - echo $' \'u\'*)' - echo $' compopt -o filenames 2>/dev/null' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -A user -W "$(_cli_completions_filter "--help --password --user -h -p -u CHANGELOG.md README.md")" -- "$cur")' + echo $' 2:1)' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur")' + echo $' return' echo $' ;;' - echo $'' - echo $' *)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --version -h -v completions d download u upload")" -- "$cur")' + echo $' 3:0)' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "README.md CHANGELOG.md" -- "$cur")' + echo $' return' echo $' ;;' - echo $'' echo $' esac' echo $'} &&' echo $' complete -F _cli_completions cli' diff --git a/examples/render-mandoc/docs/download.1 b/examples/render-mandoc/docs/download.1 index a17f28f0..769e126c 100644 --- a/examples/render-mandoc/docs/download.1 +++ b/examples/render-mandoc/docs/download.1 @@ -1,6 +1,6 @@ .\" Automatically generated by Pandoc 3.9 .\" -.TH "download" "1" "May 2026" "Version 0.1.0" "Sample application" +.TH "download" "1" "July 2026" "Version 0.1.0" "Sample application" .SH NAME \f[B]download\f[R] \- Sample application .SH SYNOPSIS diff --git a/examples/render-mandoc/docs/download.md b/examples/render-mandoc/docs/download.md index c2cc68e5..9ff37dfe 100644 --- a/examples/render-mandoc/docs/download.md +++ b/examples/render-mandoc/docs/download.md @@ -1,6 +1,6 @@ % download(1) Version 0.1.0 | Sample application % Lana Lang -% May 2026 +% July 2026 NAME ================================================== diff --git a/lib/bashly.rb b/lib/bashly.rb index 63a52501..350d3395 100644 --- a/lib/bashly.rb +++ b/lib/bashly.rb @@ -6,8 +6,9 @@ module Bashly autoloads 'bashly/refinements', %i[ComposeRefinements] autoloads 'bashly', %i[ - CLI Config ConfigValidator Library LibrarySource LibrarySourceConfig - MessageStrings RenderContext RenderSource Settings VERSION Watch + CLI CompletionBuilder Config ConfigValidator Library LibrarySource + LibrarySourceConfig MessageStrings RenderContext RenderSource Settings + VERSION Watch ] autoloads 'bashly/concerns', %i[ diff --git a/lib/bashly/completion_builder.rb b/lib/bashly/completion_builder.rb new file mode 100644 index 00000000..00cb6a05 --- /dev/null +++ b/lib/bashly/completion_builder.rb @@ -0,0 +1,188 @@ +module Bashly + class CompletionBuilder + BUILTIN_PATTERN = /\A<([^>]+)>\z/ + + def initialize(command, with_version: true) + @command = command + @with_version = with_version + @patterns = [] + @options = {} + @tokens = {} + @token_sources = {} + end + + def call + add_command @command, inherited_global_groups: [] + + result = { 'patterns' => @patterns } + result['options'] = @options if @options.any? + result['tokens'] = @tokens if @tokens.any? + result + end + + private + + def add_command(command, inherited_global_groups:) + local_group = add_local_options command + pattern_groups = inherited_global_groups.dup + pattern_groups << local_group if local_group + + @patterns << pattern_for(command, pattern_groups) + + child_global_groups = inherited_global_groups.dup + if command.global_flags? + global_group = add_global_options command + child_global_groups << global_group if global_group + end + + command.visible_commands.each do |child| + add_command child, inherited_global_groups: child_global_groups + end + end + + def pattern_for(command, option_groups) + parts = [command_path(command)] + parts.concat(option_groups.map { |group| "[#{group} options]" }) + parts.concat positional_tokens(command) + parts.join ' ' + end + + def command_path(command) + command_chain(command).map.with_index do |item, index| + index.zero? ? item.name : item.aliases.join('|') + end.join ' ' + end + + def command_chain(command) + result = [] + current = command + while current + result.unshift current + current = current.parent_command + end + result + end + + def add_local_options(command) + entries = fixed_option_entries(command) + flag_option_entries(command.visible_flags, command) + return if entries.empty? + + name = group_name command + @options[name] = entries + name + end + + def add_global_options(command) + entries = flag_option_entries command.visible_flags, command + return if entries.empty? + + name = "#{group_name(command)}_global" + @options[name] = entries + name + end + + def fixed_option_entries(command) + return [] if !command.root_command? && command.catch_all.catch_help? + + entries = %w[--help|-h] + entries << '--version|-v' if command.root_command? && @with_version + entries + end + + def flag_option_entries(flags, command) + flags.map do |flag| + token_name = flag_token_name flag, command + flag.completion_option_entry token_name + end + end + + def flag_token_name(flag, command) + return unless flag.arg || flag.allowed || flag.completions + + register_token flag.arg || flag.name, command, flag_source(flag) + end + + def positional_tokens(command) + command.args.map do |arg| + token_name = register_token arg.name, command, arg_source(arg, command) + suffix = arg.repeatable ? '...' : nil + "<#{token_name}>#{suffix}" + end + end + + def flag_source(flag) + return static_source(flag.allowed) if flag.allowed + return completion_source(flag.completions) if flag.completions + + nil + end + + def arg_source(arg, command) + return static_source(arg.allowed) if arg.allowed + return completion_source(arg.completions) if arg.completions + return completion_source(command.completions) if command.completions + + nil + end + + def static_source(values) + Array(values).compact.map { |value| escape_static_source value } + end + + def completion_source(values) + Array(values).compact.map do |value| + string = value.to_s + builtin = string[BUILTIN_PATTERN, 1] + + if builtin + "+#{builtin}" + else + escape_static_source string + end + end + end + + def escape_static_source(value) + string = value.to_s + string.start_with?('+') ? "+#{string}" : string + end + + def register_token(base_name, command, source) + preferred = token_name base_name + return preferred if register_token_name preferred, source + + scoped = token_name "#{group_name command}_#{base_name}" + return scoped if register_token_name scoped, source + + suffix = 2 + loop do + candidate = "#{scoped}_#{suffix}" + return candidate if register_token_name candidate, source + + suffix += 1 + end + end + + def register_token_name(name, source) + if @token_sources.has_key? name + return false unless @token_sources[name] == source + else + @token_sources[name] = source + @tokens[name] = source + end + + true + end + + def group_name(command) + token_name command.root_command? ? 'root' : command.action_name + end + + def token_name(value) + value.to_s + .gsub(/[^a-zA-Z0-9]+/, '_') + .gsub(/\A_+|_+\z/, '') + .downcase + end + end +end diff --git a/lib/bashly/completions/bashly-completions.bash b/lib/bashly/completions/bashly-completions.bash index 91582a03..cd199618 100644 --- a/lib/bashly/completions/bashly-completions.bash +++ b/lib/bashly/completions/bashly-completions.bash @@ -73,7 +73,7 @@ _bashly_completions() { ;; 'doc'*) - while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_bashly_completions_filter "--help -h --index -i arg arg.allowed arg.default arg.help arg.name arg.repeatable arg.required arg.validate command command.alias command.args command.catch_all command.commands command.completions command.default command.dependencies command.environment_variables command.examples command.expose command.extensible command.filename command.filters command.flags command.footer command.function command.group command.help command.name command.private command.version environment_variable environment_variable.default environment_variable.help environment_variable.name environment_variable.private environment_variable.required flag flag.allowed flag.arg flag.completions flag.conflicts flag.default flag.help flag.long flag.private flag.repeatable flag.required flag.short flag.validate")" -- "$cur" ) + while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_bashly_completions_filter "--help -h --index -i arg arg.allowed arg.completions arg.default arg.help arg.name arg.repeatable arg.required arg.validate command command.alias command.args command.catch_all command.commands command.completions command.default command.dependencies command.environment_variables command.examples command.expose command.extensible command.filename command.filters command.flags command.footer command.function command.group command.help command.name command.private command.version environment_variable environment_variable.default environment_variable.help environment_variable.name environment_variable.private environment_variable.required flag flag.allowed flag.arg flag.completions flag.conflicts flag.default flag.help flag.long flag.private flag.repeatable flag.required flag.short flag.validate")" -- "$cur" ) ;; 'i'*) diff --git a/lib/bashly/completions/completely.yaml b/lib/bashly/completions/completely.yaml index efa50faf..6916ced4 100644 --- a/lib/bashly/completions/completely.yaml +++ b/lib/bashly/completions/completely.yaml @@ -92,6 +92,7 @@ bashly doc: &doc - -i - arg - arg.allowed +- arg.completions - arg.default - arg.help - arg.name diff --git a/lib/bashly/concerns/completions.rb b/lib/bashly/concerns/completions.rb index a8e0b25b..4b942a7c 100644 --- a/lib/bashly/concerns/completions.rb +++ b/lib/bashly/concerns/completions.rb @@ -1,4 +1,5 @@ require 'completely' +require 'bashly/completion_builder' module Bashly # This is a `Command` and `Flag` concern responsible for providing bash @@ -15,25 +16,18 @@ def completion_data(command_full_name) ["#{prefix}#{name}", comps] end end + + def completion_option_entry(token_name = nil) + result = [aliases.join('|')] + result << "<#{token_name}>" if token_name + result << '(repeatable)' if repeatable + result.join ' ' + end end module Command def completion_data(with_version: true) - result = {} - - completion_full_names.each do |name| - name = "#{name}*" if name.include? '*' - result[name] = completion_words with_version: with_version - flags.each do |flag| - result.merge! flag.completion_data(name) - end - end - - visible_commands.each do |command| - result.merge! command.completion_data(with_version: false) - end - - result + CompletionBuilder.new(self, with_version: with_version).call end def completion_script @@ -44,42 +38,11 @@ def completion_function(name = nil) completion_generator.wrapper_function name end - protected - - def completion_full_names - if parent_command - glue = parent_command.global_flags? ? '*' : ' ' - parent_command.completion_full_names.product(aliases).map { |a| a.join glue } - else - aliases - end - end - private def completion_generator Completely::Completions.new completion_data end - - def completion_flag_names - visible_flags.map(&:name) + visible_flags.map(&:short) - end - - def completion_allowed_args - args.map(&:allowed).flatten - end - - def completion_words(with_version: false) - trivial_flags = %w[--help -h] - trivial_flags += %w[--version -v] if with_version - all = ( - visible_command_aliases + trivial_flags + - completion_flag_names + completion_allowed_args - ) - - all += completions if completions - all.compact.uniq.sort - end end end end diff --git a/lib/bashly/config_validator.rb b/lib/bashly/config_validator.rb index 4b3060e3..a68f44f6 100644 --- a/lib/bashly/config_validator.rb +++ b/lib/bashly/config_validator.rb @@ -90,6 +90,8 @@ def assert_expose(key, value) def assert_arg(key, value) assert_hash key, value, keys: Script::Argument.option_keys + refute value['allowed'] && value['completions'], "#{key} cannot have both nub`allowed` and nub`completions`" + assert_string "#{key}.name", value['name'] assert_optional_string "#{key}.help", value['help'] assert_string_or_array "#{key}.default", value['default'] @@ -99,6 +101,7 @@ def assert_arg(key, value) assert_boolean "#{key}.unique", value['unique'] assert_array "#{key}.allowed", value['allowed'], of: :string + assert_array "#{key}.completions", value['completions'], of: :string refute value['name'].match(/^-/), "#{key}.name must not start with '-'" diff --git a/lib/bashly/docs/arg.yml b/lib/bashly/docs/arg.yml index 7ac03e97..48eed15b 100644 --- a/lib/bashly/docs/arg.yml +++ b/lib/bashly/docs/arg.yml @@ -33,6 +33,17 @@ arg.allowed: allowed: [stage, production, development] default: development +arg.completions: + help: Specify a list of additional completion suggestions when used in conjunction with `bashly add completions`. + url: https://bashly.dev/configuration/argument/#completions + example: |- + args: + - name: path + help: File or directory to process + completions: + - + - + arg.default: help: Specify the value to apply when not provided by the user. url: https://bashly.dev/configuration/argument/#default diff --git a/lib/bashly/script/argument.rb b/lib/bashly/script/argument.rb index 5fd96f6e..d5a0e84e 100644 --- a/lib/bashly/script/argument.rb +++ b/lib/bashly/script/argument.rb @@ -8,7 +8,7 @@ class Argument < Base class << self def option_keys @option_keys ||= %i[ - allowed default help name repeatable required unique validate + allowed completions default help name repeatable required unique validate ] end end diff --git a/schemas/bashly.json b/schemas/bashly.json index 69dc00f6..b233fea2 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -67,6 +67,21 @@ ] } }, + "completions": { + "title": "completions", + "description": "Completions of the current positional argument\nhttps://bashly.dev/configuration/argument/#completions", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "A completion value or action of the current positional argument", + "type": "string", + "minLength": 1, + "examples": [ + "" + ] + } + }, "repeatable": { "title": "repeatable", "description": "Whether the current positional argument can be repeated\nhttps://bashly.dev/configuration/argument/#repeatable", diff --git a/spec/approvals/cli/add/comp-function-file b/spec/approvals/cli/add/comp-function-file index b1851a88..c2ffe03e 100644 --- a/spec/approvals/cli/add/comp-function-file +++ b/spec/approvals/cli/add/comp-function-file @@ -6,72 +6,189 @@ send_completions() { echo $'# completely (https://github.com/bashly-framework/completely)' echo $'# Modifying it manually is not recommended' echo $'' - echo $'_cli_completions_filter() {' - echo $' local words="$1"' + echo $'_cli_completions_flag_expects_value() {' + echo $' case "$1" in' + echo $' --user|-u) return 0 ;;' + echo $' --password|-p) return 0 ;;' + echo $' esac' + echo $'' + echo $' return 1' + echo $'}' + echo $'' + echo $'_cli_completions() {' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local result=()' + echo $' local prev=' + echo $' if ((COMP_CWORD > 0)); then' + echo $' prev=${COMP_WORDS[$((COMP_CWORD - 1))]}' + echo $' fi' echo $'' - echo $' # words the user already typed (excluding the command itself)' - echo $' local used=()' + echo $' local completed=()' echo $' if ((COMP_CWORD > 1)); then' - echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' # Completing an option: offer everything (including options)' - echo $' echo "$words"' - echo $'' - echo $' else' - echo $' # Completing a non-option: offer only non-options,' - echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in $words; do' - echo $' [[ "${word:0:1}" == "-" ]] && continue' - echo $'' - echo $' local seen=0' - echo $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$word" ]]; then' - echo $' seen=1' - echo $' break' - echo $' fi' - echo $' done' - echo $' ((!seen)) && result+=("$word")' - echo $' done' + echo $' local non_options=()' + echo $' local completed_options=()' + echo $' local skip_next=0' + echo $' for word in "${completed[@]}"; do' + echo $' if ((skip_next)); then' + echo $' skip_next=0' + echo $' continue' + echo $' fi' + echo $'' + echo $' if [[ "${word:0:1}" == "-" ]]; then' + echo $' completed_options+=("$word")' + echo $' if _cli_completions_flag_expects_value "$word"; then' + echo $' skip_next=1' + echo $' fi' + echo $' continue' + echo $' fi' + echo $'' + echo $' non_options+=("$word")' + echo $' done' + echo $'' + echo $' local route_id=' + echo $' local route_word_count=-1' + echo $' local route_has_positionals=0' + echo $' local positional_index=0' + echo $' if (( ${#non_options[@]} >= 0 )) &&' + echo $' (( 0 > route_word_count ))' + echo $' then' + echo $' route_id=0' + echo $' route_word_count=0' + echo $' route_has_positionals=0' + echo $' positional_index=$((${#non_options[@]} - 0))' + echo $' fi' echo $'' - echo $' echo "${result[*]}"' + echo $' if (( ${#non_options[@]} >= 1 )) &&' + echo $' (( 1 > route_word_count )) &&' + echo $' [[ "${non_options[0]}" == "download" || "${non_options[0]}" == "d" ]]' + echo $' then' + echo $' route_id=1' + echo $' route_word_count=1' + echo $' route_has_positionals=1' + echo $' positional_index=$((${#non_options[@]} - 1))' echo $' fi' - echo $'}' echo $'' - echo $'_cli_completions() {' - echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local compwords=()' - echo $' if ((COMP_CWORD > 0)); then' - echo $' compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' if (( ${#non_options[@]} >= 1 )) &&' + echo $' (( 1 > route_word_count )) &&' + echo $' [[ "${non_options[0]}" == "upload" || "${non_options[0]}" == "u" ]]' + echo $' then' + echo $' route_id=2' + echo $' route_word_count=1' + echo $' route_has_positionals=1' + echo $' positional_index=$((${#non_options[@]} - 1))' echo $' fi' - echo $' local compline="${compwords[*]}"' echo $'' echo $' COMPREPLY=()' echo $'' - echo $' case "$compline" in' - echo $' \'download\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force --help -f -h")" -- "$cur")' - echo $' ;;' + echo $' if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "download d upload u" -- "$cur")' + echo $' return' + echo $' fi' echo $'' - echo $' \'upload\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --password --user -h -p -u")" -- "$cur")' + echo $' case "$route_id:$prev" in' + echo $' 2:--user|2:-u)' + echo $' return' echo $' ;;' - echo $'' - echo $' \'d\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force --help -f -h")" -- "$cur")' + echo $' 2:--password|2:-p)' + echo $' return' echo $' ;;' + echo $' esac' echo $'' - echo $' \'u\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --password --user -h -p -u")" -- "$cur")' - echo $' ;;' + echo $' if [[ "${cur:0:1}" == "-" ]]; then' + echo $' case "$route_id" in' + echo $' 0)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --version|-v) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--version" "-v")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' 1)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --force|-f) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--force" "-f")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' 2)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --user|-u) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--user" "-u")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --password|-p) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--password" "-p")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' esac' + echo $' fi' echo $'' - echo $' *)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --version -h -v d download u upload")" -- "$cur")' + echo $' case "$route_id:$positional_index" in' + echo $' 1:0)' + echo $' return' + echo $' ;;' + echo $' 1:1)' + echo $' return' + echo $' ;;' + echo $' 2:0)' + echo $' return' echo $' ;;' - echo $'' echo $' esac' echo $'} &&' echo $' complete -F _cli_completions cli' diff --git a/spec/approvals/cli/add/comp-script-file b/spec/approvals/cli/add/comp-script-file index 81a69f94..dd070eca 100644 --- a/spec/approvals/cli/add/comp-script-file +++ b/spec/approvals/cli/add/comp-script-file @@ -4,72 +4,189 @@ # completely (https://github.com/bashly-framework/completely) # Modifying it manually is not recommended -_cli_completions_filter() { - local words="$1" +_cli_completions_flag_expects_value() { + case "$1" in + --user|-u) return 0 ;; + --password|-p) return 0 ;; + esac + + return 1 +} + +_cli_completions() { local cur=${COMP_WORDS[COMP_CWORD]} - local result=() + local prev= + if ((COMP_CWORD > 0)); then + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} + fi - # words the user already typed (excluding the command itself) - local used=() + local completed=() if ((COMP_CWORD > 1)); then - used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - echo "$words" - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in $words; do - [[ "${word:0:1}" == "-" ]] && continue - - local seen=0 - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - seen=1 - break - fi - done - ((!seen)) && result+=("$word") - done + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi + + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if _cli_completions_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi + + non_options+=("$word") + done + + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 + if (( ${#non_options[@]} >= 0 )) && + (( 0 > route_word_count )) + then + route_id=0 + route_word_count=0 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 0)) + fi - echo "${result[*]}" + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "download" || "${non_options[0]}" == "d" ]] + then + route_id=1 + route_word_count=1 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 1)) fi -} -_cli_completions() { - local cur=${COMP_WORDS[COMP_CWORD]} - local compwords=() - if ((COMP_CWORD > 0)); then - compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "upload" || "${non_options[0]}" == "u" ]] + then + route_id=2 + route_word_count=1 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 1)) fi - local compline="${compwords[*]}" COMPREPLY=() - case "$compline" in - 'download'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force --help -f -h")" -- "$cur") - ;; + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "download d upload u" -- "$cur") + return + fi - 'upload'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --password --user -h -p -u")" -- "$cur") + case "$route_id:$prev" in + 2:--user|2:-u) + return ;; - - 'd'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force --help -f -h")" -- "$cur") + 2:--password|2:-p) + return ;; + esac - 'u'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --password --user -h -p -u")" -- "$cur") - ;; + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in + 0) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --version|-v) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--version" "-v") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 1) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --force|-f) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--force" "-f") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 2) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --user|-u) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--user" "-u") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --password|-p) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--password" "-p") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + esac + fi - *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --version -h -v d download u upload")" -- "$cur") + case "$route_id:$positional_index" in + 1:0) + return + ;; + 1:1) + return + ;; + 2:0) + return ;; - esac } && complete -F _cli_completions cli diff --git a/spec/approvals/cli/add/comp-yaml-file b/spec/approvals/cli/add/comp-yaml-file index 77ee4c00..b5daf2c4 100644 --- a/spec/approvals/cli/add/comp-yaml-file +++ b/spec/approvals/cli/add/comp-yaml-file @@ -1,34 +1,21 @@ --- -cli: -- "--help" -- "--version" -- "-h" -- "-v" -- d -- download -- u -- upload -cli download: -- "--force" -- "--help" -- "-f" -- "-h" -cli d: -- "--force" -- "--help" -- "-f" -- "-h" -cli upload: -- "--help" -- "--password" -- "--user" -- "-h" -- "-p" -- "-u" -cli u: -- "--help" -- "--password" -- "--user" -- "-h" -- "-p" -- "-u" +patterns: +- cli [root options] +- cli download|d [download options] +- cli upload|u [upload options] +options: + root: + - "--help|-h" + - "--version|-v" + download: + - "--help|-h" + - "--force|-f" + upload: + - "--help|-h" + - "--user|-u " + - "--password|-p " +tokens: + source: + target: + user: + password: diff --git a/spec/approvals/cli/doc/full b/spec/approvals/cli/doc/full index ef77dcf9..62463761 100644 --- a/spec/approvals/cli/doc/full +++ b/spec/approvals/cli/doc/full @@ -38,6 +38,20 @@ arg.allowed See https://bashly.dev/configuration/argument/#allowed +arg.completions + + Specify a list of additional completion suggestions when used in conjunction + with bashly add completions. + + args: + - name: path + help: File or directory to process + completions: + - + - + + See https://bashly.dev/configuration/argument/#completions + arg.default Specify the value to apply when not provided by the user. diff --git a/spec/approvals/cli/doc/index b/spec/approvals/cli/doc/index index f2072735..bcfdb059 100644 --- a/spec/approvals/cli/doc/index +++ b/spec/approvals/cli/doc/index @@ -1,5 +1,6 @@ arg arg.allowed +arg.completions arg.default arg.help arg.name diff --git a/spec/approvals/completion_builder/aliases_and_global_flags b/spec/approvals/completion_builder/aliases_and_global_flags new file mode 100644 index 00000000..c9c16050 --- /dev/null +++ b/spec/approvals/completion_builder/aliases_and_global_flags @@ -0,0 +1,25 @@ +--- +patterns: +- cli [root options] +- cli images|img [root_global options] [images options] +- cli images|img list|ls [root_global options] [images_global options] [images_list + options] +options: + root: + - "--help|-h" + - "--version|-v" + - "--debug" + root_global: + - "--debug" + images: + - "--help|-h" + - "--verbose" + images_global: + - "--verbose" + images_list: + - "--help|-h" + - "--env " +tokens: + env: + - prod + - dev diff --git a/spec/approvals/completion_builder/root_options b/spec/approvals/completion_builder/root_options new file mode 100644 index 00000000..90bbb62e --- /dev/null +++ b/spec/approvals/completion_builder/root_options @@ -0,0 +1,8 @@ +--- +patterns: +- cli [root options] +options: + root: + - "--help|-h" + - "--force|-f" + - "--verbose" diff --git a/spec/approvals/completion_builder/source_resolution b/spec/approvals/completion_builder/source_resolution new file mode 100644 index 00000000..4887a0dc --- /dev/null +++ b/spec/approvals/completion_builder/source_resolution @@ -0,0 +1,27 @@ +--- +patterns: +- cli [root options] +- cli upload [upload options] +options: + root: + - "--help|-h" + - "--version|-v" + upload: + - "--help|-h" + - "--user|-u " + - "--tag " +tokens: + user: + - "+user" + tag: + source: + - "+directory" + - README.md + - "$(git branch)" + - "++literal" + target: + - "+file" + - README.md + extra: + - "+file" + - README.md diff --git a/spec/approvals/completion_builder/token_collisions b/spec/approvals/completion_builder/token_collisions new file mode 100644 index 00000000..2e47386b --- /dev/null +++ b/spec/approvals/completion_builder/token_collisions @@ -0,0 +1,19 @@ +--- +patterns: +- cli [root options] +options: + root: + - "--help|-h" + - "--version|-v" + - "--file " + - "--root-file " + - "--root-file-2 " +tokens: + file: + - one + root_file: + - two + root_file_2: + - three + root_file_3: + - four diff --git a/spec/approvals/completions/advanced b/spec/approvals/completions/advanced index f6088b60..a1354bd1 100644 --- a/spec/approvals/completions/advanced +++ b/spec/approvals/completions/advanced @@ -1,38 +1,30 @@ --- -say: -- "--help" -- "--version" -- "-h" -- "-v" -- goodbye -- hello -say hello: -- "--help" -- "-h" -- world -say hello world: -- "--force" -- "--help" -- "--verbose" -- "-h" -- "" -- "" -say goodbye: -- "--help" -- "-h" -- universe -say goodbye universe: -- "$(git branch)" -- "--color" -- "--help" -- "--path" -- "--verbose" -- "-c" -- "-h" -- "-v" -say goodbye universe*--color: &1 -- green -- red -say goodbye universe*-c: *1 -say goodbye universe*--path: -- "" +patterns: +- say [root options] +- say hello [hello options] +- say hello world [hello_world options] +- say goodbye [goodbye options] +- say goodbye universe [goodbye_universe options] +options: + root: + - "--help|-h" + - "--version|-v" + hello: + - "--help|-h" + hello_world: + - "--help|-h" + - "--force" + - "--verbose" + goodbye: + - "--help|-h" + goodbye_universe: + - "--help|-h" + - "--color|-c " + - "--path " + - "--verbose|-v" +tokens: + color: + - green + - red + path: + - "+file" diff --git a/spec/approvals/completions/completion_global_flags b/spec/approvals/completions/completion_global_flags index e179a5b9..c1c1adec 100644 --- a/spec/approvals/completions/completion_global_flags +++ b/spec/approvals/completions/completion_global_flags @@ -1,20 +1,24 @@ --- -cli: -- "--debug" -- "--help" -- "--version" -- "-h" -- "-v" -- images -cli*images*: -- "--help" -- "--verbose" -- "-h" -- ls -cli*images*ls*: -- "--env" -- "--help" -- "-h" -cli*images*ls*--env: -- prod -- dev +patterns: +- cli [root options] +- cli images [root_global options] [images options] +- cli images ls [root_global options] [images_global options] [images_ls options] +options: + root: + - "--help|-h" + - "--version|-v" + - "--debug" + root_global: + - "--debug" + images: + - "--help|-h" + - "--verbose" + images_global: + - "--verbose" + images_ls: + - "--help|-h" + - "--env " +tokens: + env: + - prod + - dev diff --git a/spec/approvals/completions/completion_global_flags_nested b/spec/approvals/completions/completion_global_flags_nested index e4d6e910..4193717b 100644 --- a/spec/approvals/completions/completion_global_flags_nested +++ b/spec/approvals/completions/completion_global_flags_nested @@ -1,15 +1,16 @@ --- -cli: -- "--help" -- "--version" -- "-h" -- "-v" -- images -cli images: -- "--help" -- "--verbose" -- "-h" -- ls -cli images*ls*: -- "--help" -- "-h" +patterns: +- cli [root options] +- cli images [images options] +- cli images ls [images_global options] [images_ls options] +options: + root: + - "--help|-h" + - "--version|-v" + images: + - "--help|-h" + - "--verbose" + images_global: + - "--verbose" + images_ls: + - "--help|-h" diff --git a/spec/approvals/completions/completion_global_flags_root b/spec/approvals/completions/completion_global_flags_root index 597f9270..39d4a6a7 100644 --- a/spec/approvals/completions/completion_global_flags_root +++ b/spec/approvals/completions/completion_global_flags_root @@ -1,19 +1,21 @@ --- -cli: -- "--debug" -- "--help" -- "--version" -- "-h" -- "-v" -- images -cli*images*: -- "--help" -- "-h" -- ls -cli*images ls*: -- "--env" -- "--help" -- "-h" -cli*images ls*--env: -- prod -- dev +patterns: +- cli [root options] +- cli images [root_global options] [images options] +- cli images ls [root_global options] [images_ls options] +options: + root: + - "--help|-h" + - "--version|-v" + - "--debug" + root_global: + - "--debug" + images: + - "--help|-h" + images_ls: + - "--help|-h" + - "--env " +tokens: + env: + - prod + - dev diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index 7c865f25..12ad2692 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -5,57 +5,115 @@ custom_name() { echo $'# completely (https://github.com/bashly-framework/completely)' echo $'# Modifying it manually is not recommended' echo $'' - echo $'_get_completions_filter() {' - echo $' local words="$1"' + echo $'_get_completions_flag_expects_value() {' + echo $' case "$1" in' + echo $' esac' + echo $'' + echo $' return 1' + echo $'}' + echo $'' + echo $'_get_completions() {' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local result=()' + echo $' local prev=' + echo $' if ((COMP_CWORD > 0)); then' + echo $' prev=${COMP_WORDS[$((COMP_CWORD - 1))]}' + echo $' fi' echo $'' - echo $' # words the user already typed (excluding the command itself)' - echo $' local used=()' + echo $' local completed=()' echo $' if ((COMP_CWORD > 1)); then' - echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' # Completing an option: offer everything (including options)' - echo $' echo "$words"' + echo $' local non_options=()' + echo $' local completed_options=()' + echo $' local skip_next=0' + echo $' for word in "${completed[@]}"; do' + echo $' if ((skip_next)); then' + echo $' skip_next=0' + echo $' continue' + echo $' fi' echo $'' - echo $' else' - echo $' # Completing a non-option: offer only non-options,' - echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in $words; do' - echo $' [[ "${word:0:1}" == "-" ]] && continue' + echo $' if [[ "${word:0:1}" == "-" ]]; then' + echo $' completed_options+=("$word")' + echo $' if _get_completions_flag_expects_value "$word"; then' + echo $' skip_next=1' + echo $' fi' + echo $' continue' + echo $' fi' echo $'' - echo $' local seen=0' - echo $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$word" ]]; then' - echo $' seen=1' - echo $' break' - echo $' fi' - echo $' done' - echo $' ((!seen)) && result+=("$word")' - echo $' done' + echo $' non_options+=("$word")' + echo $' done' echo $'' - echo $' echo "${result[*]}"' + echo $' local route_id=' + echo $' local route_word_count=-1' + echo $' local route_has_positionals=0' + echo $' local positional_index=0' + echo $' if (( ${#non_options[@]} >= 0 )) &&' + echo $' (( 0 > route_word_count ))' + echo $' then' + echo $' route_id=0' + echo $' route_word_count=0' + echo $' route_has_positionals=0' + echo $' positional_index=$((${#non_options[@]} - 0))' echo $' fi' - echo $'}' echo $'' - echo $'_get_completions() {' - echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local compwords=()' - echo $' if ((COMP_CWORD > 0)); then' - echo $' compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' COMPREPLY=()' + echo $'' + echo $' if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "" -- "$cur")' + echo $' return' echo $' fi' - echo $' local compline="${compwords[*]}"' echo $'' - echo $' COMPREPLY=()' + echo $' case "$route_id:$prev" in' + echo $' esac' echo $'' - echo $' case "$compline" in' - echo $' *)' - echo $' compopt -o filenames 2>/dev/null' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_get_completions_filter "--force --help --verbose --version -h -v")" -- "$cur")' - echo $' ;;' + echo $' if [[ "${cur:0:1}" == "-" ]]; then' + echo $' case "$route_id" in' + echo $' 0)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --version|-v) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--version" "-v")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --force) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--force")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --verbose) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--verbose")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' esac' + echo $' fi' echo $'' + echo $' case "$route_id:$positional_index" in' echo $' esac' echo $'} &&' echo $' complete -F _get_completions get' diff --git a/spec/approvals/completions/nested_aliases b/spec/approvals/completions/nested_aliases index 070068a2..2d648cd3 100644 --- a/spec/approvals/completions/nested_aliases +++ b/spec/approvals/completions/nested_aliases @@ -1,280 +1,24 @@ --- -cli: -- "--help" -- "--version" -- "-h" -- "-v" -- a -- alpha -cli alpha: -- "--help" -- "-h" -- b -- beta -- bravo -cli a: -- "--help" -- "-h" -- b -- beta -- bravo -cli alpha bravo: -- "--help" -- "-h" -- c -- charlie -cli alpha b: -- "--help" -- "-h" -- c -- charlie -cli alpha beta: -- "--help" -- "-h" -- c -- charlie -cli a bravo: -- "--help" -- "-h" -- c -- charlie -cli a b: -- "--help" -- "-h" -- c -- charlie -cli a beta: -- "--help" -- "-h" -- c -- charlie -cli alpha bravo charlie: -- "--help" -- "-h" -- d -- delta -cli alpha bravo c: -- "--help" -- "-h" -- d -- delta -cli alpha b charlie: -- "--help" -- "-h" -- d -- delta -cli alpha b c: -- "--help" -- "-h" -- d -- delta -cli alpha beta charlie: -- "--help" -- "-h" -- d -- delta -cli alpha beta c: -- "--help" -- "-h" -- d -- delta -cli a bravo charlie: -- "--help" -- "-h" -- d -- delta -cli a bravo c: -- "--help" -- "-h" -- d -- delta -cli a b charlie: -- "--help" -- "-h" -- d -- delta -cli a b c: -- "--help" -- "-h" -- d -- delta -cli a beta charlie: -- "--help" -- "-h" -- d -- delta -cli a beta c: -- "--help" -- "-h" -- d -- delta -cli alpha bravo charlie delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha bravo charlie delta*--color: &1 -- green -- red -cli alpha bravo charlie delta*-c: *1 -cli alpha bravo charlie d: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha bravo charlie d*--color: *1 -cli alpha bravo charlie d*-c: *1 -cli alpha bravo c delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha bravo c delta*--color: *1 -cli alpha bravo c delta*-c: *1 -cli alpha bravo c d: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha bravo c d*--color: *1 -cli alpha bravo c d*-c: *1 -cli alpha b charlie delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha b charlie delta*--color: *1 -cli alpha b charlie delta*-c: *1 -cli alpha b charlie d: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha b charlie d*--color: *1 -cli alpha b charlie d*-c: *1 -cli alpha b c delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha b c delta*--color: *1 -cli alpha b c delta*-c: *1 -cli alpha b c d: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha b c d*--color: *1 -cli alpha b c d*-c: *1 -cli alpha beta charlie delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha beta charlie delta*--color: *1 -cli alpha beta charlie delta*-c: *1 -cli alpha beta charlie d: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha beta charlie d*--color: *1 -cli alpha beta charlie d*-c: *1 -cli alpha beta c delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha beta c delta*--color: *1 -cli alpha beta c delta*-c: *1 -cli alpha beta c d: -- "--color" -- "--help" -- "-c" -- "-h" -cli alpha beta c d*--color: *1 -cli alpha beta c d*-c: *1 -cli a bravo charlie delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli a bravo charlie delta*--color: *1 -cli a bravo charlie delta*-c: *1 -cli a bravo charlie d: -- "--color" -- "--help" -- "-c" -- "-h" -cli a bravo charlie d*--color: *1 -cli a bravo charlie d*-c: *1 -cli a bravo c delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli a bravo c delta*--color: *1 -cli a bravo c delta*-c: *1 -cli a bravo c d: -- "--color" -- "--help" -- "-c" -- "-h" -cli a bravo c d*--color: *1 -cli a bravo c d*-c: *1 -cli a b charlie delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli a b charlie delta*--color: *1 -cli a b charlie delta*-c: *1 -cli a b charlie d: -- "--color" -- "--help" -- "-c" -- "-h" -cli a b charlie d*--color: *1 -cli a b charlie d*-c: *1 -cli a b c delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli a b c delta*--color: *1 -cli a b c delta*-c: *1 -cli a b c d: -- "--color" -- "--help" -- "-c" -- "-h" -cli a b c d*--color: *1 -cli a b c d*-c: *1 -cli a beta charlie delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli a beta charlie delta*--color: *1 -cli a beta charlie delta*-c: *1 -cli a beta charlie d: -- "--color" -- "--help" -- "-c" -- "-h" -cli a beta charlie d*--color: *1 -cli a beta charlie d*-c: *1 -cli a beta c delta: -- "--color" -- "--help" -- "-c" -- "-h" -cli a beta c delta*--color: *1 -cli a beta c delta*-c: *1 -cli a beta c d: -- "--color" -- "--help" -- "-c" -- "-h" -cli a beta c d*--color: *1 -cli a beta c d*-c: *1 +patterns: +- cli [root options] +- cli alpha|a [alpha options] +- cli alpha|a bravo|b|beta [alpha_bravo options] +- cli alpha|a bravo|b|beta charlie|c [alpha_bravo_charlie options] +- cli alpha|a bravo|b|beta charlie|c delta|d [alpha_bravo_charlie_delta options] +options: + root: + - "--help|-h" + - "--version|-v" + alpha: + - "--help|-h" + alpha_bravo: + - "--help|-h" + alpha_bravo_charlie: + - "--help|-h" + alpha_bravo_charlie_delta: + - "--help|-h" + - "--color|-c " +tokens: + color: + - green + - red diff --git a/spec/approvals/completions/pattern_sources b/spec/approvals/completions/pattern_sources new file mode 100644 index 00000000..368ba23c --- /dev/null +++ b/spec/approvals/completions/pattern_sources @@ -0,0 +1,29 @@ +--- +patterns: +- cli [root options] +- cli upload|up [upload options] ... +- cli inspect [inspect options] +options: + root: + - "--help|-h" + - "--version|-v" + upload: + - "--help|-h" + - "--user|-u " + - "--tag " + - "--verbose (repeatable)" + inspect: + - "--help|-h" +tokens: + user: + - "+user" + tag: + source: + - "+directory" + - README.md + - "$(git branch)" + - "++literal" + target: + - "+file" + - README.md + object: diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index 92a91a34..4cd78d65 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -4,86 +4,250 @@ # completely (https://github.com/bashly-framework/completely) # Modifying it manually is not recommended -_say_completions_filter() { - local words="$1" - local cur=${COMP_WORDS[COMP_CWORD]} - local result=() - - # words the user already typed (excluding the command itself) - local used=() - if ((COMP_CWORD > 1)); then - used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") - fi - - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - echo "$words" - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in $words; do - [[ "${word:0:1}" == "-" ]] && continue - - local seen=0 - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - seen=1 - break - fi - done - ((!seen)) && result+=("$word") - done +_say_completions_flag_expects_value() { + case "$1" in + --color|-c) return 0 ;; + --path) return 0 ;; + esac - echo "${result[*]}" - fi + return 1 } _say_completions() { local cur=${COMP_WORDS[COMP_CWORD]} - local compwords=() + local prev= if ((COMP_CWORD > 0)); then - compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} fi - local compline="${compwords[*]}" - COMPREPLY=() + local completed=() + if ((COMP_CWORD > 1)); then + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + fi - case "$compline" in - 'goodbye universe'*'--color') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_say_completions_filter "green red")" -- "$cur") - ;; + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi + + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if _say_completions_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi + + non_options+=("$word") + done + + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 + if (( ${#non_options[@]} >= 0 )) && + (( 0 > route_word_count )) + then + route_id=0 + route_word_count=0 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 0)) + fi - 'goodbye universe'*'--path') - compopt -o filenames 2>/dev/null - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur") - ;; + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "hello" ]] + then + route_id=1 + route_word_count=1 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 1)) + fi - 'goodbye universe'*'-c') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_say_completions_filter "green red")" -- "$cur") - ;; + if (( ${#non_options[@]} >= 2 )) && + (( 2 > route_word_count )) && + [[ "${non_options[0]}" == "hello" ]] && + [[ "${non_options[1]}" == "world" ]] + then + route_id=2 + route_word_count=2 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 2)) + fi - 'goodbye universe'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_say_completions_filter "$(git branch) --color --help --path --verbose -c -h -v")" -- "$cur") - ;; + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "goodbye" ]] + then + route_id=3 + route_word_count=1 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 1)) + fi - 'hello world'*) - compopt -o filenames 2>/dev/null - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -A user -W "$(_say_completions_filter "--force --help --verbose -h")" -- "$cur") - ;; + if (( ${#non_options[@]} >= 2 )) && + (( 2 > route_word_count )) && + [[ "${non_options[0]}" == "goodbye" ]] && + [[ "${non_options[1]}" == "universe" ]] + then + route_id=4 + route_word_count=2 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 2)) + fi - 'goodbye'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_say_completions_filter "--help -h universe")" -- "$cur") - ;; + COMPREPLY=() - 'hello'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_say_completions_filter "--help -h world")" -- "$cur") - ;; + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "hello goodbye" -- "$cur") + return + fi - *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_say_completions_filter "--help --version -h -v goodbye hello")" -- "$cur") + case "$route_id:$prev" in + 4:--color|4:-c) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "green red" -- "$cur") + return ;; + 4:--path) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur") + return + ;; + esac + + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in + 0) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --version|-v) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--version" "-v") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 1) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 2) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --force) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--force") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --verbose) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--verbose") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 3) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 4) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --color|-c) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--color" "-c") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --path) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--path") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --verbose|-v) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--verbose" "-v") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + esac + fi + case "$route_id:$positional_index" in esac } && complete -F _say_completions say diff --git a/spec/approvals/completions/simple b/spec/approvals/completions/simple index d677f376..8d4b71d8 100644 --- a/spec/approvals/completions/simple +++ b/spec/approvals/completions/simple @@ -1,9 +1,9 @@ --- -get: -- "--force" -- "--help" -- "--verbose" -- "--version" -- "-h" -- "-v" -- "" +patterns: +- get [root options] +options: + root: + - "--help|-h" + - "--version|-v" + - "--force" + - "--verbose" diff --git a/spec/approvals/completions/whitelist b/spec/approvals/completions/whitelist index cb31fa46..68346787 100644 --- a/spec/approvals/completions/whitelist +++ b/spec/approvals/completions/whitelist @@ -1,19 +1,23 @@ --- -download: -- "--help" -- "--method" -- "--role" -- "--version" -- "-h" -- "-v" -- '22' -- '3000' -- '80' -- https -- ssh -download*--role: -- user -- admin -download*--method: -- get -- post +patterns: +- download [root options] +options: + root: + - "--help|-h" + - "--version|-v" + - "--role " + - "--method " +tokens: + name: + - user + - admin + root_name: + - get + - post + protocol: + - https + - ssh + port: + - '80' + - '22' + - '3000' diff --git a/spec/approvals/examples/completions b/spec/approvals/examples/completions index 9dd2f329..eb62a1d2 100644 --- a/spec/approvals/examples/completions +++ b/spec/approvals/examples/completions @@ -68,104 +68,231 @@ Options: # completely (https://github.com/bashly-framework/completely) # Modifying it manually is not recommended -_cli_completions_filter() { - local words="$1" - local cur=${COMP_WORDS[COMP_CWORD]} - local result=() - - # words the user already typed (excluding the command itself) - local used=() - if ((COMP_CWORD > 1)); then - used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") - fi - - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - echo "$words" - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in $words; do - [[ "${word:0:1}" == "-" ]] && continue - - local seen=0 - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - seen=1 - break - fi - done - ((!seen)) && result+=("$word") - done +_cli_completions_flag_expects_value() { + case "$1" in + --handler) return 0 ;; + --user|-u) return 0 ;; + --password|-p) return 0 ;; + esac - echo "${result[*]}" - fi + return 1 } _cli_completions() { local cur=${COMP_WORDS[COMP_CWORD]} - local compwords=() + local prev= if ((COMP_CWORD > 0)); then - compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} fi - local compline="${compwords[*]}" - COMPREPLY=() + local completed=() + if ((COMP_CWORD > 1)); then + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + fi - case "$compline" in - 'download'*'--handler') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "curl wget")" -- "$cur") - ;; + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi + + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if _cli_completions_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi + + non_options+=("$word") + done + + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 + if (( ${#non_options[@]} >= 0 )) && + (( 0 > route_word_count )) + then + route_id=0 + route_word_count=0 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 0)) + fi - 'upload'*'--user') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "completions" ]] + then + route_id=1 + route_word_count=1 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 1)) + fi - 'completions'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help -h")" -- "$cur") - ;; + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "download" || "${non_options[0]}" == "d" ]] + then + route_id=2 + route_word_count=1 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 1)) + fi - 'd'*'--handler') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "curl wget")" -- "$cur") - ;; + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "upload" || "${non_options[0]}" == "u" ]] + then + route_id=3 + route_word_count=1 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 1)) + fi - 'upload'*'-u') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; + COMPREPLY=() - 'download'*) - compopt -o filenames 2>/dev/null - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_cli_completions_filter "--force --handler --help -f -h")" -- "$cur") - ;; + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "completions download d upload u" -- "$cur") + return + fi - 'u'*'--user') + case "$route_id:$prev" in + 2:--handler) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "curl wget" -- "$cur") + return + ;; + 3:--user|3:-u) while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") + return ;; - - 'upload'*) - compopt -o filenames 2>/dev/null - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -A user -W "$(_cli_completions_filter "--help --password --user -h -p -u CHANGELOG.md README.md")" -- "$cur") + 3:--password|3:-p) + return ;; + esac - 'u'*'-u') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in + 0) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --version|-v) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--version" "-v") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 1) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 2) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --force|-f) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--force" "-f") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --handler) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--handler") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 3) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --user|-u) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--user" "-u") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --password|-p) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--password" "-p") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + esac + fi - 'd'*) - compopt -o filenames 2>/dev/null - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_cli_completions_filter "--force --handler --help -f -h")" -- "$cur") + case "$route_id:$positional_index" in + 2:0) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur") + return ;; - - 'u'*) - compopt -o filenames 2>/dev/null - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -A user -W "$(_cli_completions_filter "--help --password --user -h -p -u CHANGELOG.md README.md")" -- "$cur") + 2:1) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur") + return ;; - - *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --version -h -v completions d download u upload")" -- "$cur") + 3:0) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "README.md CHANGELOG.md" -- "$cur") + return ;; - esac } && complete -F _cli_completions cli diff --git a/spec/approvals/fixtures/completions-private b/spec/approvals/fixtures/completions-private index 72d2525d..8a4eb737 100644 --- a/spec/approvals/fixtures/completions-private +++ b/spec/approvals/fixtures/completions-private @@ -5,20 +5,13 @@ This file can be converted to a completions script using the completely gem. + cat completions.yml --- -private: -- "--help" -- "--version" -- "-h" -- "-v" -- c -- connect -private connect: -- "--force" -- "--help" -- "-f" -- "-h" -private c: -- "--force" -- "--help" -- "-f" -- "-h" +patterns: +- private [root options] +- private connect|c [connect options] +options: + root: + - "--help|-h" + - "--version|-v" + connect: + - "--help|-h" + - "--force|-f" diff --git a/spec/approvals/libraries/completions_function/files b/spec/approvals/libraries/completions_function/files index 8964f90c..b15a6075 100644 --- a/spec/approvals/libraries/completions_function/files +++ b/spec/approvals/libraries/completions_function/files @@ -9,56 +9,112 @@ echo $'# completely (https://github.com/bashly-framework/completely)' echo $'# Modifying it manually is not recommended' echo $'' - echo $'_download_completions_filter() {' - echo $' local words="$1"' + echo $'_download_completions_flag_expects_value() {' + echo $' case "$1" in' + echo $' esac' + echo $'' + echo $' return 1' + echo $'}' + echo $'' + echo $'_download_completions() {' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local result=()' + echo $' local prev=' + echo $' if ((COMP_CWORD > 0)); then' + echo $' prev=${COMP_WORDS[$((COMP_CWORD - 1))]}' + echo $' fi' echo $'' - echo $' # words the user already typed (excluding the command itself)' - echo $' local used=()' + echo $' local completed=()' echo $' if ((COMP_CWORD > 1)); then' - echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' # Completing an option: offer everything (including options)' - echo $' echo "$words"' + echo $' local non_options=()' + echo $' local completed_options=()' + echo $' local skip_next=0' + echo $' for word in "${completed[@]}"; do' + echo $' if ((skip_next)); then' + echo $' skip_next=0' + echo $' continue' + echo $' fi' echo $'' - echo $' else' - echo $' # Completing a non-option: offer only non-options,' - echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in $words; do' - echo $' [[ "${word:0:1}" == "-" ]] && continue' + echo $' if [[ "${word:0:1}" == "-" ]]; then' + echo $' completed_options+=("$word")' + echo $' if _download_completions_flag_expects_value "$word"; then' + echo $' skip_next=1' + echo $' fi' + echo $' continue' + echo $' fi' echo $'' - echo $' local seen=0' - echo $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$word" ]]; then' - echo $' seen=1' - echo $' break' - echo $' fi' - echo $' done' - echo $' ((!seen)) && result+=("$word")' - echo $' done' + echo $' non_options+=("$word")' + echo $' done' echo $'' - echo $' echo "${result[*]}"' + echo $' local route_id=' + echo $' local route_word_count=-1' + echo $' local route_has_positionals=0' + echo $' local positional_index=0' + echo $' if (( ${#non_options[@]} >= 0 )) &&' + echo $' (( 0 > route_word_count ))' + echo $' then' + echo $' route_id=0' + echo $' route_word_count=0' + echo $' route_has_positionals=1' + echo $' positional_index=$((${#non_options[@]} - 0))' echo $' fi' - echo $'}' echo $'' - echo $'_download_completions() {' - echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local compwords=()' - echo $' if ((COMP_CWORD > 0)); then' - echo $' compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' + echo $' COMPREPLY=()' + echo $'' + echo $' if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "" -- "$cur")' + echo $' return' echo $' fi' - echo $' local compline="${compwords[*]}"' echo $'' - echo $' COMPREPLY=()' + echo $' case "$route_id:$prev" in' + echo $' esac' echo $'' - echo $' case "$compline" in' - echo $' *)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_download_completions_filter "--force --help --version -f -h -v")" -- "$cur")' - echo $' ;;' + echo $' if [[ "${cur:0:1}" == "-" ]]; then' + echo $' case "$route_id" in' + echo $' 0)' + echo $' local words=()' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --help|-h) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--help" "-h")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --version|-v) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--version" "-v")' + echo $' fi' + echo $' local option_seen=0' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' case "$completed_option" in' + echo $' --force|-f) option_seen=1 ;;' + echo $' esac' + echo $' done' + echo $' if ((!option_seen)); then' + echo $' words+=("--force" "-f")' + echo $' fi' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' esac' + echo $' fi' echo $'' + echo $' case "$route_id:$positional_index" in' + echo $' 0:0)' + echo $' return' + echo $' ;;' + echo $' 0:1)' + echo $' return' + echo $' ;;' echo $' esac' echo $'} &&' echo $' complete -F _download_completions download' diff --git a/spec/approvals/libraries/completions_script/files b/spec/approvals/libraries/completions_script/files index 518775c7..8484946f 100644 --- a/spec/approvals/libraries/completions_script/files +++ b/spec/approvals/libraries/completions_script/files @@ -7,56 +7,112 @@ # completely (https://github.com/bashly-framework/completely) # Modifying it manually is not recommended - _download_completions_filter() { - local words="$1" + _download_completions_flag_expects_value() { + case "$1" in + esac + + return 1 + } + + _download_completions() { local cur=${COMP_WORDS[COMP_CWORD]} - local result=() + local prev= + if ((COMP_CWORD > 0)); then + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} + fi - # words the user already typed (excluding the command itself) - local used=() + local completed=() if ((COMP_CWORD > 1)); then - used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - echo "$words" + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in $words; do - [[ "${word:0:1}" == "-" ]] && continue + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if _download_completions_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi - local seen=0 - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - seen=1 - break - fi - done - ((!seen)) && result+=("$word") - done + non_options+=("$word") + done - echo "${result[*]}" + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 + if (( ${#non_options[@]} >= 0 )) && + (( 0 > route_word_count )) + then + route_id=0 + route_word_count=0 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 0)) fi - } - _download_completions() { - local cur=${COMP_WORDS[COMP_CWORD]} - local compwords=() - if ((COMP_CWORD > 0)); then - compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + COMPREPLY=() + + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "" -- "$cur") + return fi - local compline="${compwords[*]}" - COMPREPLY=() + case "$route_id:$prev" in + esac - case "$compline" in - *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_download_completions_filter "--force --help --version -f -h -v")" -- "$cur") - ;; + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in + 0) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --help|-h) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--help" "-h") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --version|-v) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--version" "-v") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --force|-f) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--force" "-f") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + esac + fi + case "$route_id:$positional_index" in + 0:0) + return + ;; + 0:1) + return + ;; esac } && complete -F _download_completions download diff --git a/spec/approvals/libraries/completions_yaml/files b/spec/approvals/libraries/completions_yaml/files index b3683818..7dfe9b27 100644 --- a/spec/approvals/libraries/completions_yaml/files +++ b/spec/approvals/libraries/completions_yaml/files @@ -2,10 +2,13 @@ - :path: spec/tmp/completions.yml :content: | --- - download: - - "--force" - - "--help" - - "--version" - - "-f" - - "-h" - - "-v" + patterns: + - download [root options] + options: + root: + - "--help|-h" + - "--version|-v" + - "--force|-f" + tokens: + source: + target: diff --git a/spec/approvals/validations/arg_allowed_and_completions b/spec/approvals/validations/arg_allowed_and_completions new file mode 100644 index 00000000..4aad4e37 --- /dev/null +++ b/spec/approvals/validations/arg_allowed_and_completions @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/bashly/completion_builder_spec.rb b/spec/bashly/completion_builder_spec.rb new file mode 100644 index 00000000..6e53bed7 --- /dev/null +++ b/spec/bashly/completion_builder_spec.rb @@ -0,0 +1,19 @@ +describe CompletionBuilder do + fixtures = load_fixture('completion_builder') + + describe '#call' do + fixtures.each do |fixture, options| + context "with :#{fixture}" do + let(:command) { Script::Command.new options['command'] } + let(:builder) do + described_class.new command, with_version: options.fetch('with_version', true) + end + + it 'returns pattern config data' do + expect(builder.call.to_yaml) + .to match_approval("completion_builder/#{fixture}") + end + end + end + end +end diff --git a/spec/bashly/concerns/completions_command_spec.rb b/spec/bashly/concerns/completions_command_spec.rb index 4b3cefa3..17a15a78 100644 --- a/spec/bashly/concerns/completions_command_spec.rb +++ b/spec/bashly/concerns/completions_command_spec.rb @@ -46,6 +46,17 @@ end end + context 'with a command that uses pattern completion sources' do + let(:fixture) { :completions_pattern_sources } + + describe '#completion_data' do + it 'returns pattern config data with tokens and options' do + expect(subject.completion_data.to_yaml) + .to match_approval('completions/pattern_sources') + end + end + end + context 'with a command that has nested command aliases' do let(:fixture) { :nested_aliases } diff --git a/spec/bashly/integration/examples_spec.rb b/spec/bashly/integration/examples_spec.rb index 94874a6a..e5fe4201 100644 --- a/spec/bashly/integration/examples_spec.rb +++ b/spec/bashly/integration/examples_spec.rb @@ -29,7 +29,7 @@ { 'examples/stacktrace' => [/download:\d+/, 'download:'], 'examples/render-mandoc' => [/Version 0.1.0.*download\(1\)/, '