commander-tool 1.0.0 → 1.0.2

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/commander-tool.rb +247 -81
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fe0b76d088b96fcd3e9a46d540f79ff38d10bb49d1f35c8ed4d2b1f9bd26ddb
4
- data.tar.gz: 2c349bc20037922113ea90e74a00c6688495f1950bebe67c505d502c6bffff4d
3
+ metadata.gz: fc12456920e61c887d63c3224bbabe9b8c8850df925733a917c782da347ad2a6
4
+ data.tar.gz: fc30d88eac1a17c0effd957e5e1fc1cdcada2e83eda493087e2421b568e2ea88
5
5
  SHA512:
6
- metadata.gz: 4c20b7c34bcdb2663ee7f4554ac25b1c41c8910fe62d21e9e91b60b47f651c54fdfc7aa0e7f529db11e6a1e27e26d61ba12bc40c5f08ff8ad8fbd7fe8679b4fa
7
- data.tar.gz: 3bd9b6f788e3b2bb33e714520db2b57eb299cce3d95670b15dfb6a203cac16acc233a5c5b908647d1f3a2e5d9e2118fc32a4ca60d79a7f095afade07fe90734b
6
+ metadata.gz: f830e16aad8121ad5a59cf0a2f4ebf9ce8835e0c06ae2589e845137a06f4dc04f0bff5af628c467d25c04da944f11d393e9da67d025166035bb563819eccc24c
7
+ data.tar.gz: 759a7318b4a23153c992a3a5fb2422b3fd31efb6c2632cf38f2b279430aa92e7bf2fa539db240f07cceebe9fa4284d43dceeaf89c9325f51224001cdb49fc521
@@ -1,32 +1,54 @@
1
- require 'did_you_mean'
1
+ require "did_you_mean"
2
2
 
3
3
  class Commander
4
+ VERSION = "1.0.2"
5
+
4
6
  class Command
5
- attr_reader :name, :description, :options, :block
7
+ attr_reader :name, :description, :options, :block, :subcommands
6
8
 
7
- def initialize(name, description = '', &block)
9
+ def initialize(name, description = "", &block)
8
10
  @name = name.to_s
9
- @description = description
11
+ @description = description.to_s
10
12
  @options = []
13
+ @subcommands = {}
11
14
  @block = block
12
15
  end
13
16
 
14
- def add_option(switches, description = '')
17
+ def add_option(switches, description = "")
15
18
  @options << Option.new(switches, description)
16
19
  self
17
20
  end
21
+
22
+ def add_subcommand(name, description = "", &block)
23
+ command = Command.new(name, description, &block)
24
+ @subcommands[command.name] = command
25
+ command
26
+ end
27
+
28
+ def has_subcommands?
29
+ !@subcommands.empty?
30
+ end
18
31
  end
19
32
 
20
33
  class Option
21
34
  attr_reader :switches, :description, :key
22
35
 
23
- def initialize(switches, description = '')
24
- @switches = switches
25
- @description = description
36
+ def initialize(switches, description = "")
37
+ @switches = Array(switches).map(&:to_s)
38
+ @description = description.to_s
39
+
40
+ long =
41
+ @switches.find { |s| s.start_with?("--") }
42
+
43
+ short =
44
+ @switches.find { |s| s.start_with?("-") }
26
45
 
27
46
  @key =
28
- switches.find { |s| s.start_with?('--') }&.sub(/^--/, '')&.tr('-', '_') ||
29
- switches.find { |s| s.start_with?('-') }&.sub(/^-/, '')
47
+ if long
48
+ long.sub(/\A--/, "").tr("-", "_")
49
+ elsif short
50
+ short.sub(/\A-/, "").tr("-", "_")
51
+ end
30
52
  end
31
53
 
32
54
  def matches?(arg)
@@ -34,10 +56,9 @@ class Commander
34
56
  end
35
57
 
36
58
  def value_required?
37
- description.include?('<') ||
38
- description.include?('argument') ||
39
- description.include?('value') ||
40
- description.include?('VALUE')
59
+ @description.include?("<") ||
60
+ @description.match?(/\bargument\b/i) ||
61
+ @description.match?(/\bvalue\b/i)
41
62
  end
42
63
  end
43
64
 
@@ -46,141 +67,270 @@ class Commander
46
67
  @global_options = []
47
68
  end
48
69
 
49
- def add_command(name, description = '', &block)
50
- cmd = Command.new(name, description, &block)
51
- @commands[cmd.name] = cmd
52
- cmd
70
+ def add_command(name, description = "", &block)
71
+ command = Command.new(name, description, &block)
72
+ @commands[command.name] = command
73
+ command
53
74
  end
54
75
 
55
- def add_option(switches, description = '')
76
+ def add_option(switches, description = "")
56
77
  @global_options << Option.new(switches, description)
57
78
  self
58
79
  end
59
80
 
60
81
  def parse(args = ARGV)
61
- global_parsed, remaining_args =
62
- parse_options(args, @global_options)
82
+ args = Array(args).dup
83
+
84
+ if args.empty?
85
+ print_usage
86
+ return
87
+ end
63
88
 
64
- command_name = remaining_args.shift
89
+ if args.first == "--version" || args.first == "-v"
90
+ puts "Commander CLI v#{VERSION}"
91
+ return
92
+ end
65
93
 
66
- if command_name.nil?
67
- handle_no_command(global_parsed)
94
+ if args.first == "--help" || args.first == "-h"
95
+ print_usage
68
96
  return
69
97
  end
70
98
 
71
- if @commands.key?(command_name)
72
- command = @commands[command_name]
99
+ # Find the first registered top-level command.
100
+ command_index = nil
101
+ command = nil
73
102
 
74
- cmd_parsed, command_args =
75
- parse_options(remaining_args, command.options)
103
+ args.each_with_index do |arg, index|
104
+ next if arg.start_with?("-")
105
+
106
+ if @commands.key?(arg)
107
+ command_index = index
108
+ command = @commands[arg]
109
+ break
110
+ end
111
+ end
76
112
 
77
- # Merge global and command options for execution
78
- combined_options = global_parsed.merge(cmd_parsed)
113
+ unless command
114
+ name = args.find { |arg| !arg.start_with?("-") }
79
115
 
80
- command.block.call(combined_options, command_args) if command.block
81
- else
82
- raise_unknown_command(command_name)
116
+ if name
117
+ raise_unknown_command(name)
118
+ else
119
+ raise_unknown_option(args.first, @global_options)
120
+ end
83
121
  end
122
+
123
+ # Only arguments BEFORE the command belong to global options.
124
+ global_args = args[0...command_index]
125
+
126
+ # Everything AFTER the command belongs to that command.
127
+ command_args = args[(command_index + 1)..] || []
128
+
129
+ global_options, = parse_options(
130
+ global_args,
131
+ @global_options
132
+ )
133
+
134
+ parse_command(
135
+ command,
136
+ command_args,
137
+ global_options
138
+ )
84
139
  end
85
140
 
86
141
  private
87
142
 
143
+ def parse_command(command, args, inherited_options)
144
+ args = args.dup
145
+
146
+ if command.has_subcommands?
147
+ subcommand_name = args.shift
148
+
149
+ if subcommand_name.nil?
150
+ print_command_help(command)
151
+ return
152
+ end
153
+
154
+ if subcommand_name == "--help" || subcommand_name == "-h"
155
+ print_command_help(command)
156
+ return
157
+ end
158
+
159
+ subcommand = command.subcommands[subcommand_name]
160
+
161
+ unless subcommand
162
+ raise_unknown_subcommand(
163
+ subcommand_name,
164
+ command
165
+ )
166
+ end
167
+
168
+ command_options, command_args =
169
+ parse_options(
170
+ args,
171
+ command.options
172
+ )
173
+
174
+ combined_options =
175
+ inherited_options.merge(command_options)
176
+
177
+ if subcommand.has_subcommands?
178
+ parse_command(
179
+ subcommand,
180
+ command_args,
181
+ combined_options
182
+ )
183
+ elsif subcommand.block
184
+ subcommand.block.call(
185
+ combined_options,
186
+ command_args
187
+ )
188
+ end
189
+
190
+ return
191
+ end
192
+
193
+ command_options, command_args =
194
+ parse_options(
195
+ args,
196
+ command.options
197
+ )
198
+
199
+ combined_options =
200
+ inherited_options.merge(command_options)
201
+
202
+ command.block.call(
203
+ combined_options,
204
+ command_args
205
+ ) if command.block
206
+ end
207
+
88
208
  def parse_options(args, available_options)
89
209
  parsed = {}
90
210
  remaining = []
211
+
91
212
  i = 0
92
213
 
93
214
  while i < args.length
94
215
  arg = args[i]
95
216
 
96
- if arg.start_with?('-')
97
- option = available_options.find { |opt| opt.matches?(arg) }
217
+ if arg == "--"
218
+ remaining.concat(args[(i + 1)..] || [])
219
+ break
220
+ end
221
+
222
+ unless arg.start_with?("-")
223
+ remaining << arg
224
+ i += 1
225
+ next
226
+ end
98
227
 
99
- unless option
100
- raise_unknown_option(arg, available_options)
228
+ option =
229
+ available_options.find do |candidate|
230
+ candidate.matches?(arg)
101
231
  end
102
232
 
103
- next_arg = args[i + 1]
233
+ unless option
234
+ raise_unknown_option(
235
+ arg,
236
+ available_options
237
+ )
238
+ end
104
239
 
105
- if option.value_required?
106
- if next_arg.nil? || next_arg.start_with?('-')
107
- raise_missing_argument(arg)
108
- end
240
+ next_arg = args[i + 1]
109
241
 
110
- parsed[option.key.to_sym] = next_arg
111
- i += 2
112
- elsif next_arg && !next_arg.start_with?('-')
242
+ if option.value_required?
243
+ if next_arg.nil? || next_arg.start_with?("-")
244
+ raise_missing_argument(arg)
245
+ end
246
+
247
+ parsed[option.key.to_sym] = next_arg
248
+ i += 2
249
+ else
250
+ if next_arg && !next_arg.start_with?("-")
113
251
  parsed[option.key.to_sym] = next_arg
114
252
  i += 2
115
253
  else
116
254
  parsed[option.key.to_sym] = true
117
255
  i += 1
118
256
  end
119
- else
120
- remaining << arg
121
- i += 1
122
257
  end
123
258
  end
124
259
 
125
260
  [parsed, remaining]
126
261
  end
127
262
 
128
- def raise_missing_argument(option_name)
129
- abort "error: flag '#{option_name}' needs an argument"
263
+ def raise_missing_argument(option)
264
+ abort "error: flag '#{option}' needs an argument"
130
265
  end
131
266
 
132
- def raise_unknown_command(command_name)
133
- corrector = DidYouMean::SpellChecker.new(
134
- dictionary: @commands.keys
135
- )
267
+ def raise_unknown_command(name)
268
+ corrector =
269
+ DidYouMean::SpellChecker.new(
270
+ dictionary: @commands.keys
271
+ )
136
272
 
137
- suggestions = corrector.correct(command_name)
273
+ suggestions = corrector.correct(name)
138
274
 
139
- msg = "error: unknown command '#{command_name}'"
275
+ message =
276
+ "error: unknown command '#{name}'"
140
277
 
141
278
  unless suggestions.empty?
142
- msg += "\n\nDid you mean?\n"
143
- msg += suggestions.map { |s| "\t#{s}" }.join("\n")
279
+ message << "\n\nDid you mean?\n"
280
+ message << suggestions.map { |s| "\t#{s}" }.join("\n")
144
281
  end
145
282
 
146
- abort msg
283
+ abort message
147
284
  end
148
285
 
149
- def raise_unknown_option(option_name, available_options)
150
- all_switches = available_options.flat_map(&:switches)
151
-
152
- corrector = DidYouMean::SpellChecker.new(
153
- dictionary: all_switches
154
- )
286
+ def raise_unknown_subcommand(name, command)
287
+ corrector =
288
+ DidYouMean::SpellChecker.new(
289
+ dictionary: command.subcommands.keys
290
+ )
155
291
 
156
- suggestions = corrector.correct(option_name)
292
+ suggestions = corrector.correct(name)
157
293
 
158
- msg = "error: unknown option '#{option_name}'"
294
+ message =
295
+ "error: unknown command '#{name}'"
159
296
 
160
297
  unless suggestions.empty?
161
- msg += "\n\nDid you mean?\n"
162
- msg += suggestions.map { |s| "\t#{s}" }.join("\n")
298
+ message << "\n\nDid you mean?\n"
299
+ message << suggestions.map { |s| "\t#{s}" }.join("\n")
163
300
  end
164
301
 
165
- abort msg
302
+ abort message
166
303
  end
167
304
 
168
- def handle_no_command(options)
169
- if options[:version] || options[:v]
170
- puts "Commander CLI v1.0.0"
171
- return
305
+ def raise_unknown_option(name, options)
306
+ switches =
307
+ options.flat_map(&:switches)
308
+
309
+ corrector =
310
+ DidYouMean::SpellChecker.new(
311
+ dictionary: switches
312
+ )
313
+
314
+ suggestions = corrector.correct(name)
315
+
316
+ message =
317
+ "error: unknown option '#{name}'"
318
+
319
+ unless suggestions.empty?
320
+ message << "\n\nDid you mean?\n"
321
+ message << suggestions.map { |s| "\t#{s}" }.join("\n")
172
322
  end
173
323
 
324
+ abort message
325
+ end
326
+
327
+ def print_usage
174
328
  puts "Usage: <command> [options]"
175
329
  puts
176
330
  puts "Commands:"
177
331
 
178
- if @commands.empty?
179
- puts " No commands available."
180
- else
181
- @commands.each_value do |command|
182
- puts " #{command.name.ljust(20)} #{command.description}"
183
- end
332
+ @commands.each_value do |command|
333
+ puts " #{command.name.ljust(20)} #{command.description}"
184
334
  end
185
335
 
186
336
  unless @global_options.empty?
@@ -188,8 +338,24 @@ class Commander
188
338
  puts "Options:"
189
339
 
190
340
  @global_options.each do |option|
191
- puts " #{option.switches.join(', ').ljust(20)} #{option.description}"
341
+ puts " #{option.switches.join(", ").ljust(20)} #{option.description}"
192
342
  end
193
343
  end
194
344
  end
345
+
346
+ def print_command_help(command)
347
+ puts "Usage: #{command.name} <subcommand> [options]"
348
+ puts
349
+
350
+ unless command.description.empty?
351
+ puts command.description
352
+ puts
353
+ end
354
+
355
+ puts "Commands:"
356
+
357
+ command.subcommands.each_value do |subcommand|
358
+ puts " #{subcommand.name.ljust(20)} #{subcommand.description}"
359
+ end
360
+ end
195
361
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: commander-tool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jjjm