thor-completion 0.0.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.
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Thor
4
+ module Completion
5
+ class ZSH
6
+ include SchemaValidator
7
+
8
+ attr_reader :output, :schema, :name, :commands, :global_options
9
+
10
+ def initialize(schema)
11
+ validate_schema!(schema)
12
+
13
+ @schema = schema
14
+ @name = schema[:name]
15
+ @commands = schema[:commands] || []
16
+ @global_options = schema[:globalOptions] || []
17
+ @output = []
18
+ end
19
+
20
+ def call
21
+ output << "#compdef #{name}"
22
+ output << ""
23
+
24
+ generate_subcommand_functions
25
+ generate_main_function
26
+
27
+ output << "compdef _#{name} #{name}"
28
+ output.join("\n")
29
+ end
30
+
31
+ private def generate_main_function
32
+ output << "_#{name}() {"
33
+ output << " local context state line"
34
+ output << " typeset -A opt_args"
35
+ output << ""
36
+
37
+ if commands.any?
38
+ output << " local -a commands"
39
+ output << " commands=("
40
+ commands.each do |cmd|
41
+ next if cmd[:hidden]
42
+
43
+ cmd_name = cmd[:name]
44
+ cmd_desc = cmd[:description] || ""
45
+ output << " #{quote(cmd_name)}:#{quote(escape_description(cmd_desc))}"
46
+ end
47
+ output << " )"
48
+ output << ""
49
+ end
50
+
51
+ # Build _arguments call
52
+ args = []
53
+
54
+ # Add global options
55
+ global_options.each do |opt|
56
+ result = format_option(opt)
57
+ if result.is_a?(Array)
58
+ args.concat(result)
59
+ elsif result
60
+ args << result
61
+ end
62
+ end
63
+
64
+ # Add command selection if we have commands
65
+ if commands.any?
66
+ args << "'1: :->command'"
67
+ args << "'*::arg:->args'"
68
+ end
69
+
70
+ if args.any?
71
+ output << " _arguments -C \\"
72
+ args.each_with_index do |arg, idx|
73
+ line = " #{arg}"
74
+ line += " \\" unless idx == args.length - 1
75
+ output << line
76
+ end
77
+ output << ""
78
+ end
79
+
80
+ if commands.any?
81
+ output << " case $state in"
82
+ output << " command)"
83
+ output << " _describe 'command' commands"
84
+ output << " ;;"
85
+ output << " args)"
86
+ output << " case $words[1] in"
87
+
88
+ commands.each do |cmd|
89
+ next if cmd[:hidden]
90
+
91
+ cmd_name = cmd[:name]
92
+ output << " #{cmd_name})"
93
+ output << if cmd[:subcommands]&.any? || cmd[:options]&.any? || cmd[:arguments]&.any?
94
+ " _#{name}_#{sanitize_name(cmd_name)}"
95
+ else
96
+ " # No additional completion"
97
+ end
98
+ output << " ;;"
99
+ end
100
+
101
+ output << " esac"
102
+ output << " ;;"
103
+ output << " esac"
104
+ end
105
+
106
+ output << "}"
107
+ end
108
+
109
+ private def generate_subcommand_functions
110
+ commands.each do |cmd|
111
+ next if cmd[:hidden]
112
+ next unless cmd[:subcommands]&.any? || cmd[:options]&.any? || cmd[:arguments]&.any?
113
+
114
+ generate_command_function(cmd, [])
115
+ end
116
+ end
117
+
118
+ private def generate_command_function(cmd, parent_names)
119
+ cmd_name = cmd[:name]
120
+ func_name = "_#{name}_#{(parent_names + [sanitize_name(cmd_name)]).join('_')}"
121
+
122
+ output << "#{func_name}() {"
123
+
124
+ # Handle subcommands
125
+ if cmd[:subcommands]&.any?
126
+ output << " local -a subcommands"
127
+ output << " subcommands=("
128
+ cmd[:subcommands].each do |subcmd|
129
+ next if subcmd[:hidden]
130
+
131
+ subcmd_name = subcmd[:name]
132
+ subcmd_desc = subcmd[:description] || ""
133
+ output << " #{quote(subcmd_name)}:#{quote(escape_description(subcmd_desc))}"
134
+ end
135
+ output << " )"
136
+ output << ""
137
+ end
138
+
139
+ # Build arguments
140
+ args = []
141
+
142
+ # Add command-specific options
143
+ if cmd[:options]&.any?
144
+ cmd[:options].each do |opt|
145
+ result = format_option(opt)
146
+ if result.is_a?(Array)
147
+ args.concat(result)
148
+ elsif result
149
+ args << result
150
+ end
151
+ end
152
+ end
153
+
154
+ # Add positional arguments
155
+ if cmd[:arguments]&.any?
156
+ cmd[:arguments].each_with_index do |arg, idx|
157
+ args << format_argument(arg, idx + 1)
158
+ end
159
+ elsif cmd[:subcommands]&.any?
160
+ args << "'1: :->subcommand'"
161
+ args << "'*::arg:->args'"
162
+ end
163
+
164
+ if args.any?
165
+ output << " _arguments \\"
166
+ args.each_with_index do |arg, idx|
167
+ line = " #{arg}"
168
+ line += " \\" unless idx == args.length - 1
169
+ output << line
170
+ end
171
+
172
+ # If we have subcommands, add state handling
173
+ if cmd[:subcommands]&.any?
174
+ output << ""
175
+ output << " case $state in"
176
+ output << " subcommand)"
177
+ output << " _describe 'subcommand' subcommands"
178
+ output << " ;;"
179
+ output << " args)"
180
+ output << " case $words[1] in"
181
+ cmd[:subcommands].each do |subcmd|
182
+ next if subcmd[:hidden]
183
+
184
+ subcmd_name = subcmd[:name]
185
+ output << " #{subcmd_name})"
186
+
187
+ # Check if subcommand has options, arguments, or its own subcommands
188
+ if subcmd[:subcommands]&.any? || subcmd[:options]&.any? || subcmd[:arguments]&.any?
189
+ subcmd_func_name = "_#{name}_#{(parent_names + [
190
+ sanitize_name(cmd_name), sanitize_name(subcmd_name)
191
+ ]).join('_')}"
192
+ output << " #{subcmd_func_name}"
193
+ else
194
+ output << " # No additional completion"
195
+ end
196
+ output << " ;;"
197
+ end
198
+ output << " esac"
199
+ output << " ;;"
200
+ output << " esac"
201
+ end
202
+ elsif cmd[:subcommands]&.any?
203
+ output << " _describe 'subcommand' subcommands"
204
+ end
205
+
206
+ output << "}"
207
+ output << ""
208
+
209
+ # Recursively generate functions for subcommands
210
+ return unless cmd[:subcommands]&.any?
211
+
212
+ cmd[:subcommands].each do |subcmd|
213
+ next if subcmd[:hidden]
214
+ next unless subcmd[:subcommands]&.any? || subcmd[:options]&.any? || subcmd[:arguments]&.any?
215
+
216
+ generate_command_function(subcmd,
217
+ parent_names + [sanitize_name(cmd_name)])
218
+ end
219
+ end
220
+
221
+ private def format_option(opt)
222
+ return nil if opt[:hidden]
223
+
224
+ opt_name = opt[:name]
225
+ opt_short = opt[:short]
226
+ opt_desc = opt[:description] || ""
227
+ opt_type = opt[:type] || "boolean"
228
+
229
+ # Build the option spec
230
+ spec_parts = []
231
+
232
+ # Handle short and long options
233
+ shorts = Array(opt_short).compact
234
+ if shorts.any? && opt_name
235
+ # Both short and long options - always list separately for compatibility
236
+ exclusion = "(#{shorts.map {|s| "-#{s}" }.join(' ')} --#{opt_name})"
237
+ shorts.each do |s|
238
+ spec_parts << "'#{exclusion}-#{s}[#{escape_description(opt_desc)}]'"
239
+ end
240
+ spec_parts << "'#{exclusion}--#{opt_name}[#{escape_description(opt_desc)}]'"
241
+ elsif shorts.any?
242
+ # Only short options
243
+ if shorts.length > 1
244
+ exclusion = "(#{shorts.map {|s| "-#{s}" }.join(' ')})"
245
+ flags = "{#{shorts.map {|s| "-#{s}" }.join(',')}}"
246
+ spec_parts << "'#{exclusion}#{flags}[#{escape_description(opt_desc)}]'"
247
+ else
248
+ spec_parts << "'-#{shorts.first}[#{escape_description(opt_desc)}]'"
249
+ end
250
+ elsif opt_name
251
+ # Only long option
252
+ spec_parts << "'--#{opt_name}[#{escape_description(opt_desc)}]'"
253
+ else
254
+ return nil
255
+ end
256
+
257
+ # Add value completion for non-boolean options
258
+ if opt_type != "boolean"
259
+ spec_parts = spec_parts.map do |spec|
260
+ # Remove the closing bracket and quote
261
+ spec = spec.sub(/\]'$/, "")
262
+
263
+ if opt[:enum]&.any?
264
+ # Enum values
265
+ values = opt[:enum].map {|v| escape_value(v) }.join(" ")
266
+ spec + "]:value:(#{values})'"
267
+ elsif opt[:completion]
268
+ add_completion_spec(spec, opt[:completion])
269
+ else
270
+ # Generic value
271
+ spec + "]:#{opt_type}:'"
272
+ end
273
+ end
274
+ end
275
+
276
+ spec_parts.length == 1 ? spec_parts[0] : spec_parts
277
+ end
278
+
279
+ private def format_argument(arg, position)
280
+ arg_name = arg[:name]
281
+ arg_desc = arg[:description] || arg_name
282
+ variadic = arg[:variadic] ? "*" : ""
283
+
284
+ if arg[:completion]
285
+ completion = format_completion(arg[:completion])
286
+ "'#{variadic}#{position}:#{arg_desc}:#{completion}'"
287
+ elsif arg[:required] == false
288
+ "'#{variadic}#{position}::#{arg_desc}:_files'"
289
+ else
290
+ "'#{variadic}#{position}:#{arg_desc}:_files'"
291
+ end
292
+ end
293
+
294
+ private def add_completion_spec(spec, completion)
295
+ comp_spec = format_completion(completion)
296
+ spec.sub(/\]$/, "]:value:#{comp_spec}'")
297
+ end
298
+
299
+ private def format_completion(completion)
300
+ case completion[:type]
301
+ when "static"
302
+ values = completion[:values].map {|v| escape_value(v) }.join(" ")
303
+ "(#{values})"
304
+ when "file"
305
+ if completion[:pattern]
306
+ "_files -g #{quote(completion[:pattern])}"
307
+ elsif completion[:extensions]&.any?
308
+ patterns = completion[:extensions].map do |ext|
309
+ "*.#{ext}"
310
+ end.join(" ")
311
+ "_files -g #{quote("{#{patterns}}")}"
312
+ else
313
+ "_files"
314
+ end
315
+ when "directory"
316
+ "_directories"
317
+ when "command"
318
+ "($#{completion[:command]})"
319
+ when "dynamic"
320
+ "($(#{completion[:command]}))"
321
+ else
322
+ "_files"
323
+ end
324
+ end
325
+
326
+ private def escape_description(desc)
327
+ desc.to_s.gsub(/[\[\]']/, '\\\\\&')
328
+ end
329
+
330
+ private def escape_value(value)
331
+ value.to_s.gsub(/[\s']/, '\\\\\&')
332
+ end
333
+
334
+ private def quote(str)
335
+ "'#{str}'"
336
+ end
337
+
338
+ private def sanitize_name(name)
339
+ name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
340
+ end
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json-schema"
5
+
6
+ class Thor
7
+ module Completion
8
+ require_relative "completion/version"
9
+ require_relative "completion/schema_validator"
10
+ require_relative "completion/builder"
11
+ require_relative "completion/zsh"
12
+ require_relative "completion/bash"
13
+ require_relative "completion/powershell"
14
+ require_relative "completion/fish"
15
+
16
+ def self.generate(shell:, name:, description:, version:, cli:)
17
+ schema = Builder.call(name:, description:, version:, cli:)
18
+
19
+ case shell
20
+ when "bash"
21
+ Bash.new(schema).call
22
+ when "zsh"
23
+ ZSH.new(schema).call
24
+ when "powershell"
25
+ Powershell.new(schema).call
26
+ when "fish"
27
+ Fish.new(schema).call
28
+ else
29
+ raise "Unsupported shell: #{shell.inspect}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "thor/completion"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/thor/completion/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "thor-completion"
7
+ spec.version = Thor::Completion::VERSION
8
+ spec.authors = ["Nando Vieira"]
9
+ spec.email = ["me@fnando.com"]
10
+ spec.metadata = {"rubygems_mfa_required" => "true"}
11
+
12
+ spec.summary = "Generate shell completions for Thor CLIs."
13
+ spec.description = spec.summary
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.4.0")
16
+
17
+ github_url = "https://github.com/fnando/thor-completion"
18
+ github_tree_url = "#{github_url}/tree/v#{spec.version}"
19
+
20
+ spec.homepage = github_url
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["bug_tracker_uri"] = "#{github_url}/issues"
23
+ spec.metadata["source_code_uri"] = github_tree_url
24
+ spec.metadata["changelog_uri"] = "#{github_tree_url}/CHANGELOG.md"
25
+ spec.metadata["documentation_uri"] = "#{github_tree_url}/README.md"
26
+ spec.metadata["license_uri"] = "#{github_tree_url}/LICENSE.md"
27
+
28
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
+ `git ls-files -z`
30
+ .split("\x0")
31
+ .reject {|f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_dependency "json-schema"
39
+ spec.add_dependency "thor"
40
+ spec.add_development_dependency "minitest"
41
+ spec.add_development_dependency "minitest-utils"
42
+ spec.add_development_dependency "rake"
43
+ spec.add_development_dependency "rubocop"
44
+ spec.add_development_dependency "rubocop-fnando"
45
+ spec.add_development_dependency "simplecov"
46
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thor-completion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nando Vieira
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json-schema
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest-utils
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop-fnando
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ description: Generate shell completions for Thor CLIs.
125
+ email:
126
+ - me@fnando.com
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - ".github/CODEOWNERS"
132
+ - ".github/FUNDING.yml"
133
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
134
+ - ".github/ISSUE_TEMPLATE/config.yml"
135
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
136
+ - ".github/PULL_REQUEST_TEMPLATE.md"
137
+ - ".github/dependabot.yml"
138
+ - ".github/workflows/ruby-tests.yml"
139
+ - ".gitignore"
140
+ - ".rubocop.yml"
141
+ - CHANGELOG.md
142
+ - CODE_OF_CONDUCT.md
143
+ - CONTRIBUTING.md
144
+ - Gemfile
145
+ - LICENSE.md
146
+ - README.md
147
+ - Rakefile
148
+ - bin/console
149
+ - bin/setup
150
+ - lib/thor-completion.rb
151
+ - lib/thor/completion.rb
152
+ - lib/thor/completion/bash.rb
153
+ - lib/thor/completion/builder.rb
154
+ - lib/thor/completion/fish.rb
155
+ - lib/thor/completion/powershell.rb
156
+ - lib/thor/completion/schema.json
157
+ - lib/thor/completion/schema_validator.rb
158
+ - lib/thor/completion/version.rb
159
+ - lib/thor/completion/zsh.rb
160
+ - thor-completion.gemspec
161
+ homepage: https://github.com/fnando/thor-completion
162
+ licenses:
163
+ - MIT
164
+ metadata:
165
+ rubygems_mfa_required: 'true'
166
+ homepage_uri: https://github.com/fnando/thor-completion
167
+ bug_tracker_uri: https://github.com/fnando/thor-completion/issues
168
+ source_code_uri: https://github.com/fnando/thor-completion/tree/v0.0.0
169
+ changelog_uri: https://github.com/fnando/thor-completion/tree/v0.0.0/CHANGELOG.md
170
+ documentation_uri: https://github.com/fnando/thor-completion/tree/v0.0.0/README.md
171
+ license_uri: https://github.com/fnando/thor-completion/tree/v0.0.0/LICENSE.md
172
+ rdoc_options: []
173
+ require_paths:
174
+ - lib
175
+ required_ruby_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: 3.4.0
180
+ required_rubygems_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ requirements: []
186
+ rubygems_version: 3.6.9
187
+ specification_version: 4
188
+ summary: Generate shell completions for Thor CLIs.
189
+ test_files: []