From e996014f8affe028504b70a6224ab007afd50f32 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 18 Aug 2026 21:48:25 +0300 Subject: [PATCH 01/23] - Refactor completions as native runtime functions --- lib/bashly/views/argument/completion.gtx | 8 ++ .../completion_argument_candidates.gtx | 11 ++ .../command/completion_argument_filter.gtx | 13 ++ .../command/completion_command_candidates.gtx | 3 + .../command/completion_flag_candidates.gtx | 13 ++ .../views/command/completion_function.gtx | 15 ++ .../views/command/completion_word_filter.gtx | 33 +++++ lib/bashly/views/command/completions.gtx | 42 ++++++ lib/bashly/views/command/master_script.gtx | 1 + lib/bashly/views/command/start.gtx | 8 +- lib/bashly/views/flag/completion.gtx | 1 + lib/bashly/views/flag/completion_filter.gtx | 7 + .../views/flag/completion_filter_arg.gtx | 7 + .../views/flag/completion_filter_block.gtx | 8 ++ .../views/flag/completion_filter_no_arg.gtx | 2 + .../flag/completion_value_candidates.gtx | 8 ++ .../integration/runtime_completions_spec.rb | 32 +++++ spec/fixtures/completions/core/examples.yml | 133 ++++++++++++++++++ spec/fixtures/completions/core/src/bashly.yml | 81 +++++++++++ 19 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 lib/bashly/views/argument/completion.gtx create mode 100644 lib/bashly/views/command/completion_argument_candidates.gtx create mode 100644 lib/bashly/views/command/completion_argument_filter.gtx create mode 100644 lib/bashly/views/command/completion_command_candidates.gtx create mode 100644 lib/bashly/views/command/completion_flag_candidates.gtx create mode 100644 lib/bashly/views/command/completion_function.gtx create mode 100644 lib/bashly/views/command/completion_word_filter.gtx create mode 100644 lib/bashly/views/command/completions.gtx create mode 100644 lib/bashly/views/flag/completion.gtx create mode 100644 lib/bashly/views/flag/completion_filter.gtx create mode 100644 lib/bashly/views/flag/completion_filter_arg.gtx create mode 100644 lib/bashly/views/flag/completion_filter_block.gtx create mode 100644 lib/bashly/views/flag/completion_filter_no_arg.gtx create mode 100644 lib/bashly/views/flag/completion_value_candidates.gtx create mode 100644 spec/bashly/integration/runtime_completions_spec.rb create mode 100644 spec/fixtures/completions/core/examples.yml create mode 100644 spec/fixtures/completions/core/src/bashly.yml diff --git a/lib/bashly/views/argument/completion.gtx b/lib/bashly/views/argument/completion.gtx new file mode 100644 index 00000000..110523b2 --- /dev/null +++ b/lib/bashly/views/argument/completion.gtx @@ -0,0 +1,8 @@ +if allowed + > local -a completion_values=( + allowed.each do |value| + > "{{ value }}" + end + > ) + > completion_candidates "$completion_current" "${completion_values[@]}" +end diff --git a/lib/bashly/views/command/completion_argument_candidates.gtx b/lib/bashly/views/command/completion_argument_candidates.gtx new file mode 100644 index 00000000..f6be8ac2 --- /dev/null +++ b/lib/bashly/views/command/completion_argument_candidates.gtx @@ -0,0 +1,11 @@ +if args.any? + > if [[ $completion_current != -* ]]; then + > case "$completion_arg_index" in + args.each_with_index do |arg, index| + > {{ index }}) + = arg.render(:completion).indent 6 + > ;; + end + > esac + > fi +end diff --git a/lib/bashly/views/command/completion_argument_filter.gtx b/lib/bashly/views/command/completion_argument_filter.gtx new file mode 100644 index 00000000..cae1223b --- /dev/null +++ b/lib/bashly/views/command/completion_argument_filter.gtx @@ -0,0 +1,13 @@ +if args.last.repeatable + last_arg_index = args.length - 1 + > if [[ $completion_arg_index -lt {{ last_arg_index }} ]]; then + > completion_arg_index=$((completion_arg_index + 1)) + > fi + > shift +else + > if [[ $completion_arg_index -ge {{ args.length }} ]]; then + > return + > fi + > completion_arg_index=$((completion_arg_index + 1)) + > shift +end diff --git a/lib/bashly/views/command/completion_command_candidates.gtx b/lib/bashly/views/command/completion_command_candidates.gtx new file mode 100644 index 00000000..e958eee4 --- /dev/null +++ b/lib/bashly/views/command/completion_command_candidates.gtx @@ -0,0 +1,3 @@ +if public_command_aliases.any? + > completion_candidates "$completion_current" {{ public_command_aliases.join ' ' }} +end diff --git a/lib/bashly/views/command/completion_flag_candidates.gtx b/lib/bashly/views/command/completion_flag_candidates.gtx new file mode 100644 index 00000000..34850729 --- /dev/null +++ b/lib/bashly/views/command/completion_flag_candidates.gtx @@ -0,0 +1,13 @@ +if fixed_flags? || public_flags.any? + > if [[ $completion_current == -* ]]; then + if fixed_flags? + > completion_flag_candidates --help "$completion_current" --help -h + if root_command? + > completion_flag_candidates --version "$completion_current" --version -v + end + end + public_flags.each do |flag| + = flag.render(:completion).indent 2 + end + > fi +end diff --git a/lib/bashly/views/command/completion_function.gtx b/lib/bashly/views/command/completion_function.gtx new file mode 100644 index 00000000..90524ee0 --- /dev/null +++ b/lib/bashly/views/command/completion_function.gtx @@ -0,0 +1,15 @@ += view_marker + +> {{ function_name }}_completion() { +> local completion_current="$1" +if args.any? + > local completion_arg_index=0 +end +> shift +> += render(:completion_word_filter).indent 2 += render(:completion_command_candidates).indent 2 += render(:completion_flag_candidates).indent 2 += render(:completion_argument_candidates).indent 2 +> } +> diff --git a/lib/bashly/views/command/completion_word_filter.gtx b/lib/bashly/views/command/completion_word_filter.gtx new file mode 100644 index 00000000..7411c929 --- /dev/null +++ b/lib/bashly/views/command/completion_word_filter.gtx @@ -0,0 +1,33 @@ +> while [[ $# -gt 0 ]]; do +> case "$1" in +public_commands.each do |command| + > {{ command.aliases.join ' | ' }}) + > shift + > {{ command.function_name }}_completion "$completion_current" "$@" + > return + > ;; +end +if fixed_flags? + > --help | -h) + > completion_blocked_flags["--help"]=1 + > shift + > ;; + if root_command? + > --version | -v) + > completion_blocked_flags["--version"]=1 + > shift + > ;; + end +end +public_flags.each do |flag| + = flag.render(:completion_filter).indent 4 +end +> *) +if args.any? + = render(:completion_argument_filter).indent 6 +else + > return +end +> ;; +> esac +> done diff --git a/lib/bashly/views/command/completions.gtx b/lib/bashly/views/command/completions.gtx new file mode 100644 index 00000000..e87e4e15 --- /dev/null +++ b/lib/bashly/views/command/completions.gtx @@ -0,0 +1,42 @@ += view_marker + +> completion_run() { +> local -a completion_words=("$@") +> local -A completion_blocked_flags=() +> local completion_count=${#completion_words[@]} +> local completion_current="" +> +> if [[ $completion_count -gt 0 ]]; then +> completion_current="${completion_words[$((completion_count - 1))]}" +> unset 'completion_words[completion_count - 1]' +> fi +> +> {{ function_name }}_completion "$completion_current" "${completion_words[@]}" +> } +> +> completion_candidates() { +> local completion_current="$1" +> shift +> +> local completion_candidate +> for completion_candidate in "$@"; do +> if [[ $completion_candidate == "$completion_current"* ]]; then +> printf '%s\n' "$completion_candidate" +> fi +> done +> } +> +> completion_flag_candidates() { +> local completion_flag="$1" +> local completion_current="$2" +> shift 2 +> +> if [[ -z "${completion_blocked_flags[$completion_flag]:-}" ]]; then +> completion_candidates "$completion_current" "$@" +> fi +> } +> += render :completion_function +deep_commands.each do |command| + = command.render :completion_function +end diff --git a/lib/bashly/views/command/master_script.gtx b/lib/bashly/views/command/master_script.gtx index 0b5eccd4..7b1419b1 100644 --- a/lib/bashly/views/command/master_script.gtx +++ b/lib/bashly/views/command/master_script.gtx @@ -8,6 +8,7 @@ = render :user_lib if user_lib.any? = render :command_functions = render :parse_requirements += render :completions = render :user_hooks = render :initialize = render :run diff --git a/lib/bashly/views/command/start.gtx b/lib/bashly/views/command/start.gtx index 4991ea7b..edd9dbfa 100644 --- a/lib/bashly/views/command/start.gtx +++ b/lib/bashly/views/command/start.gtx @@ -1,5 +1,9 @@ = view_marker > command_line_args=("$@") -> {{ Settings.function_name :initialize }} -> {{ Settings.function_name :run }} "${command_line_args[@]}" +> if [[ "${command_line_args[0]:-}" == "__complete" ]]; then +> completion_run "${command_line_args[@]:1}" +> else +> {{ Settings.function_name :initialize }} +> {{ Settings.function_name :run }} "${command_line_args[@]}" +> fi diff --git a/lib/bashly/views/flag/completion.gtx b/lib/bashly/views/flag/completion.gtx new file mode 100644 index 00000000..fbd68d07 --- /dev/null +++ b/lib/bashly/views/flag/completion.gtx @@ -0,0 +1 @@ +> completion_flag_candidates {{ name }} "$completion_current" {{ aliases.join ' ' }} diff --git a/lib/bashly/views/flag/completion_filter.gtx b/lib/bashly/views/flag/completion_filter.gtx new file mode 100644 index 00000000..b0877554 --- /dev/null +++ b/lib/bashly/views/flag/completion_filter.gtx @@ -0,0 +1,7 @@ +> {{ aliases.join ' | ' }}) +if arg + = render(:completion_filter_arg).indent 2 +else + = render(:completion_filter_no_arg).indent 2 +end +> ;; diff --git a/lib/bashly/views/flag/completion_filter_arg.gtx b/lib/bashly/views/flag/completion_filter_arg.gtx new file mode 100644 index 00000000..c1b1560a --- /dev/null +++ b/lib/bashly/views/flag/completion_filter_arg.gtx @@ -0,0 +1,7 @@ +> shift +> if [[ $# -eq 0 ]]; then += render(:completion_value_candidates).indent 2 +> return +> fi +> shift += render :completion_filter_block diff --git a/lib/bashly/views/flag/completion_filter_block.gtx b/lib/bashly/views/flag/completion_filter_block.gtx new file mode 100644 index 00000000..2bd38632 --- /dev/null +++ b/lib/bashly/views/flag/completion_filter_block.gtx @@ -0,0 +1,8 @@ +unless repeatable + > completion_blocked_flags["{{ name }}"]=1 +end +if conflicts + conflicts.each do |conflict| + > completion_blocked_flags["{{ conflict }}"]=1 + end +end diff --git a/lib/bashly/views/flag/completion_filter_no_arg.gtx b/lib/bashly/views/flag/completion_filter_no_arg.gtx new file mode 100644 index 00000000..5b41d8a7 --- /dev/null +++ b/lib/bashly/views/flag/completion_filter_no_arg.gtx @@ -0,0 +1,2 @@ += render :completion_filter_block +> shift diff --git a/lib/bashly/views/flag/completion_value_candidates.gtx b/lib/bashly/views/flag/completion_value_candidates.gtx new file mode 100644 index 00000000..110523b2 --- /dev/null +++ b/lib/bashly/views/flag/completion_value_candidates.gtx @@ -0,0 +1,8 @@ +if allowed + > local -a completion_values=( + allowed.each do |value| + > "{{ value }}" + end + > ) + > completion_candidates "$completion_current" "${completion_values[@]}" +end diff --git a/spec/bashly/integration/runtime_completions_spec.rb b/spec/bashly/integration/runtime_completions_spec.rb new file mode 100644 index 00000000..f4a94384 --- /dev/null +++ b/spec/bashly/integration/runtime_completions_spec.rb @@ -0,0 +1,32 @@ +require 'open3' + +describe 'runtime completions', :slow do + workspaces = Dir['spec/fixtures/completions/*'].select { |path| File.directory? path } + + workspaces.each do |workspace| + context File.basename(workspace) do + examples = YAML.trusted_load_file "#{workspace}/examples.yml" + cli = File.expand_path 'spec/tmp/cli' + + before(:context) do + reset_tmp_dir + FileUtils.cp_r Dir["#{workspace}/*"], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + end + + examples.each do |name, example| + describe name do + it 'works' do + stdout, stderr, status = Open3.capture3( + cli, '__complete', *example['words'] + ) + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to eq example['expected'] + end + end + end + end + end +end diff --git a/spec/fixtures/completions/core/examples.yml b/spec/fixtures/completions/core/examples.yml new file mode 100644 index 00000000..b8ad58f2 --- /dev/null +++ b/spec/fixtures/completions/core/examples.yml @@ -0,0 +1,133 @@ +root commands: + words: [''] + expected: [server, s, config, cfg, deploy, convert] + +root command prefix: + words: [s] + expected: [server, s] + +root flags: + words: ['--'] + expected: [--help, --version] + +nested commands: + words: [server, ''] + expected: [start, up, stop, down, status] + +nested command prefix: + words: [server, st] + expected: [start, stop, status] + +command alias navigation: + words: [s, st] + expected: [start, stop, status] + +flag names: + words: [server, start, '--'] + expected: + - --help + - --environment + - --env + - --force + - --dry-run + - --verbose + - --color + - --no-color + +short flag names: + words: [server, start, '-'] + expected: + - --help + - -h + - --environment + - -e + - --env + - --force + - -f + - --dry-run + - -n + - --verbose + - -V + - --color + - --no-color + - -c + +flag prefix: + words: [server, start, --c] + expected: [--color] + +flag allowed values: + words: [server, start, --environment, ''] + expected: [development, staging, production] + +short flag allowed values: + words: [server, start, -e, pro] + expected: [production] + +alternate flag allowed values: + words: [server, start, --env, st] + expected: [staging] + +completed flag with value is blocked: + words: [server, start, --environment, production, '--'] + expected: [--help, --force, --dry-run, --verbose, --color, --no-color] + +non-repeatable flag is blocked: + words: [server, start, --force, '--'] + expected: [--help, --environment, --env, --verbose, --color, --no-color] + +conflicting flag is blocked: + words: [server, start, --dry-run, '--'] + expected: [--help, --environment, --env, --verbose, --color, --no-color] + +repeatable flag remains: + words: [server, start, --verbose, '--'] + expected: + - --help + - --environment + - --env + - --force + - --dry-run + - --verbose + - --color + - --no-color + +negated flag blocks every spelling: + words: [server, start, --no-color, '--'] + expected: [--help, --environment, --env, --force, --dry-run, --verbose] + +first positional argument: + words: [deploy, ''] + expected: [development, staging, production] + +positional argument prefix: + words: [deploy, st] + expected: [staging] + +second positional argument: + words: [deploy, staging, ''] + expected: [api, worker, web] + +repeatable positional argument: + words: [deploy, staging, api, blue, ''] + expected: [blue, green, canary] + +repeatable positional argument prefix: + words: [deploy, staging, api, blue, green, c] + expected: [canary] + +all positional arguments consumed: + words: [convert, one, three, ''] + expected: [] + +private command is not reachable: + words: [internal, ''] + expected: [] + +unknown command is not reachable: + words: [missing, ''] + expected: [] + +unknown prefix returns nothing: + words: [xyz] + expected: [] diff --git a/spec/fixtures/completions/core/src/bashly.yml b/spec/fixtures/completions/core/src/bashly.yml new file mode 100644 index 00000000..a43ba114 --- /dev/null +++ b/spec/fixtures/completions/core/src/bashly.yml @@ -0,0 +1,81 @@ +name: cli +help: Runtime completion fixture +version: 0.1.0 + +commands: +- name: server + alias: s + help: Manage servers + commands: + - name: start + alias: up + help: Start a server + flags: + - long: --environment + short: -e + alias: --env + arg: environment + allowed: [development, staging, production] + help: Select an environment + - long: --force + short: -f + help: Force the server to start + conflicts: [--dry-run] + - long: --dry-run + short: -n + help: Show what would happen + conflicts: [--force] + - long: --verbose + short: -V + repeatable: true + help: Increase verbosity + - long: --color + short: -c + negatable: true + help: Enable color output + - name: stop + alias: down + help: Stop a server + - name: status + help: Show server status + +- name: config + alias: cfg + help: Manage configuration + commands: + - name: get + help: Read a configuration value + - name: set + help: Set a configuration value + +- name: deploy + help: Deploy a component + args: + - name: environment + allowed: [development, staging, production] + help: Environment to deploy to + required: true + - name: component + allowed: [api, worker, web] + help: Component to deploy + required: true + - name: label + allowed: [blue, green, canary] + help: Labels to apply + repeatable: true + +- name: convert + help: Convert a document + args: + - name: source + allowed: [one, two] + help: Source document + required: true + - name: target + allowed: [three, four] + help: Target document + required: true + +- name: internal + help: Internal command that must not be suggested + private: true From 88699ab771fa86f77f18e1c31bcb8da1ff59c862 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 18 Aug 2026 22:04:18 +0300 Subject: [PATCH 02/23] implement default command completions --- .../views/command/completion_function.gtx | 3 +++ .../views/command/completion_word_filter.gtx | 3 +++ lib/bashly/views/command/completions.gtx | 5 ++++- .../completions/default-command/examples.yml | 19 ++++++++++++++++++ .../default-command/src/bashly.yml | 20 +++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/completions/default-command/examples.yml create mode 100644 spec/fixtures/completions/default-command/src/bashly.yml diff --git a/lib/bashly/views/command/completion_function.gtx b/lib/bashly/views/command/completion_function.gtx index 90524ee0..7ad21c21 100644 --- a/lib/bashly/views/command/completion_function.gtx +++ b/lib/bashly/views/command/completion_function.gtx @@ -9,6 +9,9 @@ end > = render(:completion_word_filter).indent 2 = render(:completion_command_candidates).indent 2 +if default_command + > {{ default_command.function_name }}_completion "$completion_current" +end = render(:completion_flag_candidates).indent 2 = render(:completion_argument_candidates).indent 2 > } diff --git a/lib/bashly/views/command/completion_word_filter.gtx b/lib/bashly/views/command/completion_word_filter.gtx index 7411c929..afc92c38 100644 --- a/lib/bashly/views/command/completion_word_filter.gtx +++ b/lib/bashly/views/command/completion_word_filter.gtx @@ -25,6 +25,9 @@ end > *) if args.any? = render(:completion_argument_filter).indent 6 +elsif default_command + > {{ default_command.function_name }}_completion "$completion_current" "$@" + > return else > return end diff --git a/lib/bashly/views/command/completions.gtx b/lib/bashly/views/command/completions.gtx index e87e4e15..142acf8a 100644 --- a/lib/bashly/views/command/completions.gtx +++ b/lib/bashly/views/command/completions.gtx @@ -3,6 +3,7 @@ > completion_run() { > local -a completion_words=("$@") > local -A completion_blocked_flags=() +> local -A completion_emitted=() > local completion_count=${#completion_words[@]} > local completion_current="" > @@ -20,8 +21,10 @@ > > local completion_candidate > for completion_candidate in "$@"; do -> if [[ $completion_candidate == "$completion_current"* ]]; then +> if [[ $completion_candidate == "$completion_current"* ]] && +> [[ -z "${completion_emitted[$completion_candidate]:-}" ]]; then > printf '%s\n' "$completion_candidate" +> completion_emitted["$completion_candidate"]=1 > fi > done > } diff --git a/spec/fixtures/completions/default-command/examples.yml b/spec/fixtures/completions/default-command/examples.yml new file mode 100644 index 00000000..5ca695ec --- /dev/null +++ b/spec/fixtures/completions/default-command/examples.yml @@ -0,0 +1,19 @@ +parent and default command candidates: + words: [''] + expected: [run, inspect, container, image] + +parent and default command prefix: + words: [i] + expected: [inspect, image] + +explicit default command: + words: [inspect, ''] + expected: [container, image] + +implicit default command flag: + words: [--format, ''] + expected: [json, yaml] + +implicit default command argument: + words: [container, ''] + expected: [] diff --git a/spec/fixtures/completions/default-command/src/bashly.yml b/spec/fixtures/completions/default-command/src/bashly.yml new file mode 100644 index 00000000..161a544d --- /dev/null +++ b/spec/fixtures/completions/default-command/src/bashly.yml @@ -0,0 +1,20 @@ +name: cli +help: Default command completion fixture + +commands: +- name: run + help: Run a target + +- name: inspect + help: Inspect a resource + default: true + args: + - name: resource + allowed: [container, image] + help: Resource to inspect + flags: + - long: --format + short: -f + arg: format + allowed: [json, yaml] + help: Output format From 6769570b24a3eb44ab99131a478704a21d2b9530 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 18 Aug 2026 22:14:24 +0300 Subject: [PATCH 03/23] avoid offering wildcard aliases as completions --- lib/bashly/script/introspection/commands.rb | 5 +++++ .../views/command/completion_command_candidates.gtx | 4 ++-- spec/bashly/script/introspection/commands_spec.rb | 8 ++++++++ spec/fixtures/completions/core/src/bashly.yml | 1 + spec/fixtures/script/commands.yml | 9 +++++++++ 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/bashly/script/introspection/commands.rb b/lib/bashly/script/introspection/commands.rb index 087ac203..ac5507d6 100644 --- a/lib/bashly/script/introspection/commands.rb +++ b/lib/bashly/script/introspection/commands.rb @@ -41,6 +41,11 @@ def command_names commands.map(&:name) end + # Returns command names and aliases that can be offered as completions + def completion_aliases + public_commands.flat_map(&:aliases).reject { |name| name.end_with? '*' } + end + # Returns an array of the Commands def commands return [] unless options['commands'] diff --git a/lib/bashly/views/command/completion_command_candidates.gtx b/lib/bashly/views/command/completion_command_candidates.gtx index e958eee4..7b0b5f9e 100644 --- a/lib/bashly/views/command/completion_command_candidates.gtx +++ b/lib/bashly/views/command/completion_command_candidates.gtx @@ -1,3 +1,3 @@ -if public_command_aliases.any? - > completion_candidates "$completion_current" {{ public_command_aliases.join ' ' }} +if completion_aliases.any? + > completion_candidates "$completion_current" {{ completion_aliases.join ' ' }} end diff --git a/spec/bashly/script/introspection/commands_spec.rb b/spec/bashly/script/introspection/commands_spec.rb index f514cd96..37c54da5 100644 --- a/spec/bashly/script/introspection/commands_spec.rb +++ b/spec/bashly/script/introspection/commands_spec.rb @@ -52,6 +52,14 @@ end end + describe '#completion_aliases' do + let(:fixture) { :completion_aliases } + + it 'returns public command aliases without wildcard aliases' do + expect(subject.completion_aliases).to eq %w[download d upload] + end + end + describe '#commands' do let(:fixture) { :docker } diff --git a/spec/fixtures/completions/core/src/bashly.yml b/spec/fixtures/completions/core/src/bashly.yml index a43ba114..7d162d76 100644 --- a/spec/fixtures/completions/core/src/bashly.yml +++ b/spec/fixtures/completions/core/src/bashly.yml @@ -37,6 +37,7 @@ commands: alias: down help: Stop a server - name: status + alias: stat* help: Show server status - name: config diff --git a/spec/fixtures/script/commands.yml b/spec/fixtures/script/commands.yml index ff2fa147..9a753331 100644 --- a/spec/fixtures/script/commands.yml +++ b/spec/fixtures/script/commands.yml @@ -8,6 +8,15 @@ - name: update alias: upgrade +:completion_aliases: + name: cli + commands: + - name: download + alias: [d, down*] + - name: upload + - name: internal + private: true + :basic_command: name: get alias: g From 2e631ff3b26be063239a1666fe03febd5be15600 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 08:23:14 +0300 Subject: [PATCH 04/23] - Add Settings.enable_completions --- examples/render-mandoc/docs/download.1 | 2 +- examples/render-mandoc/docs/download.md | 2 +- lib/bashly/libraries/settings/settings.yml | 1 + lib/bashly/settings.rb | 5 + lib/bashly/views/command/master_script.gtx | 2 +- lib/bashly/views/command/start.gtx | 5 + .../integration/runtime_completions_spec.rb | 7 +- spec/bashly/script/wrapper_spec.rb | 2 +- .../lib-upgrade/src/lib/send_completions.sh | 192 ++++++++---------- 9 files changed, 101 insertions(+), 117 deletions(-) diff --git a/examples/render-mandoc/docs/download.1 b/examples/render-mandoc/docs/download.1 index 769e126c..0872b2e2 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" "July 2026" "Version 0.1.0" "Sample application" +.TH "download" "1" "August 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 9ff37dfe..27fc36ea 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 -% July 2026 +% August 2026 NAME ================================================== diff --git a/lib/bashly/libraries/settings/settings.yml b/lib/bashly/libraries/settings/settings.yml index 2f646c13..350f982f 100644 --- a/lib/bashly/libraries/settings/settings.yml +++ b/lib/bashly/libraries/settings/settings.yml @@ -158,6 +158,7 @@ env: development # - never # do not render this feature enable_header_comment: always enable_bash3_bouncer: always +enable_completions: never enable_view_markers: development enable_inspect_args: development enable_deps_array: always diff --git a/lib/bashly/settings.rb b/lib/bashly/settings.rb index db2a0611..cdb02e25 100644 --- a/lib/bashly/settings.rb +++ b/lib/bashly/settings.rb @@ -10,6 +10,7 @@ class << self :conjoined_flag_args, :config_path, :enable_bash3_bouncer, + :enable_completions, :enable_deps_array, :enable_env_var_names_array, :enable_header_comment, @@ -68,6 +69,10 @@ def enable_bash3_bouncer @enable_bash3_bouncer ||= get :enable_bash3_bouncer end + def enable_completions + @enable_completions ||= get :enable_completions + end + def enable_deps_array @enable_deps_array ||= get :enable_deps_array end diff --git a/lib/bashly/views/command/master_script.gtx b/lib/bashly/views/command/master_script.gtx index 7b1419b1..49d09211 100644 --- a/lib/bashly/views/command/master_script.gtx +++ b/lib/bashly/views/command/master_script.gtx @@ -8,7 +8,7 @@ = render :user_lib if user_lib.any? = render :command_functions = render :parse_requirements -= render :completions += render :completions if Settings.enabled? :completions = render :user_hooks = render :initialize = render :run diff --git a/lib/bashly/views/command/start.gtx b/lib/bashly/views/command/start.gtx index edd9dbfa..c28ca4b4 100644 --- a/lib/bashly/views/command/start.gtx +++ b/lib/bashly/views/command/start.gtx @@ -1,9 +1,14 @@ = view_marker > command_line_args=("$@") +if Settings.enabled? :completions > if [[ "${command_line_args[0]:-}" == "__complete" ]]; then > completion_run "${command_line_args[@]:1}" > else > {{ Settings.function_name :initialize }} > {{ Settings.function_name :run }} "${command_line_args[@]}" > fi +else +> {{ Settings.function_name :initialize }} +> {{ Settings.function_name :run }} "${command_line_args[@]}" +end diff --git a/spec/bashly/integration/runtime_completions_spec.rb b/spec/bashly/integration/runtime_completions_spec.rb index f4a94384..9da8c8b7 100644 --- a/spec/bashly/integration/runtime_completions_spec.rb +++ b/spec/bashly/integration/runtime_completions_spec.rb @@ -1,5 +1,3 @@ -require 'open3' - describe 'runtime completions', :slow do workspaces = Dir['spec/fixtures/completions/*'].select { |path| File.directory? path } @@ -9,11 +7,16 @@ cli = File.expand_path 'spec/tmp/cli' before(:context) do + Settings.enable_completions = 'always' reset_tmp_dir FileUtils.cp_r Dir["#{workspace}/*"], 'spec/tmp' Commands::Generate.new.execute %w[generate --quiet] end + after(:context) do + Settings.enable_completions = 'never' + end + examples.each do |name, example| describe name do it 'works' do diff --git a/spec/bashly/script/wrapper_spec.rb b/spec/bashly/script/wrapper_spec.rb index 233bce04..be118f0e 100644 --- a/spec/bashly/script/wrapper_spec.rb +++ b/spec/bashly/script/wrapper_spec.rb @@ -11,7 +11,7 @@ lines = subject.code.split "\n" expect(lines[0..13].join("\n")).to match_approval('script/wrapper/code') .except(/\d+\.\d+\.\d+(\.rc\d)?/) - expect(lines[-2]).to eq ' run "${command_line_args[@]}"' + expect(lines[-3]).to eq ' run "${command_line_args[@]}"' end end diff --git a/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh b/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh index beac72e9..23db3f9c 100644 --- a/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh +++ b/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh @@ -6,49 +6,62 @@ send_completions() { echo $'# completely (https://github.com/bashly-framework/completely)' echo $'# Modifying it manually is not recommended' echo $'' - echo $'_cli_completions_route_flag_expects_value() {' + echo $'_cli_completions_node_flag_state() {' echo $' case "$1:$2" in' - echo $' 2:--user|2:-u) return 0 ;;' - echo $' 2:--password|2:-p) return 0 ;;' + echo $' 0:--help|0:-h) return 0 ;;' + echo $' 0:--version|0:-v) return 0 ;;' + echo $' 1:--help|1:-h) return 0 ;;' + echo $' 1:--force|1:-f) return 0 ;;' + echo $' 2:--user|2:-u) return 2 ;;' + echo $' 2:--password|2:-p) return 2 ;;' + echo $' 2:--help|2:-h) return 0 ;;' echo $' esac' echo $'' echo $' return 1' echo $'}' echo $'' - echo $'_cli_completions_resolve_route() {' - echo $' route_id=' - echo $' route_word_count=-1' - echo $' route_has_positionals=0' - echo $' 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 $'_cli_completions_option_seen() {' + echo $' local completed_option option_name' + echo $' for completed_option in "${completed_options[@]}"; do' + echo $' for option_name in "$@"; do' + echo $' [[ "$completed_option" == "$option_name" ]] && return 0' + echo $' done' + echo $' done' 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=1' - echo $' route_word_count=1' - echo $' route_has_positionals=1' - echo $' positional_index=$((${#non_options[@]} - 1))' - echo $' fi' + echo $' return 1' + echo $'}' 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=2' - echo $' route_word_count=1' - echo $' route_has_positionals=1' - echo $' positional_index=$((${#non_options[@]} - 1))' - echo $' fi' + echo $'_cli_completions_resolve_node() {' + echo $' node_id=0' + echo $' node_word_count=0' + echo $' positional_index=0' + echo $'' + echo $' local word' + echo $' for word in "${non_options[@]}"; do' + echo $' case "$node_id:$word" in' + echo $' 0:download)' + echo $' node_id=1' + echo $' node_word_count=1' + echo $' ;;' + echo $' 0:d)' + echo $' node_id=1' + echo $' node_word_count=1' + echo $' ;;' + echo $' 0:upload)' + echo $' node_id=2' + echo $' node_word_count=1' + echo $' ;;' + echo $' 0:u)' + echo $' node_id=2' + echo $' node_word_count=1' + echo $' ;;' + echo $' *)' + echo $' break' + echo $' ;;' + echo $' esac' + echo $' done' echo $'' + echo $' positional_index=$((${#non_options[@]} - node_word_count))' echo $'}' echo $'' echo $'_cli_completions() {' @@ -65,11 +78,12 @@ send_completions() { echo $'' echo $' local non_options=()' echo $' local completed_options=()' - echo $' local route_id=' - echo $' local route_word_count=-1' - echo $' local route_has_positionals=0' + echo $' local node_id=' + echo $' local node_word_count=-1' echo $' local positional_index=0' - echo $' _cli_completions_resolve_route' + echo $' local invalid_completion=0' + echo $' local flag_state=0' + echo $' _cli_completions_resolve_node' echo $'' echo $' local skip_next=0' echo $' for word in "${completed[@]}"; do' @@ -79,25 +93,28 @@ send_completions() { echo $' fi' echo $'' echo $' if [[ "${word:0:1}" == "-" ]]; then' + echo $' _cli_completions_node_flag_state "$node_id" "$word"' + echo $' flag_state=$?' + echo $' if (( flag_state == 1 )); then' + echo $' invalid_completion=1' + echo $' break' + echo $' fi' + echo $'' echo $' completed_options+=("$word")' - echo $' if _cli_completions_route_flag_expects_value "$route_id" "$word"; then' + echo $' if (( flag_state == 2 )); then' echo $' skip_next=1' echo $' fi' echo $' continue' echo $' fi' echo $'' echo $' non_options+=("$word")' - echo $' _cli_completions_resolve_route' + echo $' _cli_completions_resolve_node' echo $' done' echo $'' echo $' COMPREPLY=()' + echo $' (( invalid_completion )) && return' 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 $' case "$route_id:$prev" in' + echo $' case "$node_id:$prev" in' echo $' 2:--user|2:-u)' echo $' return' echo $' ;;' @@ -106,90 +123,43 @@ send_completions() { echo $' ;;' echo $' esac' echo $'' + echo $' if [[ "${cur:0:1}" != "-" ]] && (( positional_index == 0 )); then' + echo $' case "$node_id" in' + echo $' 0)' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "download d upload u" -- "$cur")' + echo $' return' + echo $' ;;' + echo $' esac' + echo $' fi' + echo $'' echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' case "$route_id" in' + echo $' case "$node_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 $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' + echo $' _cli_completions_option_seen "--version" "-v" || words+=("--version" "-v")' 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 $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' + echo $' _cli_completions_option_seen "--force" "-f" || words+=("--force" "-f")' 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 $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' + echo $' _cli_completions_option_seen "--user" "-u" || words+=("--user" "-u")' + echo $' _cli_completions_option_seen "--password" "-p" || words+=("--password" "-p")' 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 $' case "$node_id:$positional_index" in' echo $' 1:0)' echo $' return' echo $' ;;' From 9a55f6f36257cc0ac8175ce1b01a143ccd61a832 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 08:34:27 +0300 Subject: [PATCH 05/23] add `enable_completions` to settings JSON schema --- lib/bashly/views/argument/completion.gtx | 11 ++++++++++- lib/bashly/views/argument/completion_filter.gtx | 1 + .../views/command/completion_argument_filter.gtx | 12 ++++++++++++ lib/bashly/views/command/completion_function.gtx | 3 +++ schemas/settings.json | 14 +++++++++++++- spec/fixtures/completions/core/examples.yml | 10 +++++++++- spec/fixtures/completions/core/src/bashly.yml | 9 +++++++++ support/schema/settings.yml | 8 ++++++++ 8 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 lib/bashly/views/argument/completion_filter.gtx diff --git a/lib/bashly/views/argument/completion.gtx b/lib/bashly/views/argument/completion.gtx index 110523b2..b3af3b0d 100644 --- a/lib/bashly/views/argument/completion.gtx +++ b/lib/bashly/views/argument/completion.gtx @@ -4,5 +4,14 @@ if allowed > "{{ value }}" end > ) - > completion_candidates "$completion_current" "${completion_values[@]}" + if unique + > local completion_value + > for completion_value in "${completion_values[@]}"; do + > if [[ -z "${completion_used_arguments["{{ name }}:$completion_value"]:-}" ]]; then + > completion_candidates "$completion_current" "$completion_value" + > fi + > done + else + > completion_candidates "$completion_current" "${completion_values[@]}" + end end diff --git a/lib/bashly/views/argument/completion_filter.gtx b/lib/bashly/views/argument/completion_filter.gtx new file mode 100644 index 00000000..832021b8 --- /dev/null +++ b/lib/bashly/views/argument/completion_filter.gtx @@ -0,0 +1 @@ +> completion_used_arguments["{{ name }}:$1"]=1 diff --git a/lib/bashly/views/command/completion_argument_filter.gtx b/lib/bashly/views/command/completion_argument_filter.gtx index cae1223b..47871dc1 100644 --- a/lib/bashly/views/command/completion_argument_filter.gtx +++ b/lib/bashly/views/command/completion_argument_filter.gtx @@ -1,3 +1,15 @@ +if args.any?(&:unique) + > case "$completion_arg_index" in + args.each_with_index do |arg, index| + next unless arg.unique + + > {{ index }}) + = arg.render(:completion_filter).indent 4 + > ;; + end + > esac +end + if args.last.repeatable last_arg_index = args.length - 1 > if [[ $completion_arg_index -lt {{ last_arg_index }} ]]; then diff --git a/lib/bashly/views/command/completion_function.gtx b/lib/bashly/views/command/completion_function.gtx index 7ad21c21..6fc170ba 100644 --- a/lib/bashly/views/command/completion_function.gtx +++ b/lib/bashly/views/command/completion_function.gtx @@ -5,6 +5,9 @@ if args.any? > local completion_arg_index=0 end +if args.any?(&:unique) + > local -A completion_used_arguments=() +end > shift > = render(:completion_word_filter).indent 2 diff --git a/schemas/settings.json b/schemas/settings.json index 0838b429..db299ec1 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -190,6 +190,18 @@ ], "default": "always" }, + "enable_completions": { + "title": "enable_completions", + "description": "Whether to include runtime completion functions in the generated script\nhttps://bashly.dev/usage/settings/#enable_completions", + "type": "string", + "enum": [ + "development", + "production", + "always", + "never" + ], + "default": "never" + }, "enable_view_markers": { "title": "enable_view_markers", "description": "Whether to include view marker comments in the generated script\nhttps://bashly.dev/usage/settings/#enable_view_markers", @@ -306,7 +318,7 @@ "title": "watch latency", "description": "The latency in seconds for the file system changes watcher\nhttps://bashly.dev/usage/settings/#watch_latency", "type": "number", - "default": 1 + "default": 1.0 }, "usage_colors": { "title": "usage colors", diff --git a/spec/fixtures/completions/core/examples.yml b/spec/fixtures/completions/core/examples.yml index b8ad58f2..8f7d7f7c 100644 --- a/spec/fixtures/completions/core/examples.yml +++ b/spec/fixtures/completions/core/examples.yml @@ -1,6 +1,6 @@ root commands: words: [''] - expected: [server, s, config, cfg, deploy, convert] + expected: [server, s, config, cfg, deploy, convert, release] root command prefix: words: [s] @@ -120,6 +120,14 @@ all positional arguments consumed: words: [convert, one, three, ''] expected: [] +unique positional argument: + words: [release, stable, ''] + expected: [beta, edge] + +unique positional argument prefix: + words: [release, stable, e] + expected: [edge] + private command is not reachable: words: [internal, ''] expected: [] diff --git a/spec/fixtures/completions/core/src/bashly.yml b/spec/fixtures/completions/core/src/bashly.yml index 7d162d76..0c6bb106 100644 --- a/spec/fixtures/completions/core/src/bashly.yml +++ b/spec/fixtures/completions/core/src/bashly.yml @@ -77,6 +77,15 @@ commands: help: Target document required: true +- name: release + help: Release to channels + args: + - name: channel + allowed: [stable, beta, edge] + help: Channels to release to + repeatable: true + unique: true + - name: internal help: Internal command that must not be suggested private: true diff --git a/support/schema/settings.yml b/support/schema/settings.yml index 9fa72c80..2abce71b 100644 --- a/support/schema/settings.yml +++ b/support/schema/settings.yml @@ -169,6 +169,14 @@ properties: type: string enum: *feature_toggles default: always + enable_completions: + title: enable_completions + description: |- + Whether to include runtime completion functions in the generated script + https://bashly.dev/usage/settings/#enable_completions + type: string + enum: *feature_toggles + default: never enable_view_markers: title: enable_view_markers description: |- From a413c4dc404fa9c93729e5f1ffd3abba9d943e6a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 08:50:24 +0300 Subject: [PATCH 06/23] complete flag completion implementation --- .../command/completion_flag_candidates.gtx | 12 +++++++-- .../views/command/completion_function.gtx | 3 +++ .../views/command/completion_word_filter.gtx | 12 +++++++-- .../views/flag/completion_filter_arg.gtx | 3 +++ .../flag/completion_value_candidates.gtx | 11 +++++++- spec/fixtures/completions/core/examples.yml | 12 +++++++++ spec/fixtures/completions/core/src/bashly.yml | 11 ++++++++ .../completions/global-flags/examples.yml | 27 +++++++++++++++++++ .../completions/global-flags/src/bashly.yml | 27 +++++++++++++++++++ 9 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/completions/global-flags/examples.yml create mode 100644 spec/fixtures/completions/global-flags/src/bashly.yml diff --git a/lib/bashly/views/command/completion_flag_candidates.gtx b/lib/bashly/views/command/completion_flag_candidates.gtx index 34850729..2e8e1a5a 100644 --- a/lib/bashly/views/command/completion_flag_candidates.gtx +++ b/lib/bashly/views/command/completion_flag_candidates.gtx @@ -1,9 +1,17 @@ if fixed_flags? || public_flags.any? > if [[ $completion_current == -* ]]; then if fixed_flags? - > completion_flag_candidates --help "$completion_current" --help -h + if short_flag_exist? '-h' + > completion_flag_candidates --help "$completion_current" --help + else + > completion_flag_candidates --help "$completion_current" --help -h + end if root_command? - > completion_flag_candidates --version "$completion_current" --version -v + if short_flag_exist? '-v' + > completion_flag_candidates --version "$completion_current" --version + else + > completion_flag_candidates --version "$completion_current" --version -v + end end end public_flags.each do |flag| diff --git a/lib/bashly/views/command/completion_function.gtx b/lib/bashly/views/command/completion_function.gtx index 6fc170ba..1a170b9f 100644 --- a/lib/bashly/views/command/completion_function.gtx +++ b/lib/bashly/views/command/completion_function.gtx @@ -8,6 +8,9 @@ end if args.any?(&:unique) > local -A completion_used_arguments=() end +if flags.any?(&:unique) + > local -A completion_used_flag_values=() +end > shift > = render(:completion_word_filter).indent 2 diff --git a/lib/bashly/views/command/completion_word_filter.gtx b/lib/bashly/views/command/completion_word_filter.gtx index afc92c38..715f7ec9 100644 --- a/lib/bashly/views/command/completion_word_filter.gtx +++ b/lib/bashly/views/command/completion_word_filter.gtx @@ -8,12 +8,20 @@ public_commands.each do |command| > ;; end if fixed_flags? - > --help | -h) + if short_flag_exist? '-h' + > --help) + else + > --help | -h) + end > completion_blocked_flags["--help"]=1 > shift > ;; if root_command? - > --version | -v) + if short_flag_exist? '-v' + > --version) + else + > --version | -v) + end > completion_blocked_flags["--version"]=1 > shift > ;; diff --git a/lib/bashly/views/flag/completion_filter_arg.gtx b/lib/bashly/views/flag/completion_filter_arg.gtx index c1b1560a..a01d9560 100644 --- a/lib/bashly/views/flag/completion_filter_arg.gtx +++ b/lib/bashly/views/flag/completion_filter_arg.gtx @@ -3,5 +3,8 @@ = render(:completion_value_candidates).indent 2 > return > fi +if unique + > completion_used_flag_values["{{ name }}:$1"]=1 +end > shift = render :completion_filter_block diff --git a/lib/bashly/views/flag/completion_value_candidates.gtx b/lib/bashly/views/flag/completion_value_candidates.gtx index 110523b2..d1632334 100644 --- a/lib/bashly/views/flag/completion_value_candidates.gtx +++ b/lib/bashly/views/flag/completion_value_candidates.gtx @@ -4,5 +4,14 @@ if allowed > "{{ value }}" end > ) - > completion_candidates "$completion_current" "${completion_values[@]}" + if unique + > local completion_value + > for completion_value in "${completion_values[@]}"; do + > if [[ -z "${completion_used_flag_values["{{ name }}:$completion_value"]:-}" ]]; then + > completion_candidates "$completion_current" "$completion_value" + > fi + > done + else + > completion_candidates "$completion_current" "${completion_values[@]}" + end end diff --git a/spec/fixtures/completions/core/examples.yml b/spec/fixtures/completions/core/examples.yml index 8f7d7f7c..429c07ae 100644 --- a/spec/fixtures/completions/core/examples.yml +++ b/spec/fixtures/completions/core/examples.yml @@ -128,6 +128,18 @@ unique positional argument prefix: words: [release, stable, e] expected: [edge] +unique flag value: + words: [release, --label, blue, --label, ''] + expected: [green, canary] + +unique flag value prefix: + words: [release, --label, blue, --label, c] + expected: [canary] + +unique repeatable flag remains: + words: [release, --label, blue, '--'] + expected: [--help, --label] + private command is not reachable: words: [internal, ''] expected: [] diff --git a/spec/fixtures/completions/core/src/bashly.yml b/spec/fixtures/completions/core/src/bashly.yml index 0c6bb106..9747df48 100644 --- a/spec/fixtures/completions/core/src/bashly.yml +++ b/spec/fixtures/completions/core/src/bashly.yml @@ -33,6 +33,9 @@ commands: short: -c negatable: true help: Enable color output + - long: --secret + help: Internal flag that must not be suggested + private: true - name: stop alias: down help: Stop a server @@ -79,6 +82,14 @@ commands: - name: release help: Release to channels + flags: + - long: --label + short: -l + arg: label + allowed: [blue, green, canary] + help: Labels to apply + repeatable: true + unique: true args: - name: channel allowed: [stable, beta, edge] diff --git a/spec/fixtures/completions/global-flags/examples.yml b/spec/fixtures/completions/global-flags/examples.yml new file mode 100644 index 00000000..fc83cf9d --- /dev/null +++ b/spec/fixtures/completions/global-flags/examples.yml @@ -0,0 +1,27 @@ +global flag names: + words: ['--'] + expected: [--help, --version, --profile, --verbose, --host] + +global short flag names: + words: ['-'] + expected: [--help, --version, --profile, -p, --verbose, -v, --host, -h] + +global flag allowed values: + words: [--profile, pro] + expected: [production] + +command after global flag: + words: [--profile, production, ''] + expected: [deploy] + +local flags after global flag and command: + words: [--profile, production, deploy, '--'] + expected: [--help, --force] + +custom version short flag remains repeatable: + words: [-v, '--'] + expected: [--help, --version, --profile, --verbose, --host] + +custom help short flag accepts its value: + words: [-h, us] + expected: [user] diff --git a/spec/fixtures/completions/global-flags/src/bashly.yml b/spec/fixtures/completions/global-flags/src/bashly.yml new file mode 100644 index 00000000..f7f9d420 --- /dev/null +++ b/spec/fixtures/completions/global-flags/src/bashly.yml @@ -0,0 +1,27 @@ +name: cli +help: Global flag completion fixture +version: 0.1.0 + +flags: +- long: --profile + short: -p + arg: profile + allowed: [development, production] + help: Select a profile +- long: --verbose + short: -v + repeatable: true + help: Increase verbosity +- long: --host + short: -h + arg: host + allowed: [user, admin] + help: Select a host + +commands: +- name: deploy + help: Deploy the application + flags: + - long: --force + short: -f + help: Force the deployment From d39d21ac797995be166ca93833fe0d6c0c684a2b Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 09:37:17 +0300 Subject: [PATCH 07/23] - Remove old completions implementation and self-completion installer --- Gemfile | 5 +- bashly.gemspec | 1 - examples/README.md | 1 - examples/completions/.gitignore | 1 - examples/completions/README.md | 286 ------------------ examples/completions/src/bashly.yml | 87 ------ .../completions/src/completions_command.sh | 9 - examples/completions/src/download_command.sh | 4 - .../completions/src/lib/send_completions.sh | 198 ------------ examples/completions/src/upload_command.sh | 4 - examples/completions/test.sh | 14 - lib/bashly.rb | 7 +- lib/bashly/commands/completions.rb | 44 +-- lib/bashly/completion_builder.rb | 231 -------------- lib/bashly/completions/README.md | 10 +- .../completions/bashly-completions.bash | 2 +- lib/bashly/completions/completely.yaml | 3 - lib/bashly/concerns/completions.rb | 48 --- lib/bashly/docs/arg.yml | 4 +- lib/bashly/docs/command.yml | 2 +- lib/bashly/docs/flag.yml | 2 +- .../completions/completions_function.rb | 37 --- .../completions/completions_script.rb | 28 -- .../libraries/completions/completions_yaml.rb | 26 -- lib/bashly/libraries/libraries.yml | 15 - lib/bashly/script/command.rb | 1 - lib/bashly/script/flag.rb | 1 - spec/approvals/cli/add/comp-function | 8 - spec/approvals/cli/add/comp-script | 6 - spec/approvals/cli/add/comp-yaml | 4 - spec/approvals/cli/add/comp-yaml-file | 21 -- spec/approvals/cli/add/list | 9 - spec/approvals/cli/commands | 2 +- spec/approvals/cli/completions/help | 13 +- spec/approvals/cli/completions/install | 4 - spec/approvals/cli/completions/install-error | 2 - spec/approvals/cli/completions/uninstall | 2 - .../approvals/cli/completions/uninstall-error | 2 - spec/approvals/cli/doc/full | 10 +- spec/approvals/cli/generate/upgrade | 1 - .../cli/generate/upgrade-path-mismatch | 1 - .../cli/generate/upgrade-unknown-lib | 1 - spec/approvals/cli/shell/boot | 2 +- .../aliases_and_global_flags | 25 -- .../completion_builder/default_command | 26 -- .../approvals/completion_builder/root_options | 8 - .../completion_builder/source_resolution | 27 -- .../completion_builder/token_collisions | 19 -- spec/approvals/completions/advanced | 30 -- .../completions/completion_global_flags | 24 -- .../completion_global_flags_nested | 16 - .../completions/completion_global_flags_root | 21 -- spec/approvals/completions/flag-allowed | 6 - spec/approvals/completions/flag-completions | 5 - spec/approvals/completions/nested_aliases | 24 -- spec/approvals/completions/pattern_sources | 29 -- spec/approvals/completions/simple | 9 - spec/approvals/completions/whitelist | 23 -- spec/approvals/examples/completions | 71 ----- spec/approvals/fixtures/completions-private | 17 -- spec/approvals/fixtures/lib-custom-path | 9 - spec/approvals/fixtures/partials-extension | 10 - .../libraries/completions_function/message | 5 - .../libraries/completions_script/message | 3 - .../libraries/completions_yaml/files | 14 - .../libraries/completions_yaml/message | 1 - spec/bashly/commands/add_spec.rb | 33 -- spec/bashly/commands/completions_spec.rb | 57 ---- spec/bashly/completion_builder_spec.rb | 50 --- .../concerns/completions_command_spec.rb | 121 -------- spec/bashly/concerns/completions_flag_spec.rb | 41 --- spec/bashly/extensions/yaml_spec.rb | 11 +- .../libraries/completions_function_spec.rb | 44 --- .../libraries/completions_script_spec.rb | 39 --- .../bashly/libraries/completions_yaml_spec.rb | 27 -- spec/bashly/library_source_spec.rb | 3 +- spec/bashly/library_spec.rb | 20 -- spec/bashly/script/command_spec.rb | 1 - spec/bashly/script/flag_spec.rb | 3 +- spec/fixtures/completely/pattern.yml | 14 - spec/fixtures/completion_builder.yml | 73 ----- spec/fixtures/script/commands.yml | 111 ------- .../workspaces/completions-private/.gitignore | 3 - .../workspaces/completions-private/README.md | 4 - .../completions-private/src/bashly.yml | 24 -- .../workspaces/completions-private/test.sh | 5 - .../workspaces/lib-custom-path/test.sh | 1 - .../lib-upgrade/src/lib/send_completions.sh | 177 ----------- .../workspaces/partials-extension/test.sh | 3 +- support/runfile/examples.runfile | 1 - support/runfile/static.runfile | 1 - 91 files changed, 30 insertions(+), 2418 deletions(-) delete mode 100644 examples/completions/.gitignore delete mode 100644 examples/completions/README.md delete mode 100644 examples/completions/src/bashly.yml delete mode 100644 examples/completions/src/completions_command.sh delete mode 100644 examples/completions/src/download_command.sh delete mode 100644 examples/completions/src/lib/send_completions.sh delete mode 100644 examples/completions/src/upload_command.sh delete mode 100644 examples/completions/test.sh delete mode 100644 lib/bashly/completion_builder.rb delete mode 100644 lib/bashly/concerns/completions.rb delete mode 100644 lib/bashly/libraries/completions/completions_function.rb delete mode 100644 lib/bashly/libraries/completions/completions_script.rb delete mode 100644 lib/bashly/libraries/completions/completions_yaml.rb delete mode 100644 spec/approvals/cli/add/comp-function delete mode 100644 spec/approvals/cli/add/comp-script delete mode 100644 spec/approvals/cli/add/comp-yaml delete mode 100644 spec/approvals/cli/add/comp-yaml-file delete mode 100644 spec/approvals/cli/completions/install delete mode 100644 spec/approvals/cli/completions/install-error delete mode 100644 spec/approvals/cli/completions/uninstall delete mode 100644 spec/approvals/cli/completions/uninstall-error delete mode 100644 spec/approvals/completion_builder/aliases_and_global_flags delete mode 100644 spec/approvals/completion_builder/default_command delete mode 100644 spec/approvals/completion_builder/root_options delete mode 100644 spec/approvals/completion_builder/source_resolution delete mode 100644 spec/approvals/completion_builder/token_collisions delete mode 100644 spec/approvals/completions/advanced delete mode 100644 spec/approvals/completions/completion_global_flags delete mode 100644 spec/approvals/completions/completion_global_flags_nested delete mode 100644 spec/approvals/completions/completion_global_flags_root delete mode 100644 spec/approvals/completions/flag-allowed delete mode 100644 spec/approvals/completions/flag-completions delete mode 100644 spec/approvals/completions/nested_aliases delete mode 100644 spec/approvals/completions/pattern_sources delete mode 100644 spec/approvals/completions/simple delete mode 100644 spec/approvals/completions/whitelist delete mode 100644 spec/approvals/examples/completions delete mode 100644 spec/approvals/fixtures/completions-private delete mode 100644 spec/approvals/libraries/completions_function/message delete mode 100644 spec/approvals/libraries/completions_script/message delete mode 100644 spec/approvals/libraries/completions_yaml/files delete mode 100644 spec/approvals/libraries/completions_yaml/message delete mode 100644 spec/bashly/completion_builder_spec.rb delete mode 100644 spec/bashly/concerns/completions_command_spec.rb delete mode 100644 spec/bashly/concerns/completions_flag_spec.rb delete mode 100644 spec/bashly/libraries/completions_function_spec.rb delete mode 100644 spec/bashly/libraries/completions_script_spec.rb delete mode 100644 spec/bashly/libraries/completions_yaml_spec.rb delete mode 100644 spec/fixtures/completely/pattern.yml delete mode 100644 spec/fixtures/completion_builder.yml delete mode 100644 spec/fixtures/workspaces/completions-private/.gitignore delete mode 100644 spec/fixtures/workspaces/completions-private/README.md delete mode 100644 spec/fixtures/workspaces/completions-private/src/bashly.yml delete mode 100644 spec/fixtures/workspaces/completions-private/test.sh delete mode 100644 spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh diff --git a/Gemfile b/Gemfile index 77c2e8e7..1e94210e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,9 @@ source 'https://rubygems.org' # gem 'debug' gem 'rspec' gem 'rspec_approvals' -gem 'runfile', '~> 1.0', require: false -gem 'runfile-tasks', '~> 1.0', require: false +gem 'runfile', require: false +gem 'runfile-tasks', require: false gem 'simplecov' +gem 'completely' gemspec diff --git a/bashly.gemspec b/bashly.gemspec index 902ddfa3..244b2c15 100644 --- a/bashly.gemspec +++ b/bashly.gemspec @@ -16,7 +16,6 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 3.2' s.add_dependency 'colsole', '~> 1.0' - s.add_dependency 'completely', '~> 0.8.0' s.add_dependency 'gtx', '~> 0.1.1' s.add_dependency 'listen', '~> 3.9' s.add_dependency 'lp', '~> 0.2.0' diff --git a/examples/README.md b/examples/README.md index 6fe22c37..72ac9aae 100644 --- a/examples/README.md +++ b/examples/README.md @@ -67,7 +67,6 @@ Each of these examples demonstrates one aspect or feature of bashly. - [ini](ini#readme) - using the ini library for direct, low level access to INI files - [yaml](yaml#readme) - using the YAML reading functions - [colors](colors#readme) - using the color print feature -- [completions](completions#readme) - adding bash completion functionality - [validations](validations#readme) - adding validation functions for arguments, flags or environment variables - [hooks](hooks#readme) - adding before/after hooks - [stacktrace](stacktrace#readme) - adding stacktrace on error diff --git a/examples/completions/.gitignore b/examples/completions/.gitignore deleted file mode 100644 index 76ec9f59..00000000 --- a/examples/completions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cli \ No newline at end of file diff --git a/examples/completions/README.md b/examples/completions/README.md deleted file mode 100644 index f5c8cedd..00000000 --- a/examples/completions/README.md +++ /dev/null @@ -1,286 +0,0 @@ -# Bash Completions Example - -Demonstrates how to build a script that supports bash completions. - -This example was generated with: - -```bash -$ bashly init -# ... now edit src/bashly.yml to match the example ... -$ bashly add completions -$ bashly generate -# ... now edit src/completions_command.sh ... -$ bashly generate -``` - - - ------ - -## `bashly.yml` - -````yaml -name: cli -help: Sample application with bash completions -version: 0.1.0 - -# All commands and flags will be automatically used in the completions script -commands: -- name: completions - help: |- - Generate bash completions - Usage: eval "\$(cli completions)" - -- name: download - alias: d - help: Download a file - - # Adding custom completions for a command. In this case, typing - # `cli download ` will suggest files. - completions: - - - - args: - - name: source - required: true - help: URL to download from - - name: target - help: "Target filename (default: same as source)" - - flags: - - long: --force - short: -f - help: Overwrite existing files - - long: --handler - arg: command - - # The allowed flag arg whitelist will be added automatically. In this case, - # typing `cli download --handler ` will suggest curl or wget - allowed: - - curl - - wget - - default: curl - - examples: - - cli download example.com - - cli download example.com ./output -f - - environment_variables: - - name: default_target_location - help: Set the default location to download to - -- name: upload - alias: u - help: Upload a file - - # Add directories and users to the suggested completions. - completions: - - - - - - args: - - name: source - required: true - help: File to upload - - # The allowed argument whitelist will be added automatically. In this case - # typing `cli upload ` will suggest these files. - allowed: - - README.md - - CHANGELOG.md - - flags: - - long: --user - short: -u - arg: user - help: Username to use for logging in - required: true - - # Adding completions to a flag with an argument will add it to the suggested - # words list. In this case typing `cli upload --user ` will suggest - # users. - completions: - - - - - long: --password - short: -p - arg: password - help: Password to use for logging in -```` - -## `src/completions_command.sh` - -````bash -# Call the `send_completions` function which was added by running: -# -# $ bashly add completions -# -# Users can now enable bash completion for this script by running: -# -# $ eval "$(cli completions)" -# -send_completions - -```` - - -## Output - -### `$ ./cli` - -````shell -cli - Sample application with bash completions - -Usage: - cli COMMAND - cli [COMMAND] --help | -h - cli --version | -v - -Commands: - completions Generate bash completions - download Download a file - upload Upload a file - - - -```` - -### `$ ./cli -h` - -````shell -cli - Sample application with bash completions - -Usage: - cli COMMAND - cli [COMMAND] --help | -h - cli --version | -v - -Commands: - completions Generate bash completions - download Download a file - upload Upload a file - -Options: - --help, -h - Show this help - - --version, -v - Show version number - - - -```` - -### `$ ./cli completions -h` - -````shell -cli completions - - Generate bash completions - Usage: eval "$(cli completions)" - -Usage: - cli completions - cli completions --help | -h - -Options: - --help, -h - Show this help - - - -```` - -### `$ ./cli completions` - -````shell -# cli completion -*- shell-script -*- - -# This bash completions script was generated by -# 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=() - - if [[ "${cur:0:1}" == "-" ]]; then - echo "$words" - - else - for word in $words; do - [[ "${word:0:1}" != "-" ]] && result+=("$word") - done - - echo "${result[*]}" - - fi -} - -_cli_completions() { - local cur=${COMP_WORDS[COMP_CWORD]} - local compwords=("${COMP_WORDS[@]:1:$COMP_CWORD-1}") - local compline="${compwords[*]}" - - case "$compline" in - 'download'*'--handler') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "curl wget")" -- "$cur") - ;; - - 'upload'*'--user') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; - - 'completions'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help -h")" -- "$cur") - ;; - - 'd'*'--handler') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "curl wget")" -- "$cur") - ;; - - 'upload'*'-u') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; - - 'download'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_cli_completions_filter "--force --handler --help -f -h")" -- "$cur") - ;; - - 'u'*'--user') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; - - 'upload'*) - 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") - ;; - - 'u'*'-u') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A user -- "$cur") - ;; - - 'd'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -W "$(_cli_completions_filter "--force --handler --help -f -h")" -- "$cur") - ;; - - 'u'*) - 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") - ;; - - *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --version -h -v completions d download u upload")" -- "$cur") - ;; - - esac -} && - complete -F _cli_completions cli - -# ex: filetype=sh - - -```` - - - diff --git a/examples/completions/src/bashly.yml b/examples/completions/src/bashly.yml deleted file mode 100644 index 6284f2b1..00000000 --- a/examples/completions/src/bashly.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: cli -help: Sample application with bash completions -version: 0.1.0 - -# All commands and flags will be automatically used in the completions script -commands: -- name: completions - help: |- - Generate bash completions - Usage: eval "\$(cli completions)" - -- name: download - alias: d - help: Download a file - - # Adding custom completions for a command. In this case, typing - # `cli download ` will suggest files. - completions: - - - - args: - - name: source - required: true - help: URL to download from - - name: target - help: "Target filename (default: same as source)" - - flags: - - long: --force - short: -f - help: Overwrite existing files - - long: --handler - arg: command - - # The allowed flag arg whitelist will be added automatically. In this case, - # typing `cli download --handler ` will suggest curl or wget - allowed: - - curl - - wget - - default: curl - - examples: - - cli download example.com - - cli download example.com ./output -f - - environment_variables: - - name: default_target_location - help: Set the default location to download to - -- name: upload - alias: u - help: Upload a file - - # Add directories and users to the suggested completions. - completions: - - - - - - args: - - name: source - required: true - help: File to upload - - # The allowed argument whitelist will be added automatically. In this case - # typing `cli upload ` will suggest these files. - allowed: - - README.md - - CHANGELOG.md - - flags: - - long: --user - short: -u - arg: user - help: Username to use for logging in - required: true - - # Adding completions to a flag with an argument will add it to the suggested - # words list. In this case typing `cli upload --user ` will suggest - # users. - completions: - - - - - long: --password - short: -p - arg: password - help: Password to use for logging in diff --git a/examples/completions/src/completions_command.sh b/examples/completions/src/completions_command.sh deleted file mode 100644 index d98bb2e5..00000000 --- a/examples/completions/src/completions_command.sh +++ /dev/null @@ -1,9 +0,0 @@ -# Call the `send_completions` function which was added by running: -# -# $ bashly add completions -# -# Users can now enable bash completion for this script by running: -# -# $ eval "$(cli completions)" -# -send_completions diff --git a/examples/completions/src/download_command.sh b/examples/completions/src/download_command.sh deleted file mode 100644 index 950ed370..00000000 --- a/examples/completions/src/download_command.sh +++ /dev/null @@ -1,4 +0,0 @@ -echo "# this file is located in 'src/download_command.sh'" -echo "# code for 'cli download' goes here" -echo "# you can edit it freely and regenerate (it will not be overwritten)" -inspect_args diff --git a/examples/completions/src/lib/send_completions.sh b/examples/completions/src/lib/send_completions.sh deleted file mode 100644 index 89878876..00000000 --- a/examples/completions/src/lib/send_completions.sh +++ /dev/null @@ -1,198 +0,0 @@ -## [@bashly-upgrade completions send_completions] -send_completions() { - echo $'# cli completion -*- shell-script -*-' - echo $'' - echo $'# This bash completions script was generated by' - echo $'# completely (https://github.com/bashly-framework/completely)' - echo $'# Modifying it manually is not recommended' - echo $'' - echo $'_cli_completions_node_flag_state() {' - echo $' case "$1:$2" in' - echo $' 0:--help|0:-h) return 0 ;;' - echo $' 0:--version|0:-v) return 0 ;;' - echo $' 1:--help|1:-h) return 0 ;;' - echo $' 2:--handler) return 2 ;;' - echo $' 2:--help|2:-h) return 0 ;;' - echo $' 2:--force|2:-f) return 0 ;;' - echo $' 3:--user|3:-u) return 2 ;;' - echo $' 3:--password|3:-p) return 2 ;;' - echo $' 3:--help|3:-h) return 0 ;;' - echo $' esac' - echo $'' - echo $' return 1' - echo $'}' - echo $'' - echo $'_cli_completions_option_seen() {' - echo $' local completed_option option_name' - echo $' for completed_option in "${completed_options[@]}"; do' - echo $' for option_name in "$@"; do' - echo $' [[ "$completed_option" == "$option_name" ]] && return 0' - echo $' done' - echo $' done' - echo $'' - echo $' return 1' - echo $'}' - echo $'' - echo $'_cli_completions_resolve_node() {' - echo $' node_id=0' - echo $' node_word_count=0' - echo $' positional_index=0' - echo $'' - echo $' local word' - echo $' for word in "${non_options[@]}"; do' - echo $' case "$node_id:$word" in' - echo $' 0:completions)' - echo $' node_id=1' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:download)' - echo $' node_id=2' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:d)' - echo $' node_id=2' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:upload)' - echo $' node_id=3' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:u)' - echo $' node_id=3' - echo $' node_word_count=1' - echo $' ;;' - echo $' *)' - echo $' break' - echo $' ;;' - echo $' esac' - echo $' done' - echo $'' - echo $' positional_index=$((${#non_options[@]} - node_word_count))' - echo $'}' - echo $'' - echo $'_cli_completions() {' - echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local prev=' - echo $' if ((COMP_CWORD > 0)); then' - echo $' prev=${COMP_WORDS[$((COMP_CWORD - 1))]}' - echo $' fi' - echo $'' - echo $' local completed=()' - echo $' if ((COMP_CWORD > 1)); then' - echo $' completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' - echo $' fi' - echo $'' - echo $' local non_options=()' - echo $' local completed_options=()' - echo $' local node_id=' - echo $' local node_word_count=-1' - echo $' local positional_index=0' - echo $' local invalid_completion=0' - echo $' local flag_state=0' - echo $' _cli_completions_resolve_node' - echo $'' - 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 $' _cli_completions_node_flag_state "$node_id" "$word"' - echo $' flag_state=$?' - echo $' if (( flag_state == 1 )); then' - echo $' invalid_completion=1' - echo $' break' - echo $' fi' - echo $'' - echo $' completed_options+=("$word")' - echo $' if (( flag_state == 2 )); then' - echo $' skip_next=1' - echo $' fi' - echo $' continue' - echo $' fi' - echo $'' - echo $' non_options+=("$word")' - echo $' _cli_completions_resolve_node' - echo $' done' - echo $'' - echo $' COMPREPLY=()' - echo $' (( invalid_completion )) && return' - echo $'' - echo $' case "$node_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 $' 3:--password|3:-p)' - echo $' return' - echo $' ;;' - echo $' esac' - echo $'' - echo $' if [[ "${cur:0:1}" != "-" ]] && (( positional_index == 0 )); then' - echo $' case "$node_id" in' - echo $' 0)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "completions download d upload u" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' esac' - echo $' fi' - echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' case "$node_id" in' - echo $' 0)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' _cli_completions_option_seen "--version" "-v" || words+=("--version" "-v")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 1)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 2)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' _cli_completions_option_seen "--force" "-f" || words+=("--force" "-f")' - echo $' _cli_completions_option_seen "--handler" || words+=("--handler")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 3)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' _cli_completions_option_seen "--user" "-u" || words+=("--user" "-u")' - echo $' _cli_completions_option_seen "--password" "-p" || words+=("--password" "-p")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' esac' - echo $' fi' - echo $'' - echo $' case "$node_id:$positional_index" in' - echo $' 2:0)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 2:1)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A file -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 3:0)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "README.md CHANGELOG.md" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' esac' - echo $'} &&' - echo $' complete -F _cli_completions cli' - echo $'' - echo $'# ex: filetype=sh' -} \ No newline at end of file diff --git a/examples/completions/src/upload_command.sh b/examples/completions/src/upload_command.sh deleted file mode 100644 index 755e02ff..00000000 --- a/examples/completions/src/upload_command.sh +++ /dev/null @@ -1,4 +0,0 @@ -echo "# this file is located in 'src/upload_command.sh'" -echo "# code for 'cli upload' goes here" -echo "# you can edit it freely and regenerate (it will not be overwritten)" -inspect_args diff --git a/examples/completions/test.sh b/examples/completions/test.sh deleted file mode 100644 index c239aa2e..00000000 --- a/examples/completions/test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -x - -bashly add completions --force -bashly generate - -### Try Me ### - -./cli -./cli -h -./cli completions -h -./cli completions | head -n6 - diff --git a/lib/bashly.rb b/lib/bashly.rb index 350d3395..ed6344d4 100644 --- a/lib/bashly.rb +++ b/lib/bashly.rb @@ -6,13 +6,13 @@ module Bashly autoloads 'bashly/refinements', %i[ComposeRefinements] autoloads 'bashly', %i[ - CLI CompletionBuilder Config ConfigValidator Library LibrarySource + CLI Config ConfigValidator Library LibrarySource LibrarySourceConfig MessageStrings RenderContext RenderSource Settings VERSION Watch ] autoloads 'bashly/concerns', %i[ - AssetHelper Completions Renderable ValidationHelpers + AssetHelper Renderable ValidationHelpers ] module Script @@ -37,9 +37,6 @@ module Commands module Libraries autoload :Base, 'bashly/libraries/base' - autoload :CompletionsFunction, 'bashly/libraries/completions/completions_function' - autoload :CompletionsScript, 'bashly/libraries/completions/completions_script' - autoload :CompletionsYAML, 'bashly/libraries/completions/completions_yaml' autoload :Help, 'bashly/libraries/help/help' end end diff --git a/lib/bashly/commands/completions.rb b/lib/bashly/commands/completions.rb index 00b52e17..f1bd9837 100644 --- a/lib/bashly/commands/completions.rb +++ b/lib/bashly/commands/completions.rb @@ -1,55 +1,17 @@ -require 'completely' - module Bashly module Commands class Completions < Base - summary 'Install bash completions for bashly itself' - help 'Display the bash completions script or install it directly to your bash completions directory' + summary 'Display bash completions for bashly itself' - usage 'bashly completions [--install|--uninstall]' + usage 'bashly completions' usage 'bashly completions (-h|--help)' - option '-i --install', 'Install the completions script to your bash completions directory' - option '-u --uninstall', 'Uninstall the completions script from your bash completions directory' - def run - if args['--install'] - install_completions - elsif args['--uninstall'] - uninstall_completions - else - puts script - end - end - - def installer - @installer ||= Completely::Installer.new program: 'bashly', script_path: script_path + puts script end private - def install_completions - success = installer.install force: true - raise Error, "Failed running command:\nnb`#{installer.install_command_string}`" unless success - - say 'Completions installed' - say "Source: m`#{installer.script_path}`" - say "Target: m`#{installer.target_path}`" - say 'Restart your session for the changes to take effect' - end - - def uninstall_completions - success = installer.uninstall - raise Error, "Failed running command:\nnb`#{installer.uninstall_command_string}`" unless success - - say 'Completions uninstalled' - say 'Restart your session for the changes to take effect' - end - - def script_path - @script_path ||= asset('completions/bashly-completions.bash') - end - def script @script ||= asset_content('completions/bashly-completions.bash') end diff --git a/lib/bashly/completion_builder.rb b/lib/bashly/completion_builder.rb deleted file mode 100644 index 6c1e4503..00000000 --- a/lib/bashly/completion_builder.rb +++ /dev/null @@ -1,231 +0,0 @@ -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) unless visible_default_command(command) - - 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_default_command_pattern command, child, pattern_groups - 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 add_default_command_pattern(parent, command, parent_option_groups) - return unless command.default - - default_group = add_default_options parent, command, parent_option_groups - option_groups = default_group ? [default_group] : [] - - @patterns << pattern_for_default_command(parent, command, option_groups) - end - - def add_default_options(parent, command, parent_option_groups) - local_group = add_local_options command - group_names = parent_option_groups.dup - group_names << local_group if local_group - - entries = group_names.flat_map { |name| @options[name] || [] }.uniq - return if entries.empty? - - name = token_name "#{group_name(parent)}_#{group_name(command)}_default" - @options[name] = entries - name - end - - def pattern_for_default_command(parent, command, option_groups) - parts = [command_path(parent)] - parts.concat(option_groups.map { |group| "[#{group} options]" }) - parts.concat positional_tokens( - command, - first_source_extra: static_source(parent.visible_command_aliases) - ) - 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, first_source_extra: nil) - command.args.map.with_index do |arg, index| - source = arg_source arg, command - source = merge_sources(first_source_extra, source) if index.zero? && first_source_extra - token_name = register_token arg.name, command, source - suffix = arg.repeatable ? '...' : nil - "<#{token_name}>#{suffix}" - end - end - - def merge_sources(*sources) - sources.compact.flatten.uniq - 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 visible_default_command(command) - command.visible_commands.find(&:default) - 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/README.md b/lib/bashly/completions/README.md index a9f31f3c..e8c941c4 100644 --- a/lib/bashly/completions/README.md +++ b/lib/bashly/completions/README.md @@ -15,10 +15,6 @@ Note that for production use, only the `bashly-completions.bash` is used. ## For users -Install completions in one of two ways: - -1. Run `bashly completions --install`. This will make a best effort to copy - the completions script to your completions directory. -2. If the above fails, run `bashly completions > out.bash`, then copy the file - manually to your completions directory (or simply get the - `bashly-completions.bash` from this directory). +Run `bashly completions > out.bash`, then copy the file manually to your +completions directory (or simply get the `bashly-completions.bash` from this +directory). diff --git a/lib/bashly/completions/bashly-completions.bash b/lib/bashly/completions/bashly-completions.bash index 1554ab5a..1d226ce3 100644 --- a/lib/bashly/completions/bashly-completions.bash +++ b/lib/bashly/completions/bashly-completions.bash @@ -297,7 +297,7 @@ _bashly_completions() { case "$node_id:$positional_index" in 5:0) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "colors completions completions_script completions_yaml config help hooks ini lib render_markdown render_markdown_github render_mandoc settings stacktrace strings validations yaml" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "colors config help hooks ini lib render_markdown render_markdown_github render_mandoc settings stacktrace strings validations yaml" -- "$cur") return ;; 6:0) diff --git a/lib/bashly/completions/completely.yaml b/lib/bashly/completions/completely.yaml index cf8b09bd..eb1716e0 100644 --- a/lib/bashly/completions/completely.yaml +++ b/lib/bashly/completions/completely.yaml @@ -60,9 +60,6 @@ tokens: path: library: - colors - - completions - - completions_script - - completions_yaml - config - help - hooks diff --git a/lib/bashly/concerns/completions.rb b/lib/bashly/concerns/completions.rb deleted file mode 100644 index 4b942a7c..00000000 --- a/lib/bashly/concerns/completions.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'completely' -require 'bashly/completion_builder' - -module Bashly - # This is a `Command` and `Flag` concern responsible for providing bash - # completion data - module Completions - module Flag - def completion_data(command_full_name) - comps = allowed || completions - return {} unless comps - - aliases.to_h do |name| - prefix = command_full_name - prefix = "#{prefix}*" unless prefix.end_with? '*' - ["#{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) - CompletionBuilder.new(self, with_version: with_version).call - end - - def completion_script - completion_generator.script - end - - def completion_function(name = nil) - completion_generator.wrapper_function name - end - - private - - def completion_generator - Completely::Completions.new completion_data - end - end - end -end diff --git a/lib/bashly/docs/arg.yml b/lib/bashly/docs/arg.yml index 48eed15b..a5e55c94 100644 --- a/lib/bashly/docs/arg.yml +++ b/lib/bashly/docs/arg.yml @@ -34,7 +34,7 @@ arg.allowed: default: development arg.completions: - help: Specify a list of additional completion suggestions when used in conjunction with `bashly add completions`. + help: Specify a list of additional completion suggestions. url: https://bashly.dev/configuration/argument/#completions example: |- args: @@ -102,4 +102,4 @@ arg.validate: example: |- args: - name: path - validate: file_exists \ No newline at end of file + validate: file_exists diff --git a/lib/bashly/docs/command.yml b/lib/bashly/docs/command.yml index aea29eec..15fdb389 100644 --- a/lib/bashly/docs/command.yml +++ b/lib/bashly/docs/command.yml @@ -75,7 +75,7 @@ command.commands: help: Register a local repository command.completions: - help: Specify a list of additional completion suggestions when used in conjunction with `bashly add completions`. + help: Specify a list of additional completion suggestions. url: https://bashly.dev/configuration/command/#completions example: |- commands: diff --git a/lib/bashly/docs/flag.yml b/lib/bashly/docs/flag.yml index 5a27049f..7a44b49c 100644 --- a/lib/bashly/docs/flag.yml +++ b/lib/bashly/docs/flag.yml @@ -58,7 +58,7 @@ flag.arg: help: Specify the user name flag.completions: - help: Specify a list of additional completion suggestions when used in conjunction with `bashly add completions`. Must be accompanied by `arg`. + help: Specify a list of additional completion suggestions. Must be accompanied by `arg`. url: https://bashly.dev/configuration/flag/#completions example: |- flags: diff --git a/lib/bashly/libraries/completions/completions_function.rb b/lib/bashly/libraries/completions/completions_function.rb deleted file mode 100644 index 26f94f37..00000000 --- a/lib/bashly/libraries/completions/completions_function.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Bashly - module Libraries - class CompletionsFunction < Base - def files - [ - { - path: "#{Settings.full_lib_dir}/#{function_name}.#{Settings.partials_extension}", - content: completions_function_code(function_name), - }, - ] - end - - def post_install_message - <<~MESSAGE - In order to enable completions in your script, create a command or a flag (for example: g`#{command.name} completions` or g`#{command.name} --completions`) that calls the g`#{function_name}` function. - - Your users can then run something like this to enable completions: - - m`$ eval "$(#{command.name} completions)"` - MESSAGE - end - - private - - def function_name - @function_name ||= args[0] || 'send_completions' - end - - def completions_function_code(function_name) - [ - "## [@bashly-upgrade completions #{function_name}]", - command.completion_function(function_name), - ].join "\n" - end - end - end -end diff --git a/lib/bashly/libraries/completions/completions_script.rb b/lib/bashly/libraries/completions/completions_script.rb deleted file mode 100644 index 13ccb50c..00000000 --- a/lib/bashly/libraries/completions/completions_script.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Bashly - module Libraries - class CompletionsScript < Base - def files - [ - { - path: target_path, - content: command.completion_script, - }, - ] - end - - def post_install_message - <<~MESSAGE - In order to enable completions, run: - - m`$ source #{target_path}` - MESSAGE - end - - private - - def target_path - @target_path ||= args[0] || "#{Settings.target_dir}/completions.bash" - end - end - end -end diff --git a/lib/bashly/libraries/completions/completions_yaml.rb b/lib/bashly/libraries/completions/completions_yaml.rb deleted file mode 100644 index ba6de1a2..00000000 --- a/lib/bashly/libraries/completions/completions_yaml.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Bashly - module Libraries - class CompletionsYAML < Base - def files - [ - { - path: target_path, - content: command.completion_data.to_yaml, - }, - ] - end - - def post_install_message - <<~MESSAGE - This file can be converted to a completions script using the g`completely` gem. - MESSAGE - end - - private - - def target_path - @target_path ||= args[0] || "#{Settings.target_dir}/completions.yml" - end - end - end -end diff --git a/lib/bashly/libraries/libraries.yml b/lib/bashly/libraries/libraries.yml index 662a3028..5edde4e0 100644 --- a/lib/bashly/libraries/libraries.yml +++ b/lib/bashly/libraries/libraries.yml @@ -12,21 +12,6 @@ colors: m`$ bashly add hooks` -completions: - help: Generate a bash completions function. - usage: '[PATH]' - handler: Bashly::Libraries::CompletionsFunction - -completions_script: - help: Generate a standalone bash completions script. - usage: '[PATH]' - handler: Bashly::Libraries::CompletionsScript - -completions_yaml: - help: Generate a completions YAML configuration for Completely. - usage: '[PATH]' - handler: Bashly::Libraries::CompletionsYAML - config: help: Add functions for handling INI configuration files to the lib directory. files: diff --git a/lib/bashly/script/command.rb b/lib/bashly/script/command.rb index a356d9f6..65c841ba 100644 --- a/lib/bashly/script/command.rb +++ b/lib/bashly/script/command.rb @@ -1,7 +1,6 @@ module Bashly module Script class Command < Base - include Completions::Command include Introspection::Arguments include Introspection::Commands include Introspection::Dependencies diff --git a/lib/bashly/script/flag.rb b/lib/bashly/script/flag.rb index e3c92ff5..72534378 100644 --- a/lib/bashly/script/flag.rb +++ b/lib/bashly/script/flag.rb @@ -3,7 +3,6 @@ module Bashly module Script class Flag < Base - include Completions::Flag include Introspection::Visibility include Introspection::Validate diff --git a/spec/approvals/cli/add/comp-function b/spec/approvals/cli/add/comp-function deleted file mode 100644 index fb2ea773..00000000 --- a/spec/approvals/cli/add/comp-function +++ /dev/null @@ -1,8 +0,0 @@ -created spec/tmp/src/lib/send_completions.sh - -In order to enable completions in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function. - -Your users can then run something like this to enable completions: - - $ eval "$(cli completions)" - diff --git a/spec/approvals/cli/add/comp-script b/spec/approvals/cli/add/comp-script deleted file mode 100644 index e5d1b545..00000000 --- a/spec/approvals/cli/add/comp-script +++ /dev/null @@ -1,6 +0,0 @@ -created spec/tmp/completions.bash - -In order to enable completions, run: - - $ source spec/tmp/completions.bash - diff --git a/spec/approvals/cli/add/comp-yaml b/spec/approvals/cli/add/comp-yaml deleted file mode 100644 index 25930fcd..00000000 --- a/spec/approvals/cli/add/comp-yaml +++ /dev/null @@ -1,4 +0,0 @@ -created spec/tmp/completions.yml - -This file can be converted to a completions script using the completely gem. - diff --git a/spec/approvals/cli/add/comp-yaml-file b/spec/approvals/cli/add/comp-yaml-file deleted file mode 100644 index b5daf2c4..00000000 --- a/spec/approvals/cli/add/comp-yaml-file +++ /dev/null @@ -1,21 +0,0 @@ ---- -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/add/list b/spec/approvals/cli/add/list index 68c0368c..a8daf39e 100644 --- a/spec/approvals/cli/add/list +++ b/spec/approvals/cli/add/list @@ -2,15 +2,6 @@ colors Add standard functions for printing colorful and formatted text to the lib directory. -completions [PATH] - Generate a bash completions function. - -completions_script [PATH] - Generate a standalone bash completions script. - -completions_yaml [PATH] - Generate a completions YAML configuration for Completely. - config Add functions for handling INI configuration files to the lib directory. diff --git a/spec/approvals/cli/commands b/spec/approvals/cli/commands index 6b5b416a..f0e747d2 100644 --- a/spec/approvals/cli/commands +++ b/spec/approvals/cli/commands @@ -7,7 +7,7 @@ Commands: generate Generate the bash script and required files add Add extra features and customization to your script doc Show bashly reference documentation - completions Install bash completions for bashly itself + completions Display bash completions for bashly itself render Render the bashly data structure using custom templates shell Start an interactive bashly shell diff --git a/spec/approvals/cli/completions/help b/spec/approvals/cli/completions/help index c2fe827e..0d14da2b 100644 --- a/spec/approvals/cli/completions/help +++ b/spec/approvals/cli/completions/help @@ -1,18 +1,9 @@ -Install bash completions for bashly itself - -Display the bash completions script or install it directly to your bash -completions directory +Display bash completions for bashly itself Usage: - bashly completions [--install|--uninstall] + bashly completions bashly completions (-h|--help) Options: - -i --install - Install the completions script to your bash completions directory - - -u --uninstall - Uninstall the completions script from your bash completions directory - -h --help Show this help diff --git a/spec/approvals/cli/completions/install b/spec/approvals/cli/completions/install deleted file mode 100644 index 5b2152a1..00000000 --- a/spec/approvals/cli/completions/install +++ /dev/null @@ -1,4 +0,0 @@ -Completions installed -Source: some-script-path -Target: some-target-path -Restart your session for the changes to take effect diff --git a/spec/approvals/cli/completions/install-error b/spec/approvals/cli/completions/install-error deleted file mode 100644 index 95e1223a..00000000 --- a/spec/approvals/cli/completions/install-error +++ /dev/null @@ -1,2 +0,0 @@ -# \ No newline at end of file diff --git a/spec/approvals/cli/completions/uninstall b/spec/approvals/cli/completions/uninstall deleted file mode 100644 index c5e159c7..00000000 --- a/spec/approvals/cli/completions/uninstall +++ /dev/null @@ -1,2 +0,0 @@ -Completions uninstalled -Restart your session for the changes to take effect diff --git a/spec/approvals/cli/completions/uninstall-error b/spec/approvals/cli/completions/uninstall-error deleted file mode 100644 index 092641c8..00000000 --- a/spec/approvals/cli/completions/uninstall-error +++ /dev/null @@ -1,2 +0,0 @@ -# \ No newline at end of file diff --git a/spec/approvals/cli/doc/full b/spec/approvals/cli/doc/full index 462a70aa..1aeaafc8 100644 --- a/spec/approvals/cli/doc/full +++ b/spec/approvals/cli/doc/full @@ -40,8 +40,7 @@ arg.allowed arg.completions - Specify a list of additional completion suggestions when used in conjunction - with bashly add completions. + Specify a list of additional completion suggestions. args: - name: path @@ -211,8 +210,7 @@ command.commands command.completions - Specify a list of additional completion suggestions when used in conjunction - with bashly add completions. + Specify a list of additional completion suggestions. commands: - name: view @@ -680,8 +678,8 @@ flag.arg flag.completions - Specify a list of additional completion suggestions when used in conjunction - with bashly add completions. Must be accompanied by arg. + Specify a list of additional completion suggestions. Must be accompanied by + arg. flags: - long: --user diff --git a/spec/approvals/cli/generate/upgrade b/spec/approvals/cli/generate/upgrade index 69887a83..bb46c622 100644 --- a/spec/approvals/cli/generate/upgrade +++ b/spec/approvals/cli/generate/upgrade @@ -3,7 +3,6 @@ skipped spec/tmp/src/download_command.sh (exists) skipped spec/tmp/src/upload_command.sh (exists) updated spec/tmp/src/lib/colors.sh updated spec/tmp/src/lib/config.sh -updated spec/tmp/src/lib/send_completions.sh updated spec/tmp/src/lib/validations/validate_dir_exists.sh updated spec/tmp/src/lib/validations/validate_file_exists.sh updated spec/tmp/src/lib/validations/validate_integer.sh diff --git a/spec/approvals/cli/generate/upgrade-path-mismatch b/spec/approvals/cli/generate/upgrade-path-mismatch index 548cb953..55c533bb 100644 --- a/spec/approvals/cli/generate/upgrade-path-mismatch +++ b/spec/approvals/cli/generate/upgrade-path-mismatch @@ -3,7 +3,6 @@ skipped spec/tmp/src/download_command.sh (exists) skipped spec/tmp/src/upload_command.sh (exists) warning not upgrading spec/tmp/src/lib/colors-b.sh, path mismatch updated spec/tmp/src/lib/config.sh -updated spec/tmp/src/lib/send_completions.sh updated spec/tmp/src/lib/validations/validate_dir_exists.sh updated spec/tmp/src/lib/validations/validate_file_exists.sh updated spec/tmp/src/lib/validations/validate_integer.sh diff --git a/spec/approvals/cli/generate/upgrade-unknown-lib b/spec/approvals/cli/generate/upgrade-unknown-lib index 918489de..d356d4c3 100644 --- a/spec/approvals/cli/generate/upgrade-unknown-lib +++ b/spec/approvals/cli/generate/upgrade-unknown-lib @@ -3,7 +3,6 @@ skipped spec/tmp/src/download_command.sh (exists) skipped spec/tmp/src/upload_command.sh (exists) warning not upgrading spec/tmp/src/lib/colors.sh, unknown library 'no-such-lib' updated spec/tmp/src/lib/config.sh -updated spec/tmp/src/lib/send_completions.sh updated spec/tmp/src/lib/validations/validate_dir_exists.sh updated spec/tmp/src/lib/validations/validate_file_exists.sh updated spec/tmp/src/lib/validations/validate_integer.sh diff --git a/spec/approvals/cli/shell/boot b/spec/approvals/cli/shell/boot index cf0c94fa..824c3148 100644 --- a/spec/approvals/cli/shell/boot +++ b/spec/approvals/cli/shell/boot @@ -7,7 +7,7 @@ Commands: generate Generate the bash script and required files add Add extra features and customization to your script doc Show bashly reference documentation - completions Install bash completions for bashly itself + completions Display bash completions for bashly itself render Render the bashly data structure using custom templates Help: bashly COMMAND --help diff --git a/spec/approvals/completion_builder/aliases_and_global_flags b/spec/approvals/completion_builder/aliases_and_global_flags deleted file mode 100644 index c9c16050..00000000 --- a/spec/approvals/completion_builder/aliases_and_global_flags +++ /dev/null @@ -1,25 +0,0 @@ ---- -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/default_command b/spec/approvals/completion_builder/default_command deleted file mode 100644 index 3f53cbd2..00000000 --- a/spec/approvals/completion_builder/default_command +++ /dev/null @@ -1,26 +0,0 @@ ---- -patterns: -- cli [root_get_default options] -- cli get [get options] -options: - root: - - "--help|-h" - - "--version|-v" - get: - - "--help|-h" - - "--source " - root_get_default: - - "--help|-h" - - "--version|-v" - - "--source " -tokens: - source: - - local - - remote - package: - - get - - hello - - world - get_package: - - hello - - world diff --git a/spec/approvals/completion_builder/root_options b/spec/approvals/completion_builder/root_options deleted file mode 100644 index 90bbb62e..00000000 --- a/spec/approvals/completion_builder/root_options +++ /dev/null @@ -1,8 +0,0 @@ ---- -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 deleted file mode 100644 index 4887a0dc..00000000 --- a/spec/approvals/completion_builder/source_resolution +++ /dev/null @@ -1,27 +0,0 @@ ---- -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 deleted file mode 100644 index 2e47386b..00000000 --- a/spec/approvals/completion_builder/token_collisions +++ /dev/null @@ -1,19 +0,0 @@ ---- -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 deleted file mode 100644 index a1354bd1..00000000 --- a/spec/approvals/completions/advanced +++ /dev/null @@ -1,30 +0,0 @@ ---- -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 deleted file mode 100644 index c1c1adec..00000000 --- a/spec/approvals/completions/completion_global_flags +++ /dev/null @@ -1,24 +0,0 @@ ---- -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 deleted file mode 100644 index 4193717b..00000000 --- a/spec/approvals/completions/completion_global_flags_nested +++ /dev/null @@ -1,16 +0,0 @@ ---- -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 deleted file mode 100644 index 39d4a6a7..00000000 --- a/spec/approvals/completions/completion_global_flags_root +++ /dev/null @@ -1,21 +0,0 @@ ---- -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/flag-allowed b/spec/approvals/completions/flag-allowed deleted file mode 100644 index f68424a0..00000000 --- a/spec/approvals/completions/flag-allowed +++ /dev/null @@ -1,6 +0,0 @@ ---- -some command*--protocol: &1 -- ssh -- sftp -- https -some command*-p: *1 diff --git a/spec/approvals/completions/flag-completions b/spec/approvals/completions/flag-completions deleted file mode 100644 index c3c2a619..00000000 --- a/spec/approvals/completions/flag-completions +++ /dev/null @@ -1,5 +0,0 @@ ---- -some command*--path: &1 -- "" -- README.md -some command*-p: *1 diff --git a/spec/approvals/completions/nested_aliases b/spec/approvals/completions/nested_aliases deleted file mode 100644 index 2d648cd3..00000000 --- a/spec/approvals/completions/nested_aliases +++ /dev/null @@ -1,24 +0,0 @@ ---- -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 deleted file mode 100644 index 368ba23c..00000000 --- a/spec/approvals/completions/pattern_sources +++ /dev/null @@ -1,29 +0,0 @@ ---- -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/simple b/spec/approvals/completions/simple deleted file mode 100644 index 8d4b71d8..00000000 --- a/spec/approvals/completions/simple +++ /dev/null @@ -1,9 +0,0 @@ ---- -patterns: -- get [root options] -options: - root: - - "--help|-h" - - "--version|-v" - - "--force" - - "--verbose" diff --git a/spec/approvals/completions/whitelist b/spec/approvals/completions/whitelist deleted file mode 100644 index 68346787..00000000 --- a/spec/approvals/completions/whitelist +++ /dev/null @@ -1,23 +0,0 @@ ---- -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 deleted file mode 100644 index 6c80b755..00000000 --- a/spec/approvals/examples/completions +++ /dev/null @@ -1,71 +0,0 @@ -+ bashly add completions --force -created src/lib/send_completions.sh - -In order to enable completions in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function. - -Your users can then run something like this to enable completions: - - $ eval "$(cli completions)" - -+ bashly generate -creating user files in src -skipped src/completions_command.sh (exists) -skipped src/download_command.sh (exists) -skipped src/upload_command.sh (exists) -created ./cli -run ./cli --help to test your bash script -+ ./cli -cli - Sample application with bash completions - -Usage: - cli COMMAND - cli [COMMAND] --help | -h - cli --version | -v - -Commands: - completions Generate bash completions - download Download a file - upload Upload a file - -+ ./cli -h -cli - Sample application with bash completions - -Usage: - cli COMMAND - cli [COMMAND] --help | -h - cli --version | -v - -Commands: - completions Generate bash completions - download Download a file - upload Upload a file - -Options: - --help, -h - Show this help - - --version, -v - Show version number - -+ ./cli completions -h -cli completions - - Generate bash completions - Usage: eval "$(cli completions)" - -Usage: - cli completions - cli completions --help | -h - -Options: - --help, -h - Show this help - -+ ./cli completions -+ head -n6 -# cli completion -*- shell-script -*- - -# This bash completions script was generated by -# completely (https://github.com/bashly-framework/completely) -# Modifying it manually is not recommended - diff --git a/spec/approvals/fixtures/completions-private b/spec/approvals/fixtures/completions-private deleted file mode 100644 index 8a4eb737..00000000 --- a/spec/approvals/fixtures/completions-private +++ /dev/null @@ -1,17 +0,0 @@ -+ bundle exec bashly add completions_yaml --force -created ./completions.yml - -This file can be converted to a completions script using the completely gem. - -+ cat completions.yml ---- -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/fixtures/lib-custom-path b/spec/approvals/fixtures/lib-custom-path index 02a3e73a..8bb2d9d5 100644 --- a/spec/approvals/fixtures/lib-custom-path +++ b/spec/approvals/fixtures/lib-custom-path @@ -29,12 +29,3 @@ skipped src/root_command.sh (exists) updated src/my-libz/colors.sh created ./cli run ./cli --help to test your bash script -+ bundle exec bashly add completions -created src/my-libz/send_completions.sh - -In order to enable completions in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function. - -Your users can then run something like this to enable completions: - - $ eval "$(cli completions)" - diff --git a/spec/approvals/fixtures/partials-extension b/spec/approvals/fixtures/partials-extension index 825e2278..bc26615f 100644 --- a/spec/approvals/fixtures/partials-extension +++ b/spec/approvals/fixtures/partials-extension @@ -20,15 +20,6 @@ src/initialize.sh. You may run the following command to add this file: $ bashly add hooks -+ bundle exec bashly add completions -created src/lib/send_completions.bash - -In order to enable completions in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function. - -Your users can then run something like this to enable completions: - - $ eval "$(cli completions)" - + bundle exec bashly add config created src/lib/config.bash created src/lib/ini.bash @@ -62,7 +53,6 @@ updated src/help_command.bash updated src/lib/colors.bash updated src/lib/config.bash updated src/lib/ini.bash -updated src/lib/send_completions.bash updated src/lib/validations/validate_dir_exists.bash updated src/lib/validations/validate_file_exists.bash updated src/lib/validations/validate_integer.bash diff --git a/spec/approvals/libraries/completions_function/message b/spec/approvals/libraries/completions_function/message deleted file mode 100644 index 25e8b6cb..00000000 --- a/spec/approvals/libraries/completions_function/message +++ /dev/null @@ -1,5 +0,0 @@ -In order to enable completions in your script, create a command or a flag (for example: g`download completions` or g`download --completions`) that calls the g`send_completions` function. - -Your users can then run something like this to enable completions: - - m`$ eval "$(download completions)"` diff --git a/spec/approvals/libraries/completions_script/message b/spec/approvals/libraries/completions_script/message deleted file mode 100644 index fef41de6..00000000 --- a/spec/approvals/libraries/completions_script/message +++ /dev/null @@ -1,3 +0,0 @@ -In order to enable completions, run: - - m`$ source spec/tmp/completions.bash` diff --git a/spec/approvals/libraries/completions_yaml/files b/spec/approvals/libraries/completions_yaml/files deleted file mode 100644 index 7dfe9b27..00000000 --- a/spec/approvals/libraries/completions_yaml/files +++ /dev/null @@ -1,14 +0,0 @@ ---- -- :path: spec/tmp/completions.yml - :content: | - --- - patterns: - - download [root options] - options: - root: - - "--help|-h" - - "--version|-v" - - "--force|-f" - tokens: - source: - target: diff --git a/spec/approvals/libraries/completions_yaml/message b/spec/approvals/libraries/completions_yaml/message deleted file mode 100644 index b2157db3..00000000 --- a/spec/approvals/libraries/completions_yaml/message +++ /dev/null @@ -1 +0,0 @@ -This file can be converted to a completions script using the g`completely` gem. diff --git a/spec/bashly/commands/add_spec.rb b/spec/bashly/commands/add_spec.rb index 1d940745..4076c7d3 100644 --- a/spec/bashly/commands/add_spec.rb +++ b/spec/bashly/commands/add_spec.rb @@ -58,39 +58,6 @@ end end - describe 'add completions' do - before { reset_tmp_dir init: true } - - it 'creates lib/send_completions.sh' do - expect { subject.execute %w[add completions] }.to output_approval('cli/add/comp-function') - content = File.read("#{source_dir}/lib/send_completions.sh") - - expect(content).to include '## [@bashly-upgrade completions send_completions]' - expect(content).to include 'send_completions() {' - expect(content).to include 'generated by' - expect(content).to include 'completely' - end - end - - describe 'add completions_script' do - it 'creates completions.bash' do - expect { subject.execute %w[add completions_script] }.to output_approval('cli/add/comp-script') - content = File.read("#{target_dir}/completions.bash") - - expect(content).to include '# cli completion' - expect(content).to include 'generated by' - expect(content).to include 'completely' - expect(content).to include 'complete -F _cli_completions cli' - end - end - - describe 'add completions_yaml' do - it 'creates completions.yml' do - expect { subject.execute %w[add completions_yaml] }.to output_approval('cli/add/comp-yaml') - expect(File.read("#{target_dir}/completions.yml")).to match_approval('cli/add/comp-yaml-file') - end - end - describe 'add config' do let(:lib_file) { "#{source_dir}/lib/config.sh" } diff --git a/spec/bashly/commands/completions_spec.rb b/spec/bashly/commands/completions_spec.rb index 22247298..7b191f1e 100644 --- a/spec/bashly/commands/completions_spec.rb +++ b/spec/bashly/commands/completions_spec.rb @@ -1,26 +1,8 @@ describe Commands::Completions do subject { described_class.new } - let(:leeway) { RUBY_VERSION < '3.2.0' ? 0 : 5 } let(:completions_path) { File.expand_path 'lib/bashly/completions/bashly-completions.bash' } let(:completions_script) { File.read completions_path } - let :mock_installer do - instance_double Completely::Installer, - install: true, - uninstall: true, - target_path: 'some-target-path', - script_path: 'some-script-path', - install_command_string: 'cp source target', - uninstall_command_string: 'rm -f some files' - end - - describe '#installer' do - it 'returns a properly configured Completely::Installer instance' do - expect(subject.installer).to be_a Completely::Installer - expect(subject.installer.program).to eq 'bashly' - expect(subject.installer.script_path).to end_with '/bashly-completions.bash' - end - end describe 'completions --help' do it 'shows long usage' do @@ -36,43 +18,4 @@ end end - describe 'completions --install' do - it 'installs the completions script to the completions directory' do - allow(subject).to receive(:installer).and_return mock_installer - - expect { subject.execute %w[completions --install] } - .to output_approval('cli/completions/install') - end - - context 'when the installer fails' do - it 'raises an error' do - allow(subject).to receive(:installer).and_return mock_installer - allow(mock_installer).to receive(:install).and_return(false) - - expect { subject.execute %w[completions --install] } - .to raise_approval('cli/completions/install-error') - .diff(leeway) - end - end - end - - describe 'completions --uninstall' do - it 'uninstalls the completions script from all completions directories' do - allow(subject).to receive(:installer).and_return mock_installer - - expect { subject.execute %w[completions --uninstall] } - .to output_approval('cli/completions/uninstall') - end - - context 'when the installer fails' do - it 'raises an error' do - allow(subject).to receive(:installer).and_return mock_installer - allow(mock_installer).to receive(:uninstall).and_return(false) - - expect { subject.execute %w[completions --uninstall] } - .to raise_approval('cli/completions/uninstall-error') - .diff(leeway) - end - end - end end diff --git a/spec/bashly/completion_builder_spec.rb b/spec/bashly/completion_builder_spec.rb deleted file mode 100644 index ff0f7d10..00000000 --- a/spec/bashly/completion_builder_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -describe CompletionBuilder do - describe '#call' do - load_fixture('completion_builder').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 - - context 'with a default command' do - let(:fixtures) { load_fixture('completion_builder') } - let(:command) { Script::Command.new fixtures[:default_command]['command'] } - let(:data) { described_class.new(command).call } - - it 'adds default command argument completions to the parent command route' do - expect(data['patterns']).to include( - 'cli [root_get_default options] ', - 'cli get [get options] ' - ) - - expect(data['tokens']['package']).to eq %w[get hello world] - expect(data['tokens']['get_package']).to eq %w[hello world] - end - end - - context 'with a negatable flag' do - let(:command) do - Script::Command.new( - 'name' => 'cli', - 'flags' => [ - { 'long' => '--color', 'short' => '-c', 'negatable' => true }, - ] - ) - end - - it 'adds the negated long option to the same option entry' do - data = described_class.new(command).call - - expect(data['options']['root']).to include '--color|--no-color|-c' - end - end -end diff --git a/spec/bashly/concerns/completions_command_spec.rb b/spec/bashly/concerns/completions_command_spec.rb deleted file mode 100644 index 93e030c4..00000000 --- a/spec/bashly/concerns/completions_command_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -describe Script::Command do - subject { described_class.new fixtures[fixture] } - - let(:fixtures) { load_fixture('script/commands') } - let(:fixture) { :completions_simple } - let(:completion_generator) { instance_double Completely::Completions } - - describe '#completion_data' do - it 'returns a data structure for completely' do - expect(subject.completion_data.to_yaml).to match_approval('completions/simple') - end - end - - describe '#completion_function' do - before do - allow(Completely::Completions).to receive(:new) - .with(subject.completion_data) - .and_return completion_generator - allow(completion_generator).to receive(:wrapper_function) - .with('custom_name') - .and_return 'wrapped completion script' - end - - it 'returns the generated bash completion script wrapped in a function' do - expect(subject.completion_function('custom_name')) - .to eq 'wrapped completion script' - end - end - - context 'with a more complex command' do - let(:fixture) { :completions_advanced } - - describe '#completion_data' do - it 'returns a data structure for completely' do - expect(subject.completion_data.to_yaml) - .to match_approval('completions/advanced') - end - end - - describe '#completion_script' do - before do - allow(Completely::Completions).to receive(:new) - .with(subject.completion_data) - .and_return completion_generator - allow(completion_generator).to receive(:script) - .and_return 'completion script' - end - - it 'returns the generated bash completion script' do - expect(subject.completion_script) - .to eq 'completion script' - end - end - end - - context 'with a command that uses whitelist args' do - let(:fixture) { :completions_whitelist } - - describe '#completion_data' do - it 'returns a data structure that includes the whitelist' do - expect(subject.completion_data.to_yaml) - .to match_approval('completions/whitelist') - end - 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 } - - describe '#completion_data' do - it 'returns a data structure that includes all command full names' do - expect(subject.completion_data.to_yaml) - .to match_approval('completions/nested_aliases') - end - end - end - - context 'with a command that has global flags on the root command' do - let(:fixture) { :completions_global_flags_root } - - describe '#completion_data' do - it 'returns a data structure that includes all command full names' do - expect(subject.completion_data.to_yaml) - .to match_approval('completions/completion_global_flags_root') - end - end - end - - context 'with a command that has global flags on a nested command' do - let(:fixture) { :completions_global_flags_nested } - - describe '#completion_data' do - it 'returns a data structure that includes all command full names' do - expect(subject.completion_data.to_yaml) - .to match_approval('completions/completion_global_flags_nested') - end - end - end - - context 'with a command that has global flags on the root and a nested command' do - let(:fixture) { :completions_global_flags } - - describe '#completion_data' do - it 'returns a data structure that includes all command full names' do - expect(subject.completion_data.to_yaml) - .to match_approval('completions/completion_global_flags') - end - end - end -end diff --git a/spec/bashly/concerns/completions_flag_spec.rb b/spec/bashly/concerns/completions_flag_spec.rb deleted file mode 100644 index cea6cb2e..00000000 --- a/spec/bashly/concerns/completions_flag_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe Script::Flag do - subject { described_class.new fixtures[fixture] } - - let(:fixtures) { load_fixture 'script/flags' } - let(:fixture) { :basic_flag } - let(:command) { 'some command' } - - describe '#completion_data' do - context 'when the flag has allowed defined' do - let(:fixture) { :completions_allowed } - - it 'returns a data structure for completely with the allowed list' do - expect(subject.completion_data(command).to_yaml).to match_approval('completions/flag-allowed') - end - end - - context 'when the flag has completions defined' do - let(:fixture) { :completions_completions } - - it 'returns a data structure for completely with the completions list' do - expect(subject.completion_data(command).to_yaml).to match_approval('completions/flag-completions') - end - end - - context 'when the flag does not have allowed or completions' do - it 'returns an empty hash' do - expect(subject.completion_data(command)).to eq({}) - end - end - end - - describe '#completion_option_entry' do - context 'when the flag has aliases' do - let(:fixture) { :aliases } - - it 'includes all aliases in the option entry' do - expect(subject.completion_option_entry('name')).to eq '--container|-c|--pod|-p ' - end - end - end -end diff --git a/spec/bashly/extensions/yaml_spec.rb b/spec/bashly/extensions/yaml_spec.rb index 13287010..68a647f3 100644 --- a/spec/bashly/extensions/yaml_spec.rb +++ b/spec/bashly/extensions/yaml_spec.rb @@ -1,12 +1,7 @@ -require 'completely' - describe YAML do describe '::trusted_load' do it 'does not override YAML.load for other gems' do - completions = Completely::Completions.load 'spec/fixtures/completely/pattern.yml' - - expect(completions.config).to be_a Completely::PatternConfig - expect(completions).to be_valid + expect(described_class.load('name: bashly')).to eq 'name' => 'bashly' end it 'falls back to YAML.load when unsafe_load is not available' do @@ -18,10 +13,10 @@ end describe '::trusted_load_file' do - let(:path) { 'spec/fixtures/completely/pattern.yml' } + let(:path) { 'spec/fixtures/script/commands.yml' } it 'loads a trusted YAML file' do - expect(described_class.trusted_load_file(path)).to include 'patterns' + expect(described_class.trusted_load_file(path)).to include :basic_command end end diff --git a/spec/bashly/libraries/completions_function_spec.rb b/spec/bashly/libraries/completions_function_spec.rb deleted file mode 100644 index 56c1e8f5..00000000 --- a/spec/bashly/libraries/completions_function_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -describe Libraries::CompletionsFunction do - subject { described_class.new(*args) } - - let(:args) { nil } - - before { reset_tmp_dir example: 'minimal' } - - describe '#files' do - it 'returns one file at the default path' do - files = subject.files - - expect(files.size).to eq 1 - expect(files.first[:path]).to eq "#{Settings.source_dir}/lib/send_completions.sh" - end - - it 'returns the completions function content' do - content = subject.files.first[:content] - - expect(content).to include '## [@bashly-upgrade completions send_completions]' - expect(content).to include 'send_completions() {' - expect(content).to include 'generated by' - expect(content).to include 'completely' - end - - context 'with an argument' do - let(:args) { ['my_function'] } - - it 'uses the first argument in the filename of [:path]' do - expect(subject.files.first[:path]).to eq "#{Settings.source_dir}/lib/#{args.first}.sh" - end - - it 'uses the first argument as the function of [:content]' do - expect(subject.files.first[:content]).to include 'my_function()' - expect(subject.files.first[:content]).to include '[@bashly-upgrade completions my_function]' - end - end - end - - describe '#post_install_message' do - it 'returns a message' do - expect(subject.post_install_message).to match_approval('libraries/completions_function/message') - end - end -end diff --git a/spec/bashly/libraries/completions_script_spec.rb b/spec/bashly/libraries/completions_script_spec.rb deleted file mode 100644 index 8cc867a4..00000000 --- a/spec/bashly/libraries/completions_script_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -describe Libraries::CompletionsScript do - subject { described_class.new(*args) } - - let(:args) { nil } - - before { reset_tmp_dir example: 'minimal' } - - describe '#files' do - it 'returns one file at the default path' do - files = subject.files - - expect(files.size).to eq 1 - expect(files.first[:path]).to eq "#{Settings.target_dir}/completions.bash" - end - - it 'returns the completions script content' do - content = subject.files.first[:content] - - expect(content).to include '# download completion' - expect(content).to include 'generated by' - expect(content).to include 'completely' - expect(content).to include 'complete -F _download_completions download' - end - - context 'with an argument' do - let(:args) { ['filename.bash'] } - - it 'uses the first argument as a filename' do - expect(subject.files.first[:path]).to eq args.first - end - end - end - - describe '#post_install_message' do - it 'returns a message' do - expect(subject.post_install_message).to match_approval('libraries/completions_script/message') - end - end -end diff --git a/spec/bashly/libraries/completions_yaml_spec.rb b/spec/bashly/libraries/completions_yaml_spec.rb deleted file mode 100644 index fdc93b53..00000000 --- a/spec/bashly/libraries/completions_yaml_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -describe Libraries::CompletionsYAML do - subject { described_class.new(*args) } - - let(:args) { nil } - - before { reset_tmp_dir example: 'minimal' } - - describe '#files' do - it 'returns an array with a single hash' do - expect(subject.files.to_yaml).to match_approval('libraries/completions_yaml/files') - end - - context 'with an argument' do - let(:args) { ['filename.yml'] } - - it 'uses the first argument as a filename' do - expect(subject.files.first[:path]).to eq args.first - end - end - end - - describe '#post_install_message' do - it 'returns a message' do - expect(subject.post_install_message).to match_approval('libraries/completions_yaml/message') - end - end -end diff --git a/spec/bashly/library_source_spec.rb b/spec/bashly/library_source_spec.rb index ffdc3a03..c0d2e5ac 100644 --- a/spec/bashly/library_source_spec.rb +++ b/spec/bashly/library_source_spec.rb @@ -100,8 +100,7 @@ it 'returns all libraries as keys' do expect(subject.libraries.keys).to match_array %i[ - colors completions completions_script completions_yaml config - help hooks ini lib settings stacktrace strings validations yaml + colors config help hooks ini lib settings stacktrace strings validations yaml render_markdown render_markdown_github render_mandoc ] end diff --git a/spec/bashly/library_spec.rb b/spec/bashly/library_spec.rb index 269daf02..069de589 100644 --- a/spec/bashly/library_spec.rb +++ b/spec/bashly/library_spec.rb @@ -17,17 +17,6 @@ expect(matter[:content]).to eq File.read("#{lib_dir}/colors.sh") end - context 'when the library has a custom handler' do - let(:name) { :completions } - - before { reset_tmp_dir example: 'minimal' } - - it 'delegaes the request to a custom handler' do - expect(subject.files).to be_an Array - expect(subject.files.first).to be_a Hash - expect(subject.files.first.keys).to match_array %i[path content] - end - end end describe '#post_install_message' do @@ -48,15 +37,6 @@ end end - context 'when the library has a custom handler' do - let(:name) { :completions_yaml } - - before { reset_tmp_dir example: 'minimal' } - - it 'returns the message form the handler' do - expect(subject.post_install_message).to include 'completely' - end - end end describe '#find_file' do diff --git a/spec/bashly/script/command_spec.rb b/spec/bashly/script/command_spec.rb index df84b888..e95ce326 100644 --- a/spec/bashly/script/command_spec.rb +++ b/spec/bashly/script/command_spec.rb @@ -19,7 +19,6 @@ Script::Introspection::Flags, Script::Introspection::Variables, Script::Introspection::Visibility, - Completions::Command, ] expect(described_class.ancestors).to include(*modules) end diff --git a/spec/bashly/script/flag_spec.rb b/spec/bashly/script/flag_spec.rb index 07b319cc..85b40a3f 100644 --- a/spec/bashly/script/flag_spec.rb +++ b/spec/bashly/script/flag_spec.rb @@ -9,8 +9,7 @@ describe 'composition' do it 'includes the necessary modules' do modules = [ - Script::Introspection::Visibility, Script::Introspection::Validate, - Completions::Flag + Script::Introspection::Visibility, Script::Introspection::Validate ] expect(described_class.ancestors).to include(*modules) end diff --git a/spec/fixtures/completely/pattern.yml b/spec/fixtures/completely/pattern.yml deleted file mode 100644 index d1e51d6c..00000000 --- a/spec/fixtures/completely/pattern.yml +++ /dev/null @@ -1,14 +0,0 @@ -patterns: -- bashly [root options] -- bashly generate [generate options] - -options: - root: - - --help|-h - generate: - - --env|-e - -tokens: - env: - - development - - production diff --git a/spec/fixtures/completion_builder.yml b/spec/fixtures/completion_builder.yml deleted file mode 100644 index c967faa5..00000000 --- a/spec/fixtures/completion_builder.yml +++ /dev/null @@ -1,73 +0,0 @@ -:root_options: - with_version: false - command: - name: cli - flags: - - long: --force - short: -f - - long: --verbose - -:source_resolution: - command: - name: cli - commands: - - name: upload - completions: [, README.md] - args: - - name: source - completions: [, README.md, $(git branch), +literal] - - name: target - - name: extra - flags: - - long: --user - short: -u - arg: user - completions: [] - - long: --tag - arg: tag - -:aliases_and_global_flags: - command: - name: cli - flags: - - long: --debug - commands: - - name: images - alias: img - flags: - - long: --verbose - commands: - - name: list - alias: ls - flags: - - long: --env - arg: env - allowed: [prod, dev] - -:default_command: - command: - name: cli - commands: - - name: get - default: true - args: - - name: package - completions: [hello, world] - flags: - - long: --source - arg: source - completions: [local, remote] - -:token_collisions: - command: - name: cli - flags: - - long: --file - allowed: [one] - - long: --root-file - allowed: [two] - - long: --root-file-2 - allowed: [three] - args: - - name: file - allowed: [four] diff --git a/spec/fixtures/script/commands.yml b/spec/fixtures/script/commands.yml index 9a753331..89731821 100644 --- a/spec/fixtures/script/commands.yml +++ b/spec/fixtures/script/commands.yml @@ -57,117 +57,6 @@ help: Any additional argument or flag required: true -:completions_simple: - name: get - completions: - - - - flags: - - long: --force - - long: --verbose - -:completions_advanced: - name: say - commands: - - name: hello - commands: - - name: world - completions: - - - - - flags: - - long: --force - - long: --verbose - - name: goodbye - commands: - - name: universe - completions: - - $(git branch) - flags: - - long: --color - short: -c - allowed: [green, red] - - long: --path - completions: [] - - long: --verbose - short: -v - -:completions_whitelist: - name: download - args: - - name: protocol - allowed: [https, ssh] - - name: port - allowed: ["80", "22", "3000"] - flags: - - long: --role - arg: name - allowed: [user, admin] - - long: --method - arg: name - allowed: [get, post] - -:completions_pattern_sources: - name: cli - commands: - - name: upload - alias: up - completions: [, README.md] - args: - - name: source - completions: [, README.md, $(git branch), +literal] - - name: target - repeatable: true - flags: - - long: --user - short: -u - arg: user - completions: [] - - long: --tag - arg: tag - - long: --verbose - repeatable: true - - name: inspect - args: - - name: object - -:completions_global_flags: - name: cli - flags: - - long: --debug - commands: - - name: images - flags: - - long: --verbose - commands: - - name: ls - flags: - - long: --env - arg: env - allowed: [prod, dev] - -:completions_global_flags_nested: - name: cli - commands: - - name: images - flags: - - long: --verbose - commands: - - name: ls - -:completions_global_flags_root: - name: cli - flags: - - long: --debug - commands: - - name: images - commands: - - name: ls - flags: - - long: --env - arg: env - allowed: [prod, dev] - :custom_filename: name: run filename: ops/run_command.sh diff --git a/spec/fixtures/workspaces/completions-private/.gitignore b/spec/fixtures/workspaces/completions-private/.gitignore deleted file mode 100644 index 53749eba..00000000 --- a/spec/fixtures/workspaces/completions-private/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -completions.yml -private -src/*.sh diff --git a/spec/fixtures/workspaces/completions-private/README.md b/spec/fixtures/workspaces/completions-private/README.md deleted file mode 100644 index 3f25a619..00000000 --- a/spec/fixtures/workspaces/completions-private/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This fixture tests that private commands and flags are not added to the -generated completions scripts. - -Reference issue: https://github.com/bashly-framework/bashly/issues/388 diff --git a/spec/fixtures/workspaces/completions-private/src/bashly.yml b/spec/fixtures/workspaces/completions-private/src/bashly.yml deleted file mode 100644 index 0ca88613..00000000 --- a/spec/fixtures/workspaces/completions-private/src/bashly.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: private -help: Test private commands - -commands: -- name: connect - alias: c - help: Connect to the metaverse - flags: - - long: --force - short: -f - # Private flag below - should not be present in completions - - long: --hidden-flag - short: -d - private: true - -# Private commands below - should not be present in completions -- name: connect-ftp - help: Connect via FTP - alias: cf - private: true -- name: connect-ssh - help: Connect via SSH - alias: cs - private: true diff --git a/spec/fixtures/workspaces/completions-private/test.sh b/spec/fixtures/workspaces/completions-private/test.sh deleted file mode 100644 index ce8e9a36..00000000 --- a/spec/fixtures/workspaces/completions-private/test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -x -bundle exec bashly add completions_yaml --force -cat completions.yml \ No newline at end of file diff --git a/spec/fixtures/workspaces/lib-custom-path/test.sh b/spec/fixtures/workspaces/lib-custom-path/test.sh index 56cc4c6a..5839ac89 100644 --- a/spec/fixtures/workspaces/lib-custom-path/test.sh +++ b/spec/fixtures/workspaces/lib-custom-path/test.sh @@ -16,4 +16,3 @@ bundle exec bashly add colors bundle exec bashly generate ./cli bundle exec bashly generate --upgrade -bundle exec bashly add completions diff --git a/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh b/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh deleted file mode 100644 index 23db3f9c..00000000 --- a/spec/fixtures/workspaces/lib-upgrade/src/lib/send_completions.sh +++ /dev/null @@ -1,177 +0,0 @@ -## [@bashly-upgrade completions send_completions] -send_completions() { - echo $'# cli completion -*- shell-script -*-' - echo $'' - echo $'# This bash completions script was generated by' - echo $'# completely (https://github.com/bashly-framework/completely)' - echo $'# Modifying it manually is not recommended' - echo $'' - echo $'_cli_completions_node_flag_state() {' - echo $' case "$1:$2" in' - echo $' 0:--help|0:-h) return 0 ;;' - echo $' 0:--version|0:-v) return 0 ;;' - echo $' 1:--help|1:-h) return 0 ;;' - echo $' 1:--force|1:-f) return 0 ;;' - echo $' 2:--user|2:-u) return 2 ;;' - echo $' 2:--password|2:-p) return 2 ;;' - echo $' 2:--help|2:-h) return 0 ;;' - echo $' esac' - echo $'' - echo $' return 1' - echo $'}' - echo $'' - echo $'_cli_completions_option_seen() {' - echo $' local completed_option option_name' - echo $' for completed_option in "${completed_options[@]}"; do' - echo $' for option_name in "$@"; do' - echo $' [[ "$completed_option" == "$option_name" ]] && return 0' - echo $' done' - echo $' done' - echo $'' - echo $' return 1' - echo $'}' - echo $'' - echo $'_cli_completions_resolve_node() {' - echo $' node_id=0' - echo $' node_word_count=0' - echo $' positional_index=0' - echo $'' - echo $' local word' - echo $' for word in "${non_options[@]}"; do' - echo $' case "$node_id:$word" in' - echo $' 0:download)' - echo $' node_id=1' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:d)' - echo $' node_id=1' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:upload)' - echo $' node_id=2' - echo $' node_word_count=1' - echo $' ;;' - echo $' 0:u)' - echo $' node_id=2' - echo $' node_word_count=1' - echo $' ;;' - echo $' *)' - echo $' break' - echo $' ;;' - echo $' esac' - echo $' done' - echo $'' - echo $' positional_index=$((${#non_options[@]} - node_word_count))' - echo $'}' - echo $'' - echo $'_cli_completions() {' - echo $' local cur=${COMP_WORDS[COMP_CWORD]}' - echo $' local prev=' - echo $' if ((COMP_CWORD > 0)); then' - echo $' prev=${COMP_WORDS[$((COMP_CWORD - 1))]}' - echo $' fi' - echo $'' - echo $' local completed=()' - echo $' if ((COMP_CWORD > 1)); then' - echo $' completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' - echo $' fi' - echo $'' - echo $' local non_options=()' - echo $' local completed_options=()' - echo $' local node_id=' - echo $' local node_word_count=-1' - echo $' local positional_index=0' - echo $' local invalid_completion=0' - echo $' local flag_state=0' - echo $' _cli_completions_resolve_node' - echo $'' - 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 $' _cli_completions_node_flag_state "$node_id" "$word"' - echo $' flag_state=$?' - echo $' if (( flag_state == 1 )); then' - echo $' invalid_completion=1' - echo $' break' - echo $' fi' - echo $'' - echo $' completed_options+=("$word")' - echo $' if (( flag_state == 2 )); then' - echo $' skip_next=1' - echo $' fi' - echo $' continue' - echo $' fi' - echo $'' - echo $' non_options+=("$word")' - echo $' _cli_completions_resolve_node' - echo $' done' - echo $'' - echo $' COMPREPLY=()' - echo $' (( invalid_completion )) && return' - echo $'' - echo $' case "$node_id:$prev" in' - echo $' 2:--user|2:-u)' - echo $' return' - echo $' ;;' - echo $' 2:--password|2:-p)' - echo $' return' - echo $' ;;' - echo $' esac' - echo $'' - echo $' if [[ "${cur:0:1}" != "-" ]] && (( positional_index == 0 )); then' - echo $' case "$node_id" in' - echo $' 0)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "download d upload u" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' esac' - echo $' fi' - echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' case "$node_id" in' - echo $' 0)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' _cli_completions_option_seen "--version" "-v" || words+=("--version" "-v")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 1)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' _cli_completions_option_seen "--force" "-f" || words+=("--force" "-f")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' 2)' - echo $' local words=()' - echo $' _cli_completions_option_seen "--help" "-h" || words+=("--help" "-h")' - echo $' _cli_completions_option_seen "--user" "-u" || words+=("--user" "-u")' - echo $' _cli_completions_option_seen "--password" "-p" || words+=("--password" "-p")' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")' - echo $' return' - echo $' ;;' - echo $' esac' - echo $' fi' - echo $'' - echo $' case "$node_id:$positional_index" in' - echo $' 1:0)' - echo $' return' - echo $' ;;' - echo $' 1:1)' - echo $' return' - echo $' ;;' - echo $' 2:0)' - echo $' return' - echo $' ;;' - echo $' esac' - echo $'} &&' - echo $' complete -F _cli_completions cli' - echo $'' - echo $'# ex: filetype=sh' -} \ No newline at end of file diff --git a/spec/fixtures/workspaces/partials-extension/test.sh b/spec/fixtures/workspaces/partials-extension/test.sh index 7a317f16..ff38971f 100644 --- a/spec/fixtures/workspaces/partials-extension/test.sh +++ b/spec/fixtures/workspaces/partials-extension/test.sh @@ -16,7 +16,6 @@ bundle exec bashly generate # Add all libraries known to mankind (those that generate .sh files) bundle exec bashly add colors -bundle exec bashly add completions bundle exec bashly add config bundle exec bashly add help bundle exec bashly add lib @@ -29,4 +28,4 @@ bundle exec bashly generate --upgrade # Finally, verify the script actually works ./cli ./cli --help -./cli download something \ No newline at end of file +./cli download something diff --git a/support/runfile/examples.runfile b/support/runfile/examples.runfile index 213d6c3a..7032f889 100644 --- a/support/runfile/examples.runfile +++ b/support/runfile/examples.runfile @@ -7,7 +7,6 @@ action :regen do |args| ENV['PATH']="#{Dir.pwd}/examples/extensible:#{ENV['PATH']}" blacklist = %w[ - spec/fixtures/workspaces/completions-private spec/fixtures/workspaces/custom-paths spec/fixtures/workspaces/import spec/fixtures/workspaces/lib-custom-source diff --git a/support/runfile/static.runfile b/support/runfile/static.runfile index 523f6cb3..15421021 100644 --- a/support/runfile/static.runfile +++ b/support/runfile/static.runfile @@ -7,7 +7,6 @@ action do |args| examples/render-mandoc examples/render-markdown examples/settings - spec/fixtures/workspaces/completions-private spec/fixtures/workspaces/custom-paths spec/fixtures/workspaces/import spec/fixtures/workspaces/lib-custom-source From 9f4582fc5605a3cc9297f1b45d638e7ff130c3a5 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 10:27:17 +0300 Subject: [PATCH 08/23] - Generate `send_completions()` function natively when completions generation is enabled --- examples/README.md | 1 + examples/catch-all/README.md | 6 +- examples/completions/.gitignore | 1 + examples/completions/README.md | 103 ++++++++++++++++++ examples/completions/settings.yml | 1 + examples/completions/src/bashly.yml | 23 ++++ .../completions/src/completions_command.sh | 1 + examples/completions/src/download_command.sh | 5 + examples/completions/test.sh | 11 ++ examples/default-values/README.md | 8 +- examples/dependencies-alt/README.md | 10 ++ examples/footer/README.md | 2 +- examples/help-header-override/README.md | 2 +- examples/minimal/README.md | 2 +- examples/minus-v/README.md | 6 +- examples/render-mandoc/README.md | 32 +++--- examples/stacktrace/README.md | 4 +- examples/validations/README.md | 14 ++- examples/whitelist/README.md | 4 +- .../views/command/completion_script.gtx | 15 +++ .../views/command/completion_script_bash.gtx | 28 +++++ lib/bashly/views/command/completions.gtx | 1 + spec/approvals/examples/completions | 19 ++++ .../integration/runtime_completions_spec.rb | 47 ++++++++ 24 files changed, 312 insertions(+), 34 deletions(-) create mode 100644 examples/completions/.gitignore create mode 100644 examples/completions/README.md create mode 100644 examples/completions/settings.yml create mode 100644 examples/completions/src/bashly.yml create mode 100644 examples/completions/src/completions_command.sh create mode 100644 examples/completions/src/download_command.sh create mode 100644 examples/completions/test.sh create mode 100644 lib/bashly/views/command/completion_script.gtx create mode 100644 lib/bashly/views/command/completion_script_bash.gtx create mode 100644 spec/approvals/examples/completions diff --git a/examples/README.md b/examples/README.md index 72ac9aae..aff2a361 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,6 +21,7 @@ Each of these examples demonstrates one aspect or feature of bashly. - [default-values](default-values#readme) - arguments and flags with default values - [minus-v](minus-v#readme) - using `-v` and `-h` in your script - [multiline](multiline#readme) - help messages with multiple lines +- [completions](completions#readme) - exposing runtime shell completions ## Advanced configuration features diff --git a/examples/catch-all/README.md b/examples/catch-all/README.md index 49189fbc..1e1f2642 100644 --- a/examples/catch-all/README.md +++ b/examples/catch-all/README.md @@ -79,7 +79,7 @@ Arguments: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. -# The code you write here will be wrapped by a function named 'download_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[message]} = something @@ -92,7 +92,7 @@ args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. -# The code you write here will be wrapped by a function named 'download_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[message]} = something @@ -111,7 +111,7 @@ other_args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. -# The code you write here will be wrapped by a function named 'download_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--debug]} = 1 diff --git a/examples/completions/.gitignore b/examples/completions/.gitignore new file mode 100644 index 00000000..573c0c4f --- /dev/null +++ b/examples/completions/.gitignore @@ -0,0 +1 @@ +cli diff --git a/examples/completions/README.md b/examples/completions/README.md new file mode 100644 index 00000000..0f135541 --- /dev/null +++ b/examples/completions/README.md @@ -0,0 +1,103 @@ +# Runtime Completions Example + +Demonstrates how to expose the generated `send_completions` function through +an application command. Runtime completions are enabled in `settings.yml`. + +Users can load the Bash wrapper with: + +```bash +source <(cli completions) +``` + +This example was generated with: + +```bash +$ bashly init +# ... now edit src/bashly.yml to match the example ... +# ... now edit settings.yml to match the example ... +$ bashly generate +# ... now edit completions_command.sh to match the example ... +$ bashly generate +``` + + + +----- + +## `bashly.yml` + +````yaml +name: cli +help: Runtime completions example +version: 0.1.0 + +commands: +- name: completions + help: Generate a shell completion script + args: + - name: shell + help: Shell to generate completions for + allowed: [bash] + default: bash + +- name: download + help: Download a file + args: + - name: source + help: URL to download + required: true + flags: + - long: --force + short: -f + help: Overwrite an existing file +```` + +## `settings.yml` + +````yaml +enable_completions: always + +```` + +## `src/completions_command.sh` + +````bash +send_completions "${args[shell]}" + +```` + + +## Output + +### `$ ./cli completions | head -n3` + +````shell +_cli_completions() { + local completion_command="${COMP_WORDS[0]}" + local completion_current="${COMP_WORDS[COMP_CWORD]:-}" + + +```` + +### `$ ./cli __complete` + +````shell +completions +download + + +```` + +### `$ ./cli __complete download -` + +````shell +--help +-h +--force +-f + + +```` + + + diff --git a/examples/completions/settings.yml b/examples/completions/settings.yml new file mode 100644 index 00000000..c9337546 --- /dev/null +++ b/examples/completions/settings.yml @@ -0,0 +1 @@ +enable_completions: always diff --git a/examples/completions/src/bashly.yml b/examples/completions/src/bashly.yml new file mode 100644 index 00000000..62f82689 --- /dev/null +++ b/examples/completions/src/bashly.yml @@ -0,0 +1,23 @@ +name: cli +help: Runtime completions example +version: 0.1.0 + +commands: +- name: completions + help: Generate a shell completion script + args: + - name: shell + help: Shell to generate completions for + allowed: [bash] + default: bash + +- name: download + help: Download a file + args: + - name: source + help: URL to download + required: true + flags: + - long: --force + short: -f + help: Overwrite an existing file diff --git a/examples/completions/src/completions_command.sh b/examples/completions/src/completions_command.sh new file mode 100644 index 00000000..e981385f --- /dev/null +++ b/examples/completions/src/completions_command.sh @@ -0,0 +1 @@ +send_completions "${args[shell]}" diff --git a/examples/completions/src/download_command.sh b/examples/completions/src/download_command.sh new file mode 100644 index 00000000..308ab9cf --- /dev/null +++ b/examples/completions/src/download_command.sh @@ -0,0 +1,5 @@ +echo "# This file is located at 'src/download_command.sh'." +echo "# It contains the implementation for the 'cli download' command." +echo "# The code you write here will be wrapped by a function named 'cli_download_command()'." +echo "# Feel free to edit this file; your changes will persist when regenerating." +inspect_args diff --git a/examples/completions/test.sh b/examples/completions/test.sh new file mode 100644 index 00000000..df30d428 --- /dev/null +++ b/examples/completions/test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -x + +bashly generate + +### Try Me ### + +./cli completions | head -n3 +./cli __complete +./cli __complete download - diff --git a/examples/default-values/README.md b/examples/default-values/README.md index b7c7984c..e2c8223f 100644 --- a/examples/default-values/README.md +++ b/examples/default-values/README.md @@ -51,7 +51,7 @@ examples: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'convert' command. -# The code you write here will be wrapped by a function named 'convert_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--format]} = png @@ -100,7 +100,7 @@ Examples: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'convert' command. -# The code you write here will be wrapped by a function named 'convert_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--format]} = png @@ -114,7 +114,7 @@ args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'convert' command. -# The code you write here will be wrapped by a function named 'convert_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--format]} = pdf @@ -128,7 +128,7 @@ args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'convert' command. -# The code you write here will be wrapped by a function named 'convert_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--format]} = gif diff --git a/examples/dependencies-alt/README.md b/examples/dependencies-alt/README.md index 269dfd7c..954fd546 100644 --- a/examples/dependencies-alt/README.md +++ b/examples/dependencies-alt/README.md @@ -52,6 +52,16 @@ commands: ### `$ ./cli download` ````shell +# This file is located at 'src/download_command.sh'. +# It contains the implementation for the 'cli download' command. +# The code you write here will be wrapped by a function named 'cli_download_command()'. +# Feel free to edit this file; your changes will persist when regenerating. +args: none + +deps: +- ${deps[git]} = /usr/bin/git +- ${deps[http_client]} = /usr/bin/curl +- ${deps[ruby]} = /home/vagrant/.local/share/rv/rubies/ruby-4.0.1/bin/ruby ```` diff --git a/examples/footer/README.md b/examples/footer/README.md index af61861c..bcd1ead7 100644 --- a/examples/footer/README.md +++ b/examples/footer/README.md @@ -38,7 +38,7 @@ args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. -# The code you write here will be wrapped by a function named 'download_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: none diff --git a/examples/help-header-override/README.md b/examples/help-header-override/README.md index a4585178..5a734178 100644 --- a/examples/help-header-override/README.md +++ b/examples/help-header-override/README.md @@ -64,7 +64,7 @@ examples: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. -# The code you write here will be wrapped by a function named 'download_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: none diff --git a/examples/minimal/README.md b/examples/minimal/README.md index 29e6f722..f69b3f96 100644 --- a/examples/minimal/README.md +++ b/examples/minimal/README.md @@ -96,7 +96,7 @@ Examples: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. -# The code you write here will be wrapped by a function named 'download_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--force]} = 1 diff --git a/examples/minus-v/README.md b/examples/minus-v/README.md index e85e2240..5a02ae88 100644 --- a/examples/minus-v/README.md +++ b/examples/minus-v/README.md @@ -44,7 +44,7 @@ flags: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'cli' command. -# The code you write here will be wrapped by a function named 'cli_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: none @@ -99,7 +99,7 @@ Options: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'cli' command. -# The code you write here will be wrapped by a function named 'cli_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--verbose]} = 1 @@ -112,7 +112,7 @@ args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'cli' command. -# The code you write here will be wrapped by a function named 'cli_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--host]} = localhost diff --git a/examples/render-mandoc/README.md b/examples/render-mandoc/README.md index e466175a..31258246 100644 --- a/examples/render-mandoc/README.md +++ b/examples/render-mandoc/README.md @@ -58,51 +58,51 @@ flags: ### `$ man docs/download.1 | col -bx` ````shell -download(1) Sample application download(1) +download(1) Sample application download(1) NAME - download - Sample application + download - Sample application SYNOPSIS - download SOURCE [TARGET...] OPTIONS + download SOURCE [TARGET...] OPTIONS DESCRIPTION - Sample application + Sample application ARGUMENTS SOURCE - Source to download from + Source to download from - • Required + • Required - • Allowed Values: server1, server2 + • Allowed Values: server1, server2 TARGET - Target filename (default: same as source) + Target filename (default: same as source) - • Repeatable + • Repeatable OPTIONS --force, -f - Overwrite existing files + Overwrite existing files --debug, -d - Show debug information + Show debug information DEPENDENCIES aws-cli - Download from + Download from SEE ALSO - docker(1), docker-compose.yml(5) + docker(1), docker-compose.yml(5) ISSUE TRACKER - Report issues at + Report issues at AUTHORS - Lana Lang. + Lana Lang. -Version 0.1.0 August 2025 download(1) +Version 0.1.0 August 2026 download(1) ```` diff --git a/examples/stacktrace/README.md b/examples/stacktrace/README.md index 4746973b..c9686870 100644 --- a/examples/stacktrace/README.md +++ b/examples/stacktrace/README.md @@ -117,8 +117,8 @@ Examples: Stack trace: from ./download:15 in `root_command` - from ./download:259 in `run` - from ./download:267 in `main` + from ./download:264 in `run` + from ./download:272 in `main` ```` diff --git a/examples/validations/README.md b/examples/validations/README.md index 8fea4e36..e1767d99 100644 --- a/examples/validations/README.md +++ b/examples/validations/README.md @@ -39,9 +39,12 @@ commands: # Bashly will look for a function named `validate_integer` in your # script, you can use any name as long as it has a matching function. validate: integer + - name: second help: Second number - validate: integer + + # Multiple validations can be provided as an array. + validate: [not_empty, integer] flags: - long: --save @@ -97,6 +100,15 @@ validation error in FIRST: must be an integer +```` + +### `$ ./validate calc 1 ''` + +````shell +validation error in SECOND: +must not be empty + + ```` ### `$ ./validate calc 1 B` diff --git a/examples/whitelist/README.md b/examples/whitelist/README.md index b0f07b3a..d331ce79 100644 --- a/examples/whitelist/README.md +++ b/examples/whitelist/README.md @@ -133,7 +133,7 @@ region must be one of: eu, us ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'login' command. -# The code you write here will be wrapped by a function named 'login_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[environment]} = development @@ -157,7 +157,7 @@ args: ````shell # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'login' command. -# The code you write here will be wrapped by a function named 'login_command()'. +# The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[environment]} = production diff --git a/lib/bashly/views/command/completion_script.gtx b/lib/bashly/views/command/completion_script.gtx new file mode 100644 index 00000000..0ca9a265 --- /dev/null +++ b/lib/bashly/views/command/completion_script.gtx @@ -0,0 +1,15 @@ += view_marker + +> send_completions() { +> local completion_shell="${1:-bash}" +> +> case "$completion_shell" in +> bash) send_completions_bash ;; +> *) +> printf 'unsupported shell: %s\n' "$completion_shell" >&2 +> return 1 +> ;; +> esac +> } +> += render :completion_script_bash diff --git a/lib/bashly/views/command/completion_script_bash.gtx b/lib/bashly/views/command/completion_script_bash.gtx new file mode 100644 index 00000000..21f06d0d --- /dev/null +++ b/lib/bashly/views/command/completion_script_bash.gtx @@ -0,0 +1,28 @@ += view_marker + +> send_completions_bash() { +> cat <<'BASHLY_COMPLETIONS' +> _{{ name.to_underscore }}_completions() { +> local completion_command="${COMP_WORDS[0]}" +> local completion_current="${COMP_WORDS[COMP_CWORD]:-}" +> local completion_word_count=$((COMP_CWORD - 1)) +> local -a completion_words=() +> +> if [[ $completion_word_count -gt 0 ]]; then +> completion_words=("${COMP_WORDS[@]:1:completion_word_count}") +> fi +> +> COMPREPLY=() +> while IFS= read -r completion_candidate; do +> COMPREPLY+=("$completion_candidate") +> done < <( +> "$completion_command" __complete \ +> "${completion_words[@]}" \ +> "$completion_current" +> ) +> } +> +> complete -F _{{ name.to_underscore }}_completions {{ name }} +> BASHLY_COMPLETIONS +> } +> diff --git a/lib/bashly/views/command/completions.gtx b/lib/bashly/views/command/completions.gtx index 142acf8a..83792c93 100644 --- a/lib/bashly/views/command/completions.gtx +++ b/lib/bashly/views/command/completions.gtx @@ -39,6 +39,7 @@ > fi > } > += render :completion_script = render :completion_function deep_commands.each do |command| = command.render :completion_function diff --git a/spec/approvals/examples/completions b/spec/approvals/examples/completions new file mode 100644 index 00000000..93031076 --- /dev/null +++ b/spec/approvals/examples/completions @@ -0,0 +1,19 @@ ++ bashly generate +creating user files in src +skipped src/completions_command.sh (exists) +skipped src/download_command.sh (exists) +created ./cli +run ./cli --help to test your bash script ++ ./cli completions ++ head -n3 +_cli_completions() { + local completion_command="${COMP_WORDS[0]}" + local completion_current="${COMP_WORDS[COMP_CWORD]:-}" ++ ./cli __complete +completions +download ++ ./cli __complete download - +--help +-h +--force +-f diff --git a/spec/bashly/integration/runtime_completions_spec.rb b/spec/bashly/integration/runtime_completions_spec.rb index 9da8c8b7..ab08cc25 100644 --- a/spec/bashly/integration/runtime_completions_spec.rb +++ b/spec/bashly/integration/runtime_completions_spec.rb @@ -32,4 +32,51 @@ end end end + + context 'completion scripts' do + let(:cli) { File.expand_path 'spec/tmp/cli' } + + before(:context) do + Settings.enable_completions = 'always' + reset_tmp_dir + FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + end + + after(:context) do + Settings.enable_completions = 'never' + end + + it 'prints Bash completions by default' do + stdout, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions" + ) + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout).to include '_cli_completions() {' + expect(stdout).to end_with "complete -F _cli_completions cli\n" + end + + it 'accepts Bash explicitly' do + default_output, = Open3.capture3('bash', '-c', "source #{cli}; send_completions") + bash_output, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions bash" + ) + + expect(status).to be_success + expect(stderr).to be_empty + expect(bash_output).to eq default_output + end + + it 'rejects unsupported shells' do + stdout, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions zsh" + ) + + expect(status).not_to be_success + expect(stdout).to be_empty + expect(stderr).to eq "unsupported shell: zsh\n" + end + end end From b7530351febfcf44236848b16fa75e854540dd7e Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 12:42:12 +0300 Subject: [PATCH 09/23] - Add structured runtime completions --- examples/README.md | 1 + examples/completions-advanced/.gitignore | 1 + examples/completions-advanced/README.md | 139 ++++++++++++++++++ examples/completions-advanced/settings.yml | 1 + examples/completions-advanced/src/bashly.yml | 50 +++++++ .../src/completions_command.sh | 1 + .../src/deploy_command.sh | 1 + .../src/lib/completions.sh | 3 + examples/completions-advanced/test.sh | 13 ++ examples/completions/README.md | 4 +- examples/completions/src/download_command.sh | 4 - examples/completions/test.sh | 2 +- .../completions/bashly-completions.bash | 2 +- lib/bashly/completions/completely.yaml | 1 - lib/bashly/config_validator.rb | 19 ++- lib/bashly/docs/arg.yml | 16 +- lib/bashly/docs/command.yml | 12 -- lib/bashly/docs/flag.yml | 12 +- lib/bashly/script/argument.rb | 12 ++ lib/bashly/script/command.rb | 2 +- lib/bashly/script/flag.rb | 12 ++ lib/bashly/views/argument/completion.gtx | 14 ++ .../views/command/completion_script_bash.gtx | 46 +++++- lib/bashly/views/command/completions.gtx | 32 ++++ .../flag/completion_value_candidates.gtx | 14 ++ schemas/bashly.json | 105 ++++++------- spec/approvals/cli/doc/full | 41 +++--- spec/approvals/cli/doc/index | 1 - spec/approvals/examples/completions | 4 +- spec/approvals/examples/completions-advanced | 22 +++ .../validations/arg_completions_array | 1 + .../arg_completions_unknown_option | 1 + .../approvals/validations/command_completions | 1 + .../completion_script_bash_spec.rb | 111 ++++++++++++++ .../integration/runtime_completions_spec.rb | 49 +----- spec/bashly/library_spec.rb | 23 +++ spec/bashly/script/argument_spec.rb | 6 +- spec/bashly/script/command_spec.rb | 20 +++ spec/bashly/script/flag_spec.rb | 10 ++ .../completions/configured/examples.yml | 48 ++++++ .../completions/configured/src/bashly.yml | 66 +++++++++ .../configured/src/lib/completions.sh | 9 ++ spec/fixtures/schemas_invalid/bashly/5.yml | 4 + spec/fixtures/schemas_invalid/bashly/6.yml | 3 + spec/fixtures/script/arguments.yml | 5 +- spec/fixtures/script/commands.yml | 14 ++ spec/fixtures/script/flags.yml | 6 +- spec/fixtures/script/validations.yml | 28 +++- support/schema/bashly.yml | 95 +++++------- 49 files changed, 847 insertions(+), 240 deletions(-) create mode 100644 examples/completions-advanced/.gitignore create mode 100644 examples/completions-advanced/README.md create mode 100644 examples/completions-advanced/settings.yml create mode 100644 examples/completions-advanced/src/bashly.yml create mode 100644 examples/completions-advanced/src/completions_command.sh create mode 100644 examples/completions-advanced/src/deploy_command.sh create mode 100644 examples/completions-advanced/src/lib/completions.sh create mode 100644 examples/completions-advanced/test.sh create mode 100644 spec/approvals/examples/completions-advanced create mode 100644 spec/approvals/validations/arg_completions_array create mode 100644 spec/approvals/validations/arg_completions_unknown_option create mode 100644 spec/approvals/validations/command_completions create mode 100644 spec/bashly/integration/completion_script_bash_spec.rb create mode 100644 spec/fixtures/completions/configured/examples.yml create mode 100644 spec/fixtures/completions/configured/src/bashly.yml create mode 100644 spec/fixtures/completions/configured/src/lib/completions.sh create mode 100644 spec/fixtures/schemas_invalid/bashly/5.yml create mode 100644 spec/fixtures/schemas_invalid/bashly/6.yml diff --git a/examples/README.md b/examples/README.md index aff2a361..c6c939dd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,6 +25,7 @@ Each of these examples demonstrates one aspect or feature of bashly. ## Advanced configuration features +- [completions-advanced](completions-advanced#readme) - configuring static, dynamic, and option-based runtime completions - [catch-all](catch-all#readme) - a command that can receive an arbitrary number of arguments - [catch-all-advanced](catch-all-advanced#readme) - another example for the `catch_all` option - [catch-all-stdin](catch-all-stdin#readme) - combining `catch_all` with `stdin` to read multiple files diff --git a/examples/completions-advanced/.gitignore b/examples/completions-advanced/.gitignore new file mode 100644 index 00000000..573c0c4f --- /dev/null +++ b/examples/completions-advanced/.gitignore @@ -0,0 +1 @@ +cli diff --git a/examples/completions-advanced/README.md b/examples/completions-advanced/README.md new file mode 100644 index 00000000..d6c6f3d4 --- /dev/null +++ b/examples/completions-advanced/README.md @@ -0,0 +1,139 @@ +# Advanced Runtime Completions Example + +Demonstrates configured runtime completions, including static candidates, +dynamic external commands and internal functions, file and directory sources, +and the `no-space` option. + +Runtime completions are enabled in `settings.yml`. Users can load the generated +Bash wrapper with: + +```bash +source <(cli completions) +``` + + + +----- + +## `bashly.yml` + +````yaml +name: cli +help: Advanced runtime completions example +version: 0.1.0 + +commands: +- name: completions + help: Generate a shell completion script + args: + - name: shell + help: Shell to generate completions for + allowed: [bash] + default: bash + +- name: deploy + help: Deploy a branch + args: + - name: branch + help: Branch to deploy + required: true + + # Run an external Bash command and add each output line as a candidate. + completions: + dynamic: + - git branch --format='%(refname:short)' + - name: environment + help: Environment to deploy to + + # Combine literal candidates with an internal function. Prevent the shell + # from appending a space after the selected completion. + completions: + static: [staging, production] + dynamic: [completion_environments] + options: [no-space] + flags: + - long: --config + arg: file + help: Deployment configuration file + + # Ask the shell to add file and directory candidates. + completions: + options: [files] + - long: --directory + arg: path + help: Deployment directory + + # Ask the shell to add directory candidates only. + completions: + options: [directories] +```` +## `settings.yml` + +````yaml +enable_completions: always + +```` + +## `src/completions_command.sh` + +````bash +send_completions "${args[shell]}" + +```` + +## `src/lib/completions.sh` + +````bash +completion_environments() { + printf 'development\nstaging\n' +} + +```` + + +## Output + +### `$ ./cli completions | head -n3` + +````shell +_cli_completions() { + local completion_command="${COMP_WORDS[0]}" + local completion_current="${COMP_WORDS[COMP_CWORD]:-}" + + +```` + +### `$ ./cli __complete ""` + +````shell +completions +deploy +:options= + + +```` + +### `$ ./cli __complete deploy main st` + +````shell +staging +:options=no-space + + +```` + +### `$ ./cli __complete deploy --config ""` + +````shell +:options=files + + +```` + +### `$ ./cli __complete deploy --directory ""` + +````shell +:options=directories + + +```` diff --git a/examples/completions-advanced/settings.yml b/examples/completions-advanced/settings.yml new file mode 100644 index 00000000..c9337546 --- /dev/null +++ b/examples/completions-advanced/settings.yml @@ -0,0 +1 @@ +enable_completions: always diff --git a/examples/completions-advanced/src/bashly.yml b/examples/completions-advanced/src/bashly.yml new file mode 100644 index 00000000..53772230 --- /dev/null +++ b/examples/completions-advanced/src/bashly.yml @@ -0,0 +1,50 @@ +name: cli +help: Advanced runtime completions example +version: 0.1.0 + +commands: +- name: completions + help: Generate a shell completion script + args: + - name: shell + help: Shell to generate completions for + allowed: [bash] + default: bash + +- name: deploy + help: Deploy a branch + args: + - name: branch + help: Branch to deploy + required: true + + # Run an external Bash command and add each output line as a candidate. + completions: + dynamic: + - git branch --format='%(refname:short)' + - name: environment + help: Environment to deploy to + + # Combine literal candidates with an internal function. Prevent the shell + # from appending a space after the selected completion. + completions: + static: [staging, production] + dynamic: [completion_environments] + options: [no-space] + + flags: + - long: --config + arg: file + help: Deployment configuration file + + # Ask the shell to add file and directory candidates. + completions: + options: [files] + + - long: --directory + arg: path + help: Deployment directory + + # Ask the shell to add directory candidates only. + completions: + options: [directories] diff --git a/examples/completions-advanced/src/completions_command.sh b/examples/completions-advanced/src/completions_command.sh new file mode 100644 index 00000000..e981385f --- /dev/null +++ b/examples/completions-advanced/src/completions_command.sh @@ -0,0 +1 @@ +send_completions "${args[shell]}" diff --git a/examples/completions-advanced/src/deploy_command.sh b/examples/completions-advanced/src/deploy_command.sh new file mode 100644 index 00000000..dc4e7735 --- /dev/null +++ b/examples/completions-advanced/src/deploy_command.sh @@ -0,0 +1 @@ +inspect_args diff --git a/examples/completions-advanced/src/lib/completions.sh b/examples/completions-advanced/src/lib/completions.sh new file mode 100644 index 00000000..7c5538f9 --- /dev/null +++ b/examples/completions-advanced/src/lib/completions.sh @@ -0,0 +1,3 @@ +completion_environments() { + printf 'development\nstaging\n' +} diff --git a/examples/completions-advanced/test.sh b/examples/completions-advanced/test.sh new file mode 100644 index 00000000..e09f2119 --- /dev/null +++ b/examples/completions-advanced/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -x + +bashly generate + +### Try Me ### + +./cli completions | head -n3 +./cli __complete "" +./cli __complete deploy main st +./cli __complete deploy --config "" +./cli __complete deploy --directory "" diff --git a/examples/completions/README.md b/examples/completions/README.md index 0f135541..eb1a7920 100644 --- a/examples/completions/README.md +++ b/examples/completions/README.md @@ -79,11 +79,12 @@ _cli_completions() { ```` -### `$ ./cli __complete` +### `$ ./cli __complete ""` ````shell completions download +:options= ```` @@ -95,6 +96,7 @@ download -h --force -f +:options= ```` diff --git a/examples/completions/src/download_command.sh b/examples/completions/src/download_command.sh index 308ab9cf..dc4e7735 100644 --- a/examples/completions/src/download_command.sh +++ b/examples/completions/src/download_command.sh @@ -1,5 +1 @@ -echo "# This file is located at 'src/download_command.sh'." -echo "# It contains the implementation for the 'cli download' command." -echo "# The code you write here will be wrapped by a function named 'cli_download_command()'." -echo "# Feel free to edit this file; your changes will persist when regenerating." inspect_args diff --git a/examples/completions/test.sh b/examples/completions/test.sh index df30d428..843d0eea 100644 --- a/examples/completions/test.sh +++ b/examples/completions/test.sh @@ -7,5 +7,5 @@ bashly generate ### Try Me ### ./cli completions | head -n3 -./cli __complete +./cli __complete "" ./cli __complete download - diff --git a/lib/bashly/completions/bashly-completions.bash b/lib/bashly/completions/bashly-completions.bash index 1d226ce3..24fd736b 100644 --- a/lib/bashly/completions/bashly-completions.bash +++ b/lib/bashly/completions/bashly-completions.bash @@ -301,7 +301,7 @@ _bashly_completions() { return ;; 6:0) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "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.help_header_override command.name command.private command.variables command.version environment_variable environment_variable.default environment_variable.help environment_variable.name environment_variable.private environment_variable.required environment_variable.validate flag flag.alias flag.allowed flag.arg flag.completions flag.conflicts flag.default flag.help flag.long flag.needs flag.negatable flag.private flag.repeatable flag.required flag.short flag.unique flag.validate variable variable.name variable.value" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "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.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.help_header_override command.name command.private command.variables command.version environment_variable environment_variable.default environment_variable.help environment_variable.name environment_variable.private environment_variable.required environment_variable.validate flag flag.alias flag.allowed flag.arg flag.completions flag.conflicts flag.default flag.help flag.long flag.needs flag.negatable flag.private flag.repeatable flag.required flag.short flag.unique flag.validate variable variable.name variable.value" -- "$cur") return ;; 8:0) diff --git a/lib/bashly/completions/completely.yaml b/lib/bashly/completions/completely.yaml index eb1716e0..b04e2593 100644 --- a/lib/bashly/completions/completely.yaml +++ b/lib/bashly/completions/completely.yaml @@ -88,7 +88,6 @@ tokens: - command.args - command.catch_all - command.commands - - command.completions - command.default - command.dependencies - command.environment_variables diff --git a/lib/bashly/config_validator.rb b/lib/bashly/config_validator.rb index 6aac6fdb..96db0811 100644 --- a/lib/bashly/config_validator.rb +++ b/lib/bashly/config_validator.rb @@ -88,6 +88,20 @@ def assert_expose(key, value) assert [true, false, nil, 'always'].include?(value), "#{key} must be a boolean, or the string 'always'" end + def assert_completions(key, value) + return unless value + + assert_hash key, value, keys: %i[static dynamic options] + assert_array "#{key}.static", value['static'], of: :string + assert_array "#{key}.dynamic", value['dynamic'], of: :string + assert_array "#{key}.options", value['options'], of: :string + + Array(value['options']).each do |option| + assert %w[files directories no-space].include?(option), + "#{key}.options contains an unknown option: #{option}" + end + end + 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`" @@ -101,7 +115,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 + assert_completions "#{key}.completions", value['completions'] refute value['name'].match(/^-/), "#{key}.name must not start with '-'" @@ -137,7 +151,7 @@ def assert_flag(key, value) assert_boolean "#{key}.required", value['required'] assert_array "#{key}.allowed", value['allowed'], of: :string assert_array "#{key}.conflicts", value['conflicts'], of: :string - assert_array "#{key}.completions", value['completions'], of: :string + assert_completions "#{key}.completions", value['completions'] assert value['long'].match(/^--[a-zA-Z0-9_-]+$/), "#{key}.long must be in the form of '--name'" if value['long'] assert value['short'].match(/^-[a-zA-Z0-9]$/), "#{key}.short must be in the form of '-n'" if value['short'] @@ -236,7 +250,6 @@ def assert_command(key, value) assert_array "#{key}.args", value['args'], of: :arg assert_array "#{key}.flags", value['flags'], of: :flag assert_array "#{key}.commands", value['commands'], of: :command - assert_array "#{key}.completions", value['completions'], of: :string assert_array "#{key}.filters", value['filters'], of: :string assert_array "#{key}.environment_variables", value['environment_variables'], of: :env_var assert_array "#{key}.variables", value['variables'], of: :var diff --git a/lib/bashly/docs/arg.yml b/lib/bashly/docs/arg.yml index a5e55c94..955670b9 100644 --- a/lib/bashly/docs/arg.yml +++ b/lib/bashly/docs/arg.yml @@ -34,15 +34,21 @@ arg.allowed: default: development arg.completions: - help: Specify a list of additional completion suggestions. + help: |- + Configure additional runtime completions. + `static` contains literal candidates. `dynamic` contains Bash commands that + print one candidate per line. `options` accepts `files`, `directories`, and + `no-space`. url: https://bashly.dev/configuration/argument/#completions example: |- args: - - name: path - help: File or directory to process + - name: branch + help: Branch to process completions: - - - - + static: [main, develop] + dynamic: + - git branch --format='%(refname:short)' + options: [no-space] arg.default: help: Specify the value to apply when not provided by the user. diff --git a/lib/bashly/docs/command.yml b/lib/bashly/docs/command.yml index 15fdb389..56c36a3f 100644 --- a/lib/bashly/docs/command.yml +++ b/lib/bashly/docs/command.yml @@ -74,18 +74,6 @@ command.commands: alias: a help: Register a local repository -command.completions: - help: Specify a list of additional completion suggestions. - url: https://bashly.dev/configuration/command/#completions - example: |- - commands: - - name: view - help: View a directory, system user or a git branch - completions: - - - - - - $(git branch 2> /dev/null) - command.default: help: Specify that this sub-command will be executed implicitly. url: https://bashly.dev/configuration/command/#default diff --git a/lib/bashly/docs/flag.yml b/lib/bashly/docs/flag.yml index 7a44b49c..6a029d4f 100644 --- a/lib/bashly/docs/flag.yml +++ b/lib/bashly/docs/flag.yml @@ -58,14 +58,18 @@ flag.arg: help: Specify the user name flag.completions: - help: Specify a list of additional completion suggestions. Must be accompanied by `arg`. + help: |- + Configure additional runtime completions. Must be accompanied by `arg`. + `static` contains literal candidates. `dynamic` contains Bash commands that + print one candidate per line. `options` accepts `files`, `directories`, and + `no-space`. url: https://bashly.dev/configuration/flag/#completions example: |- flags: - - long: --user - arg: username + - long: --config + arg: file completions: - - + options: [files] # Anything in the 'allowed' option is automatically added as a completion. - long: --protocol diff --git a/lib/bashly/script/argument.rb b/lib/bashly/script/argument.rb index d5a0e84e..bdae5fcf 100644 --- a/lib/bashly/script/argument.rb +++ b/lib/bashly/script/argument.rb @@ -23,6 +23,18 @@ def default_string end end + def completion_static + completions&.fetch('static', []) || [] + end + + def completion_dynamic + completions&.fetch('dynamic', []) || [] + end + + def completion_options + completions&.fetch('options', []) || [] + end + def label repeatable ? "#{name.upcase}..." : name.upcase end diff --git a/lib/bashly/script/command.rb b/lib/bashly/script/command.rb index 65c841ba..6a1a41ce 100644 --- a/lib/bashly/script/command.rb +++ b/lib/bashly/script/command.rb @@ -13,7 +13,7 @@ class Command < Base class << self def option_keys @option_keys ||= %i[ - alias argfile args catch_all commands completions + alias argfile args catch_all commands default dependencies environment_variables examples extensible expose filename filters flags footer function group help help_header_override name diff --git a/lib/bashly/script/flag.rb b/lib/bashly/script/flag.rb index 72534378..6e8fcdf3 100644 --- a/lib/bashly/script/flag.rb +++ b/lib/bashly/script/flag.rb @@ -43,6 +43,18 @@ def default_string end end + def completion_static + completions&.fetch('static', []) || [] + end + + def completion_dynamic + completions&.fetch('dynamic', []) || [] + end + + def completion_options + completions&.fetch('options', []) || [] + end + def name long || short end diff --git a/lib/bashly/views/argument/completion.gtx b/lib/bashly/views/argument/completion.gtx index b3af3b0d..b22b8a12 100644 --- a/lib/bashly/views/argument/completion.gtx +++ b/lib/bashly/views/argument/completion.gtx @@ -15,3 +15,17 @@ if allowed > completion_candidates "$completion_current" "${completion_values[@]}" end end + +if completion_static.any? + > completion_candidates "$completion_current" {{ completion_static.map { |value| Shellwords.shellescape value }.join ' ' }} +end + +completion_dynamic.each do |command| + > if completion_output="$({ {{ command }}; } 2>/dev/null)"; then + > completion_dynamic_candidates "$completion_current" "$completion_output" + > fi +end + +if completion_options.any? + > completion_add_options {{ completion_options.join ' ' }} +end diff --git a/lib/bashly/views/command/completion_script_bash.gtx b/lib/bashly/views/command/completion_script_bash.gtx index 21f06d0d..9791008e 100644 --- a/lib/bashly/views/command/completion_script_bash.gtx +++ b/lib/bashly/views/command/completion_script_bash.gtx @@ -7,19 +7,61 @@ > local completion_current="${COMP_WORDS[COMP_CWORD]:-}" > local completion_word_count=$((COMP_CWORD - 1)) > local -a completion_words=() +> local -a completion_response=() +> local -a completion_options=() +> local -A completion_seen=() > > if [[ $completion_word_count -gt 0 ]]; then > completion_words=("${COMP_WORDS[@]:1:completion_word_count}") > fi > > COMPREPLY=() -> while IFS= read -r completion_candidate; do -> COMPREPLY+=("$completion_candidate") +> while IFS= read -r completion_line; do +> completion_response+=("$completion_line") > done < <( > "$completion_command" __complete \ > "${completion_words[@]}" \ > "$completion_current" > ) +> +> local completion_directive="${completion_response[-1]:-}" +> if [[ $completion_directive == :options=* ]]; then +> unset 'completion_response[-1]' +> IFS=, read -r -a completion_options <<< "${completion_directive#:options=}" +> fi +> +> COMPREPLY=("${completion_response[@]}") +> local completion_candidate +> for completion_candidate in "${COMPREPLY[@]}"; do +> completion_seen["$completion_candidate"]=1 +> done +> +> local completion_option +> local completion_files=false +> local completion_directories=false +> for completion_option in "${completion_options[@]}"; do +> case "$completion_option" in +> files) completion_files=true ;; +> directories) completion_directories=true ;; +> no-space) compopt -o nospace ;; +> esac +> done +> +> if [[ $completion_files == true ]]; then +> while IFS= read -r completion_candidate; do +> if [[ -z "${completion_seen[$completion_candidate]:-}" ]]; then +> COMPREPLY+=("$completion_candidate") +> completion_seen["$completion_candidate"]=1 +> fi +> done < <(compgen -f -- "$completion_current") +> elif [[ $completion_directories == true ]]; then +> while IFS= read -r completion_candidate; do +> if [[ -z "${completion_seen[$completion_candidate]:-}" ]]; then +> COMPREPLY+=("$completion_candidate") +> completion_seen["$completion_candidate"]=1 +> fi +> done < <(compgen -d -- "$completion_current") +> fi > } > > complete -F _{{ name.to_underscore }}_completions {{ name }} diff --git a/lib/bashly/views/command/completions.gtx b/lib/bashly/views/command/completions.gtx index 83792c93..ec7a98aa 100644 --- a/lib/bashly/views/command/completions.gtx +++ b/lib/bashly/views/command/completions.gtx @@ -4,6 +4,8 @@ > local -a completion_words=("$@") > local -A completion_blocked_flags=() > local -A completion_emitted=() +> local -a completion_options=() +> local -A completion_options_seen=() > local completion_count=${#completion_words[@]} > local completion_current="" > @@ -13,6 +15,12 @@ > fi > > {{ function_name }}_completion "$completion_current" "${completion_words[@]}" +> +> local completion_options_string="" +> if [[ ${#completion_options[@]} -gt 0 ]]; then +> completion_options_string="$(IFS=,; printf '%s' "${completion_options[*]}")" +> fi +> printf ':options=%s\n' "$completion_options_string" > } > > completion_candidates() { @@ -39,6 +47,30 @@ > fi > } > +> completion_add_options() { +> local completion_option +> for completion_option in "$@"; do +> if [[ -z "${completion_options_seen[$completion_option]:-}" ]]; then +> completion_options+=("$completion_option") +> completion_options_seen["$completion_option"]=1 +> fi +> done +> } +> +> completion_dynamic_candidates() { +> local completion_current="$1" +> local completion_output="$2" +> +> [[ -n $completion_output ]] || return +> +> local completion_candidate +> while IFS= read -r completion_candidate; do +> if [[ -n $completion_candidate ]]; then +> completion_candidates "$completion_current" "$completion_candidate" +> fi +> done <<< "$completion_output" +> } +> = render :completion_script = render :completion_function deep_commands.each do |command| diff --git a/lib/bashly/views/flag/completion_value_candidates.gtx b/lib/bashly/views/flag/completion_value_candidates.gtx index d1632334..30c55f6c 100644 --- a/lib/bashly/views/flag/completion_value_candidates.gtx +++ b/lib/bashly/views/flag/completion_value_candidates.gtx @@ -15,3 +15,17 @@ if allowed > completion_candidates "$completion_current" "${completion_values[@]}" end end + +if completion_static.any? + > completion_candidates "$completion_current" {{ completion_static.map { |value| Shellwords.shellescape value }.join ' ' }} +end + +completion_dynamic.each do |command| + > if completion_output="$({ {{ command }}; } 2>/dev/null)"; then + > completion_dynamic_candidates "$completion_current" "$completion_output" + > fi +end + +if completion_options.any? + > completion_add_options {{ completion_options.join ' ' }} +end diff --git a/schemas/bashly.json b/schemas/bashly.json index 340d4557..c4a57edc 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -68,19 +68,7 @@ } }, "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": [ - "" - ] - } + "$ref": "#/definitions/completions-property" }, "repeatable": { "title": "repeatable", @@ -297,16 +285,7 @@ "default": false }, "completions": { - "title": "completions", - "description": "Completions of the current flag\nhttps://bashly.dev/configuration/flag/#completions", - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "description": "A completion of the current flag", - "type": "string", - "minLength": 1 - } + "$ref": "#/definitions/completions-property" }, "private": { "title": "private", @@ -743,41 +722,45 @@ }, "completions-property": { "title": "completions", - "description": "Completions of the current script or sub-command\nhttps://bashly.dev/configuration/command/#completions", - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "description": "A completion of the current script or sub-command", - "type": "string", - "minLength": 1, - "examples": [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - ] - } + "description": "Additional completions for an argument or flag value", + "type": "object", + "minProperties": 1, + "properties": { + "static": { + "description": "Literal completion candidates", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "dynamic": { + "description": "Bash commands that print one completion candidate per line", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "options": { + "description": "Additional completion sources and shell behavior", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "files", + "directories", + "no-space" + ] + } + } + }, + "additionalProperties": false }, "dependencies-command-property": { "title": "command", @@ -995,9 +978,6 @@ "catch_all": { "$ref": "#/definitions/catch-all-property" }, - "completions": { - "$ref": "#/definitions/completions-property" - }, "dependencies": { "$ref": "#/definitions/dependencies-property" }, @@ -1075,9 +1055,6 @@ "catch_all": { "$ref": "#/definitions/catch-all-property" }, - "completions": { - "$ref": "#/definitions/completions-property" - }, "dependencies": { "$ref": "#/definitions/dependencies-property" }, diff --git a/spec/approvals/cli/doc/full b/spec/approvals/cli/doc/full index 1aeaafc8..14283ac6 100644 --- a/spec/approvals/cli/doc/full +++ b/spec/approvals/cli/doc/full @@ -40,14 +40,19 @@ arg.allowed arg.completions - Specify a list of additional completion suggestions. + Configure additional runtime completions. + static contains literal candidates. dynamic contains Bash commands that + print one candidate per line. options accepts files, directories, and + no-space. args: - - name: path - help: File or directory to process + - name: branch + help: Branch to process completions: - - - - + static: [main, develop] + dynamic: + - git branch --format='%(refname:short)' + options: [no-space] See https://bashly.dev/configuration/argument/#completions @@ -208,20 +213,6 @@ command.commands See https://bashly.dev/configuration/command/#commands -command.completions - - Specify a list of additional completion suggestions. - - commands: - - name: view - help: View a directory, system user or a git branch - completions: - - - - - - $(git branch 2> /dev/null) - - See https://bashly.dev/configuration/command/#completions - command.default Specify that this sub-command will be executed implicitly. @@ -678,14 +669,16 @@ flag.arg flag.completions - Specify a list of additional completion suggestions. Must be accompanied by - arg. + Configure additional runtime completions. Must be accompanied by arg. + static contains literal candidates. dynamic contains Bash commands that + print one candidate per line. options accepts files, directories, and + no-space. flags: - - long: --user - arg: username + - long: --config + arg: file completions: - - + options: [files] # Anything in the 'allowed' option is automatically added as a completion. - long: --protocol diff --git a/spec/approvals/cli/doc/index b/spec/approvals/cli/doc/index index 2e744849..b85bfd21 100644 --- a/spec/approvals/cli/doc/index +++ b/spec/approvals/cli/doc/index @@ -12,7 +12,6 @@ command.alias command.args command.catch_all command.commands -command.completions command.default command.dependencies command.environment_variables diff --git a/spec/approvals/examples/completions b/spec/approvals/examples/completions index 93031076..e26d74c2 100644 --- a/spec/approvals/examples/completions +++ b/spec/approvals/examples/completions @@ -9,11 +9,13 @@ run ./cli --help to test your bash script _cli_completions() { local completion_command="${COMP_WORDS[0]}" local completion_current="${COMP_WORDS[COMP_CWORD]:-}" -+ ./cli __complete ++ ./cli __complete '' completions download +:options= + ./cli __complete download - --help -h --force -f +:options= diff --git a/spec/approvals/examples/completions-advanced b/spec/approvals/examples/completions-advanced new file mode 100644 index 00000000..371bebd2 --- /dev/null +++ b/spec/approvals/examples/completions-advanced @@ -0,0 +1,22 @@ ++ bashly generate +creating user files in src +skipped src/completions_command.sh (exists) +skipped src/deploy_command.sh (exists) +created ./cli +run ./cli --help to test your bash script ++ ./cli completions ++ head -n3 +_cli_completions() { + local completion_command="${COMP_WORDS[0]}" + local completion_current="${COMP_WORDS[COMP_CWORD]:-}" ++ ./cli __complete '' +completions +deploy +:options= ++ ./cli __complete deploy main st +staging +:options=no-space ++ ./cli __complete deploy --config '' +:options=files ++ ./cli __complete deploy --directory '' +:options=directories diff --git a/spec/approvals/validations/arg_completions_array b/spec/approvals/validations/arg_completions_array new file mode 100644 index 00000000..56773281 --- /dev/null +++ b/spec/approvals/validations/arg_completions_array @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/validations/arg_completions_unknown_option b/spec/approvals/validations/arg_completions_unknown_option new file mode 100644 index 00000000..54bf58f5 --- /dev/null +++ b/spec/approvals/validations/arg_completions_unknown_option @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/validations/command_completions b/spec/approvals/validations/command_completions new file mode 100644 index 00000000..c68fe4ae --- /dev/null +++ b/spec/approvals/validations/command_completions @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/bashly/integration/completion_script_bash_spec.rb b/spec/bashly/integration/completion_script_bash_spec.rb new file mode 100644 index 00000000..34ed61a6 --- /dev/null +++ b/spec/bashly/integration/completion_script_bash_spec.rb @@ -0,0 +1,111 @@ +describe 'Bash completion script', :slow do + def complete_with_bash(*words, trace_options: false) + completion_words = words.map { |word| Shellwords.shellescape word }.join ' ' + option_trace = if trace_options + "compopt() { printf 'option %s\\n' \"$*\"; }" + end + + Open3.capture3( + 'bash', '-c', <<~BASH + cd ./spec/tmp + source cli + eval "$(send_completions)" + #{option_trace} + COMP_WORDS=(./cli #{completion_words}) + COMP_CWORD=#{words.length} + _cli_completions + printf 'candidate %s\n' "${COMPREPLY[@]}" + BASH + ) + end + + context 'generation' do + let(:cli) { File.expand_path 'spec/tmp/cli' } + + before(:context) do + Settings.enable_completions = 'always' + reset_tmp_dir + FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + end + + after(:context) do + Settings.enable_completions = 'never' + end + + it 'prints Bash completions by default' do + stdout, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions" + ) + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout).to include '_cli_completions() {' + expect(stdout).to end_with "complete -F _cli_completions cli\n" + end + + it 'accepts Bash explicitly' do + default_output, = Open3.capture3('bash', '-c', "source #{cli}; send_completions") + bash_output, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions bash" + ) + + expect(status).to be_success + expect(stderr).to be_empty + expect(bash_output).to eq default_output + end + + it 'rejects unsupported shells' do + stdout, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions zsh" + ) + + expect(status).not_to be_success + expect(stdout).to be_empty + expect(stderr).to eq "unsupported shell: zsh\n" + end + end + + context 'configured options' do + before(:context) do + Settings.enable_completions = 'always' + reset_tmp_dir + FileUtils.cp_r Dir['spec/fixtures/completions/configured/*'], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + FileUtils.touch 'spec/tmp/apple.txt' + FileUtils.mkdir 'spec/tmp/apricot' + end + + after(:context) do + Settings.enable_completions = 'never' + end + + it 'adds files when requested' do + stdout, stderr, status = complete_with_bash 'files', 'a' + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to contain_exactly( + 'candidate apple.txt', 'candidate apricot' + ) + end + + it 'adds only directories when requested' do + stdout, stderr, status = complete_with_bash 'directories', 'a' + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to eq ['candidate apricot'] + end + + it 'applies no-space alongside candidate-source options' do + stdout, stderr, status = complete_with_bash 'combined', 'a', trace_options: true + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to contain_exactly( + 'option -o nospace', 'candidate apple.txt', 'candidate apricot' + ) + end + end +end diff --git a/spec/bashly/integration/runtime_completions_spec.rb b/spec/bashly/integration/runtime_completions_spec.rb index ab08cc25..9d8fa51e 100644 --- a/spec/bashly/integration/runtime_completions_spec.rb +++ b/spec/bashly/integration/runtime_completions_spec.rb @@ -26,57 +26,12 @@ expect(status).to be_success expect(stderr).to be_empty - expect(stdout.lines(chomp: true)).to eq example['expected'] + expected = example['expected'] + [":options=#{example['options']}"] + expect(stdout.lines(chomp: true)).to eq expected end end end end end - context 'completion scripts' do - let(:cli) { File.expand_path 'spec/tmp/cli' } - - before(:context) do - Settings.enable_completions = 'always' - reset_tmp_dir - FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' - Commands::Generate.new.execute %w[generate --quiet] - end - - after(:context) do - Settings.enable_completions = 'never' - end - - it 'prints Bash completions by default' do - stdout, stderr, status = Open3.capture3( - 'bash', '-c', "source #{cli}; send_completions" - ) - - expect(status).to be_success - expect(stderr).to be_empty - expect(stdout).to include '_cli_completions() {' - expect(stdout).to end_with "complete -F _cli_completions cli\n" - end - - it 'accepts Bash explicitly' do - default_output, = Open3.capture3('bash', '-c', "source #{cli}; send_completions") - bash_output, stderr, status = Open3.capture3( - 'bash', '-c', "source #{cli}; send_completions bash" - ) - - expect(status).to be_success - expect(stderr).to be_empty - expect(bash_output).to eq default_output - end - - it 'rejects unsupported shells' do - stdout, stderr, status = Open3.capture3( - 'bash', '-c', "source #{cli}; send_completions zsh" - ) - - expect(status).not_to be_success - expect(stdout).to be_empty - expect(stderr).to eq "unsupported shell: zsh\n" - end - end end diff --git a/spec/bashly/library_spec.rb b/spec/bashly/library_spec.rb index 069de589..f5e3e051 100644 --- a/spec/bashly/library_spec.rb +++ b/spec/bashly/library_spec.rb @@ -17,6 +17,18 @@ expect(matter[:content]).to eq File.read("#{lib_dir}/colors.sh") end + context 'when the library has a custom handler' do + let(:name) { :help } + + before { reset_tmp_dir example: 'minimal' } + + it 'delegates the request to the custom handler' do + expect(subject.files).to contain_exactly( + path: 'spec/tmp/src/help_command.sh', + content: include('help_function=download_usage') + ) + end + end end describe '#post_install_message' do @@ -37,6 +49,17 @@ end end + context 'when the library has a custom handler' do + let(:name) { :help } + + before { reset_tmp_dir example: 'minimal' } + + it 'returns the message from the custom handler' do + expect(subject.post_install_message).to include( + 'Add this as a command to your bashly.yml:' + ) + end + end end describe '#find_file' do diff --git a/spec/bashly/script/argument_spec.rb b/spec/bashly/script/argument_spec.rb index 76175534..ae2d6237 100644 --- a/spec/bashly/script/argument_spec.rb +++ b/spec/bashly/script/argument_spec.rb @@ -42,8 +42,10 @@ describe '#completions' do let(:fixture) { :completions } - it 'returns the completion suggestions' do - expect(subject.completions).to eq ['', 'README.md'] + it 'separates completion values, commands, and options' do + expect(subject.completion_static).to eq ['README.md'] + expect(subject.completion_dynamic).to eq ['recent_files'] + expect(subject.completion_options).to eq %w[files no-space] end end diff --git a/spec/bashly/script/command_spec.rb b/spec/bashly/script/command_spec.rb index e95ce326..d68a318b 100644 --- a/spec/bashly/script/command_spec.rb +++ b/spec/bashly/script/command_spec.rb @@ -78,6 +78,26 @@ end end + describe 'configured completions' do + let(:fixture) { :completions } + + it 'exposes structured argument completions' do + argument = subject.args.first + + expect(argument.completion_static).to eq %w[main develop] + expect(argument.completion_dynamic).to eq ['git branch'] + expect(argument.completion_options).to eq ['no-space'] + end + + it 'exposes structured flag completions' do + flag = subject.flags.first + + expect(flag.completion_static).to be_empty + expect(flag.completion_dynamic).to be_empty + expect(flag.completion_options).to eq ['files'] + end + end + describe '#filename' do context 'when it is the root command' do it 'returns root_command.sh' do diff --git a/spec/bashly/script/flag_spec.rb b/spec/bashly/script/flag_spec.rb index 85b40a3f..7591a0a0 100644 --- a/spec/bashly/script/flag_spec.rb +++ b/spec/bashly/script/flag_spec.rb @@ -89,6 +89,16 @@ end end + describe '#completions' do + let(:fixture) { :completions_completions } + + it 'separates completion values, commands, and options' do + expect(subject.completion_static).to eq ['README.md'] + expect(subject.completion_dynamic).to eq ['recent_files'] + expect(subject.completion_options).to eq %w[files no-space] + end + end + describe '#name' do context 'with both short and long options' do it 'returns the long option' do diff --git a/spec/fixtures/completions/configured/examples.yml b/spec/fixtures/completions/configured/examples.yml new file mode 100644 index 00000000..c88b446a --- /dev/null +++ b/spec/fixtures/completions/configured/examples.yml @@ -0,0 +1,48 @@ +static candidates: + words: [static, ''] + expected: [main, develop, two words, '$literal', ':options=files'] + +static candidate prefix: + words: [static, dev] + expected: [develop] + +external command: + words: [external, ''] + expected: [alpha, beta] + +external command prefix: + words: [external, b] + expected: [beta] + +internal function: + words: [internal, ''] + expected: [red, green, blue] + +failing command: + words: [failure, ''] + expected: [fallback] + +files option: + words: [files, ''] + expected: [apple.txt] + options: files + +directories option: + words: [directories, ''] + expected: [] + options: directories + +combined options: + words: [combined, ''] + expected: [] + options: files,directories,no-space + +flag values: + words: [flag, --target, ''] + expected: [production, preview, red, green, blue] + options: no-space + +flag value prefix: + words: [flag, --target, pr] + expected: [production, preview] + options: no-space diff --git a/spec/fixtures/completions/configured/src/bashly.yml b/spec/fixtures/completions/configured/src/bashly.yml new file mode 100644 index 00000000..80ab2a42 --- /dev/null +++ b/spec/fixtures/completions/configured/src/bashly.yml @@ -0,0 +1,66 @@ +name: cli +help: Configured runtime completion fixture +version: 0.1.0 + +commands: +- name: static + help: Complete static values + args: + - name: value + completions: + static: [main, develop, two words, '$literal', ':options=files'] + +- name: external + help: Complete values from an external command + args: + - name: value + completions: + dynamic: + - printf 'alpha\nbeta\n' + +- name: internal + help: Complete values from an internal function + args: + - name: value + completions: + dynamic: [completion_colors] + +- name: failure + help: Ignore a failing dynamic producer + args: + - name: value + completions: + static: [fallback] + dynamic: [completion_failure] + +- name: files + help: Request file additions + args: + - name: value + completions: + static: [apple.txt] + options: [files] + +- name: directories + help: Request directory additions + args: + - name: value + completions: + options: [directories] + +- name: combined + help: Combine completion options + args: + - name: value + completions: + options: [files, directories, no-space] + +- name: flag + help: Complete a flag value + flags: + - long: --target + arg: target + completions: + static: [production, preview] + dynamic: [completion_colors] + options: [no-space] diff --git a/spec/fixtures/completions/configured/src/lib/completions.sh b/spec/fixtures/completions/configured/src/lib/completions.sh new file mode 100644 index 00000000..6e78c91d --- /dev/null +++ b/spec/fixtures/completions/configured/src/lib/completions.sh @@ -0,0 +1,9 @@ +completion_colors() { + printf 'red\ngreen\nblue\n' +} + +completion_failure() { + printf 'partial\n' + printf 'completion failed\n' >&2 + return 1 +} diff --git a/spec/fixtures/schemas_invalid/bashly/5.yml b/spec/fixtures/schemas_invalid/bashly/5.yml new file mode 100644 index 00000000..fde7eaa0 --- /dev/null +++ b/spec/fixtures/schemas_invalid/bashly/5.yml @@ -0,0 +1,4 @@ +name: invalid +args: +- name: path + completions: [] diff --git a/spec/fixtures/schemas_invalid/bashly/6.yml b/spec/fixtures/schemas_invalid/bashly/6.yml new file mode 100644 index 00000000..3571c10f --- /dev/null +++ b/spec/fixtures/schemas_invalid/bashly/6.yml @@ -0,0 +1,3 @@ +name: invalid +completions: + static: [one] diff --git a/spec/fixtures/script/arguments.yml b/spec/fixtures/script/arguments.yml index 3e0b119e..38174cb5 100644 --- a/spec/fixtures/script/arguments.yml +++ b/spec/fixtures/script/arguments.yml @@ -3,7 +3,10 @@ :completions: name: file - completions: [, README.md] + completions: + static: [README.md] + dynamic: [recent_files] + options: [files, no-space] :required: name: file diff --git a/spec/fixtures/script/commands.yml b/spec/fixtures/script/commands.yml index 89731821..4d7ed8ae 100644 --- a/spec/fixtures/script/commands.yml +++ b/spec/fixtures/script/commands.yml @@ -17,6 +17,20 @@ - name: internal private: true +:completions: + name: checkout + args: + - name: branch + completions: + static: [main, develop] + dynamic: [git branch] + options: [no-space] + flags: + - long: --config + arg: file + completions: + options: [files] + :basic_command: name: get alias: g diff --git a/spec/fixtures/script/flags.yml b/spec/fixtures/script/flags.yml index dbea438b..a689cc87 100644 --- a/spec/fixtures/script/flags.yml +++ b/spec/fixtures/script/flags.yml @@ -46,7 +46,11 @@ :completions_completions: long: --path short: -p - completions: [, README.md] + arg: path + completions: + static: [README.md] + dynamic: [recent_files] + options: [files, no-space] :default_string: long: --file diff --git a/spec/fixtures/script/validations.yml b/spec/fixtures/script/validations.yml index cfbfae2e..bb42a911 100644 --- a/spec/fixtures/script/validations.yml +++ b/spec/fixtures/script/validations.yml @@ -10,8 +10,24 @@ args: - name: path allowed: [one, two] + completions: + options: [files] + +:arg_completions_array: + name: invalid + help: completions must use the structured contract + args: + - name: path completions: [] +:arg_completions_unknown_option: + name: invalid + help: completion options use a fixed vocabulary + args: + - name: path + completions: + options: [users] + :arg_default_array_without_repeatable: name: invalid help: default cannot be an array without repeatable @@ -284,7 +300,8 @@ flags: - long: --file allowed: [one, two] - completions: [] + completions: + options: [files] :flag_allowed_without_arg: name: invalid @@ -298,7 +315,14 @@ help: flag must have an arg when using completions flags: - long: --target - completions: [] + completions: + options: [directories] + +:command_completions: + name: invalid + help: commands no longer accept completions + completions: + static: [one] :flag_conflicts_array: name: invalid diff --git a/support/schema/bashly.yml b/support/schema/bashly.yml index 166471ee..56307538 100644 --- a/support/schema/bashly.yml +++ b/support/schema/bashly.yml @@ -62,19 +62,7 @@ definitions: examples: - eu completions: - title: completions - description: |- - Completions of the current positional argument - https://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: - - + $ref: '#/definitions/completions-property' repeatable: title: repeatable description: |- @@ -255,17 +243,7 @@ definitions: type: boolean default: false completions: - title: completions - description: |- - Completions of the current flag - https://bashly.dev/configuration/flag/#completions - type: array - minItems: 1 - uniqueItems: true - items: - description: A completion of the current flag - type: string - minLength: 1 + $ref: '#/definitions/completions-property' private: title: private description: |- @@ -631,40 +609,37 @@ definitions: completions-property: title: completions description: |- - Completions of the current script or sub-command - https://bashly.dev/configuration/command/#completions - type: array - minItems: 1 - uniqueItems: true - items: - description: A completion of the current script or sub-command - type: string - minLength: 1 - examples: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Additional completions for an argument or flag value + type: object + minProperties: 1 + properties: + static: + description: Literal completion candidates + type: array + minItems: 1 + uniqueItems: true + items: + type: string + minLength: 1 + dynamic: + description: Bash commands that print one completion candidate per line + type: array + minItems: 1 + uniqueItems: true + items: + type: string + minLength: 1 + options: + description: Additional completion sources and shell behavior + type: array + minItems: 1 + uniqueItems: true + items: + enum: + - files + - directories + - no-space + additionalProperties: false dependencies-command-property: title: command description: |- @@ -845,8 +820,6 @@ definitions: $ref: '#/definitions/sub-command-group-property' catch_all: $ref: '#/definitions/catch-all-property' - completions: - $ref: '#/definitions/completions-property' dependencies: $ref: '#/definitions/dependencies-property' expose: @@ -895,8 +868,6 @@ properties: $ref: '#/definitions/footer-property' catch_all: $ref: '#/definitions/catch-all-property' - completions: - $ref: '#/definitions/completions-property' dependencies: $ref: '#/definitions/dependencies-property' extensible: From 15860f6fcce6ee2f9c1d511961be35fe0c7a0374 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 12:54:06 +0300 Subject: [PATCH 10/23] fix shfmt --- lib/bashly/views/command/completions.gtx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/bashly/views/command/completions.gtx b/lib/bashly/views/command/completions.gtx index ec7a98aa..72f345ea 100644 --- a/lib/bashly/views/command/completions.gtx +++ b/lib/bashly/views/command/completions.gtx @@ -18,7 +18,10 @@ > > local completion_options_string="" > if [[ ${#completion_options[@]} -gt 0 ]]; then -> completion_options_string="$(IFS=,; printf '%s' "${completion_options[*]}")" +> completion_options_string="$( +> IFS=, +> printf '%s' "${completion_options[*]}" +> )" > fi > printf ':options=%s\n' "$completion_options_string" > } @@ -68,7 +71,7 @@ > if [[ -n $completion_candidate ]]; then > completion_candidates "$completion_current" "$completion_candidate" > fi -> done <<< "$completion_output" +> done <<<"$completion_output" > } > = render :completion_script From 15723e04386af51c58c7a6effef6bae2b11ffac2 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 14:36:54 +0300 Subject: [PATCH 11/23] - Drop support for Ruby 3.2 --- .github/workflows/test.yml | 2 +- .rubocop.yml | 2 +- bashly.gemspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63574e7f..20df152e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: LC_ALL: en_US.UTF-8 strategy: - matrix: { ruby: ['3.2', '3.3', '3.4', '4.0'] } + matrix: { ruby: ['3.3', '3.3', '3.4', '4.0'] } steps: - name: Checkout code diff --git a/.rubocop.yml b/.rubocop.yml index c918e5c4..ae4877b0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ inherit_gem: - rspec.yml AllCops: - TargetRubyVersion: 3.2 + TargetRubyVersion: 3.3 SuggestExtensions: false Exclude: - dev/**/* diff --git a/bashly.gemspec b/bashly.gemspec index 244b2c15..25f4d2e5 100644 --- a/bashly.gemspec +++ b/bashly.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.executables = ['bashly'] s.homepage = 'https://github.com/bashly-framework/bashly' s.license = 'MIT' - s.required_ruby_version = '>= 3.2' + s.required_ruby_version = '>= 3.3' s.add_dependency 'colsole', '~> 1.0' s.add_dependency 'gtx', '~> 0.1.1' From 07e9fdeb4fbdd8ed7de3c2fd6d9aefec0c3e402c Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 15:05:02 +0300 Subject: [PATCH 12/23] - Rename settings.enable_bash3_bouncer to enable_bash_version_bouncer --- lib/bashly/libraries/settings/settings.yml | 2 +- lib/bashly/script/wrapper.rb | 2 +- lib/bashly/settings.rb | 6 +++--- .../wrapper/{bash3_bouncer.gtx => bash_version_bouncer.gtx} | 0 schemas/settings.json | 6 +++--- spec/approvals/script/wrapper/code | 2 +- spec/bashly/commands/generate_spec.rb | 2 +- spec/bashly/script/wrapper_spec.rb | 2 +- support/schema/settings.yml | 6 +++--- 9 files changed, 14 insertions(+), 14 deletions(-) rename lib/bashly/views/wrapper/{bash3_bouncer.gtx => bash_version_bouncer.gtx} (100%) diff --git a/lib/bashly/libraries/settings/settings.yml b/lib/bashly/libraries/settings/settings.yml index 350f982f..38f84811 100644 --- a/lib/bashly/libraries/settings/settings.yml +++ b/lib/bashly/libraries/settings/settings.yml @@ -157,7 +157,7 @@ env: development # - always # render this feature in any environment # - never # do not render this feature enable_header_comment: always -enable_bash3_bouncer: always +enable_bash_version_bouncer: always enable_completions: never enable_view_markers: development enable_inspect_args: development diff --git a/lib/bashly/script/wrapper.rb b/lib/bashly/script/wrapper.rb index 537766e4..93b6b780 100644 --- a/lib/bashly/script/wrapper.rb +++ b/lib/bashly/script/wrapper.rb @@ -46,7 +46,7 @@ def header! def default_header result = render 'header' - result += render('bash3_bouncer') unless function_name || !Settings.enabled?(:bash3_bouncer) + result += render('bash_version_bouncer') unless function_name || !Settings.enabled?(:bash_version_bouncer) result end diff --git a/lib/bashly/settings.rb b/lib/bashly/settings.rb index cdb02e25..38694d75 100644 --- a/lib/bashly/settings.rb +++ b/lib/bashly/settings.rb @@ -9,7 +9,7 @@ class << self :compact_short_flags, :conjoined_flag_args, :config_path, - :enable_bash3_bouncer, + :enable_bash_version_bouncer, :enable_completions, :enable_deps_array, :enable_env_var_names_array, @@ -65,8 +65,8 @@ def enabled?(feature) (send(:"enable_#{feature}") == 'development' && !production?) end - def enable_bash3_bouncer - @enable_bash3_bouncer ||= get :enable_bash3_bouncer + def enable_bash_version_bouncer + @enable_bash_version_bouncer ||= get :enable_bash_version_bouncer end def enable_completions diff --git a/lib/bashly/views/wrapper/bash3_bouncer.gtx b/lib/bashly/views/wrapper/bash_version_bouncer.gtx similarity index 100% rename from lib/bashly/views/wrapper/bash3_bouncer.gtx rename to lib/bashly/views/wrapper/bash_version_bouncer.gtx diff --git a/schemas/settings.json b/schemas/settings.json index db299ec1..36c8b138 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -178,9 +178,9 @@ ], "default": "always" }, - "enable_bash3_bouncer": { - "title": "enable_bash3_bouncer", - "description": "Whether to include the code snippet that aborts when an old version of bash is detected in the generated script\nhttps://bashly.dev/usage/settings/#enable_bash3_bouncer", + "enable_bash_version_bouncer": { + "title": "enable_bash_version_bouncer", + "description": "Whether to include the code snippet that aborts when an old version of bash is detected in the generated script\nhttps://bashly.dev/usage/settings/#enable_bash_version_bouncer", "type": "string", "enum": [ "development", diff --git a/spec/approvals/script/wrapper/code b/spec/approvals/script/wrapper/code index e1cd1df7..ac2ae2bf 100644 --- a/spec/approvals/script/wrapper/code +++ b/spec/approvals/script/wrapper/code @@ -2,7 +2,7 @@ # This script was generated by bashly ... (https://bashly.dev) # Modifying it manually is not recommended -# :wrapper.bash3_bouncer +# :wrapper.bash_version_bouncer if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 2))); then printf "bash version 4.2 or higher is required\n" >&2 exit 1 diff --git a/spec/bashly/commands/generate_spec.rb b/spec/bashly/commands/generate_spec.rb index 4975b669..f29dc610 100644 --- a/spec/bashly/commands/generate_spec.rb +++ b/spec/bashly/commands/generate_spec.rb @@ -103,7 +103,7 @@ cp 'lib/bashly/templates/bashly.yml', "#{source_dir}/bashly.yml" end - it 'generates the cli script wrapped in a function without bash3 bouncer' do + it 'generates the cli script wrapped in a function without bash version bouncer' do expect { subject.execute %w[generate --wrap function] }.to output_approval('cli/generate/wrap-function') expect(File).to exist(cli_script) lines = File.readlines cli_script diff --git a/spec/bashly/script/wrapper_spec.rb b/spec/bashly/script/wrapper_spec.rb index be118f0e..963c63f5 100644 --- a/spec/bashly/script/wrapper_spec.rb +++ b/spec/bashly/script/wrapper_spec.rb @@ -18,7 +18,7 @@ context 'with function name' do subject { described_class.new command, 'my_super_function' } - it 'returns the complete script wrapped in a function without a bash3 bouncer' do + it 'returns the complete script wrapped in a function without a bash version bouncer' do lines = subject.code.split "\n" expect(lines[0..13].join("\n")).to match_approval('script/wrapper/code-wrapped') .except(/\d+\.\d+\.\d+(\.rc\d)?/) diff --git a/support/schema/settings.yml b/support/schema/settings.yml index 2abce71b..a2141a91 100644 --- a/support/schema/settings.yml +++ b/support/schema/settings.yml @@ -161,11 +161,11 @@ properties: - always - never default: always - enable_bash3_bouncer: - title: enable_bash3_bouncer + enable_bash_version_bouncer: + title: enable_bash_version_bouncer description: |- Whether to include the code snippet that aborts when an old version of bash is detected in the generated script - https://bashly.dev/usage/settings/#enable_bash3_bouncer + https://bashly.dev/usage/settings/#enable_bash_version_bouncer type: string enum: *feature_toggles default: always From 26c4a81e659700e5a01aff049912bac87f6170b1 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 15:09:44 +0300 Subject: [PATCH 13/23] remove duplicate ruby 3.3 from ci matrix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20df152e..35fc4177 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: LC_ALL: en_US.UTF-8 strategy: - matrix: { ruby: ['3.3', '3.3', '3.4', '4.0'] } + matrix: { ruby: ['3.3', '3.4', '4.0'] } steps: - name: Checkout code From ec5599b21a0fa951698af14149cda9f5e834f241 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 16:02:08 +0300 Subject: [PATCH 14/23] - Add zsh completions wrapper --- examples/completions-advanced/README.md | 8 +- examples/completions-advanced/src/bashly.yml | 2 +- examples/completions/README.md | 9 +- examples/completions/src/bashly.yml | 2 +- .../views/command/completion_script.gtx | 2 + .../views/command/completion_script_zsh.gtx | 58 ++++++++ .../completion_script_bash_spec.rb | 4 +- .../integration/completion_script_zsh_spec.rb | 140 ++++++++++++++++++ 8 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 lib/bashly/views/command/completion_script_zsh.gtx create mode 100644 spec/bashly/integration/completion_script_zsh_spec.rb diff --git a/examples/completions-advanced/README.md b/examples/completions-advanced/README.md index d6c6f3d4..e3b00866 100644 --- a/examples/completions-advanced/README.md +++ b/examples/completions-advanced/README.md @@ -5,10 +5,14 @@ dynamic external commands and internal functions, file and directory sources, and the `no-space` option. Runtime completions are enabled in `settings.yml`. Users can load the generated -Bash wrapper with: +wrapper for their shell with: ```bash +# Bash source <(cli completions) + +# Zsh +source <(cli completions zsh) ``` @@ -28,7 +32,7 @@ commands: args: - name: shell help: Shell to generate completions for - allowed: [bash] + allowed: [bash, zsh] default: bash - name: deploy diff --git a/examples/completions-advanced/src/bashly.yml b/examples/completions-advanced/src/bashly.yml index 53772230..ac2e980d 100644 --- a/examples/completions-advanced/src/bashly.yml +++ b/examples/completions-advanced/src/bashly.yml @@ -8,7 +8,7 @@ commands: args: - name: shell help: Shell to generate completions for - allowed: [bash] + allowed: [bash, zsh] default: bash - name: deploy diff --git a/examples/completions/README.md b/examples/completions/README.md index eb1a7920..e700dffb 100644 --- a/examples/completions/README.md +++ b/examples/completions/README.md @@ -3,10 +3,14 @@ Demonstrates how to expose the generated `send_completions` function through an application command. Runtime completions are enabled in `settings.yml`. -Users can load the Bash wrapper with: +Users can load the wrapper for their shell with: ```bash +# Bash source <(cli completions) + +# Zsh +source <(cli completions zsh) ``` This example was generated with: @@ -37,7 +41,7 @@ commands: args: - name: shell help: Shell to generate completions for - allowed: [bash] + allowed: [bash, zsh] default: bash - name: download @@ -102,4 +106,3 @@ download ```` - diff --git a/examples/completions/src/bashly.yml b/examples/completions/src/bashly.yml index 62f82689..fce8126e 100644 --- a/examples/completions/src/bashly.yml +++ b/examples/completions/src/bashly.yml @@ -8,7 +8,7 @@ commands: args: - name: shell help: Shell to generate completions for - allowed: [bash] + allowed: [bash, zsh] default: bash - name: download diff --git a/lib/bashly/views/command/completion_script.gtx b/lib/bashly/views/command/completion_script.gtx index 0ca9a265..05410e52 100644 --- a/lib/bashly/views/command/completion_script.gtx +++ b/lib/bashly/views/command/completion_script.gtx @@ -5,6 +5,7 @@ > > case "$completion_shell" in > bash) send_completions_bash ;; +> zsh) send_completions_zsh ;; > *) > printf 'unsupported shell: %s\n' "$completion_shell" >&2 > return 1 @@ -13,3 +14,4 @@ > } > = render :completion_script_bash += render :completion_script_zsh diff --git a/lib/bashly/views/command/completion_script_zsh.gtx b/lib/bashly/views/command/completion_script_zsh.gtx new file mode 100644 index 00000000..724eae6d --- /dev/null +++ b/lib/bashly/views/command/completion_script_zsh.gtx @@ -0,0 +1,58 @@ += view_marker + +> send_completions_zsh() { +> cat <<'BASHLY_COMPLETIONS' +> #compdef {{ name }} +> +> _{{ name.to_underscore }}_completions() { +> local completion_command="${words[1]}" +> local completion_current="${words[CURRENT]:-}" +> local completion_output +> local completion_directive +> local -a completion_words=() +> local -a completion_response=() +> local -a completion_options=() +> local -a completion_add_args=() +> +> if (( CURRENT > 2 )); then +> completion_words=("${words[2,$((CURRENT - 1))]}") +> fi +> +> completion_output="$( +> "$completion_command" __complete \ +> "${completion_words[@]}" \ +> "$completion_current" +> )" +> completion_response=("${(@f)completion_output}") +> +> completion_directive="${completion_response[-1]:-}" +> if [[ $completion_directive == :options=* ]]; then +> completion_response[-1]=() +> completion_options=("${(@s:,:)${completion_directive#:options=}}") +> fi +> +> local completion_option +> local completion_files=false +> local completion_directories=false +> for completion_option in "${completion_options[@]}"; do +> case "$completion_option" in +> files) completion_files=true ;; +> directories) completion_directories=true ;; +> no-space) completion_add_args=(-S '') ;; +> esac +> done +> +> if (( ${#completion_response[@]} > 0 )); then +> compadd "${completion_add_args[@]}" -- "${completion_response[@]}" +> fi +> +> if [[ $completion_files == true ]]; then +> _files "${completion_add_args[@]}" +> elif [[ $completion_directories == true ]]; then +> _files -/ "${completion_add_args[@]}" +> fi +> } +> +> compdef _{{ name.to_underscore }}_completions {{ name }} +> BASHLY_COMPLETIONS +> } diff --git a/spec/bashly/integration/completion_script_bash_spec.rb b/spec/bashly/integration/completion_script_bash_spec.rb index 34ed61a6..99134338 100644 --- a/spec/bashly/integration/completion_script_bash_spec.rb +++ b/spec/bashly/integration/completion_script_bash_spec.rb @@ -57,12 +57,12 @@ def complete_with_bash(*words, trace_options: false) it 'rejects unsupported shells' do stdout, stderr, status = Open3.capture3( - 'bash', '-c', "source #{cli}; send_completions zsh" + 'bash', '-c', "source #{cli}; send_completions fish" ) expect(status).not_to be_success expect(stdout).to be_empty - expect(stderr).to eq "unsupported shell: zsh\n" + expect(stderr).to eq "unsupported shell: fish\n" end end diff --git a/spec/bashly/integration/completion_script_zsh_spec.rb b/spec/bashly/integration/completion_script_zsh_spec.rb new file mode 100644 index 00000000..bebe61ff --- /dev/null +++ b/spec/bashly/integration/completion_script_zsh_spec.rb @@ -0,0 +1,140 @@ +describe 'Zsh completion script', :slow do + def complete_with_zsh(*words, trace_files: false) + completion_words = words.map { |word| Shellwords.shellescape word }.join ' ' + file_trace = if trace_files + <<~ZSH + _files() { + if [[ "$1" == "-/" ]]; then + shift + compadd "$@" -- apricot + else + compadd "$@" -- apple.txt apricot + fi + } + ZSH + end + + Open3.capture3( + 'zsh', '-f', '-c', <<~ZSH + cd ./spec/tmp + compdef() { true } + eval "$(bash -c 'source ./cli; send_completions zsh')" + typeset -A completion_test_seen=() + compadd() { + local no_space=false + if [[ "$1" == "-S" ]]; then + [[ -z "$2" ]] && no_space=true + shift 2 + fi + [[ "$1" == "--" ]] && shift + [[ $no_space == true ]] && print 'option no-space' + local candidate + for candidate in "$@"; do + if [[ -z "${completion_test_seen[$candidate]:-}" ]]; then + printf 'candidate %s\n' "$candidate" + completion_test_seen[$candidate]=1 + fi + done + } + #{file_trace} + words=(./cli #{completion_words}) + CURRENT=#{words.length + 1} + _cli_completions + ZSH + ) + end + + context 'generation' do + let(:cli) { File.expand_path 'spec/tmp/cli' } + + before(:context) do + Settings.enable_completions = 'always' + reset_tmp_dir + FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + end + + after(:context) do + Settings.enable_completions = 'never' + end + + it 'prints Zsh completions' do + stdout, stderr, status = Open3.capture3( + 'bash', '-c', "source #{cli}; send_completions zsh" + ) + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout).to start_with "#compdef cli\n" + expect(stdout).to include '_cli_completions() {' + expect(stdout).to end_with "compdef _cli_completions cli\n" + end + + it 'completes commands' do + stdout, stderr, status = complete_with_zsh 'd' + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to eq ['candidate deploy'] + end + + it 'preserves an empty current word' do + stdout, stderr, status = complete_with_zsh 'server', '' + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to include 'candidate start' + end + end + + context 'configured options' do + before(:context) do + Settings.enable_completions = 'always' + reset_tmp_dir + FileUtils.cp_r Dir['spec/fixtures/completions/configured/*'], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + end + + after(:context) do + Settings.enable_completions = 'never' + end + + it 'preserves literal candidates' do + stdout, stderr, status = complete_with_zsh 'static', '', trace_files: true + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to include( + 'candidate two words', 'candidate $literal', 'candidate :options=files' + ) + end + + it 'adds files when requested' do + stdout, stderr, status = complete_with_zsh 'files', 'a', trace_files: true + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to contain_exactly( + 'candidate apple.txt', 'candidate apricot' + ) + end + + it 'adds only directories when requested' do + stdout, stderr, status = complete_with_zsh 'directories', 'a', trace_files: true + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to eq ['candidate apricot'] + end + + it 'applies no-space to candidates and files' do + stdout, stderr, status = complete_with_zsh 'combined', 'a', trace_files: true + + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout.lines(chomp: true)).to contain_exactly( + 'option no-space', 'candidate apple.txt', 'candidate apricot' + ) + end + end +end From 85c17c2fd11bdfe7716faf22cb79dd14630e542e Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 16:06:40 +0300 Subject: [PATCH 15/23] install zsh in ci --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 35fc4177..b9fa3542 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install OS dependencies - run: sudo apt-get -y install mandoc + run: sudo apt-get -y install mandoc zsh # Rush needed for easy installation of stuff - name: Install rush From f16450c190eb284466a36150ae07117bc1e2ac0d Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 16:37:09 +0300 Subject: [PATCH 16/23] remove heredoc from completion wrappers --- .../views/command/completion_script_bash.gtx | 125 +++++++++--------- .../views/command/completion_script_zsh.gtx | 102 +++++++------- 2 files changed, 107 insertions(+), 120 deletions(-) diff --git a/lib/bashly/views/command/completion_script_bash.gtx b/lib/bashly/views/command/completion_script_bash.gtx index 9791008e..74a410c9 100644 --- a/lib/bashly/views/command/completion_script_bash.gtx +++ b/lib/bashly/views/command/completion_script_bash.gtx @@ -1,70 +1,63 @@ = view_marker > send_completions_bash() { -> cat <<'BASHLY_COMPLETIONS' -> _{{ name.to_underscore }}_completions() { -> local completion_command="${COMP_WORDS[0]}" -> local completion_current="${COMP_WORDS[COMP_CWORD]:-}" -> local completion_word_count=$((COMP_CWORD - 1)) -> local -a completion_words=() -> local -a completion_response=() -> local -a completion_options=() -> local -A completion_seen=() -> -> if [[ $completion_word_count -gt 0 ]]; then -> completion_words=("${COMP_WORDS[@]:1:completion_word_count}") -> fi -> -> COMPREPLY=() -> while IFS= read -r completion_line; do -> completion_response+=("$completion_line") -> done < <( -> "$completion_command" __complete \ -> "${completion_words[@]}" \ -> "$completion_current" -> ) -> -> local completion_directive="${completion_response[-1]:-}" -> if [[ $completion_directive == :options=* ]]; then -> unset 'completion_response[-1]' -> IFS=, read -r -a completion_options <<< "${completion_directive#:options=}" -> fi -> -> COMPREPLY=("${completion_response[@]}") -> local completion_candidate -> for completion_candidate in "${COMPREPLY[@]}"; do -> completion_seen["$completion_candidate"]=1 -> done -> -> local completion_option -> local completion_files=false -> local completion_directories=false -> for completion_option in "${completion_options[@]}"; do -> case "$completion_option" in -> files) completion_files=true ;; -> directories) completion_directories=true ;; -> no-space) compopt -o nospace ;; -> esac -> done -> -> if [[ $completion_files == true ]]; then -> while IFS= read -r completion_candidate; do -> if [[ -z "${completion_seen[$completion_candidate]:-}" ]]; then -> COMPREPLY+=("$completion_candidate") -> completion_seen["$completion_candidate"]=1 -> fi -> done < <(compgen -f -- "$completion_current") -> elif [[ $completion_directories == true ]]; then -> while IFS= read -r completion_candidate; do -> if [[ -z "${completion_seen[$completion_candidate]:-}" ]]; then -> COMPREPLY+=("$completion_candidate") -> completion_seen["$completion_candidate"]=1 -> fi -> done < <(compgen -d -- "$completion_current") -> fi +> echo $'_{{ name.to_underscore }}_completions() {' +> echo $' local completion_command="${COMP_WORDS[0]}"' +> echo $' local completion_current="${COMP_WORDS[COMP_CWORD]:-}"' +> echo $' local completion_word_count=$((COMP_CWORD - 1))' +> echo $' local -a completion_words=()' +> echo $' local -a completion_response=()' +> echo $' local -a completion_options=()' +> echo $' local -A completion_seen=()' +> echo $'' +> echo $' if [[ $completion_word_count -gt 0 ]]; then' +> echo $' completion_words=("${COMP_WORDS[@]:1:completion_word_count}")' +> echo $' fi' +> echo $'' +> echo $' COMPREPLY=()' +> echo $' while IFS= read -r completion_line; do' +> echo $' completion_response+=("$completion_line")' +> echo $' done < <("$completion_command" __complete "${completion_words[@]}" "$completion_current")' +> echo $'' +> echo $' local completion_directive="${completion_response[-1]:-}"' +> echo $' if [[ $completion_directive == :options=* ]]; then' +> echo $' unset "completion_response[-1]"' +> echo $' IFS=, read -r -a completion_options <<< "${completion_directive#:options=}"' +> echo $' fi' +> echo $'' +> echo $' COMPREPLY=("${completion_response[@]}")' +> echo $' local completion_candidate' +> echo $' for completion_candidate in "${COMPREPLY[@]}"; do' +> echo $' completion_seen["$completion_candidate"]=1' +> echo $' done' +> echo $'' +> echo $' local completion_option' +> echo $' local completion_files=false' +> echo $' local completion_directories=false' +> echo $' for completion_option in "${completion_options[@]}"; do' +> echo $' case "$completion_option" in' +> echo $' files) completion_files=true ;;' +> echo $' directories) completion_directories=true ;;' +> echo $' no-space) compopt -o nospace ;;' +> echo $' esac' +> echo $' done' +> echo $'' +> echo $' if [[ $completion_files == true ]]; then' +> echo $' while IFS= read -r completion_candidate; do' +> echo $' if [[ -z "${completion_seen[$completion_candidate]:-}" ]]; then' +> echo $' COMPREPLY+=("$completion_candidate")' +> echo $' completion_seen["$completion_candidate"]=1' +> echo $' fi' +> echo $' done < <(compgen -f -- "$completion_current")' +> echo $' elif [[ $completion_directories == true ]]; then' +> echo $' while IFS= read -r completion_candidate; do' +> echo $' if [[ -z "${completion_seen[$completion_candidate]:-}" ]]; then' +> echo $' COMPREPLY+=("$completion_candidate")' +> echo $' completion_seen["$completion_candidate"]=1' +> echo $' fi' +> echo $' done < <(compgen -d -- "$completion_current")' +> echo $' fi' +> echo $'}' +> echo $'' +> echo $'complete -F _{{ name.to_underscore }}_completions {{ name }}' > } -> -> complete -F _{{ name.to_underscore }}_completions {{ name }} -> BASHLY_COMPLETIONS -> } -> diff --git a/lib/bashly/views/command/completion_script_zsh.gtx b/lib/bashly/views/command/completion_script_zsh.gtx index 724eae6d..0f5e3a17 100644 --- a/lib/bashly/views/command/completion_script_zsh.gtx +++ b/lib/bashly/views/command/completion_script_zsh.gtx @@ -1,58 +1,52 @@ = view_marker > send_completions_zsh() { -> cat <<'BASHLY_COMPLETIONS' -> #compdef {{ name }} -> -> _{{ name.to_underscore }}_completions() { -> local completion_command="${words[1]}" -> local completion_current="${words[CURRENT]:-}" -> local completion_output -> local completion_directive -> local -a completion_words=() -> local -a completion_response=() -> local -a completion_options=() -> local -a completion_add_args=() -> -> if (( CURRENT > 2 )); then -> completion_words=("${words[2,$((CURRENT - 1))]}") -> fi -> -> completion_output="$( -> "$completion_command" __complete \ -> "${completion_words[@]}" \ -> "$completion_current" -> )" -> completion_response=("${(@f)completion_output}") -> -> completion_directive="${completion_response[-1]:-}" -> if [[ $completion_directive == :options=* ]]; then -> completion_response[-1]=() -> completion_options=("${(@s:,:)${completion_directive#:options=}}") -> fi -> -> local completion_option -> local completion_files=false -> local completion_directories=false -> for completion_option in "${completion_options[@]}"; do -> case "$completion_option" in -> files) completion_files=true ;; -> directories) completion_directories=true ;; -> no-space) completion_add_args=(-S '') ;; -> esac -> done -> -> if (( ${#completion_response[@]} > 0 )); then -> compadd "${completion_add_args[@]}" -- "${completion_response[@]}" -> fi -> -> if [[ $completion_files == true ]]; then -> _files "${completion_add_args[@]}" -> elif [[ $completion_directories == true ]]; then -> _files -/ "${completion_add_args[@]}" -> fi -> } -> -> compdef _{{ name.to_underscore }}_completions {{ name }} -> BASHLY_COMPLETIONS +> echo $'#compdef {{ name }}' +> echo $'' +> echo $'_{{ name.to_underscore }}_completions() {' +> echo $' local completion_command="${words[1]}"' +> echo $' local completion_current="${words[CURRENT]:-}"' +> echo $' local completion_output' +> echo $' local completion_directive' +> echo $' local -a completion_words=()' +> echo $' local -a completion_response=()' +> echo $' local -a completion_options=()' +> echo $' local -a completion_add_args=()' +> echo $'' +> echo $' if (( CURRENT > 2 )); then' +> echo $' completion_words=("${words[2,$((CURRENT - 1))]}")' +> echo $' fi' +> echo $'' +> echo $' completion_output="$("$completion_command" __complete "${completion_words[@]}" "$completion_current")"' +> echo $' completion_response=("${(@f)completion_output}")' +> echo $'' +> echo $' completion_directive="${completion_response[-1]:-}"' +> echo $' if [[ $completion_directive == :options=* ]]; then' +> echo $' completion_response[-1]=()' +> echo $' completion_options=("${(@s:,:)${completion_directive#:options=}}")' +> echo $' fi' +> echo $'' +> echo $' local completion_option' +> echo $' local completion_files=false' +> echo $' local completion_directories=false' +> echo $' for completion_option in "${completion_options[@]}"; do' +> echo $' case "$completion_option" in' +> echo $' files) completion_files=true ;;' +> echo $' directories) completion_directories=true ;;' +> echo $' no-space) completion_add_args=(-S "") ;;' +> echo $' esac' +> echo $' done' +> echo $'' +> echo $' if (( ${#completion_response[@]} > 0 )); then' +> echo $' compadd "${completion_add_args[@]}" -- "${completion_response[@]}"' +> echo $' fi' +> echo $'' +> echo $' if [[ $completion_files == true ]]; then' +> echo $' _files "${completion_add_args[@]}"' +> echo $' elif [[ $completion_directories == true ]]; then' +> echo $' _files -/ "${completion_add_args[@]}"' +> echo $' fi' +> echo $'}' +> echo $'' +> echo $'compdef _{{ name.to_underscore }}_completions {{ name }}' > } From cffb490ae87c22bfaa34d23a6efddb7e7facc3f2 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 17:33:19 +0300 Subject: [PATCH 17/23] - Refactor completions settings --- examples/completions-advanced/README.md | 2 +- examples/completions-advanced/settings.yml | 2 +- examples/completions/README.md | 4 +- examples/completions/settings.yml | 2 +- lib/bashly/libraries/settings/settings.yml | 9 ++- lib/bashly/settings.rb | 30 ++++++++-- .../views/command/completion_script.gtx | 14 +++-- lib/bashly/views/command/completions.gtx | 2 +- lib/bashly/views/command/master_script.gtx | 2 +- lib/bashly/views/command/start.gtx | 2 +- schemas/settings.json | 25 ++++---- .../completion_script_bash_spec.rb | 8 +-- .../integration/completion_script_zsh_spec.rb | 8 +-- .../integration/completion_settings_spec.rb | 50 ++++++++++++++++ .../integration/runtime_completions_spec.rb | 4 +- spec/bashly/settings_spec.rb | 58 +++++++++++++++++++ spec/fixtures/schemas_invalid/settings/3.yml | 1 + support/schema/settings.yml | 18 +++--- 18 files changed, 194 insertions(+), 47 deletions(-) create mode 100644 spec/bashly/integration/completion_settings_spec.rb create mode 100644 spec/fixtures/schemas_invalid/settings/3.yml diff --git a/examples/completions-advanced/README.md b/examples/completions-advanced/README.md index e3b00866..67f25a0f 100644 --- a/examples/completions-advanced/README.md +++ b/examples/completions-advanced/README.md @@ -74,7 +74,7 @@ commands: ## `settings.yml` ````yaml -enable_completions: always +completions: full ```` diff --git a/examples/completions-advanced/settings.yml b/examples/completions-advanced/settings.yml index c9337546..394ccc6d 100644 --- a/examples/completions-advanced/settings.yml +++ b/examples/completions-advanced/settings.yml @@ -1 +1 @@ -enable_completions: always +completions: full diff --git a/examples/completions/README.md b/examples/completions/README.md index e700dffb..468260c4 100644 --- a/examples/completions/README.md +++ b/examples/completions/README.md @@ -59,7 +59,7 @@ commands: ## `settings.yml` ````yaml -enable_completions: always +completions: full ```` @@ -104,5 +104,3 @@ download ```` - - diff --git a/examples/completions/settings.yml b/examples/completions/settings.yml index c9337546..394ccc6d 100644 --- a/examples/completions/settings.yml +++ b/examples/completions/settings.yml @@ -1 +1 @@ -enable_completions: always +completions: full diff --git a/lib/bashly/libraries/settings/settings.yml b/lib/bashly/libraries/settings/settings.yml index 38f84811..3aa87c61 100644 --- a/lib/bashly/libraries/settings/settings.yml +++ b/lib/bashly/libraries/settings/settings.yml @@ -158,13 +158,20 @@ env: development # - never # do not render this feature enable_header_comment: always enable_bash_version_bouncer: always -enable_completions: never enable_view_markers: development enable_inspect_args: development enable_deps_array: always enable_env_var_names_array: always enable_sourcing: development +# Generate native runtime completions. Supported values: +# - ~ or false # disable completions (default) +# - minimal # generate the runtime completion engine without shell adapters +# - bash or zsh # generate the runtime engine and one shell adapter +# - bash,zsh # generate the runtime engine and selected shell adapters +# - full # generate the runtime engine and all available shell adapters +completions: ~ + #------------------------------------------------------------------------------- # DEVELOPER OPTIONS diff --git a/lib/bashly/settings.rb b/lib/bashly/settings.rb index 38694d75..b7143047 100644 --- a/lib/bashly/settings.rb +++ b/lib/bashly/settings.rb @@ -1,5 +1,7 @@ module Bashly class Settings + COMPLETION_SHELLS = %w[bash zsh].freeze + class << self include AssetHelper @@ -7,10 +9,10 @@ class << self :argfile_var, :commands_dir, :compact_short_flags, + :completions, :conjoined_flag_args, :config_path, :enable_bash_version_bouncer, - :enable_completions, :enable_deps_array, :enable_env_var_names_array, :enable_header_comment, @@ -51,6 +53,28 @@ def compact_short_flags @compact_short_flags ||= get :compact_short_flags end + def completions + @completions ||= get :completions + end + + def completions? + completions == 'minimal' || completion_shells.any? + end + + def completion_shells + value = completions + return [] if value.nil? || value == false || value == 'minimal' + return COMPLETION_SHELLS if value == 'full' + + shells = value.split(',', -1).map(&:strip) if value.is_a? String + valid = shells&.any? && shells.all? { |shell| COMPLETION_SHELLS.include? shell } + unique = shells&.uniq == shells + return shells if valid && unique + + raise ConfigurationError, + "completions must be false, minimal, full, or a comma-separated list of: #{COMPLETION_SHELLS.join ', '}" + end + def conjoined_flag_args @conjoined_flag_args ||= get :conjoined_flag_args end @@ -69,10 +93,6 @@ def enable_bash_version_bouncer @enable_bash_version_bouncer ||= get :enable_bash_version_bouncer end - def enable_completions - @enable_completions ||= get :enable_completions - end - def enable_deps_array @enable_deps_array ||= get :enable_deps_array end diff --git a/lib/bashly/views/command/completion_script.gtx b/lib/bashly/views/command/completion_script.gtx index 05410e52..3a604f15 100644 --- a/lib/bashly/views/command/completion_script.gtx +++ b/lib/bashly/views/command/completion_script.gtx @@ -1,11 +1,14 @@ = view_marker +completion_shells = Settings.completion_shells + > send_completions() { -> local completion_shell="${1:-bash}" +> local completion_shell="${1:-{{ completion_shells.first }}}" > > case "$completion_shell" in -> bash) send_completions_bash ;; -> zsh) send_completions_zsh ;; +completion_shells.each do |shell| + > {{ shell }}) send_completions_{{ shell }} ;; +end > *) > printf 'unsupported shell: %s\n' "$completion_shell" >&2 > return 1 @@ -13,5 +16,6 @@ > esac > } > -= render :completion_script_bash -= render :completion_script_zsh +completion_shells.each do |shell| + = render :"completion_script_#{shell}" +end diff --git a/lib/bashly/views/command/completions.gtx b/lib/bashly/views/command/completions.gtx index 72f345ea..5ffbf89a 100644 --- a/lib/bashly/views/command/completions.gtx +++ b/lib/bashly/views/command/completions.gtx @@ -74,7 +74,7 @@ > done <<<"$completion_output" > } > -= render :completion_script += render :completion_script if Settings.completion_shells.any? = render :completion_function deep_commands.each do |command| = command.render :completion_function diff --git a/lib/bashly/views/command/master_script.gtx b/lib/bashly/views/command/master_script.gtx index 49d09211..d0b353a2 100644 --- a/lib/bashly/views/command/master_script.gtx +++ b/lib/bashly/views/command/master_script.gtx @@ -8,7 +8,7 @@ = render :user_lib if user_lib.any? = render :command_functions = render :parse_requirements -= render :completions if Settings.enabled? :completions += render :completions if Settings.completions? = render :user_hooks = render :initialize = render :run diff --git a/lib/bashly/views/command/start.gtx b/lib/bashly/views/command/start.gtx index c28ca4b4..342eaa85 100644 --- a/lib/bashly/views/command/start.gtx +++ b/lib/bashly/views/command/start.gtx @@ -1,7 +1,7 @@ = view_marker > command_line_args=("$@") -if Settings.enabled? :completions +if Settings.completions? > if [[ "${command_line_args[0]:-}" == "__complete" ]]; then > completion_run "${command_line_args[@]:1}" > else diff --git a/schemas/settings.json b/schemas/settings.json index 36c8b138..3fa7d42c 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -190,17 +190,22 @@ ], "default": "always" }, - "enable_completions": { - "title": "enable_completions", - "description": "Whether to include runtime completion functions in the generated script\nhttps://bashly.dev/usage/settings/#enable_completions", - "type": "string", - "enum": [ - "development", - "production", - "always", - "never" + "completions": { + "title": "completions", + "description": "Which runtime completion functions and shell adapters to include in the generated script.\nUse minimal for the runtime engine only, full for all available adapters, or a comma-separated shell list.\nhttps://bashly.dev/usage/settings/#completions", + "oneOf": [ + { + "type": "null" + }, + { + "const": false + }, + { + "type": "string", + "pattern": "^(minimal|full|(bash|zsh)(\\s*,\\s*(bash|zsh))*)$" + } ], - "default": "never" + "default": null }, "enable_view_markers": { "title": "enable_view_markers", diff --git a/spec/bashly/integration/completion_script_bash_spec.rb b/spec/bashly/integration/completion_script_bash_spec.rb index 99134338..07fab158 100644 --- a/spec/bashly/integration/completion_script_bash_spec.rb +++ b/spec/bashly/integration/completion_script_bash_spec.rb @@ -23,14 +23,14 @@ def complete_with_bash(*words, trace_options: false) let(:cli) { File.expand_path 'spec/tmp/cli' } before(:context) do - Settings.enable_completions = 'always' + Settings.completions = 'full' reset_tmp_dir FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' Commands::Generate.new.execute %w[generate --quiet] end after(:context) do - Settings.enable_completions = 'never' + Settings.completions = nil end it 'prints Bash completions by default' do @@ -68,7 +68,7 @@ def complete_with_bash(*words, trace_options: false) context 'configured options' do before(:context) do - Settings.enable_completions = 'always' + Settings.completions = 'full' reset_tmp_dir FileUtils.cp_r Dir['spec/fixtures/completions/configured/*'], 'spec/tmp' Commands::Generate.new.execute %w[generate --quiet] @@ -77,7 +77,7 @@ def complete_with_bash(*words, trace_options: false) end after(:context) do - Settings.enable_completions = 'never' + Settings.completions = nil end it 'adds files when requested' do diff --git a/spec/bashly/integration/completion_script_zsh_spec.rb b/spec/bashly/integration/completion_script_zsh_spec.rb index bebe61ff..af3a922f 100644 --- a/spec/bashly/integration/completion_script_zsh_spec.rb +++ b/spec/bashly/integration/completion_script_zsh_spec.rb @@ -48,14 +48,14 @@ def complete_with_zsh(*words, trace_files: false) let(:cli) { File.expand_path 'spec/tmp/cli' } before(:context) do - Settings.enable_completions = 'always' + Settings.completions = 'full' reset_tmp_dir FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' Commands::Generate.new.execute %w[generate --quiet] end after(:context) do - Settings.enable_completions = 'never' + Settings.completions = nil end it 'prints Zsh completions' do @@ -89,14 +89,14 @@ def complete_with_zsh(*words, trace_files: false) context 'configured options' do before(:context) do - Settings.enable_completions = 'always' + Settings.completions = 'full' reset_tmp_dir FileUtils.cp_r Dir['spec/fixtures/completions/configured/*'], 'spec/tmp' Commands::Generate.new.execute %w[generate --quiet] end after(:context) do - Settings.enable_completions = 'never' + Settings.completions = nil end it 'preserves literal candidates' do diff --git a/spec/bashly/integration/completion_settings_spec.rb b/spec/bashly/integration/completion_settings_spec.rb new file mode 100644 index 00000000..3c0b781f --- /dev/null +++ b/spec/bashly/integration/completion_settings_spec.rb @@ -0,0 +1,50 @@ +describe 'Completion settings', :slow do + def generate_with_completions(value) + Settings.completions = value + reset_tmp_dir + FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp' + Commands::Generate.new.execute %w[generate --quiet] + File.read 'spec/tmp/cli' + end + + after do + Settings.completions = nil + end + + it 'omits the runtime engine and adapters by default' do + script = generate_with_completions nil + + expect(script).not_to include 'completion_run() {' + expect(script).not_to include 'send_completions() {' + end + + it 'generates only the runtime engine for minimal' do + script = generate_with_completions 'minimal' + + expect(script).to include 'completion_run() {' + expect(script).not_to include 'send_completions() {' + end + + it 'generates only the Bash adapter when Bash is selected' do + script = generate_with_completions 'bash' + + expect(script).to include 'send_completions_bash() {' + expect(script).not_to include 'send_completions_zsh() {' + expect(script).to include 'local completion_shell="${1:-bash}"' + end + + it 'generates only the Zsh adapter when Zsh is selected' do + script = generate_with_completions 'zsh' + + expect(script).not_to include 'send_completions_bash() {' + expect(script).to include 'send_completions_zsh() {' + expect(script).to include 'local completion_shell="${1:-zsh}"' + end + + it 'generates every adapter for full' do + script = generate_with_completions 'full' + + expect(script).to include 'send_completions_bash() {' + expect(script).to include 'send_completions_zsh() {' + end +end diff --git a/spec/bashly/integration/runtime_completions_spec.rb b/spec/bashly/integration/runtime_completions_spec.rb index 9d8fa51e..b96e0a3c 100644 --- a/spec/bashly/integration/runtime_completions_spec.rb +++ b/spec/bashly/integration/runtime_completions_spec.rb @@ -7,14 +7,14 @@ cli = File.expand_path 'spec/tmp/cli' before(:context) do - Settings.enable_completions = 'always' + Settings.completions = 'minimal' reset_tmp_dir FileUtils.cp_r Dir["#{workspace}/*"], 'spec/tmp' Commands::Generate.new.execute %w[generate --quiet] end after(:context) do - Settings.enable_completions = 'never' + Settings.completions = nil end examples.each do |name, example| diff --git a/spec/bashly/settings_spec.rb b/spec/bashly/settings_spec.rb index 9af15d9c..025d92a7 100644 --- a/spec/bashly/settings_spec.rb +++ b/spec/bashly/settings_spec.rb @@ -129,6 +129,64 @@ end end + describe '::completion_shells' do + it 'disables completions by default' do + expect(subject.completions?).to be false + expect(subject.completion_shells).to be_empty + end + + it 'disables completions when set to false' do + subject.completions = false + + expect(subject.completions?).to be false + expect(subject.completion_shells).to be_empty + end + + it 'enables only the runtime engine when set to minimal' do + subject.completions = 'minimal' + + expect(subject.completions?).to be true + expect(subject.completion_shells).to be_empty + end + + it 'enables every supported shell when set to full' do + subject.completions = 'full' + + expect(subject.completion_shells).to eq %w[bash zsh] + end + + it 'accepts one shell' do + subject.completions = 'zsh' + + expect(subject.completion_shells).to eq %w[zsh] + end + + it 'accepts comma-separated shells with optional whitespace' do + subject.completions = 'bash, zsh' + + expect(subject.completion_shells).to eq %w[bash zsh] + end + + it 'accepts the setting from the environment' do + original_value = ENV['BASHLY_COMPLETIONS'] + ENV['BASHLY_COMPLETIONS'] = 'zsh,bash' + subject.completions = nil + + expect(subject.completion_shells).to eq %w[zsh bash] + ensure + ENV['BASHLY_COMPLETIONS'] = original_value + end + + it 'rejects invalid values' do + invalid_values = [true, '', 'all', 'bash+zsh', 'fish', 'bash,', 'bash,,zsh', 'bash,bash'] + + invalid_values.each do |value| + subject.completions = value + expect { subject.completion_shells }.to raise_error ConfigurationError + end + end + end + describe '::production?' do it 'returns false by default' do expect(subject.production?).to be false diff --git a/spec/fixtures/schemas_invalid/settings/3.yml b/spec/fixtures/schemas_invalid/settings/3.yml new file mode 100644 index 00000000..10ca8f1e --- /dev/null +++ b/spec/fixtures/schemas_invalid/settings/3.yml @@ -0,0 +1 @@ +completions: bash+zsh diff --git a/support/schema/settings.yml b/support/schema/settings.yml index a2141a91..0441def6 100644 --- a/support/schema/settings.yml +++ b/support/schema/settings.yml @@ -169,14 +169,18 @@ properties: type: string enum: *feature_toggles default: always - enable_completions: - title: enable_completions + completions: + title: completions description: |- - Whether to include runtime completion functions in the generated script - https://bashly.dev/usage/settings/#enable_completions - type: string - enum: *feature_toggles - default: never + Which runtime completion functions and shell adapters to include in the generated script. + Use minimal for the runtime engine only, full for all available adapters, or a comma-separated shell list. + https://bashly.dev/usage/settings/#completions + oneOf: + - type: "null" + - const: false + - type: string + pattern: '^(minimal|full|(bash|zsh)(\s*,\s*(bash|zsh))*)$' + default: null enable_view_markers: title: enable_view_markers description: |- From f3e6505f42d458e61309374926addc0e2f01a4e8 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 17:35:08 +0300 Subject: [PATCH 18/23] - Make `inspect_args` sorting deterministic --- lib/bashly/views/command/inspect_args.gtx | 6 +++--- spec/approvals/examples/commands | 2 +- spec/approvals/examples/validations | 2 +- spec/approvals/examples/whitelist | 8 ++++---- spec/approvals/fixtures/default-validations | 2 +- spec/approvals/fixtures/required-args-order | 6 +++--- spec/approvals/fixtures/whitelist-optional | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/bashly/views/command/inspect_args.gtx b/lib/bashly/views/command/inspect_args.gtx index 07a7d6b0..e371b65b 100644 --- a/lib/bashly/views/command/inspect_args.gtx +++ b/lib/bashly/views/command/inspect_args.gtx @@ -4,7 +4,7 @@ > local k > > if ((${#args[@]})); then -> readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort) +> readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | LC_ALL=C sort) > echo args: > for k in "${sorted_keys[@]}"; do > echo "- \${args[$k]} = ${args[$k]}" @@ -28,7 +28,7 @@ end if Settings.enabled? :deps_array > if ((${#deps[@]})); then - > readarray -t sorted_keys < <(printf '%s\n' "${!deps[@]}" | sort) + > readarray -t sorted_keys < <(printf '%s\n' "${!deps[@]}" | LC_ALL=C sort) > echo > echo deps: > for k in "${sorted_keys[@]}"; do @@ -40,7 +40,7 @@ end if Settings.enabled? :env_var_names_array > if ((${#env_var_names[@]})); then - > readarray -t sorted_names < <(printf '%s\n' "${env_var_names[@]}" | sort) + > readarray -t sorted_names < <(printf '%s\n' "${env_var_names[@]}" | LC_ALL=C sort) > echo > echo "environment variables:" > for k in "${sorted_names[@]}"; do diff --git a/spec/approvals/examples/commands b/spec/approvals/examples/commands index e0953a0e..b295ac13 100644 --- a/spec/approvals/examples/commands +++ b/spec/approvals/examples/commands @@ -119,8 +119,8 @@ missing required flag: --user, -u USER # The code you write here will be wrapped by a function named 'cli_upload_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: -- ${args[source]} = sourcefile - ${args[--user]} = username +- ${args[source]} = sourcefile environment variables: - $API_KEY = diff --git a/spec/approvals/examples/validations b/spec/approvals/examples/validations index 22966367..58489137 100644 --- a/spec/approvals/examples/validations +++ b/spec/approvals/examples/validations @@ -16,8 +16,8 @@ run ./validate --help to test your bash script # The code you write here will be wrapped by a function named 'validate_calc_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: -- ${args[first]} = 1 - ${args[--save]} = README.md +- ${args[first]} = 1 - ${args[second]} = 2 + ./validate calc A validation error in FIRST: diff --git a/spec/approvals/examples/whitelist b/spec/approvals/examples/whitelist index 9ec3343a..3bfa88ff 100644 --- a/spec/approvals/examples/whitelist +++ b/spec/approvals/examples/whitelist @@ -52,10 +52,10 @@ region must be one of: eu, us # The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: -- ${args[environment]} = development - ${args[--protocol]} = ssh -- ${args[region]} = eu - ${args[--user]} = admin +- ${args[environment]} = development +- ${args[region]} = eu + ./login us --user user --protocol icmp --protocol must be one of: ftp, ssh, http + ./login eu production --user admin --protocol ssh @@ -64,7 +64,7 @@ args: # The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: -- ${args[environment]} = production - ${args[--protocol]} = ssh -- ${args[region]} = eu - ${args[--user]} = admin +- ${args[environment]} = production +- ${args[region]} = eu diff --git a/spec/approvals/fixtures/default-validations b/spec/approvals/fixtures/default-validations index a31b30b7..8d32a687 100644 --- a/spec/approvals/fixtures/default-validations +++ b/spec/approvals/fixtures/default-validations @@ -18,5 +18,5 @@ must be an existing file # The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: -- ${args[file]} = README.md - ${args[--template]} = cli +- ${args[file]} = README.md diff --git a/spec/approvals/fixtures/required-args-order b/spec/approvals/fixtures/required-args-order index 64049618..401583fe 100644 --- a/spec/approvals/fixtures/required-args-order +++ b/spec/approvals/fixtures/required-args-order @@ -27,9 +27,9 @@ args: # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--method]} = GET +- ${args[--role]} = admin - ${args[port]} = 3000 - ${args[protocol]} = http -- ${args[--role]} = admin + ./download http --role admin --method GET 3000 # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. @@ -37,9 +37,9 @@ args: # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--method]} = GET +- ${args[--role]} = admin - ${args[port]} = 3000 - ${args[protocol]} = http -- ${args[--role]} = admin + ./download --role admin --method GET http 3000 # This file is located at 'src/root_command.sh'. # It contains the implementation for the 'download' command. @@ -47,6 +47,6 @@ args: # Feel free to edit this file; your changes will persist when regenerating. args: - ${args[--method]} = GET +- ${args[--role]} = admin - ${args[port]} = 3000 - ${args[protocol]} = http -- ${args[--role]} = admin diff --git a/spec/approvals/fixtures/whitelist-optional b/spec/approvals/fixtures/whitelist-optional index fd04d487..5b28ee2a 100644 --- a/spec/approvals/fixtures/whitelist-optional +++ b/spec/approvals/fixtures/whitelist-optional @@ -43,5 +43,5 @@ action must be one of: push, commit # The code you write here will be wrapped by a function named 'root_command()'. # Feel free to edit this file; your changes will persist when regenerating. args: -- ${args[action]} = push - ${args[--notify]} = slack +- ${args[action]} = push From df47bd870a948477dcebe24aec2e6d6bd7cc318a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 18:21:04 +0300 Subject: [PATCH 19/23] fix schema property --- schemas/settings.json | 2 +- support/schema/settings.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/schemas/settings.json b/schemas/settings.json index 3fa7d42c..b20c9377 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -154,7 +154,7 @@ "title": "show examples on error", "description": "Whether to show command examples when the input line is missing required arguments\nhttps://bashly.dev/usage/settings/#show_examples_on_error", "type": "boolean", - "default": true + "default": false }, "env": { "title": "env", diff --git a/support/schema/settings.yml b/support/schema/settings.yml index 0441def6..fbcc0085 100644 --- a/support/schema/settings.yml +++ b/support/schema/settings.yml @@ -138,7 +138,7 @@ properties: Whether to show command examples when the input line is missing required arguments https://bashly.dev/usage/settings/#show_examples_on_error type: boolean - default: true + default: false env: title: env description: |- From d1deb7e837b6ccb3ab35dae4fc3dfe5e28fe5b0e Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 18:40:16 +0300 Subject: [PATCH 20/23] version 2.0.0.rc1 --- lib/bashly/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bashly/version.rb b/lib/bashly/version.rb index 8a0effa2..472c313a 100644 --- a/lib/bashly/version.rb +++ b/lib/bashly/version.rb @@ -1,3 +1,3 @@ module Bashly - VERSION = '1.4.0' + VERSION = '2.0.0.rc1' end From 154686367dd890a58d561b24d839e21797ba280a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 20:46:26 +0300 Subject: [PATCH 21/23] - Add `start()` bash function to wrap `__complete`, initialize` and `run` --- lib/bashly/libraries/settings/settings.yml | 1 + lib/bashly/views/command/master_script.gtx | 10 ++++++---- lib/bashly/views/command/start.gtx | 18 +++++++++--------- schemas/settings.json | 13 +++++++++++++ spec/approvals/examples/stacktrace | 1 + spec/bashly/script/wrapper_spec.rb | 17 ++++++++++++++++- .../completions/core/src/initialize.sh | 2 ++ support/schema/settings.yml | 6 ++++++ 8 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 spec/fixtures/completions/core/src/initialize.sh diff --git a/lib/bashly/libraries/settings/settings.yml b/lib/bashly/libraries/settings/settings.yml index 3aa87c61..20fb374a 100644 --- a/lib/bashly/libraries/settings/settings.yml +++ b/lib/bashly/libraries/settings/settings.yml @@ -200,5 +200,6 @@ var_aliases: # Choose different names for some of the internal functions. function_names: + start: ~ run: ~ initialize: ~ diff --git a/lib/bashly/views/command/master_script.gtx b/lib/bashly/views/command/master_script.gtx index d0b353a2..6c29215b 100644 --- a/lib/bashly/views/command/master_script.gtx +++ b/lib/bashly/views/command/master_script.gtx @@ -13,12 +13,14 @@ = render :initialize = render :run -> +> += render :start +> if Settings.enabled? :sourcing > if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - = render(:start).indent 2 + > {{ Settings.function_name :start }} "$@" > fi else - = render :start + > {{ Settings.function_name :start }} "$@" end -> +> diff --git a/lib/bashly/views/command/start.gtx b/lib/bashly/views/command/start.gtx index 342eaa85..9b5619ab 100644 --- a/lib/bashly/views/command/start.gtx +++ b/lib/bashly/views/command/start.gtx @@ -1,14 +1,14 @@ = view_marker -> command_line_args=("$@") +> {{ Settings.function_name :start }}() { if Settings.completions? -> if [[ "${command_line_args[0]:-}" == "__complete" ]]; then -> completion_run "${command_line_args[@]:1}" -> else +> if [[ ${1:-} == "__complete" ]]; then +> completion_run "${@:2}" +> return +> fi +end +> +> command_line_args=("$@") > {{ Settings.function_name :initialize }} > {{ Settings.function_name :run }} "${command_line_args[@]}" -> fi -else -> {{ Settings.function_name :initialize }} -> {{ Settings.function_name :run }} "${command_line_args[@]}" -end +> } diff --git a/schemas/settings.json b/schemas/settings.json index b20c9377..3f61d4b3 100644 --- a/schemas/settings.json +++ b/schemas/settings.json @@ -423,6 +423,19 @@ "description": "Choose different names for some of the internal functions.\nhttps://bashly.dev/usage/settings/#function_names", "type": "object", "properties": { + "start": { + "title": "start", + "description": "Name for the start() function\nhttps://bashly.dev/usage/settings/#function_names", + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, "run": { "title": "run", "description": "Name for the run() function\nhttps://bashly.dev/usage/settings/#function_names", diff --git a/spec/approvals/examples/stacktrace b/spec/approvals/examples/stacktrace index 84ebc903..2261ad18 100644 --- a/spec/approvals/examples/stacktrace +++ b/spec/approvals/examples/stacktrace @@ -52,4 +52,5 @@ Examples: Stack trace: from ./download: in `root_command` from ./download: in `run` + from ./download: in `start` from ./download: in `main` diff --git a/spec/bashly/script/wrapper_spec.rb b/spec/bashly/script/wrapper_spec.rb index 963c63f5..b64b0975 100644 --- a/spec/bashly/script/wrapper_spec.rb +++ b/spec/bashly/script/wrapper_spec.rb @@ -11,7 +11,7 @@ lines = subject.code.split "\n" expect(lines[0..13].join("\n")).to match_approval('script/wrapper/code') .except(/\d+\.\d+\.\d+(\.rc\d)?/) - expect(lines[-3]).to eq ' run "${command_line_args[@]}"' + expect(lines).to include ' start "$@"' end end @@ -42,5 +42,20 @@ expect(lines[2]).to eq '# :command.root_command' end end + + context 'with a custom start function name' do + around do |example| + original_function_names = Settings.function_names + Settings.function_names = { 'start' => 'custom_start' } + example.run + ensure + Settings.function_names = original_function_names + end + + it 'uses the configured name for the definition and invocation' do + expect(subject.code).to include "custom_start() {\n" + expect(subject.code).to include "\n custom_start \"$@\"\n" + end + end end end diff --git a/spec/fixtures/completions/core/src/initialize.sh b/spec/fixtures/completions/core/src/initialize.sh new file mode 100644 index 00000000..326d1469 --- /dev/null +++ b/spec/fixtures/completions/core/src/initialize.sh @@ -0,0 +1,2 @@ +printf "initialize unexpectedly called\n" +return 1 diff --git a/support/schema/settings.yml b/support/schema/settings.yml index fbcc0085..108b4477 100644 --- a/support/schema/settings.yml +++ b/support/schema/settings.yml @@ -348,6 +348,12 @@ properties: https://bashly.dev/usage/settings/#function_names type: object properties: + start: + title: start + description: |- + Name for the start() function + https://bashly.dev/usage/settings/#function_names + oneOf: *optional_string run: title: run description: |- From 54c51e55f0b9c77581dc212c0ae8498072de2488 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 19 Aug 2026 20:59:55 +0300 Subject: [PATCH 22/23] - Avoid empty argument branches in generated completions --- lib/bashly/script/argument.rb | 5 +++++ .../completion_argument_candidates.gtx | 7 +++--- .../integration/runtime_completions_spec.rb | 5 +++++ spec/bashly/script/argument_spec.rb | 22 +++++++++++++++++++ .../completions/configured/src/bashly.yml | 8 +++++++ spec/fixtures/script/arguments.yml | 4 ++++ 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/bashly/script/argument.rb b/lib/bashly/script/argument.rb index bdae5fcf..d2e4f8f9 100644 --- a/lib/bashly/script/argument.rb +++ b/lib/bashly/script/argument.rb @@ -35,6 +35,11 @@ def completion_options completions&.fetch('options', []) || [] end + def completion? + allowed&.any? || completion_static.any? || + completion_dynamic.any? || completion_options.any? + end + def label repeatable ? "#{name.upcase}..." : name.upcase end diff --git a/lib/bashly/views/command/completion_argument_candidates.gtx b/lib/bashly/views/command/completion_argument_candidates.gtx index f6be8ac2..4c643bc8 100644 --- a/lib/bashly/views/command/completion_argument_candidates.gtx +++ b/lib/bashly/views/command/completion_argument_candidates.gtx @@ -1,9 +1,10 @@ -if args.any? +completion_args = args.each_with_index.select { |arg, _index| arg.completion? } +if completion_args.any? > if [[ $completion_current != -* ]]; then > case "$completion_arg_index" in - args.each_with_index do |arg, index| + completion_args.each do |arg, index| > {{ index }}) - = arg.render(:completion).indent 6 + = arg.render(:completion).strip.indent 6 > ;; end > esac diff --git a/spec/bashly/integration/runtime_completions_spec.rb b/spec/bashly/integration/runtime_completions_spec.rb index b96e0a3c..a1f24160 100644 --- a/spec/bashly/integration/runtime_completions_spec.rb +++ b/spec/bashly/integration/runtime_completions_spec.rb @@ -17,6 +17,11 @@ Settings.completions = nil end + it 'does not generate empty positional candidate branches' do + empty_case = /^[ \t]+\d+\)\n[ \t]*\n[ \t]*;;$/ + expect(File.read(cli)).not_to match empty_case + end + examples.each do |name, example| describe name do it 'works' do diff --git a/spec/bashly/script/argument_spec.rb b/spec/bashly/script/argument_spec.rb index ae2d6237..13b1a557 100644 --- a/spec/bashly/script/argument_spec.rb +++ b/spec/bashly/script/argument_spec.rb @@ -49,6 +49,28 @@ end end + describe '#completion?' do + it 'returns false without candidate sources or options' do + expect(subject.completion?).to be false + end + + context 'with allowed values' do + let(:fixture) { :allowed } + + it 'returns true' do + expect(subject.completion?).to be true + end + end + + context 'with configured completions' do + let(:fixture) { :completions } + + it 'returns true' do + expect(subject.completion?).to be true + end + end + end + describe '#usage_string' do it 'returns a string suitable to be used as a usage pattern' do expect(subject.usage_string).to eq '[FILE]' diff --git a/spec/fixtures/completions/configured/src/bashly.yml b/spec/fixtures/completions/configured/src/bashly.yml index 80ab2a42..537e2298 100644 --- a/spec/fixtures/completions/configured/src/bashly.yml +++ b/spec/fixtures/completions/configured/src/bashly.yml @@ -55,6 +55,14 @@ commands: completions: options: [files, directories, no-space] +- name: mixed + help: Complete only selected arguments + args: + - name: source + - name: target + completions: + options: [directories] + - name: flag help: Complete a flag value flags: diff --git a/spec/fixtures/script/arguments.yml b/spec/fixtures/script/arguments.yml index 38174cb5..0c102d65 100644 --- a/spec/fixtures/script/arguments.yml +++ b/spec/fixtures/script/arguments.yml @@ -8,6 +8,10 @@ dynamic: [recent_files] options: [files, no-space] +:allowed: + name: file + allowed: [README.md] + :required: name: file required: true From 8243165558ff21f9d4292218db7d72bca3d787b8 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Thu, 20 Aug 2026 08:23:33 +0300 Subject: [PATCH 23/23] fix empty line in `start()` --- lib/bashly/views/command/start.gtx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bashly/views/command/start.gtx b/lib/bashly/views/command/start.gtx index 9b5619ab..200ad1d0 100644 --- a/lib/bashly/views/command/start.gtx +++ b/lib/bashly/views/command/start.gtx @@ -6,8 +6,8 @@ if Settings.completions? > completion_run "${@:2}" > return > fi -end > +end > command_line_args=("$@") > {{ Settings.function_name :initialize }} > {{ Settings.function_name :run }} "${command_line_args[@]}"