command_mapper 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +27 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +1 -0
  6. data/ChangeLog.md +25 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +369 -0
  10. data/Rakefile +12 -0
  11. data/commnad_mapper.gemspec +61 -0
  12. data/gemspec.yml +23 -0
  13. data/lib/command_mapper/arg.rb +75 -0
  14. data/lib/command_mapper/argument.rb +142 -0
  15. data/lib/command_mapper/command.rb +606 -0
  16. data/lib/command_mapper/exceptions.rb +19 -0
  17. data/lib/command_mapper/option.rb +282 -0
  18. data/lib/command_mapper/option_value.rb +21 -0
  19. data/lib/command_mapper/sudo.rb +73 -0
  20. data/lib/command_mapper/types/enum.rb +35 -0
  21. data/lib/command_mapper/types/hex.rb +82 -0
  22. data/lib/command_mapper/types/input_dir.rb +35 -0
  23. data/lib/command_mapper/types/input_file.rb +35 -0
  24. data/lib/command_mapper/types/input_path.rb +29 -0
  25. data/lib/command_mapper/types/key_value.rb +131 -0
  26. data/lib/command_mapper/types/key_value_list.rb +45 -0
  27. data/lib/command_mapper/types/list.rb +90 -0
  28. data/lib/command_mapper/types/map.rb +64 -0
  29. data/lib/command_mapper/types/num.rb +50 -0
  30. data/lib/command_mapper/types/str.rb +85 -0
  31. data/lib/command_mapper/types/type.rb +102 -0
  32. data/lib/command_mapper/types.rb +6 -0
  33. data/lib/command_mapper/version.rb +4 -0
  34. data/lib/command_mapper.rb +2 -0
  35. data/spec/arg_spec.rb +137 -0
  36. data/spec/argument_spec.rb +513 -0
  37. data/spec/commnad_spec.rb +1175 -0
  38. data/spec/exceptions_spec.rb +14 -0
  39. data/spec/option_spec.rb +882 -0
  40. data/spec/option_value_spec.rb +17 -0
  41. data/spec/spec_helper.rb +6 -0
  42. data/spec/sudo_spec.rb +24 -0
  43. data/spec/types/enum_spec.rb +31 -0
  44. data/spec/types/hex_spec.rb +158 -0
  45. data/spec/types/input_dir_spec.rb +30 -0
  46. data/spec/types/input_file_spec.rb +34 -0
  47. data/spec/types/input_path_spec.rb +32 -0
  48. data/spec/types/key_value_list_spec.rb +100 -0
  49. data/spec/types/key_value_spec.rb +272 -0
  50. data/spec/types/list_spec.rb +143 -0
  51. data/spec/types/map_spec.rb +62 -0
  52. data/spec/types/num_spec.rb +90 -0
  53. data/spec/types/str_spec.rb +232 -0
  54. data/spec/types/type_spec.rb +59 -0
  55. metadata +118 -0
@@ -0,0 +1,606 @@
1
+ require 'command_mapper/types'
2
+ require 'command_mapper/argument'
3
+ require 'command_mapper/option'
4
+
5
+ require 'shellwords'
6
+
7
+ module CommandMapper
8
+ class Command
9
+
10
+ include Types
11
+
12
+ # The command name.
13
+ #
14
+ # @return [String]
15
+ attr_reader :command_name
16
+
17
+ # The optional path to the command.
18
+ #
19
+ # @return [String, nil]
20
+ attr_reader :command_path
21
+
22
+ # The environment variables to execute the command with.
23
+ #
24
+ # @return [Hash{String => String}]
25
+ attr_reader :command_env
26
+
27
+ # The option values to execute the command with.
28
+ #
29
+ # @return [Hash{String => Object}]
30
+ attr_reader :command_options
31
+
32
+ # The argument values to execute the command with.
33
+ #
34
+ # @return [Hash{String => Object}]
35
+ attr_reader :command_arguments
36
+
37
+ # The subcommand's options and arguments.
38
+ #
39
+ # @return [Command, nil]
40
+ attr_reader :command_subcommand
41
+
42
+ #
43
+ # Initializes the command.
44
+ #
45
+ # @param [Hash{Symbol => Object}] params
46
+ # The option and argument values.
47
+ #
48
+ # @param [String] command_name
49
+ # Overrides the command with a custom command name.
50
+ #
51
+ # @param [String, nil] command_path
52
+ # Overrides the command with a custom path to the command.
53
+ #
54
+ # @param [Hash{String => String}] env
55
+ # Custom environment variables to pass to the command.
56
+ #
57
+ # @param [Hash{Symbol => Object}] kwargs
58
+ # Additional keywords arguments. These will be used to populate
59
+ # {#options} and {#arguments}, along with `params`.
60
+ #
61
+ # @yield [self]
62
+ # The newly initialized command.
63
+ #
64
+ # @yieldparam [Command] self
65
+ #
66
+ # @example with a symbol Hash
67
+ # MyCommand.new({foo: 'bar', baz: 'qux'})
68
+ #
69
+ # @example with a keyword arguments
70
+ # MyCommand.new(foo: 'bar', baz: 'qux')
71
+ #
72
+ # @example with a custom env Hash:
73
+ # MyCommand.new({foo: 'bar', baz: 'qux'}, env: {'FOO' =>'bar'})
74
+ # MyCommand.new(foo: 'bar', baz: 'qux', env: {'FOO' => 'bar'})
75
+ #
76
+ def initialize(params={}, command_name: self.class.command_name,
77
+ command_path: nil,
78
+ command_env: {},
79
+ **kwargs)
80
+ @command_name = command_name
81
+ @command_path = command_path
82
+ @command_env = command_env
83
+
84
+ params = params.merge(kwargs)
85
+
86
+ params.each do |name,value|
87
+ self[name] = value
88
+ end
89
+
90
+ yield self if block_given?
91
+ end
92
+
93
+ #
94
+ # Initializes and runs the command.
95
+ #
96
+ # @param [Hash{Symbol => Object}] params
97
+ # The option values.
98
+ #
99
+ # @yield [self]
100
+ # The newly initialized command.
101
+ #
102
+ # @yieldparam [Command] self
103
+ #
104
+ # @return [Boolean, nil]
105
+ #
106
+ def self.run(params={},**kwargs,&block)
107
+ command = new(params,**kwargs,&block)
108
+ command.run_command
109
+ end
110
+
111
+ #
112
+ # Runs the command in a shell and captures all stdout output.
113
+ #
114
+ # @param [Hash{Symbol => Object}] params
115
+ # The option values.
116
+ #
117
+ # @yield [self]
118
+ # The newly initialized command.
119
+ #
120
+ # @yieldparam [Command] self
121
+ #
122
+ # @return [String]
123
+ # The stdout output of the command.
124
+ #
125
+ def self.capture(params={},**kwargs,&block)
126
+ command = new(params,**kwargs,&block)
127
+ command.capture_command
128
+ end
129
+
130
+ #
131
+ # Executes the command and returns an IO object to it.
132
+ #
133
+ # @param [Hash{Symbol => Object}] params
134
+ # The option values.
135
+ #
136
+ # @yield [self]
137
+ # The newly initialized command.
138
+ #
139
+ # @yieldparam [Command] self
140
+ #
141
+ # @return [IO]
142
+ #
143
+ def self.popen(params={}, mode: 'r', **kwargs,&block)
144
+ command = new(params,**kwargs,&block)
145
+ command.popen_command
146
+ end
147
+
148
+ #
149
+ # Initializes and runs the command through sudo.
150
+ #
151
+ # @param [Hash{Symbol => Object}] params
152
+ # The option values.
153
+ #
154
+ # @param [Hash{Symbol => Object}] kwargs
155
+ # Additional keyword arguments for {#initialize}.
156
+ #
157
+ # @yield [self]
158
+ # The newly initialized command.
159
+ #
160
+ # @yieldparam [Command] self
161
+ #
162
+ # @return [Boolean, nil]
163
+ #
164
+ def self.sudo(params={}, sudo: {}, **kwargs,&block)
165
+ command = new(params,**kwargs,&block)
166
+ command.sudo_command(**sudo)
167
+ end
168
+
169
+ #
170
+ # Gets or sets the command name.
171
+ #
172
+ # @param [#to_s] new_name
173
+ # The optional new command name.
174
+ #
175
+ # @return [String]
176
+ # The command name.
177
+ #
178
+ # @raise [NotImplementedError]
179
+ # The command class did not call {command}.
180
+ #
181
+ # @api semipublic
182
+ #
183
+ def self.command_name
184
+ @command_name || raise(NotImplementedError,"#{self} did not call command(...)")
185
+ end
186
+
187
+ #
188
+ # @param [#to_s] new_command_name
189
+ #
190
+ # @yield [self]
191
+ #
192
+ # @example
193
+ # command 'grep'
194
+ # # ...
195
+ #
196
+ # @example
197
+ # command 'grep' do
198
+ # option "--regexp", equals: true, value: true
199
+ # # ...
200
+ # end
201
+ #
202
+ # @api public
203
+ #
204
+ def self.command(new_command_name,&block)
205
+ @command_name = new_command_name.to_s.freeze
206
+ yield self if block_given?
207
+ end
208
+
209
+ #
210
+ # All defined options.
211
+ #
212
+ # @return [Hash{Symbol => Option}]
213
+ #
214
+ # @api semipublic
215
+ #
216
+ def self.options
217
+ @options ||= if superclass < Command
218
+ superclass.options.dup
219
+ else
220
+ {}
221
+ end
222
+ end
223
+
224
+ #
225
+ # Defines an option for the command.
226
+ #
227
+ # @param [String] flag
228
+ # The option's command-line flag.
229
+ #
230
+ # @param [Symbol, nil] name
231
+ # The option's name.
232
+ #
233
+ # @param [Boolean] equals
234
+ # Specifies whether the option's flag and value should be separated with a
235
+ # `=` character.
236
+ #
237
+ # @param [Hash, nil] value
238
+ # The option's value.
239
+ #
240
+ # @option value [Boolean] :required
241
+ # Specifies whether the option requires a value or not.
242
+ #
243
+ # @option value [Types:Type, Hash, nil] :type
244
+ # The explicit type for the option's value.
245
+ #
246
+ # @param [Boolean] repeats
247
+ # Specifies whether the option can be given multiple times.
248
+ #
249
+ # @api public
250
+ #
251
+ # @example Defining an option:
252
+ # option '--foo'
253
+ #
254
+ # @example Defining an option with a custom name:
255
+ # option '-F', name: :foo
256
+ #
257
+ # @example Defining an option who's value is required:
258
+ # option '--file', value: true
259
+ #
260
+ # @example Defining an option who's value is optional:
261
+ # option '--file', value: {required: false}
262
+ #
263
+ # @example Defining an `--opt=value` option:
264
+ # option '--foo', equals: true, value: true
265
+ #
266
+ # @example Defining an option that can be repeated multiple times:
267
+ # option '--foo', repeats: true
268
+ #
269
+ # @example Defining an option that takes a comma-separated list:
270
+ # option '--list', value: List.new
271
+ #
272
+ # @raise [ArgumentError]
273
+ # The option flag conflicts with a pre-existing internal method.
274
+ #
275
+ def self.option(flag, name: nil, equals: nil, value: nil, repeats: false, &block)
276
+ option = Option.new(flag, name: name,
277
+ equals: equals,
278
+ value: value,
279
+ repeats: repeats,
280
+ &block)
281
+
282
+ self.options[option.name] = option
283
+
284
+ if is_internal_method?(option.name)
285
+ if name
286
+ raise(ArgumentError,"option #{flag.inspect} with name #{name.inspect} cannot override the internal method with same name: ##{option.name}")
287
+ else
288
+ raise(ArgumentError,"option #{flag.inspect} maps to method name ##{option.name} and cannot override the internal method with same name: ##{option.name}")
289
+ end
290
+ end
291
+
292
+ attr_accessor option.name
293
+ end
294
+
295
+ #
296
+ # All defined options.
297
+ #
298
+ # @return [Hash{Symbol => Argument}]
299
+ #
300
+ # @api semipublic
301
+ #
302
+ def self.arguments
303
+ @arguments ||= if superclass < Command
304
+ superclass.arguments.dup
305
+ else
306
+ {}
307
+ end
308
+ end
309
+
310
+ #
311
+ # Defines an option for the command.
312
+ #
313
+ # @param [Symbol] name
314
+ #
315
+ # @param [Boolean] required
316
+ # Specifies whether the argument is required or can be omitted.
317
+ #
318
+ # @param [Types::Type, Hash, nil] type
319
+ # The explicit type for the argument.
320
+ #
321
+ # @param [Boolean] repeats
322
+ # Specifies whether the option can be repeated multiple times.
323
+ #
324
+ # @api public
325
+ #
326
+ # @example Define an argument:
327
+ # argument :file
328
+ #
329
+ # @example Define an argument that can be specified multiple times:
330
+ # argument :files, repeats: true
331
+ #
332
+ # @example Define an optional argument:
333
+ # argument :file, required: false
334
+ #
335
+ # @raise [ArgumentError]
336
+ # The argument name conflicts with a pre-existing internal method.
337
+ #
338
+ def self.argument(name, required: true, type: Str.new, repeats: false)
339
+ name = name.to_sym
340
+ argument = Argument.new(name, required: required,
341
+ type: type,
342
+ repeats: repeats)
343
+
344
+ self.arguments[argument.name] = argument
345
+
346
+ if is_internal_method?(argument.name)
347
+ raise(ArgumentError,"argument #{name.inspect} cannot override internal method with same name: ##{argument.name}")
348
+ end
349
+
350
+ attr_accessor name
351
+ end
352
+
353
+ #
354
+ # All defined subcommands.
355
+ #
356
+ # @return [Hash{Symbol => Command}]
357
+ #
358
+ # @api semipublic
359
+ #
360
+ def self.subcommands
361
+ @subcommands ||= if superclass < Command
362
+ superclass.subcommands.dup
363
+ else
364
+ {}
365
+ end
366
+ end
367
+
368
+ #
369
+ # Defines a subcommand.
370
+ #
371
+ # @param [String] name
372
+ # The name of the subcommand.
373
+ #
374
+ # @yield [subcommand]
375
+ # The given block will be used to populate the subcommand's options.
376
+ #
377
+ # @yieldparam [Command] subcommand
378
+ # The newly created subcommand class.
379
+ #
380
+ # @note
381
+ # Also defines a class within the command class using the subcommand's
382
+ # name.
383
+ #
384
+ # @example Defining a sub-command:
385
+ # class Git
386
+ # command 'git' do
387
+ # subcommand 'clone' do
388
+ # option '--bare'
389
+ # # ...
390
+ # end
391
+ # end
392
+ # end
393
+ #
394
+ # @raise [ArgumentError]
395
+ # The subcommand name conflicts with a pre-existing internal method.
396
+ #
397
+ def self.subcommand(name,&block)
398
+ name = name.to_s
399
+
400
+ subcommand_class = Class.new(Command)
401
+ subcommand_class.command(name)
402
+ subcommand_class.class_eval(&block)
403
+
404
+ method_name = name.tr('-','_')
405
+ class_name = name.split(/[_-]+/).map(&:capitalize).join
406
+
407
+ self.subcommands[method_name.to_sym] = subcommand_class
408
+ const_set(class_name,subcommand_class)
409
+
410
+ if is_internal_method?(method_name)
411
+ raise(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
412
+ end
413
+
414
+ define_method(method_name) do |&block|
415
+ if block then @command_subcommand = subcommand_class.new(&block)
416
+ else @command_subcommand
417
+ end
418
+ end
419
+
420
+ define_method(:"#{method_name}=") do |options|
421
+ @command_subcommand = if options
422
+ subcommand_class.new(options)
423
+ end
424
+ end
425
+ end
426
+
427
+ #
428
+ # Gets the value of an option or an argument.
429
+ #
430
+ # @param [Symbol] name
431
+ #
432
+ # @return [Object]
433
+ #
434
+ # @raise [ArgumentError]
435
+ # The given name was not match any option or argument.
436
+ #
437
+ def [](name)
438
+ name = name.to_s
439
+
440
+ if respond_to?(name)
441
+ send(name)
442
+ else
443
+ raise(ArgumentError,"#{self.class} does not define ##{name}")
444
+ end
445
+ end
446
+
447
+ #
448
+ # Sets an option or an argument with the given name.
449
+ #
450
+ # @param [Symbol] name
451
+ #
452
+ # @param [Object] value
453
+ #
454
+ # @return [Object]
455
+ #
456
+ # @raise [ArgumentError]
457
+ # The given name was not match any option or argument.
458
+ #
459
+ def []=(name,value)
460
+ if respond_to?("#{name}=")
461
+ send("#{name}=",value)
462
+ else
463
+ raise(ArgumentError,"#{self.class} does not define ##{name}=")
464
+ end
465
+ end
466
+
467
+ #
468
+ # Returns an Array of command-line arguments for the command.
469
+ #
470
+ # @return [Array<String>]
471
+ #
472
+ # @raise [ArgumentReqired]
473
+ # A required argument was not set.
474
+ #
475
+ def command_argv
476
+ argv = [@command_path || @command_name]
477
+
478
+ self.class.options.each do |name,option|
479
+ unless (value = self[name]).nil?
480
+ option.argv(argv,value)
481
+ end
482
+ end
483
+
484
+ if @command_subcommand
485
+ # a subcommand takes precedence over any command arguments
486
+ argv.concat(@command_subcommand.command_argv)
487
+ else
488
+ additional_args = []
489
+
490
+ self.class.arguments.each do |name,argument|
491
+ value = self[name]
492
+
493
+ if value.nil? && argument.required?
494
+ raise(ArgumentRequired,"argument #{name} is required")
495
+ else
496
+ argument.argv(additional_args,value)
497
+ end
498
+ end
499
+
500
+ if additional_args.any? { |arg| arg.start_with?('-') }
501
+ # append a '--' separator if any of the arguments start with a '-'
502
+ argv << '--'
503
+ end
504
+
505
+ argv.concat(additional_args)
506
+ end
507
+
508
+ return argv
509
+ end
510
+
511
+ #
512
+ # Escapes any shell control-characters so that it can be ran in a shell.
513
+ #
514
+ # @return [String]
515
+ # The shell-escaped command.
516
+ #
517
+ def command_string
518
+ escaped_command = Shellwords.shelljoin(command_argv)
519
+
520
+ unless @command_env.empty?
521
+ escaped_env = @command_env.map { |name,value|
522
+ "#{Shellwords.shellescape(name)}=#{Shellwords.shellescape(value)}"
523
+ }.join(' ')
524
+
525
+ escaped_command = "#{escaped_env} #{escaped_command}"
526
+ end
527
+
528
+ return escaped_command
529
+ end
530
+
531
+ #
532
+ # Initializes and runs the command.
533
+ #
534
+ # @return [Boolean, nil]
535
+ #
536
+ def run_command
537
+ system(@command_env,*command_argv)
538
+ end
539
+
540
+ #
541
+ # Runs the command in a shell and captures all stdout output.
542
+ #
543
+ # @return [String]
544
+ # The stdout output of the command.
545
+ #
546
+ def capture_command
547
+ `#{command_string}`
548
+ end
549
+
550
+ #
551
+ # Executes the command and returns an IO object to it.
552
+ #
553
+ # @return [IO]
554
+ #
555
+ def popen_command(mode=nil)
556
+ if mode then IO.popen(@command_env,command_argv,mode)
557
+ else IO.popen(@command_env,command_argv)
558
+ end
559
+ end
560
+
561
+ #
562
+ # Initializes and runs the command through sudo.
563
+ #
564
+ # @param [Hash{Symbol => Object}] sudo_params
565
+ # Additional keyword arguments for {Sudo#initialize}.
566
+ #
567
+ # @return [Boolean, nil]
568
+ #
569
+ def sudo_command(**sudo_kwargs,&block)
570
+ sudo_params = sudo_kwargs.merge(command: command_argv)
571
+
572
+ Sudo.run(sudo_params, command_env: @command_env, &block)
573
+ end
574
+
575
+ #
576
+ # @see #argv
577
+ #
578
+ def to_a
579
+ command_argv
580
+ end
581
+
582
+ #
583
+ # @see #shellescape
584
+ #
585
+ def to_s
586
+ command_string
587
+ end
588
+
589
+ private
590
+
591
+ #
592
+ # Determines if there is an internal method of the same name.
593
+ #
594
+ # @param [#to_sym] name
595
+ # The method name.
596
+ #
597
+ # @return [Boolean]
598
+ #
599
+ def self.is_internal_method?(name)
600
+ Command.instance_methods(false).include?(name.to_sym)
601
+ end
602
+
603
+ end
604
+ end
605
+
606
+ require 'command_mapper/sudo'
@@ -0,0 +1,19 @@
1
+ module CommandMapper
2
+ #
3
+ # Commaon base class for all {CommandMapper} exceptions.
4
+ #
5
+ class Error < RuntimeError
6
+ end
7
+
8
+ #
9
+ # Represents a argument or option value validation error.
10
+ #
11
+ class ValidationError < Error
12
+ end
13
+
14
+ #
15
+ # Indicates that a required argument was not set.
16
+ #
17
+ class ArgumentRequired < Error
18
+ end
19
+ end