command_kit-completion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3ed090b5e927e5c88118a00518943d89d5dc405896469291c0bcd50ec44ea243
4
+ data.tar.gz: 3786b5ae7c7fa784ace3c2f173ce1b5121529674bbcb5899e45bf0bf5b6e79da
5
+ SHA512:
6
+ metadata.gz: e23917b5fb372dfaeed9316015b0e7a21f7e748c8afa8d8737e82008c8c8a45c365ae97dbf093537766bd67ce30a61487e9d062744b77ad1fcf72cf177a9f7db
7
+ data.tar.gz: 8de5d327ece13f2c287663a18c62859635f5dd13ae29c5052f405898cafbacc17c2619d27e7d3877492f86b285c18274b5916b7c2005f7b88a6475faf261ced4
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.md
3
+ LICENSE.txt
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on: [ push, pull_request ]
4
+
5
+ jobs:
6
+ tests:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby:
12
+ - 3.0
13
+ - 3.1
14
+ - 3.2
15
+ - jruby
16
+ - truffleruby
17
+ name: OS ${{ matrix.os }} / Ruby ${{ matrix.ruby }}
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Set up Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby }}
24
+ - name: Install Ruby dependencies
25
+ run: bundle install --jobs 4 --retry 3
26
+ - name: Run tests
27
+ run: bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle
2
+ /.yardoc/
3
+ /Gemfile.lock
4
+ /coverage/
5
+ /example-completion.sh
6
+ /doc/
7
+ /pkg/
8
+ /vendor/cache/*.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --title "CommandKit Documentation" --protected
data/ChangeLog.md ADDED
@@ -0,0 +1,8 @@
1
+ ### 0.1.0 / 2023-12-18
2
+
3
+ * Initial release:
4
+ * Supports automatically generating completion rules from a [command_kit] CLI
5
+ class's options and sub-commands.
6
+ * Supports loading additional completion rules from a YAML file.
7
+
8
+ [command_kit]: https://github.com/postmodern/command_kit.rb#readme
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'rake'
7
+ gem 'rubygems-tasks', '~> 0.2'
8
+
9
+ gem 'rspec', '~> 3.0'
10
+ gem 'simplecov', '~> 0.20', require: false
11
+
12
+ gem 'kramdown'
13
+ gem 'redcarpet', platform: :mri
14
+ gem 'yard', '~> 0.9'
15
+ gem 'yard-spellcheck', require: false
16
+ gem 'dead_end'
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2023 Hal Brodigan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # command_kit-completion
2
+
3
+ [![CI](https://github.com/postmodern/command_kit-completion/actions/workflows/ruby.yml/badge.svg)](https://github.com/postmodern/command_kit-completion/actions/workflows/ruby.yml)
4
+ [![Code Climate](https://codeclimate.com/github/postmodern/command_kit-completion.svg)](https://codeclimate.com/github/postmodern/command_kit-completion)
5
+ [![Gem Version](https://badge.fury.io/rb/wordlist.svg)](https://badge.fury.io/rb/wordlist)
6
+
7
+ * [Source](https://github.com/postmodern/command_kit-completion#readme)
8
+ * [Issues](https://github.com/postmodern/command_kit-completion/issues)
9
+ * [Documentation](https://rubydoc.info/gems/command_kit-complete)
10
+
11
+ ## Description
12
+
13
+ Adds a rake task that generates shell completion rules for a [command_kit] CLI.
14
+ The rake task loads the CLI class and uses the [completely] library to generate
15
+ the shell completion rules.
16
+
17
+ ## Features
18
+
19
+ * Supports automatically generating completion rules from a [command_kit] CLI
20
+ class's options and sub-commands.
21
+ * Supports loading additional completion rules from a YAML file.
22
+
23
+ ## Examples
24
+
25
+ ```ruby
26
+ require 'command_kit/completion/task'
27
+ CommandKit::Completion::Task.new(
28
+ class_file: './examples/cli',
29
+ class_name: 'Foo::CLI',
30
+ output_file: 'completion.sh'
31
+ )
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ * [Ruby] >= 3.0.0
37
+ * [command_kit] ~> 0.1
38
+ * [completely] ~> 0.6
39
+
40
+ ## License
41
+
42
+ Copyright (c) 2023 Hal Brodigan
43
+
44
+ See {file:LICENSE.txt} for details.
45
+
46
+ [Ruby]: https://www.ruby-lang.org/
47
+ [command_kit]: https://github.com/postmodern/command_kit.rb#readme
48
+ [completely]: https://rubygems.org/gems/completely
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError => e
6
+ abort e.message
7
+ end
8
+
9
+ require 'rake'
10
+ require 'rubygems/tasks'
11
+ Gem::Tasks.new
12
+
13
+ require 'rspec/core/rake_task'
14
+ RSpec::Core::RakeTask.new
15
+
16
+ task :test => :spec
17
+ task :default => :spec
18
+
19
+ require 'yard'
20
+ YARD::Rake::YardocTask.new
21
+ task :doc => :yard
22
+
23
+ namespace :example do
24
+ require 'command_kit/completion/task'
25
+ CommandKit::Completion::Task.new(
26
+ class_file: './examples/cli',
27
+ class_name: 'Foo::CLI',
28
+ output_file: 'example-completion.sh'
29
+ )
30
+ end
@@ -0,0 +1,59 @@
1
+ require 'yaml'
2
+
3
+ Gem::Specification.new do |gem|
4
+ gemspec = YAML.load_file('gemspec.yml')
5
+
6
+ gem.name = gemspec.fetch('name')
7
+ gem.version = gemspec.fetch('version') do
8
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
9
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
10
+
11
+ require 'command_kit/completion/version'
12
+ CommandKit::Completion::VERSION
13
+ end
14
+
15
+ gem.summary = gemspec['summary']
16
+ gem.description = gemspec['description']
17
+ gem.licenses = Array(gemspec['license'])
18
+ gem.authors = Array(gemspec['authors'])
19
+ gem.email = gemspec['email']
20
+ gem.homepage = gemspec['homepage']
21
+ gem.metadata = gemspec['metadata'] if gemspec['metadata']
22
+
23
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
24
+
25
+ gem.files = if gemspec['files'] then glob[gemspec['files']]
26
+ else `git ls-files`.split($/)
27
+ end
28
+
29
+ gem.executables = gemspec.fetch('executables') do
30
+ glob['bin/*'].map { |path| File.basename(path) }
31
+ end
32
+ gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
33
+
34
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
35
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
36
+
37
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
38
+ %w[ext lib].select { |dir| File.directory?(dir) }
39
+ })
40
+
41
+ gem.requirements = Array(gemspec['requirements'])
42
+ gem.required_ruby_version = gemspec['required_ruby_version']
43
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
44
+ gem.post_install_message = gemspec['post_install_message']
45
+
46
+ split = lambda { |string| string.split(/,\s*/) }
47
+
48
+ if gemspec['dependencies']
49
+ gemspec['dependencies'].each do |name,versions|
50
+ gem.add_dependency(name,split[versions])
51
+ end
52
+ end
53
+
54
+ if gemspec['development_dependencies']
55
+ gemspec['development_dependencies'].each do |name,versions|
56
+ gem.add_development_dependency(name,split[versions])
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,47 @@
1
+ require 'command_kit/command'
2
+
3
+ module Foo
4
+ class CLI
5
+ class Config < CommandKit::Command
6
+ #
7
+ # The `config get` sub-command.
8
+ #
9
+ class Get < CommandKit::Command
10
+
11
+ usage '[options] NAME'
12
+
13
+ argument :name, required: false,
14
+ desc: 'Configuration variable name'
15
+
16
+ description 'Gets a configuration variable'
17
+
18
+ CONFIG = {
19
+ 'name' => 'John Smith',
20
+ 'email' => 'john.smith@example.com'
21
+ }
22
+
23
+ #
24
+ # Runs the `config get` sub-command.
25
+ #
26
+ # @param [String, nil] name
27
+ # The optional name argument.
28
+ #
29
+ def run(name=nil)
30
+ if name
31
+ unless CONFIG.has_key?(name)
32
+ print_error "unknown config variable: #{name}"
33
+ exit(1)
34
+ end
35
+
36
+ puts CONFIG.fetch(name)
37
+ else
38
+ CONFIG.each do |name,value|
39
+ puts "#{name}:\t#{value}"
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ require 'command_kit/command'
2
+
3
+ module Foo
4
+ class CLI
5
+ class Config < CommandKit::Command
6
+ #
7
+ # The `config set` sub-command.
8
+ #
9
+ class Set < CommandKit::Command
10
+
11
+ usage '[options] NAME'
12
+
13
+ argument :name, required: true,
14
+ desc: 'Configuration variable name to set'
15
+
16
+ argument :value, required: true,
17
+ desc: 'Configuration variable value to set'
18
+
19
+ description 'Sets a configuration variable'
20
+
21
+ CONFIG = {
22
+ 'name' => 'John Smith',
23
+ 'email' => 'john.smith@example.com'
24
+ }
25
+
26
+ #
27
+ # Runs the `config get` sub-command.
28
+ #
29
+ # @param [String] name
30
+ # The name argument.
31
+ #
32
+ def run(name,value)
33
+ unless CONFIG.has_key?(name)
34
+ print_error "unknown config variable: #{name}"
35
+ exit(1)
36
+ end
37
+
38
+ puts "Configuration variable #{name} was #{CONFIG.fetch(name)}, but is now #{value}"
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ require 'command_kit/command'
2
+ require 'command_kit/commands'
3
+
4
+ require_relative 'config/get'
5
+ require_relative 'config/set'
6
+
7
+ module Foo
8
+ class CLI
9
+ #
10
+ # The `config` sub-command.
11
+ #
12
+ class Config < CommandKit::Command
13
+
14
+ include CommandKit::Commands
15
+
16
+ command Get
17
+ command Set
18
+
19
+ description 'Get or set the configuration'
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ require 'command_kit/command'
2
+
3
+ module Foo
4
+ class CLI
5
+ #
6
+ # The `list` sub-command.
7
+ #
8
+ class List < CommandKit::Command
9
+
10
+ usage '[options] [NAME]'
11
+
12
+ argument :name, required: false,
13
+ desc: 'Optional name to list'
14
+
15
+ description 'Lists the contents'
16
+
17
+ ITEMS = %w[foo bar baz]
18
+
19
+ #
20
+ # Runs the `list` sub-command.
21
+ #
22
+ # @param [String, nil] name
23
+ # The optional name argument.
24
+ #
25
+ def run(name=nil)
26
+ if name
27
+ puts ITEMS.grep(name)
28
+ else
29
+ puts ITEMS
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ require 'command_kit/command'
2
+
3
+ module Foo
4
+ class CLI
5
+ #
6
+ # The `update` sub-command.
7
+ #
8
+ class Update < CommandKit::Command
9
+
10
+ usage '[options] [NAME]'
11
+
12
+ option :quiet, short: '-q',
13
+ desc: 'Suppresses logging messages'
14
+
15
+ argument :name, required: false,
16
+ desc: 'Optional name to update'
17
+
18
+ description 'Updates an item or all items'
19
+
20
+ ITEMS = %w[foo bar baz]
21
+
22
+ #
23
+ # Runs the `update` sub-command.
24
+ #
25
+ # @param [String, nil] name
26
+ # The optional name argument.
27
+ #
28
+ def run(name=nil)
29
+ if name
30
+ unless ITEMS.include?(name)
31
+ print_error "unknown item: #{name}"
32
+ exit(1)
33
+ end
34
+
35
+ puts "Updating #{name} ..." unless options[:quiet]
36
+ sleep 1
37
+ puts "Item #{name} updated." unless options[:quiet]
38
+ else
39
+ puts "Updating ..." unless options[:quiet]
40
+ sleep 2
41
+ puts "All items updated." unless options[:quiet]
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
data/examples/cli.rb ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../../lib',__FILE__))
4
+
5
+ require 'command_kit/commands'
6
+
7
+ require_relative 'cli/config'
8
+ require_relative 'cli/list'
9
+ require_relative 'cli/update'
10
+
11
+ module Foo
12
+ #
13
+ # The main CLI command.
14
+ #
15
+ class CLI
16
+
17
+ include CommandKit::Commands
18
+
19
+ class << self
20
+ # The global configuration file setting.
21
+ #
22
+ # @return [String, nil]
23
+ attr_accessor :config_file
24
+ end
25
+
26
+ command_name 'foo'
27
+
28
+ # Commands must be explicitly registered, unless
29
+ # CommandKit::Commands::AutoLoad.new(...) is included.
30
+ command Config
31
+ command List
32
+ command Update
33
+
34
+ # Commands may have aliases
35
+ command_aliases['ls'] = 'list'
36
+ command_aliases['up'] = 'update'
37
+
38
+ # Global options may be defined which are parsed before the sub-command's
39
+ # options are parsed and the sub-command is executed.
40
+ option :config_file, short: '-C',
41
+ value: {
42
+ type: String,
43
+ usage: 'FILE'
44
+ },
45
+ desc: 'Global option to set the config file' do |file|
46
+ CLI.config_file = file
47
+ end
48
+
49
+ end
50
+ end
51
+
52
+ if $0 == __FILE__
53
+ # Normally you would invoke Foo::CLI.start from a bin/ script.
54
+ Foo::CLI.start
55
+ end
data/gemspec.yml ADDED
@@ -0,0 +1,27 @@
1
+ name: command_kit-completion
2
+ summary: Generate shell completions for command_kit commands
3
+ description:
4
+ Adds a rake task that generates shell completion rules for a command_kit CLI.
5
+ The rake task loads the CLI class and uses the 'completely' library to
6
+ generate shell completion rules.
7
+
8
+ license: MIT
9
+ authors: Postmodern
10
+ email: postmodern.mod3@gmail.com
11
+ homepage: https://github.com/postmodern/command_kit-completion#readme
12
+
13
+ metadata:
14
+ documentation_uri: https://rubydoc.info/gems/command_kit-completion
15
+ source_code_uri: https://github.com/postmodern/command_kit-completion
16
+ bug_tracker_uri: https://github.com/postmodern/command_kit-completion/issues
17
+ changelog_uri: https://github.com/postmodern/command_kit-completion/blob/main/ChangeLog.md
18
+ rubygems_mfa_required: 'true'
19
+
20
+ required_ruby_version: ">= 3.0.0"
21
+
22
+ dependencies:
23
+ command_kit: ~> 0.1
24
+ completely: ~> 0.6
25
+
26
+ development_dependencies:
27
+ bundler: ~> 2.0
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/tasklib'
4
+
5
+ require 'command_kit/options'
6
+ require 'command_kit/commands'
7
+ require 'completely'
8
+ require 'yaml'
9
+
10
+ module CommandKit
11
+ module Completion
12
+ class Task < Rake::TaskLib
13
+
14
+ # The file that the command_kit CLI is defined in.
15
+ #
16
+ # @return [String]
17
+ attr_reader :class_file
18
+
19
+ # The class name of the command_kit CLI.
20
+ #
21
+ # @return [String]
22
+ attr_reader :class_name
23
+
24
+ # The output file to write the shell completions to.
25
+ #
26
+ # @return [String]
27
+ attr_reader :output_file
28
+
29
+ # Optional input YAML file to read additional shell completions from.
30
+ #
31
+ # @return [String, nil]
32
+ attr_reader :input_file
33
+
34
+ # Specifies whether the shell completion logic should be wrapped in a
35
+ # function.
36
+ #
37
+ # @return [Boolean]
38
+ attr_reader :wrap_function
39
+
40
+ # Optional function name to wrap the shell completions within.
41
+ #
42
+ # @return [String, nil]
43
+ attr_reader :function_name
44
+
45
+ #
46
+ # Initializes the `command_kit:completion` task.
47
+ #
48
+ # @param [String] class_file
49
+ # The file that contains the comand_kit CLI.
50
+ #
51
+ # @param [String] class_name
52
+ # The class name of the command_kit CLI.
53
+ #
54
+ # @param [String] output_file
55
+ # The output file to write the completions rules to.
56
+ #
57
+ # @param [String, nil] input_file
58
+ # The optional YAML input file of additional completion rules.
59
+ # See [completely examples] for YAML syntax.
60
+ #
61
+ # [completely examples]: https://github.com/DannyBen/completely?tab=readme-ov-file#using-the-completely-command-line
62
+ #
63
+ def initialize(class_file: ,
64
+ class_name: ,
65
+ output_file: ,
66
+ input_file: nil,
67
+ wrap_function: false,
68
+ function_name: nil)
69
+ @class_file = class_file
70
+ @class_name = class_name
71
+ @output_file = output_file
72
+
73
+ @input_file = input_file
74
+ @wrap_function = wrap_function
75
+ @function_name = function_name
76
+
77
+ define
78
+ end
79
+
80
+ #
81
+ # Defines the `command_kit:completion` task.
82
+ #
83
+ def define
84
+ task(@output_file) do
85
+ completions = Completely::Completions.new(completion_rules)
86
+ shell_script = if @wrap_function
87
+ completions.wrap_function(*@function_name)
88
+ else
89
+ completions.script
90
+ end
91
+
92
+ File.write(@output_file,shell_script)
93
+ end
94
+
95
+ desc 'Generates the shell completions'
96
+ task 'command_kit:completion' => @output_file
97
+
98
+ task :completion => 'command_kit:completion'
99
+ end
100
+
101
+ #
102
+ # Loads the {#class_name} from the {#class_file}.
103
+ #
104
+ # @return [Class]
105
+ #
106
+ def load_class
107
+ require(@class_file)
108
+ Object.const_get(@class_name)
109
+ end
110
+
111
+ # Mapping of command usage strings to completely `<keyword>`s.
112
+ USAGE_COMPLETIONS = {
113
+ 'FILE' => '<file>',
114
+ 'DIR' => '<directory>',
115
+ 'HOST' => '<hostname>',
116
+ 'USER' => '<user>'
117
+ }
118
+
119
+ #
120
+ # Generates the completion rules for the given [command_kit] command
121
+ # class.
122
+ #
123
+ # [command_kit]: https://github.com/postmodern/command_kit.rb#readme
124
+ #
125
+ # @param [Class] command_class
126
+ # The command class.
127
+ #
128
+ # @return [Hash{String => Array<String>}]
129
+ # The completion rules for the command class and any sub-commands.
130
+ #
131
+ def completion_rules_for(command_class)
132
+ command_name = command_class.command_name
133
+ completions = {command_name => []}
134
+
135
+ # options
136
+ if command_class.include?(CommandKit::Options)
137
+ # add all long option flags
138
+ command_class.options.each_value do |option|
139
+ completions[command_name] << option.long
140
+
141
+ if option.value
142
+ if (option_value_completion = USAGE_COMPLETIONS[option.value.usage])
143
+ # add a special rule if the option's value USAGE maps to a
144
+ # 'completely' completion keyword (ex: `FILE` -> `<file>`).
145
+ completions["#{command_name}*#{option.long}"] = [
146
+ option_value_completion
147
+ ]
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ # sub-commands
154
+ if command_class.include?(CommandKit::Commands)
155
+ command_class.commands.each do |subcommand_name,subcommand|
156
+ # add all sub-command names
157
+ completions[command_name] << subcommand_name
158
+
159
+ # generate completions for the sub-command and merge them in
160
+ completion_rules_for(subcommand.command).each do |subcommand_string,subcommand_completions|
161
+ completions["#{command_name} #{subcommand_string}"] = subcommand_completions
162
+ end
163
+ end
164
+
165
+ completions[command_name].concat(command_class.command_aliases.keys)
166
+ end
167
+
168
+ # filter out any command's that have no options/sub-commands
169
+ completions.reject! do |command_string,command_completions|
170
+ command_completions.empty?
171
+ end
172
+
173
+ return completions
174
+ end
175
+
176
+ #
177
+ # Builds the completion rules for the command_kit CLI command, and merges
178
+ # in any additional completion rules from the input file.
179
+ #
180
+ # @return [Hash{String => Array<String>}]
181
+ #
182
+ def completion_rules
183
+ completion_rules = completion_rules_for(load_class)
184
+
185
+ if @input_file
186
+ # load the additional rules from the input file
187
+ additional_completion_rules = YAML.load_file(@input_file)
188
+
189
+ # merge the additional completion rules
190
+ additional_completion_rules.each do |command_string,completions|
191
+ if completion_rules[command_string]
192
+ completion_rules[command_string].concat(completions)
193
+ else
194
+ completion_rules[command_string] = completions
195
+ end
196
+ end
197
+ end
198
+
199
+ return completion_rules
200
+ end
201
+
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ module Completion
5
+ # command_kit-completion version
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ ---
2
+ foo update:
3
+ - $(foo list)
@@ -0,0 +1,3 @@
1
+ require 'rspec'
2
+ require 'simplecov'
3
+ SimpleCov.start
data/spec/task_spec.rb ADDED
@@ -0,0 +1,474 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/completion/task'
3
+
4
+ require 'tempfile'
5
+ require 'command_kit/command'
6
+ require 'command_kit/commands'
7
+
8
+ describe CommandKit::Completion::Task do
9
+ let(:class_file) { './examples/cli' }
10
+ let(:class_name) { 'Foo::CLI' }
11
+ let(:tempfile) { Tempfile.new(['command_kit-completion','.sh']) }
12
+ let(:output_file) { tempfile.path }
13
+
14
+ let(:fixtures_dir) { File.join(__dir__,'fixtures') }
15
+
16
+ subject do
17
+ described_class.new(
18
+ class_file: class_file,
19
+ class_name: class_name,
20
+ output_file: output_file
21
+ )
22
+ end
23
+
24
+ describe "#define" do
25
+ before { subject }
26
+
27
+ it "must define a task for the output file" do
28
+ expect(Rake::Task[output_file]).to_not be_nil
29
+ end
30
+
31
+ it "must define a 'command_kit:completion' task" do
32
+ expect(Rake::Task['command_kit:completion']).to_not be_nil
33
+ end
34
+
35
+ it "must define a 'completion' task" do
36
+ expect(Rake::Task['completion']).to_not be_nil
37
+ end
38
+ end
39
+
40
+ describe "#initialize" do
41
+ it "must set #class_file" do
42
+ expect(subject.class_file).to eq(class_file)
43
+ end
44
+
45
+ it "must set #class_name" do
46
+ expect(subject.class_name).to eq(class_name)
47
+ end
48
+
49
+ it "must set #output_file" do
50
+ expect(subject.output_file).to eq(output_file)
51
+ end
52
+
53
+ it "must default #input_file to nil" do
54
+ expect(subject.input_file).to be(nil)
55
+ end
56
+
57
+ it "must default #wrap_function to false" do
58
+ expect(subject.wrap_function).to be(false)
59
+ end
60
+
61
+ it "must default #function_name to nil" do
62
+ expect(subject.function_name).to be(nil)
63
+ end
64
+ end
65
+
66
+ describe "#load_class" do
67
+ it "must return the Class object for #class_name in #class_file" do
68
+ expect(subject.load_class).to be(Foo::CLI)
69
+ end
70
+ end
71
+
72
+ describe "#completion_rules_for" do
73
+ context "when given a simple CommandKit::Command class" do
74
+ class TestBasicCommand < CommandKit::Command
75
+
76
+ command_name 'test'
77
+
78
+ option :foo, desc: 'Foo option'
79
+
80
+ option :bar, value: {
81
+ type: String
82
+ },
83
+ desc: 'Bar option'
84
+
85
+ end
86
+
87
+ let(:command_class) { TestBasicCommand }
88
+
89
+ it "must return a Hash of completion rules for the command" do
90
+ expect(subject.completion_rules_for(command_class)).to eq(
91
+ {
92
+ "test" => %w[--foo --bar]
93
+ }
94
+ )
95
+ end
96
+
97
+ context "when one of the options accepts a FILE value" do
98
+ class TestCommandWithFILEOption < CommandKit::Command
99
+
100
+ command_name 'test'
101
+
102
+ option :foo, desc: 'Foo option'
103
+
104
+ option :bar, value: {
105
+ type: String,
106
+ usage: 'FILE'
107
+ },
108
+ desc: 'Bar option'
109
+
110
+ end
111
+
112
+ let(:command_class) { TestCommandWithFILEOption }
113
+
114
+ it "must add a separate completion rule for the option using the <file> keyword" do
115
+ expect(subject.completion_rules_for(command_class)).to eq(
116
+ {
117
+ "test" => %w[--foo --bar],
118
+ 'test*--bar' => %w[<file>]
119
+ }
120
+ )
121
+ end
122
+ end
123
+
124
+ context "when one of the options accepts a DIR value" do
125
+ class TestCommandWithDIROption < CommandKit::Command
126
+
127
+ command_name 'test'
128
+
129
+ option :foo, desc: 'Foo option'
130
+
131
+ option :bar, value: {
132
+ type: String,
133
+ usage: 'DIR'
134
+ },
135
+ desc: 'Bar option'
136
+
137
+ end
138
+
139
+ let(:command_class) { TestCommandWithDIROption }
140
+
141
+ it "must add a separate completion rule for the option using the <directory> keyword" do
142
+ expect(subject.completion_rules_for(command_class)).to eq(
143
+ {
144
+ "test" => %w[--foo --bar],
145
+ 'test*--bar' => %w[<directory>]
146
+ }
147
+ )
148
+ end
149
+ end
150
+
151
+ context "when one of the options accepts a HOST value" do
152
+ class TestCommandWithHOSTOption < CommandKit::Command
153
+
154
+ command_name 'test'
155
+
156
+ option :foo, desc: 'Foo option'
157
+
158
+ option :bar, value: {
159
+ type: String,
160
+ usage: 'HOST'
161
+ },
162
+ desc: 'Bar option'
163
+
164
+ end
165
+
166
+ let(:command_class) { TestCommandWithHOSTOption }
167
+
168
+ it "must add a separate completion rule for the option using the <hostname> keyword" do
169
+ expect(subject.completion_rules_for(command_class)).to eq(
170
+ {
171
+ "test" => %w[--foo --bar],
172
+ 'test*--bar' => %w[<hostname>]
173
+ }
174
+ )
175
+ end
176
+ end
177
+
178
+ context "when one of the options accepts a USER value" do
179
+ class TestCommandWithUSEROption < CommandKit::Command
180
+
181
+ command_name 'test'
182
+
183
+ option :foo, desc: 'Foo option'
184
+
185
+ option :bar, value: {
186
+ type: String,
187
+ usage: 'USER'
188
+ },
189
+ desc: 'Bar option'
190
+
191
+ end
192
+
193
+ let(:command_class) { TestCommandWithUSEROption }
194
+
195
+ it "must add a separate completion rule for the option using the <user> keyword" do
196
+ expect(subject.completion_rules_for(command_class)).to eq(
197
+ {
198
+ "test" => %w[--foo --bar],
199
+ 'test*--bar' => %w[<user>]
200
+ }
201
+ )
202
+ end
203
+ end
204
+
205
+ context "but the command class does not include CommandKit::Options" do
206
+ class TestCommandWithoutOptions
207
+ include CommandKit::CommandName
208
+ include CommandKit::Usage
209
+ include CommandKit::Arguments
210
+
211
+ command_name 'test'
212
+ end
213
+
214
+ let(:command_class) { TestCommandWithoutOptions }
215
+
216
+ it "must return an empty Hash" do
217
+ expect(subject.completion_rules_for(command_class)).to eq({})
218
+ end
219
+ end
220
+ end
221
+
222
+ context "when the command class includes CommandKit::Commands" do
223
+ class TestCommandWithSubCommands < CommandKit::Command
224
+ include CommandKit::Commands
225
+
226
+ option :global_option, short: '-g',
227
+ desc: 'A global option'
228
+
229
+ class Foo < CommandKit::Command
230
+
231
+ option :foo_opt1, desc: 'Foo option 1'
232
+
233
+ option :foo_opt2, value: {
234
+ type: String
235
+ },
236
+ desc: 'Foo option 2'
237
+
238
+ end
239
+
240
+ class Bar < CommandKit::Command
241
+
242
+ option :bar_opt1, desc: 'Bar option 1'
243
+
244
+ option :bar_opt2, value: {
245
+ type: String
246
+ },
247
+ desc: 'Bar option 2'
248
+
249
+ end
250
+
251
+ command_name 'test'
252
+ command Foo
253
+ command Bar
254
+
255
+ end
256
+
257
+ let(:command_class) { TestCommandWithSubCommands }
258
+
259
+ it "must add completion rules for the other commands" do
260
+ expect(subject.completion_rules_for(command_class)).to eq(
261
+ {
262
+ "test" => %w[--global-option help foo bar],
263
+ "test foo" => %w[--foo-opt1 --foo-opt2],
264
+ "test bar" => %w[--bar-opt1 --bar-opt2]
265
+ }
266
+ )
267
+ end
268
+
269
+ context "when the command has command aliases" do
270
+ class TestCommandWithSubCommandsAndCommandAliases < CommandKit::Command
271
+ include CommandKit::Commands
272
+
273
+ option :global_option, short: '-g',
274
+ desc: 'A global option'
275
+
276
+ class Foo < CommandKit::Command
277
+
278
+ option :foo_opt1, desc: 'Foo option 1'
279
+
280
+ option :foo_opt2, value: {
281
+ type: String
282
+ },
283
+ desc: 'Foo option 2'
284
+
285
+ end
286
+
287
+ class Bar < CommandKit::Command
288
+
289
+ option :bar_opt1, desc: 'Bar option 1'
290
+
291
+ option :bar_opt2, value: {
292
+ type: String
293
+ },
294
+ desc: 'Bar option 2'
295
+
296
+ end
297
+
298
+ command_name 'test'
299
+ command Foo
300
+ command Bar
301
+
302
+ command_aliases['foo2'] = 'foo'
303
+ command_aliases['bar2'] = 'bar'
304
+
305
+ end
306
+
307
+ let(:command_class) { TestCommandWithSubCommandsAndCommandAliases }
308
+
309
+ it "must include the command aliases in the completion rules" do
310
+ expect(subject.completion_rules_for(command_class)).to eq(
311
+ {
312
+ "test" => %w[--global-option help foo bar foo2 bar2],
313
+ "test foo" => %w[--foo-opt1 --foo-opt2],
314
+ "test bar" => %w[--bar-opt1 --bar-opt2]
315
+ }
316
+ )
317
+ end
318
+ end
319
+
320
+ context "but when one of the commands does not define any options" do
321
+ class TestCommandWithSubCommandsWithNoOptions < CommandKit::Command
322
+ include CommandKit::Commands
323
+
324
+ option :global_option, short: '-g',
325
+ desc: 'A global option'
326
+
327
+ class Foo < CommandKit::Command
328
+
329
+ option :foo_opt1, desc: 'Foo option 1'
330
+
331
+ option :foo_opt2, value: {
332
+ type: String
333
+ },
334
+ desc: 'Foo option 2'
335
+
336
+ end
337
+
338
+ class Bar < CommandKit::Command
339
+ end
340
+
341
+ command_name 'test'
342
+ command Foo
343
+ command Bar
344
+
345
+ end
346
+
347
+ let(:command_class) { TestCommandWithSubCommandsWithNoOptions }
348
+
349
+ it "must omit the command from the completion rules" do
350
+ expect(subject.completion_rules_for(command_class)).to eq(
351
+ {
352
+ "test" => %w[--global-option help foo bar],
353
+ "test foo" => %w[--foo-opt1 --foo-opt2]
354
+ }
355
+ )
356
+ end
357
+ end
358
+
359
+ context "and when one of the sub-commands also includes CommandKit::Commands" do
360
+ class TestCommandWithSubSubCommands < CommandKit::Command
361
+ include CommandKit::Commands
362
+
363
+ option :global_option, short: '-g',
364
+ desc: 'A global option'
365
+
366
+ class Foo < CommandKit::Command
367
+
368
+ option :foo_opt1, desc: 'Foo option 1'
369
+
370
+ option :foo_opt2, value: {
371
+ type: String
372
+ },
373
+ desc: 'Foo option 2'
374
+
375
+ end
376
+
377
+ class Bar < CommandKit::Command
378
+
379
+ include CommandKit::Commands
380
+
381
+ option :bar_opt1, desc: 'Bar option 1'
382
+
383
+ option :bar_opt2, value: {
384
+ type: String
385
+ },
386
+ desc: 'Bar option 2'
387
+
388
+ class Baz < CommandKit::Command
389
+
390
+ option :baz_opt1, desc: 'Baz option 1'
391
+
392
+ option :baz_opt2, value: {
393
+ type: String
394
+ },
395
+ desc: 'Baz option 2'
396
+
397
+ end
398
+
399
+ class Qux < CommandKit::Command
400
+
401
+ option :qux_opt1, desc: 'Qux option 1'
402
+
403
+ option :qux_opt2, value: {
404
+ type: String
405
+ },
406
+ desc: 'Qux option 2'
407
+
408
+ end
409
+
410
+ command Baz
411
+ command Qux
412
+
413
+ end
414
+
415
+ command_name 'test'
416
+ command Foo
417
+ command Bar
418
+
419
+ end
420
+
421
+ let(:command_class) { TestCommandWithSubSubCommands }
422
+
423
+ it "must recursively include completion rules for the sub-sub-commands" do
424
+ expect(subject.completion_rules_for(command_class)).to eq(
425
+ {
426
+ "test" => %w[--global-option help foo bar],
427
+ "test foo" => %w[--foo-opt1 --foo-opt2],
428
+ "test bar" => %w[--bar-opt1 --bar-opt2 help baz qux],
429
+ "test bar baz" => %w[--baz-opt1 --baz-opt2],
430
+ "test bar qux" => %w[--qux-opt1 --qux-opt2]
431
+ }
432
+ )
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ describe "#completion_rules" do
439
+ it "must load the class from #class_file and return the generated completion rules for it" do
440
+ expect(subject.completion_rules).to eq(
441
+ {
442
+ "foo" => %w[--config-file help config list update ls up],
443
+ "foo config" => %w[help get set],
444
+ "foo update" => %w[--quiet],
445
+ "foo*--config-file" => %w[<file>]
446
+ }
447
+ )
448
+ end
449
+
450
+ context "when #input_file is set" do
451
+ let(:input_file) { File.join(fixtures_dir,'additional_rules.yml') }
452
+
453
+ subject do
454
+ described_class.new(
455
+ class_file: class_file,
456
+ class_name: class_name,
457
+ output_file: output_file,
458
+ input_file: input_file
459
+ )
460
+ end
461
+
462
+ it "must merge the additional completion rules with the generated ones" do
463
+ expect(subject.completion_rules).to eq(
464
+ {
465
+ "foo" => %w[--config-file help config list update ls up],
466
+ "foo config" => %w[help get set],
467
+ "foo update" => ['--quiet', '$(foo list)'],
468
+ "foo*--config-file" => %w[<file>]
469
+ }
470
+ )
471
+ end
472
+ end
473
+ end
474
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: command_kit-completion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Postmodern
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: command_kit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: completely
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ description: Adds a rake task that generates shell completion rules for a command_kit
56
+ CLI. The rake task loads the CLI class and uses the 'completely' library to generate
57
+ shell completion rules.
58
+ email: postmodern.mod3@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files:
62
+ - ChangeLog.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ files:
66
+ - ".document"
67
+ - ".github/workflows/ruby.yml"
68
+ - ".gitignore"
69
+ - ".rspec"
70
+ - ".yardopts"
71
+ - ChangeLog.md
72
+ - Gemfile
73
+ - LICENSE.txt
74
+ - README.md
75
+ - Rakefile
76
+ - command_kit-completion.gemspec
77
+ - examples/cli.rb
78
+ - examples/cli/config.rb
79
+ - examples/cli/config/get.rb
80
+ - examples/cli/config/set.rb
81
+ - examples/cli/list.rb
82
+ - examples/cli/update.rb
83
+ - gemspec.yml
84
+ - lib/command_kit/completion/task.rb
85
+ - lib/command_kit/completion/version.rb
86
+ - spec/fixtures/additional_rules.yml
87
+ - spec/spec_helper.rb
88
+ - spec/task_spec.rb
89
+ homepage: https://github.com/postmodern/command_kit-completion#readme
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ documentation_uri: https://rubydoc.info/gems/command_kit-completion
94
+ source_code_uri: https://github.com/postmodern/command_kit-completion
95
+ bug_tracker_uri: https://github.com/postmodern/command_kit-completion/issues
96
+ changelog_uri: https://github.com/postmodern/command_kit-completion/blob/main/ChangeLog.md
97
+ rubygems_mfa_required: 'true'
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 3.0.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.4.10
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Generate shell completions for command_kit commands
117
+ test_files: []