command_kit-completion 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []