command_mapper 0.1.0.pre1

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 (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,1175 @@
1
+ require 'spec_helper'
2
+ require 'command_mapper/command'
3
+
4
+ describe CommandMapper::Command do
5
+ module TestCommand
6
+ class WithCommandName < CommandMapper::Command
7
+ command 'foo'
8
+ end
9
+
10
+ class NoCommandName < CommandMapper::Command
11
+ end
12
+ end
13
+
14
+ describe ".command_name" do
15
+ subject { command_class }
16
+
17
+ context "when @command_name has been set" do
18
+ let(:command_class) { TestCommand::WithCommandName }
19
+
20
+ it "must return the defined command name" do
21
+ expect(subject.command_name).to eq('foo')
22
+ end
23
+
24
+ it "must freeze the given command name" do
25
+ expect(subject.command_name).to be_frozen
26
+ end
27
+ end
28
+
29
+ context "when no .command has been defined" do
30
+ let(:command_class) { TestCommand::NoCommandName }
31
+
32
+ it "must raise NotImplementedError" do
33
+ expect {
34
+ subject.command_name
35
+ }.to raise_error(NotImplementedError,"#{command_class} did not call command(...)")
36
+ end
37
+ end
38
+ end
39
+
40
+ describe ".command_name" do
41
+ subject { command_class }
42
+
43
+ context "when @command_name has been set" do
44
+ let(:command_class) { TestCommand::WithCommandName }
45
+
46
+ it "must set .command_name" do
47
+ expect(subject.command_name).to eq('foo')
48
+ end
49
+ end
50
+ end
51
+
52
+ module TestCommand
53
+ class EmptyCommand < CommandMapper::Command
54
+ end
55
+ end
56
+
57
+ describe ".options" do
58
+ subject { command_class }
59
+
60
+ context "when the command has no defined options" do
61
+ let(:command_class) { TestCommand::EmptyCommand }
62
+
63
+ it { expect(subject.options).to be_empty }
64
+ end
65
+
66
+ context "and when the command inherits from another command class" do
67
+ module TestCommand
68
+ class BaseClassWithOptions < CommandMapper::Command
69
+ option "--foo"
70
+ option "--bar"
71
+ end
72
+
73
+ class InheritedOptions < BaseClassWithOptions
74
+ end
75
+ end
76
+
77
+ let(:command_class) { TestCommand::InheritedOptions }
78
+ let(:command_superclass) { TestCommand::BaseClassWithOptions }
79
+
80
+ it "must copy the options defined in the superclass" do
81
+ expect(subject.options).to eq(command_superclass.options)
82
+ end
83
+
84
+ context "and when the class defines options of it's own" do
85
+ module TestCommand
86
+ class InheritsAndDefinesOptions < BaseClassWithOptions
87
+ option "--baz"
88
+ end
89
+ end
90
+
91
+ let(:command_class) { TestCommand::InheritsAndDefinesOptions }
92
+
93
+ it "must copy the options defined in the superclass" do
94
+ expect(subject.options).to include(command_superclass.options)
95
+ end
96
+
97
+ it "must define it's own options" do
98
+ expect(command_class.options[:baz]).to be_kind_of(CommandMapper::Option)
99
+ end
100
+
101
+ it "must not modify the superclass's options" do
102
+ expect(command_superclass.options[:baz]).to be(nil)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ describe ".option" do
109
+ module TestCommand
110
+ class DefinesItsOwnOptions < CommandMapper::Command
111
+ command 'test' do
112
+ option '--foo'
113
+ option '--bar'
114
+ end
115
+ end
116
+ end
117
+
118
+ let(:command_class) { TestCommand::DefinesItsOwnOptions }
119
+
120
+ subject { command_class }
121
+
122
+ it "must add options to .options" do
123
+ expect(subject.options[:foo]).to be_kind_of(CommandMapper::Option)
124
+ expect(subject.options[:foo].flag).to eq('--foo')
125
+
126
+ expect(subject.options[:bar]).to be_kind_of(CommandMapper::Option)
127
+ expect(subject.options[:bar].flag).to eq('--bar')
128
+ end
129
+
130
+ it "must define a reader method for each option" do
131
+ expect(subject.instance_methods(false)).to include(:foo)
132
+ expect(subject.instance_methods(false)).to include(:bar)
133
+ end
134
+
135
+ it "must define a writter method for each option" do
136
+ expect(subject.instance_methods(false)).to include(:foo=)
137
+ expect(subject.instance_methods(false)).to include(:bar=)
138
+ end
139
+
140
+ describe "reader method" do
141
+ subject { command_class.new }
142
+
143
+ let(:value) { "test_reading" }
144
+
145
+ before do
146
+ subject.instance_variable_set('@foo',value)
147
+ end
148
+
149
+ it "must read the options value from @options" do
150
+ expect(subject.foo).to be(value)
151
+ end
152
+ end
153
+
154
+ describe "writter method" do
155
+ subject { command_class.new }
156
+
157
+ let(:value) { "test_writing" }
158
+
159
+ before { subject.foo = value }
160
+
161
+ it "must read the options value from @options" do
162
+ expect(subject.instance_variable_get('@foo')).to be(value)
163
+ end
164
+ end
165
+
166
+ context "when given a short flag" do
167
+ context "and it's length is < 3" do
168
+ module TestCommand
169
+ class EmptyCommand < CommandMapper::Command
170
+ end
171
+ end
172
+
173
+ let(:command_class) { TestCommand::EmptyCommand }
174
+
175
+ it "must raise an ArgumentError" do
176
+ expect {
177
+ subject.option '-o'
178
+ }.to raise_error(ArgumentError,"cannot infer a name from short option flag: \"-o\"")
179
+ end
180
+ end
181
+
182
+ context "but it's length is >= 3" do
183
+ module TestCommand
184
+ class ShortOptionWithoutName < CommandMapper::Command
185
+ option "-ip"
186
+ end
187
+ end
188
+
189
+ let(:command_class) { TestCommand::ShortOptionWithoutName }
190
+
191
+ it "must register an option based on the short flag" do
192
+ expect(subject.options[:ip]).to be_kind_of(Option)
193
+ expect(subject.options[:ip].name).to eq(:ip)
194
+ expect(subject.options[:ip].flag).to eq("-ip")
195
+ end
196
+
197
+ it "must define a reader method for the option" do
198
+ expect(subject.instance_methods(false)).to include(:ip)
199
+ end
200
+
201
+ it "must define a writter method for the option" do
202
+ expect(subject.instance_methods(false)).to include(:ip=)
203
+ end
204
+ end
205
+ end
206
+
207
+ context "when the argument shares the same name as an internal method" do
208
+ let(:command_class) { Class.new(described_class) }
209
+ let(:flag) { "--flag" }
210
+ let(:name) { :command_argv }
211
+
212
+ it do
213
+ expect {
214
+ command_class.option flag, name: name
215
+ }.to raise_error(ArgumentError,"option #{flag.inspect} with name #{name.inspect} cannot override the internal method with same name: ##{name}")
216
+ end
217
+ end
218
+
219
+ context "when the argument flag maps to an existing internal method" do
220
+ let(:command_class) { Class.new(described_class) }
221
+ let(:flag) { "--command-argv" }
222
+ let(:name) { :command_argv }
223
+
224
+ it do
225
+ expect {
226
+ command_class.option(flag)
227
+ }.to raise_error(ArgumentError,"option #{flag.inspect} maps to method name ##{name} and cannot override the internal method with same name: ##{name}")
228
+ end
229
+ end
230
+ end
231
+
232
+ describe ".arguments" do
233
+ subject { command_class }
234
+
235
+ context "when the command has no defined arguments" do
236
+ let(:command_class) { TestCommand::EmptyCommand }
237
+
238
+ it { expect(subject.arguments).to be_empty }
239
+ end
240
+
241
+ context "when the comand does have defined arguments" do
242
+ module TestCommand
243
+ class BaseClassWithOptions < CommandMapper::Command
244
+ argument :foo
245
+ argument :bar
246
+ end
247
+
248
+ class InheritedOptions < BaseClassWithOptions
249
+ end
250
+ end
251
+
252
+ let(:command_class) { TestCommand::InheritedOptions }
253
+ let(:command_superclass) { TestCommand::BaseClassWithOptions }
254
+
255
+ it "must copy the arguments defined in the superclass" do
256
+ expect(subject.arguments).to eq(command_superclass.arguments)
257
+ end
258
+
259
+ context "and when the class defines arguments of it's own" do
260
+ module TestCommand
261
+ class InheritsAndDefinesOptions < BaseClassWithOptions
262
+ argument :baz
263
+ end
264
+ end
265
+
266
+ let(:command_class) { TestCommand::InheritsAndDefinesOptions }
267
+
268
+ it "must copy the arguments defined in the superclass" do
269
+ expect(subject.arguments).to include(command_superclass.arguments)
270
+ end
271
+
272
+ it "must define it's own arguments" do
273
+ expect(command_class.arguments[:baz]).to be_kind_of(Argument)
274
+ end
275
+
276
+ it "must not modify the superclass's arguments" do
277
+ expect(command_superclass.arguments[:baz]).to be(nil)
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ describe ".argument" do
284
+ module TestCommand
285
+ class DefinesArgument < CommandMapper::Command
286
+ command 'test' do
287
+ argument :foo
288
+ end
289
+ end
290
+ end
291
+
292
+ let(:command_class) { TestCommand::DefinesArgument }
293
+
294
+ subject { command_class }
295
+
296
+ it "must register an argument with the given name" do
297
+ expect(subject.arguments[:foo]).to be_kind_of(Argument)
298
+ expect(subject.arguments[:foo].name).to eq(:foo)
299
+ end
300
+
301
+ it "must define a reader method for the argument" do
302
+ expect(subject.instance_methods(false)).to include(:foo)
303
+ end
304
+
305
+ it "must define a writter method for the argument" do
306
+ expect(subject.instance_methods(false)).to include(:foo=)
307
+ end
308
+
309
+ describe "reader method" do
310
+ subject { command_class.new }
311
+
312
+ let(:value) { "test_reading" }
313
+
314
+ before do
315
+ subject.instance_variable_set('@foo',value)
316
+ end
317
+
318
+ it "must read the options value from @arguments" do
319
+ expect(subject.foo).to be(value)
320
+ end
321
+ end
322
+
323
+ describe "writter method" do
324
+ subject { command_class.new }
325
+
326
+ let(:value) { "test_writing" }
327
+
328
+ before { subject.foo = value }
329
+
330
+ it "must read the options value from @arguments" do
331
+ expect(subject.instance_variable_get('@foo')).to be(value)
332
+ end
333
+ end
334
+
335
+ context "when the argument shares the same name as an internal method" do
336
+ let(:command_class) { Class.new(described_class) }
337
+ let(:name) { :command_argv }
338
+
339
+ it do
340
+ expect {
341
+ command_class.argument(name)
342
+ }.to raise_error(ArgumentError,"argument #{name.inspect} cannot override internal method with same name: ##{name}")
343
+ end
344
+ end
345
+ end
346
+
347
+ describe ".subcommands" do
348
+ subject { command_class }
349
+
350
+ context "when the command has no defined subcommands" do
351
+ let(:command_class) { TestCommand::EmptyCommand }
352
+
353
+ it { expect(subject.subcommands).to be_empty }
354
+ end
355
+
356
+ context "when the comand does have defined subcommands" do
357
+ module TestCommand
358
+ class BaseClassWithOptions < CommandMapper::Command
359
+ subcommand :foo do
360
+ end
361
+
362
+ subcommand :bar do
363
+ end
364
+ end
365
+
366
+ class InheritedOptions < BaseClassWithOptions
367
+ end
368
+ end
369
+
370
+ let(:command_class) { TestCommand::InheritedOptions }
371
+ let(:command_superclass) { TestCommand::BaseClassWithOptions }
372
+
373
+ it "must copy the subcommands defined in the superclass" do
374
+ expect(subject.subcommands).to eq(command_superclass.subcommands)
375
+ end
376
+
377
+ context "and when the class defines subcommands of it's own" do
378
+ module TestCommand
379
+ class InheritsAndDefinesOptions < BaseClassWithOptions
380
+
381
+ subcommand :baz do
382
+ end
383
+
384
+ end
385
+ end
386
+
387
+ let(:command_class) { TestCommand::InheritsAndDefinesOptions }
388
+
389
+ it "must copy the subcommands defined in the superclass" do
390
+ expect(subject.subcommands).to include(command_superclass.subcommands)
391
+ end
392
+
393
+ it "must define it's own subcommands" do
394
+ expect(command_class.subcommands[:baz]).to eq(command_class::Baz)
395
+ end
396
+
397
+ it "must not modify the superclass's subcommands" do
398
+ expect(command_superclass.subcommands[:baz]).to be(nil)
399
+ end
400
+ end
401
+ end
402
+ end
403
+
404
+ describe ".subcommand" do
405
+ module TestCommand
406
+ class DefinesSubcommand < CommandMapper::Command
407
+ command 'cmd' do
408
+ subcommand "subcmd" do
409
+ option '--foo'
410
+ option '--bar'
411
+ argument :baz
412
+ end
413
+ end
414
+ end
415
+ end
416
+
417
+ let(:command_class) { TestCommand::DefinesSubcommand }
418
+
419
+ subject { command_class }
420
+
421
+ it "must add the subcommand to .subcommands using the method name" do
422
+ expect(subject.subcommands[:subcmd]).to be(command_class::Subcmd)
423
+ end
424
+
425
+ it "must define a constant for the new Subcommand class" do
426
+ expect(subject.const_get('Subcmd')).to (be < described_class)
427
+ end
428
+
429
+ it "must initialize a new Command with the given subcommand name" do
430
+ expect(subject.const_get('Subcmd').command_name).to eq("subcmd")
431
+ end
432
+
433
+ it "must define a reader method for the subcommand" do
434
+ expect(subject.instance_methods(false)).to include(:subcmd)
435
+ end
436
+
437
+ it "must define a writter method for the subcommand" do
438
+ expect(subject.instance_methods(false)).to include(:subcmd=)
439
+ end
440
+
441
+ context "when the reader method is called" do
442
+ let(:foo) { 'value1' }
443
+ let(:bar) { 'value2' }
444
+ let(:baz) { 'value3' }
445
+
446
+ subject { command_class.new }
447
+
448
+ context "when no subcommand data has been populated" do
449
+ it "must return nil" do
450
+ expect(subject.subcmd).to be(nil)
451
+ end
452
+ end
453
+
454
+ context "when the subcommand has been set" do
455
+ before do
456
+ subject.subcmd = {foo: foo, bar: bar, baz: baz}
457
+ end
458
+
459
+ it "must return an instance of the defined subcommand class" do
460
+ expect(subject.subcmd).to be_kind_of(command_class::Subcmd)
461
+ end
462
+ end
463
+
464
+ context "with a block" do
465
+ before do
466
+ subject.subcmd do |sub|
467
+ sub.foo = foo
468
+ sub.bar = bar
469
+ sub.baz = baz
470
+ end
471
+ end
472
+
473
+ it "must use the block to populate the new subcommand instance" do
474
+ expect(subject.subcmd.foo).to eq(foo)
475
+ expect(subject.subcmd.bar).to eq(bar)
476
+ expect(subject.subcmd.baz).to eq(baz)
477
+ end
478
+ end
479
+ end
480
+
481
+ context "when the writter method is called" do
482
+ let(:foo) { 'value1' }
483
+ let(:bar) { 'value2' }
484
+ let(:baz) { 'value3' }
485
+
486
+ context "with a Hash" do
487
+ subject { command_class.new }
488
+
489
+ before do
490
+ subject.subcmd = {foo: foo, bar: bar, baz: baz}
491
+ end
492
+
493
+ it "must set @command_subcommand to an instance of the defined subcommand class " do
494
+ expect(subject.instance_variable_get('@command_subcommand')).to be_kind_of(command_class::Subcmd)
495
+ end
496
+
497
+ it "must set the attributes of the subcommand" do
498
+ expect(subject.subcmd.foo).to eq(foo)
499
+ expect(subject.subcmd.bar).to eq(bar)
500
+ expect(subject.subcmd.baz).to eq(baz)
501
+ end
502
+ end
503
+
504
+ context "when nil" do
505
+ subject { command_class.new }
506
+
507
+ before do
508
+ subject.subcmd = {foo: foo, bar: bar, baz: baz}
509
+ subject.subcmd = nil
510
+ end
511
+
512
+ it "must set @commnad_subcommand to nil" do
513
+ expect(subject.instance_variable_get('@command_subcommand')).to be(nil)
514
+ end
515
+ end
516
+ end
517
+
518
+ context "when the subcommand name contains a '-'" do
519
+ module TestCommand
520
+ class DefinesSubcommand < CommandMapper::Command
521
+ subcommand 'sub-cmd' do
522
+ option '--foo'
523
+ option '--bar'
524
+ argument :baz
525
+ end
526
+ end
527
+ end
528
+
529
+ let(:command_class) { TestCommand::DefinesSubcommand }
530
+
531
+ it "must add the subcommand to .subcommands using the method name" do
532
+ expect(subject.subcommands[:sub_cmd]).to be(command_class::SubCmd)
533
+ end
534
+
535
+ it "must define a CamelCased subcommand constant" do
536
+ expect(subject.const_get('SubCmd')).to (be < described_class)
537
+ end
538
+
539
+ it "must replace any '-' characters with '_' for the reader method" do
540
+ expect(subject.instance_methods(false)).to include(:sub_cmd)
541
+ end
542
+
543
+ it "must replace any '-' characters with '_' for the writer method" do
544
+ expect(subject.instance_methods(false)).to include(:sub_cmd=)
545
+ end
546
+ end
547
+
548
+ context "when the subcommand shares the same name as an internal method" do
549
+ let(:command_class) { Class.new(described_class) }
550
+ let(:name) { "command-argv" }
551
+ let(:method_name) { 'command_argv' }
552
+
553
+ it do
554
+ expect {
555
+ command_class.subcommand(name) do
556
+ end
557
+ }.to raise_error(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
558
+ end
559
+ end
560
+ end
561
+
562
+ module TestCommand
563
+ class ExampleCommand < CommandMapper::Command
564
+ command 'test' do
565
+ option '--opt1', value: {required: true}
566
+ option '--opt2', value: {required: true}
567
+ option '--opt3', value: {required: true}
568
+
569
+ argument :arg1, required: false
570
+ argument :arg2, required: false
571
+ argument :arg3, required: false
572
+
573
+ subcommand 'subcmd' do
574
+ option '--sub-opt1', value: {required: true}
575
+
576
+ argument :sub_arg1, required: true
577
+ end
578
+ end
579
+ end
580
+ end
581
+
582
+ let(:opt1) { "foo" }
583
+ let(:opt2) { "bar" }
584
+ let(:opt3) { "baz" }
585
+ let(:arg1) { "foo" }
586
+ let(:arg2) { "bar" }
587
+ let(:arg3) { "baz" }
588
+ let(:env) { {'FOO' => 'bar'} }
589
+
590
+ let(:command_class) { TestCommand::ExampleCommand }
591
+
592
+ describe ".run" do
593
+ let(:command_instance) { double(:command_instance) }
594
+ let(:return_value) { double(:boolean) }
595
+
596
+ subject { command_class }
597
+
598
+ context "when called with a Hash of params" do
599
+ let(:params) do
600
+ {opt1: opt1, arg1: arg1}
601
+ end
602
+
603
+ it "must initialize a new command with the Hash of params and call #run" do
604
+ if RUBY_VERSION < '3.'
605
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
606
+ else
607
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
608
+ end
609
+
610
+ expect(command_instance).to receive(:run_command).and_return(return_value)
611
+
612
+ expect(subject.run(params)).to be(return_value)
613
+ end
614
+ end
615
+
616
+ context "when called with keyword aguments" do
617
+ let(:kwargs) do
618
+ {opt1: opt1, arg1: arg1}
619
+ end
620
+
621
+ it "must initialize a new command with the keyword arguments and call #run" do
622
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
623
+ expect(command_instance).to receive(:run_command).and_return(return_value)
624
+
625
+ expect(subject.run(**kwargs)).to be(return_value)
626
+ end
627
+ end
628
+ end
629
+
630
+ describe ".capture" do
631
+ let(:command_instance) { double(:command_instance) }
632
+ let(:return_value) { double(:string) }
633
+
634
+ subject { command_class }
635
+
636
+ context "when called with a Hash of params" do
637
+ let(:params) do
638
+ {opt1: opt1, arg1: arg1}
639
+ end
640
+
641
+ it "must initialize a new command with the Hash of params and call #capture" do
642
+ if RUBY_VERSION < '3.'
643
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
644
+ else
645
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
646
+ end
647
+
648
+ expect(command_instance).to receive(:capture_command).and_return(return_value)
649
+
650
+ expect(subject.capture(params)).to be(return_value)
651
+ end
652
+ end
653
+
654
+ context "when called with keyword aguments" do
655
+ let(:kwargs) do
656
+ {opt1: opt1, arg1: arg1}
657
+ end
658
+
659
+ it "must initialize a new command with the keyword arguments and call #capture" do
660
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
661
+ expect(command_instance).to receive(:capture_command).and_return(return_value)
662
+
663
+ expect(subject.capture(**kwargs)).to be(return_value)
664
+ end
665
+ end
666
+ end
667
+
668
+ describe ".popen" do
669
+ let(:command_instance) { double(:command_instance) }
670
+ let(:return_value) { double(:io) }
671
+
672
+ subject { command_class }
673
+
674
+ context "when called with a Hash of params" do
675
+ let(:params) do
676
+ {opt1: opt1, arg1: arg1}
677
+ end
678
+
679
+ it "must initialize a new command with the Hash of params and call #popen" do
680
+ if RUBY_VERSION < '3.'
681
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
682
+ else
683
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
684
+ end
685
+
686
+ expect(command_instance).to receive(:popen_command).and_return(return_value)
687
+
688
+ expect(subject.popen(params)).to be(return_value)
689
+ end
690
+ end
691
+
692
+ context "when called with keyword aguments" do
693
+ let(:kwargs) do
694
+ {opt1: opt1, arg1: arg1}
695
+ end
696
+
697
+ it "must initialize a new command with the keyword arguments and call #popen" do
698
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
699
+ expect(command_instance).to receive(:popen_command).and_return(return_value)
700
+
701
+ expect(subject.popen(**kwargs)).to be(return_value)
702
+ end
703
+ end
704
+ end
705
+
706
+ describe ".sudo" do
707
+ let(:command_instance) { double(:command_instance) }
708
+ let(:return_value) { double(:boolean) }
709
+
710
+ subject { command_class }
711
+
712
+ context "when called with a Hash of params" do
713
+ let(:params) do
714
+ {opt1: opt1, arg1: arg1}
715
+ end
716
+
717
+ it "must initialize a new command with the Hash of params and call #sudo" do
718
+ if RUBY_VERSION < '3.'
719
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
720
+ else
721
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
722
+ end
723
+
724
+ expect(command_instance).to receive(:sudo_command).and_return(return_value)
725
+
726
+ expect(subject.sudo(params)).to be(return_value)
727
+ end
728
+ end
729
+
730
+ context "when called with keyword aguments" do
731
+ let(:kwargs) do
732
+ {opt1: opt1, arg1: arg1}
733
+ end
734
+
735
+ it "must initialize a new command with the keyword arguments and call #sudo" do
736
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
737
+ expect(command_instance).to receive(:sudo_command).and_return(return_value)
738
+
739
+ expect(subject.sudo(**kwargs)).to be(return_value)
740
+ end
741
+ end
742
+ end
743
+
744
+ describe "#initialize" do
745
+ subject { command_class.new() }
746
+
747
+ it "must default #command_name to self.class.command" do
748
+ expect(subject.command_name).to eq(command_class.command_name)
749
+ end
750
+
751
+ it "must default #command_env to {}" do
752
+ expect(subject.command_env).to eq({})
753
+ end
754
+
755
+ it "must default #command_subcommand to nil" do
756
+ expect(subject.command_subcommand).to be(nil)
757
+ end
758
+
759
+ it "must default option values to nil" do
760
+ expect(subject.opt1).to be(nil)
761
+ expect(subject.opt2).to be(nil)
762
+ expect(subject.opt3).to be(nil)
763
+ end
764
+
765
+ it "must default argument values to nil" do
766
+ expect(subject.arg1).to be(nil)
767
+ expect(subject.arg2).to be(nil)
768
+ expect(subject.arg3).to be(nil)
769
+ end
770
+
771
+ context "when initialized with a Hash of options and arguments" do
772
+ let(:params) do
773
+ {opt1: opt1, opt2: opt2, opt3: opt3, arg1: arg1, arg2: arg2, arg3: arg3}
774
+ end
775
+
776
+ subject { command_class.new(params) }
777
+
778
+ it "must set option values" do
779
+ expect(subject.opt1).to be(opt1)
780
+ expect(subject.opt2).to be(opt2)
781
+ expect(subject.opt3).to be(opt3)
782
+ end
783
+
784
+ it "must set argument values" do
785
+ expect(subject.arg1).to be(arg1)
786
+ expect(subject.arg2).to be(arg2)
787
+ expect(subject.arg3).to be(arg3)
788
+ end
789
+ end
790
+
791
+ context "when initialized with additional keywords" do
792
+ let(:keywords) do
793
+ {opt1: opt1, opt2: opt2, opt3: opt3, arg1: arg1, arg2: arg2, arg3: arg3}
794
+ end
795
+
796
+ subject { command_class.new(**keywords) }
797
+
798
+ it "must set option values" do
799
+ expect(subject.opt1).to be(opt1)
800
+ expect(subject.opt2).to be(opt2)
801
+ expect(subject.opt3).to be(opt3)
802
+ end
803
+
804
+ it "must set argument values" do
805
+ expect(subject.arg1).to be(arg1)
806
+ expect(subject.arg2).to be(arg2)
807
+ expect(subject.arg3).to be(arg3)
808
+ end
809
+ end
810
+
811
+ context "when initialized with command_name: ..." do
812
+ let(:command_name) { 'foo2' }
813
+
814
+ subject { command_class.new(command_name: command_name) }
815
+
816
+ it "must set #command_name" do
817
+ expect(subject.command_name).to eq(command_name)
818
+ end
819
+ end
820
+
821
+ context "when initialized with command_path: ..." do
822
+ let(:command_path) { '/path/to/foo' }
823
+
824
+ subject { command_class.new(command_path: command_path) }
825
+
826
+ it "must set #command_path" do
827
+ expect(subject.command_path).to eq(command_path)
828
+ end
829
+ end
830
+
831
+ context "when initialized with command_env: {...}" do
832
+ subject { command_class.new(command_env: env) }
833
+
834
+ it "must populate #command_env" do
835
+ expect(subject.command_env).to eq(env)
836
+ end
837
+ end
838
+ end
839
+
840
+ describe "#[]" do
841
+ let(:name) { :opt1 }
842
+ let(:value) { 'test' }
843
+
844
+ subject { command_class.new(opt1: value) }
845
+
846
+ it "must call the method with the same given name" do
847
+ expect(subject).to receive(name).and_return(value)
848
+
849
+ expect(subject[name]).to be(value)
850
+ end
851
+
852
+ context "when there is no reader method of the same name" do
853
+ let(:name) { :fubar }
854
+
855
+ it do
856
+ expect {
857
+ subject[name]
858
+ }.to raise_error(ArgumentError,"#{command_class} does not define ##{name}")
859
+ end
860
+ end
861
+ end
862
+
863
+ describe "#[]=" do
864
+ let(:name) { :opt2 }
865
+ let(:value) { 'new_value' }
866
+
867
+ subject { command_class.new }
868
+
869
+ it "must call the writter method with the same given name" do
870
+ expect(subject).to receive(:"#{name}=").with(value).and_return(value)
871
+
872
+ subject[name] = value
873
+ end
874
+
875
+ it "must return the new value" do
876
+ expect(subject[name] = value).to be(value)
877
+ end
878
+
879
+ context "when there is no reader method of the same name" do
880
+ let(:name) { :fubar }
881
+
882
+ it do
883
+ expect {
884
+ subject[name] = value
885
+ }.to raise_error(ArgumentError,"#{command_class} does not define ##{name}=")
886
+ end
887
+ end
888
+ end
889
+
890
+ describe "#command_argv" do
891
+ context "when the command has no options or arguments set" do
892
+ subject { command_class.new }
893
+
894
+ it "must return an argv only containing the command name" do
895
+ expect(subject.command_argv).to eq([subject.class.command_name])
896
+ end
897
+
898
+ context "but the command has required arguments" do
899
+ module TestCommand
900
+ class CommandWithRequiredArguments < CommandMapper::Command
901
+ command "test" do
902
+ option '--opt1', value: {required: true}
903
+ option '--opt2', value: {required: true}
904
+ option '--opt3', value: {required: true}
905
+
906
+ argument :arg1, required: false
907
+ argument :arg2, required: true
908
+ argument :arg3, required: false
909
+ end
910
+ end
911
+ end
912
+
913
+ let(:command_class) { TestCommand::CommandWithRequiredArguments }
914
+
915
+ it do
916
+ expect {
917
+ subject.command_argv
918
+ }.to raise_error(ArgumentRequired,"argument arg2 is required")
919
+ end
920
+ end
921
+ end
922
+
923
+ context "when the command is initialized with the command_path: keyword" do
924
+ let(:command_path) { '/path/to/foo' }
925
+
926
+ subject { command_class.new(command_path: command_path) }
927
+
928
+ it "must override the command name" do
929
+ expect(subject.command_argv).to eq([subject.command_path])
930
+ end
931
+ end
932
+
933
+ context "when the command has options set" do
934
+ subject { command_class.new({opt1: opt1, opt2: opt2, opt3: opt3}) }
935
+
936
+ it "must return an argv containing the command name and option flags followed by values" do
937
+ expect(subject.command_argv).to eq(
938
+ [
939
+ subject.class.command_name,
940
+ '--opt1', opt1,
941
+ '--opt2', opt2,
942
+ '--opt3', opt3
943
+ ]
944
+ )
945
+ end
946
+ end
947
+
948
+ context "when the command has arguments set" do
949
+ subject { command_class.new({arg1: arg1, arg2: arg2, arg3: arg3}) }
950
+
951
+ it "must return an argv containing the command name and argument values" do
952
+ expect(subject.command_argv).to eq(
953
+ [subject.command_name, arg1, arg2, arg3]
954
+ )
955
+ end
956
+
957
+ context "when the arguments are initialized in a different order" do
958
+ subject { command_class.new({arg2: arg2, arg1: arg1, arg3: arg3}) }
959
+
960
+ it "must return the argument values in the order the arguments were defined" do
961
+ expect(subject.command_argv).to eq(
962
+ [subject.command_name, arg1, arg2, arg3]
963
+ )
964
+ end
965
+ end
966
+
967
+ context "and when one of the argument values starts with a '-'" do
968
+ let(:arg2) { "--bar" }
969
+
970
+ it "must separate the arguments with a '--'" do
971
+ expect(subject.command_argv).to eq(
972
+ [subject.command_name, "--", arg1, arg2, arg3]
973
+ )
974
+ end
975
+ end
976
+ end
977
+
978
+ context "when the command has both options and arguments set" do
979
+ subject do
980
+ command_class.new(
981
+ {
982
+ opt1: opt1, opt2: opt2, opt3: opt3,
983
+ arg1: arg1, arg2: arg2, arg3: arg3
984
+ }
985
+ )
986
+ end
987
+
988
+ it "must return an argv containing the command name, options flags and values, then argument values" do
989
+ expect(subject.command_argv).to eq(
990
+ [
991
+ subject.command_name,
992
+ '--opt1', opt1,
993
+ '--opt2', opt2,
994
+ '--opt3', opt3,
995
+ arg1, arg2, arg3
996
+ ]
997
+ )
998
+ end
999
+ end
1000
+
1001
+ context "when the command has a subcommand set" do
1002
+ let(:sub_opt1) { 'foo' }
1003
+ let(:sub_arg1) { 'bar' }
1004
+
1005
+ subject do
1006
+ command_class.new(
1007
+ {
1008
+ subcmd: {sub_opt1: sub_opt1, sub_arg1: sub_arg1}
1009
+ }
1010
+ )
1011
+ end
1012
+
1013
+ it "must return an argv containing the command name, sub-command name, subcommand options and arguments" do
1014
+ expect(subject.command_argv).to eq(
1015
+ [
1016
+ subject.command_name,
1017
+ 'subcmd', '--sub-opt1', sub_opt1, sub_arg1
1018
+ ]
1019
+ )
1020
+ end
1021
+
1022
+ context "and when the command also has options set" do
1023
+ subject do
1024
+ command_class.new(
1025
+ {
1026
+ opt1: opt1, opt2: opt2, opt3: opt3,
1027
+ subcmd: {sub_opt1: sub_opt1, sub_arg1: sub_arg1}
1028
+ }
1029
+ )
1030
+ end
1031
+
1032
+ it "must return an argv containing the command name, global options, sub-command name, subcommand options and arguments" do
1033
+ expect(subject.command_argv).to eq(
1034
+ [
1035
+ subject.command_name,
1036
+ '--opt1', opt1,
1037
+ '--opt2', opt2,
1038
+ '--opt3', opt3,
1039
+ 'subcmd', '--sub-opt1', sub_opt1, sub_arg1
1040
+ ]
1041
+ )
1042
+ end
1043
+ end
1044
+
1045
+ context "and when the command also has arguments set" do
1046
+ subject do
1047
+ command_class.new(
1048
+ {
1049
+ opt1: opt1, opt2: opt2, opt3: opt3,
1050
+ arg1: arg1, arg2: arg2, arg3: arg3,
1051
+ subcmd: {sub_opt1: sub_opt1, sub_arg1: sub_arg1}
1052
+ }
1053
+ )
1054
+ end
1055
+
1056
+ it "must return an argv containing the sub-command's options and arguments, instead of the command's arguments" do
1057
+ expect(subject.command_argv).to eq(
1058
+ [
1059
+ subject.command_name,
1060
+ '--opt1', opt1,
1061
+ '--opt2', opt2,
1062
+ '--opt3', opt3,
1063
+ 'subcmd', '--sub-opt1', sub_opt1, sub_arg1
1064
+ ]
1065
+ )
1066
+ end
1067
+ end
1068
+ end
1069
+ end
1070
+
1071
+ describe "#command_string" do
1072
+ let(:opt1) { "foo bar" }
1073
+ let(:arg1) { "baz qux" }
1074
+
1075
+ subject { command_class.new({opt1: opt1, arg1: arg1}) }
1076
+
1077
+ let(:escaped_command) { Shellwords.shelljoin(subject.command_argv) }
1078
+
1079
+ it "must escape the command option values and argument values" do
1080
+ expect(subject.command_string).to eq(escaped_command)
1081
+ end
1082
+
1083
+ context "when initialized with command_env: {...}" do
1084
+ let(:env) { {"FOO" => "bar baz"} }
1085
+
1086
+ let(:escaped_env) do
1087
+ env.map { |name,value|
1088
+ "#{Shellwords.shellescape(name)}=#{Shellwords.shellescape(value)}"
1089
+ }.join(' ')
1090
+ end
1091
+
1092
+ let(:escaped_command) { Shellwords.shelljoin(subject.command_argv) }
1093
+
1094
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1095
+
1096
+ it "must escape both the env variables and the command" do
1097
+ expect(subject.command_string).to eq(
1098
+ "#{escaped_env} #{escaped_command}"
1099
+ )
1100
+ end
1101
+ end
1102
+ end
1103
+
1104
+ describe "#run_command" do
1105
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1106
+
1107
+ it "must pass the command's env and argv to Kenrel.system" do
1108
+ expect(subject).to receive(:system).with(env,*subject.command_argv)
1109
+
1110
+ subject.run_command
1111
+ end
1112
+ end
1113
+
1114
+ describe "#capture_command" do
1115
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1116
+
1117
+ it "must pass the command's env and argv to `...`" do
1118
+ expect(subject).to receive(:`).with(subject.command_string)
1119
+
1120
+ subject.capture_command
1121
+ end
1122
+ end
1123
+
1124
+ describe "#popen_command" do
1125
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1126
+
1127
+ it "must pass the command's env, argv, and to IO.popen" do
1128
+ expect(IO).to receive(:popen).with(env,subject.command_argv)
1129
+
1130
+ subject.popen_command
1131
+ end
1132
+
1133
+ context "when a open mode is given" do
1134
+ let(:mode) { 'w' }
1135
+
1136
+ it "must pass the command's env, argv, and the mode to IO.popen" do
1137
+ expect(IO).to receive(:popen).with(env,subject.command_argv,mode)
1138
+
1139
+ subject.popen_command(mode)
1140
+ end
1141
+ end
1142
+ end
1143
+
1144
+ describe "#sudo!" do
1145
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1146
+
1147
+ let(:expected_argv) { [command_class.command, "--opt1", opt1, arg1] }
1148
+
1149
+ it "must pass the command's env and argv, and to IO.popen" do
1150
+ expect(Sudo).to receive(:run).with({command: subject.command_argv}, command_env: env)
1151
+
1152
+ subject.sudo_command
1153
+ end
1154
+ end
1155
+
1156
+ describe "#to_s" do
1157
+ let(:opt1) { "foo bar" }
1158
+ let(:arg1) { "baz qux" }
1159
+ let(:env) { {"FOO" => "bar baz"} }
1160
+
1161
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1162
+
1163
+ it "must call #command_string" do
1164
+ expect(subject.to_s).to eq(subject.command_string)
1165
+ end
1166
+ end
1167
+
1168
+ describe "#to_a" do
1169
+ subject { command_class.new({opt1: opt1, arg1: arg1}) }
1170
+
1171
+ it "must call #command_argv" do
1172
+ expect(subject.to_a).to eq(subject.command_argv)
1173
+ end
1174
+ end
1175
+ end