From 5a8ed488893738a3152644488da87c142b8fa11c Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 1 Jul 2026 14:23:15 +0300 Subject: [PATCH 1/2] version 1.4.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 1c86cd90..6f4bc1be 100644 --- a/lib/bashly/version.rb +++ b/lib/bashly/version.rb @@ -1,3 +1,3 @@ module Bashly - VERSION = '1.3.8' + VERSION = '1.4.0.rc1' end From 27c9f468e4a4f1c5a49104cff7faaf9c9b75f51e Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 1 Jul 2026 16:33:24 +0300 Subject: [PATCH 2/2] - Fix completions for a default command --- lib/bashly/completion_builder.rb | 51 +++++++++++++++++-- .../completion_builder/default_command | 26 ++++++++++ spec/bashly/completion_builder_spec.rb | 15 ++++++ spec/fixtures/completion_builder.yml | 14 +++++ 4 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 spec/approvals/completion_builder/default_command diff --git a/lib/bashly/completion_builder.rb b/lib/bashly/completion_builder.rb index 00cb6a05..6c1e4503 100644 --- a/lib/bashly/completion_builder.rb +++ b/lib/bashly/completion_builder.rb @@ -27,7 +27,7 @@ def add_command(command, inherited_global_groups:) pattern_groups = inherited_global_groups.dup pattern_groups << local_group if local_group - @patterns << pattern_for(command, pattern_groups) + @patterns << pattern_for(command, pattern_groups) unless visible_default_command(command) child_global_groups = inherited_global_groups.dup if command.global_flags? @@ -36,6 +36,7 @@ def add_command(command, inherited_global_groups:) 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 @@ -47,6 +48,38 @@ def pattern_for(command, option_groups) 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('|') @@ -102,14 +135,20 @@ def flag_token_name(flag, command) register_token flag.arg || flag.name, command, flag_source(flag) end - def positional_tokens(command) - command.args.map do |arg| - token_name = register_token arg.name, command, arg_source(arg, command) + 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 @@ -178,6 +217,10 @@ 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]+/, '_') diff --git a/spec/approvals/completion_builder/default_command b/spec/approvals/completion_builder/default_command new file mode 100644 index 00000000..3f53cbd2 --- /dev/null +++ b/spec/approvals/completion_builder/default_command @@ -0,0 +1,26 @@ +--- +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/bashly/completion_builder_spec.rb b/spec/bashly/completion_builder_spec.rb index 6e53bed7..93132063 100644 --- a/spec/bashly/completion_builder_spec.rb +++ b/spec/bashly/completion_builder_spec.rb @@ -16,4 +16,19 @@ end end end + + context 'with a default command' do + 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 end diff --git a/spec/fixtures/completion_builder.yml b/spec/fixtures/completion_builder.yml index bbddc92f..c967faa5 100644 --- a/spec/fixtures/completion_builder.yml +++ b/spec/fixtures/completion_builder.yml @@ -44,6 +44,20 @@ 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