command_kit 0.2.0 → 0.2.1

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.
data/spec/command_spec.rb CHANGED
@@ -26,14 +26,14 @@ describe CommandKit::Command do
26
26
  expect(described_class).to include(CommandKit::Usage)
27
27
  end
28
28
 
29
- it "must include CommandKit::Arguments" do
30
- expect(described_class).to include(CommandKit::Arguments)
31
- end
32
-
33
29
  it "must include CommandKit::Options" do
34
30
  expect(described_class).to include(CommandKit::Options)
35
31
  end
36
32
 
33
+ it "must include CommandKit::Arguments" do
34
+ expect(described_class).to include(CommandKit::Arguments)
35
+ end
36
+
37
37
  it "must include CommandKit::Examples" do
38
38
  expect(described_class).to include(CommandKit::Examples)
39
39
  end
@@ -49,4 +49,75 @@ describe CommandKit::Command do
49
49
  it "must include FileUtils" do
50
50
  expect(described_class).to include(FileUtils)
51
51
  end
52
+
53
+ module TestCommandClass
54
+ class TestCommand < CommandKit::Command
55
+
56
+ usage '[OPTIONS] ARG1 [ARG2]'
57
+
58
+ option :option1, short: '-a',
59
+ value: {
60
+ type: Integer,
61
+ default: 1
62
+ },
63
+ desc: "Option 1"
64
+
65
+ option :option2, short: '-b',
66
+ value: {
67
+ type: String,
68
+ usage: 'FILE'
69
+ },
70
+ desc: "Option 2"
71
+
72
+ argument :argument1, required: true,
73
+ usage: 'ARG1',
74
+ desc: "Argument 1"
75
+
76
+ argument :argument2, required: false,
77
+ usage: 'ARG2',
78
+ desc: "Argument 2"
79
+
80
+ examples [
81
+ '-a 42 foo/bar/baz',
82
+ '-a 42 -b bar.txt baz qux'
83
+ ]
84
+
85
+ description 'Example command'
86
+
87
+ end
88
+ end
89
+
90
+ let(:command_class) { TestCommandClass::TestCommand }
91
+ subject { command_class.new }
92
+
93
+ describe "#help" do
94
+ let(:option1) { command_class.options[:option1] }
95
+ let(:option2) { command_class.options[:option2] }
96
+ let(:argument1) { command_class.arguments[:argument1] }
97
+ let(:argument2) { command_class.arguments[:argument2] }
98
+
99
+ it "must print the usage, options, arguments, examples, and description" do
100
+ expect { subject.help }.to output(
101
+ [
102
+ "Usage: #{subject.usage}",
103
+ '',
104
+ 'Options:',
105
+ " #{option1.usage.join(', ').ljust(33 - 1)} #{option1.desc}",
106
+ " #{option2.usage.join(', ').ljust(33 - 1)} #{option2.desc}",
107
+ ' -h, --help Print help information',
108
+ '',
109
+ "Arguments:",
110
+ " #{argument1.usage.ljust(33)}#{argument1.desc}",
111
+ " #{argument2.usage.ljust(33)}#{argument2.desc}",
112
+ '',
113
+ "Examples:",
114
+ " #{subject.command_name} #{command_class.examples[0]}",
115
+ " #{subject.command_name} #{command_class.examples[1]}",
116
+ '',
117
+ command_class.description,
118
+ ''
119
+ ].join($/)
120
+ ).to_stdout
121
+ end
122
+ end
52
123
  end
@@ -54,16 +54,27 @@ describe CommandKit::Commands::AutoLoad do
54
54
  end
55
55
 
56
56
  module TestAutoLoad
57
- class CLI
57
+ class TestCommand
58
58
  include CommandKit::Commands
59
59
  include CommandKit::Commands::AutoLoad.new(
60
60
  dir: File.expand_path('../fixtures/test_auto_load/cli/commands',__FILE__),
61
61
  namespace: "#{self}::Commands"
62
62
  )
63
63
  end
64
+
65
+ class TestCommandWithBlock
66
+ include CommandKit::Commands
67
+ include(CommandKit::Commands::AutoLoad.new(
68
+ dir: File.expand_path('../fixtures/test_auto_load/cli/commands',__FILE__),
69
+ namespace: "#{self}::Commands"
70
+ ) { |autoload|
71
+ autoload.command 'test-1', 'Test1', 'test1.rb', aliases: %w[test_1]
72
+ autoload.command 'test-2', 'Test2', 'test2.rb', aliases: %w[test_2]
73
+ })
74
+ end
64
75
  end
65
76
 
66
- let(:command_class) { TestAutoLoad::CLI }
77
+ let(:command_class) { TestAutoLoad::TestCommand }
67
78
  let(:autoload_module) { command_class.included_modules.first }
68
79
 
69
80
  describe "#included" do
@@ -76,6 +87,26 @@ describe CommandKit::Commands::AutoLoad do
76
87
  it "must merge the module's #commands into the class'es #commands" do
77
88
  expect(command_class.commands).to include(autoload_module.commands)
78
89
  end
90
+
91
+ context "when CommandKit::Commands::AutoLoad has an explicit mapping" do
92
+ let(:command_class) { TestAutoLoad::TestCommandWithBlock }
93
+
94
+ it "must merge the module's #commands into the class'es #commands" do
95
+ expect(command_class.commands).to include(autoload_module.commands)
96
+ end
97
+
98
+ it "must also merge the any aliases into the class'es #command_aliases" do
99
+ expected_command_aliases = {}
100
+
101
+ autoload_module.commands.each do |name,subcommand|
102
+ subcommand.aliases.each do |alias_name|
103
+ expected_command_aliases[alias_name] = name
104
+ end
105
+ end
106
+
107
+ expect(command_class.command_aliases).to include(expected_command_aliases)
108
+ end
109
+ end
79
110
  end
80
111
 
81
112
  describe "#join" do
@@ -126,6 +126,33 @@ describe CommandKit::Commands do
126
126
  command Test2
127
127
 
128
128
  end
129
+
130
+ class TestCommandsWithCommandArguments
131
+
132
+ include CommandKit::Commands
133
+
134
+ class Test1 < CommandKit::Command
135
+ argument :arg1, required: false,
136
+ desc: 'Argument 1'
137
+
138
+ argument :arg2, required: false,
139
+ desc: 'Argument 2'
140
+ end
141
+
142
+ class Test2 < CommandKit::Command
143
+ argument :arg1, required: false,
144
+ desc: 'Argument 1'
145
+
146
+ argument :arg2, required: false,
147
+ desc: 'Argument 2'
148
+ end
149
+
150
+ p method(:command).source_location
151
+ command Test1
152
+ command Test2
153
+
154
+ end
155
+
129
156
  end
130
157
 
131
158
  let(:command_class) { TestCommands::TestCommands }
@@ -569,7 +596,7 @@ describe CommandKit::Commands do
569
596
  expect(subject).to receive(:exit).with(1)
570
597
 
571
598
  expect { subject.command_not_found(unknown_command) }.to output(
572
- "'#{unknown_command}' is not a #{subject.command_name} command. See `#{subject.command_name} help`" + $/
599
+ "#{subject.command_name}: '#{unknown_command}' is not a #{subject.command_name} command. See `#{subject.command_name} help`" + $/
573
600
  ).to_stderr
574
601
  end
575
602
  end
@@ -582,6 +609,44 @@ describe CommandKit::Commands do
582
609
  end
583
610
  end
584
611
 
612
+ describe "#option_parser" do
613
+ let(:command_name) { 'test1' }
614
+ let(:command_argv) { %w[foo bar baz] }
615
+ let(:argv) { [command_name, *command_argv] }
616
+
617
+ it "must stop before the first non-option argument" do
618
+ expect(subject.option_parser.parse(argv)).to eq(
619
+ [command_name, *command_argv]
620
+ )
621
+ end
622
+
623
+ context "when an unknown command name is given" do
624
+ let(:command_argv) { %w[bar baz] }
625
+ let(:argv) { ['foo', command_name, *command_argv] }
626
+
627
+ it "must stop before the first non-option argument" do
628
+ expect(subject.option_parser.parse(argv)).to eq(argv)
629
+ end
630
+ end
631
+
632
+ context "when additional global options are defined" do
633
+ let(:command_class) { TestCommands::TestCommandsWithGlobalOptions }
634
+ let(:bar) { '2' }
635
+ let(:argv) do
636
+ ['--foo', '--bar', bar.to_s, command_name, *command_argv]
637
+ end
638
+
639
+ it "must parse the global options, but stop before the first non-option associated argument" do
640
+ expect(subject.option_parser.parse(argv)).to eq(
641
+ [command_name, *command_argv]
642
+ )
643
+
644
+ expect(subject.options[:foo]).to be(true)
645
+ expect(subject.options[:bar]).to eq(bar)
646
+ end
647
+ end
648
+ end
649
+
585
650
  describe "#run" do
586
651
  context "when a command name is the first argument" do
587
652
  let(:command) { 'test1' }
@@ -607,48 +672,45 @@ describe CommandKit::Commands do
607
672
  end
608
673
 
609
674
  context "when given no arguments" do
610
- it "must default to calling #help" do
675
+ let(:exit_status) { 1 }
676
+
677
+ it "must default to calling #help and exit with 1" do
611
678
  expect(subject).to receive(:help)
679
+ expect(subject).to receive(:exit).with(exit_status)
612
680
 
613
681
  subject.run()
614
682
  end
615
683
  end
616
684
  end
617
685
 
618
- describe "#option_parser" do
619
- let(:command_name) { 'test1' }
620
- let(:command_argv) { %w[foo bar baz] }
621
- let(:argv) { [command_name, *command_argv] }
686
+ describe "#main" do
687
+ let(:command_class) { TestCommands::TestCommandsWithCommandArguments }
622
688
 
623
- it "must stop before the first non-option argument" do
624
- expect(subject.option_parser.parse(argv)).to eq(
625
- [command_name, *command_argv]
626
- )
689
+ context "when a command name is the first argument" do
690
+ let(:command) { 'test1' }
691
+ let(:argv) { [command] }
692
+
693
+ it "must return 0" do
694
+ expect(subject.main(argv)).to eq(0)
695
+ end
627
696
  end
628
697
 
629
- context "when an unknown command name is given" do
630
- let(:command_argv) { %w[bar baz] }
631
- let(:argv) { ['foo', command_name, *command_argv] }
698
+ context "when given additional command arguments" do
699
+ let(:argv) { %w[test1 arg1 arg2] }
632
700
 
633
- it "must stop before the first non-option argument" do
634
- expect(subject.option_parser.parse(argv)).to eq(argv)
701
+ it "must pass them to the command" do
702
+ expect(subject.main(argv)).to eq(0)
635
703
  end
636
704
  end
637
705
 
638
- context "when additional global options are defined" do
639
- let(:command_class) { TestCommands::TestCommandsWithGlobalOptions }
640
- let(:bar) { '2' }
641
- let(:argv) do
642
- ['--foo', '--bar', bar.to_s, command_name, *command_argv]
643
- end
706
+ context "when given no arguments" do
707
+ let(:exit_status) { 1 }
708
+ let(:argv) { %w[] }
644
709
 
645
- it "must parse the global options, but stop before the first non-option associated argument" do
646
- expect(subject.option_parser.parse(argv)).to eq(
647
- [command_name, *command_argv]
648
- )
710
+ it "must call #help and return 1" do
711
+ expect(subject).to receive(:help)
649
712
 
650
- expect(subject.options[:foo]).to be(true)
651
- expect(subject.options[:bar]).to eq(bar)
713
+ expect(subject.main(argv)).to eq(exit_status)
652
714
  end
653
715
  end
654
716
  end
@@ -711,6 +773,10 @@ describe CommandKit::Commands do
711
773
  "Options:",
712
774
  " -h, --help Print help information",
713
775
  "",
776
+ "Arguments:",
777
+ " [COMMAND] The command name to run",
778
+ " [ARGS ...] Additional arguments for the command",
779
+ "",
714
780
  "Commands:",
715
781
  " help",
716
782
  " test1",
@@ -731,6 +797,10 @@ describe CommandKit::Commands do
731
797
  "Options:",
732
798
  " -h, --help Print help information",
733
799
  "",
800
+ "Arguments:",
801
+ " [COMMAND] The command name to run",
802
+ " [ARGS ...] Additional arguments for the command",
803
+ "",
734
804
  "Commands:",
735
805
  " help",
736
806
  " test1, t1",
@@ -754,6 +824,10 @@ describe CommandKit::Commands do
754
824
  " -b, --bar BAR Global --bar option",
755
825
  " -h, --help Print help information",
756
826
  "",
827
+ "Arguments:",
828
+ " [COMMAND] The command name to run",
829
+ " [ARGS ...] Additional arguments for the command",
830
+ "",
757
831
  "Commands:",
758
832
  " help",
759
833
  " test1",
@@ -13,6 +13,12 @@ describe CommandKit::Env::Path do
13
13
  let(:command_class) { TestEnvPath::TestCommand }
14
14
  subject { command_class.new }
15
15
 
16
+ describe ".included" do
17
+ it "must include CommandKit::Env" do
18
+ expect(command_class).to be_include(CommandKit::Env)
19
+ end
20
+ end
21
+
16
22
  describe "#initialize" do
17
23
  it "must split ENV['PATH'] into an Array of directories" do
18
24
  expect(subject.path_dirs).to eq(ENV['PATH'].split(File::PATH_SEPARATOR))
@@ -16,7 +16,13 @@ describe CommandKit::Help::Man do
16
16
  man_page 'foo.1'
17
17
  end
18
18
 
19
- class EmptyCommand
19
+ class TestCommandWithRelativeManDir
20
+ include CommandKit::Help::Man
21
+
22
+ man_dir File.join(__dir__,'..','..','..','man')
23
+ end
24
+
25
+ class TestCommandWithManDirNotSet
20
26
  include CommandKit::Help::Man
21
27
  end
22
28
  end
@@ -41,7 +47,7 @@ describe CommandKit::Help::Man do
41
47
 
42
48
  describe ".man_dir" do
43
49
  context "when no man_dir have been set" do
44
- subject { TestHelpMan::EmptyCommand }
50
+ subject { TestHelpMan::TestCommandWithManDirNotSet }
45
51
 
46
52
  it "should default to nil" do
47
53
  expect(subject.man_dir).to be_nil
@@ -52,7 +58,17 @@ describe CommandKit::Help::Man do
52
58
  subject { TestHelpMan::TestCommand }
53
59
 
54
60
  it "must return the explicitly set man_dir" do
55
- expect(subject.man_dir).to eq(File.expand_path('../fixtures/man',__FILE__))
61
+ expect(subject.man_dir).to eq(File.join(__dir__,'fixtures','man'))
62
+ end
63
+
64
+ context "but it's a relative path" do
65
+ subject { TestHelpMan::TestCommandWithRelativeManDir }
66
+
67
+ it "must expand the given man_dir path" do
68
+ expect(subject.man_dir).to eq(
69
+ File.expand_path(File.join(__dir__,'..','..','..','man'))
70
+ )
71
+ end
56
72
  end
57
73
  end
58
74
 
@@ -96,7 +112,7 @@ describe CommandKit::Help::Man do
96
112
  end
97
113
 
98
114
  it "must not change the superclass'es man_dir" do
99
- expect(super_subject.man_dir).to eq('set/in/baseclass')
115
+ expect(super_subject.man_dir).to eq(File.expand_path('set/in/baseclass'))
100
116
  end
101
117
  end
102
118
 
@@ -115,7 +131,7 @@ describe CommandKit::Help::Man do
115
131
  subject { TestHelpMan::ExplicitInheritedCmd }
116
132
 
117
133
  it "must return the subclass'es man_dir" do
118
- expect(subject.man_dir).to eq('set/in/subclass')
134
+ expect(subject.man_dir).to eq(File.expand_path('set/in/subclass'))
119
135
  end
120
136
 
121
137
  it "must not change the superclass'es man_dir" do
@@ -134,11 +150,11 @@ describe CommandKit::Help::Man do
134
150
  subject { TestHelpMan::ExplicitOverridingInheritedCmd }
135
151
 
136
152
  it "must return the subclass'es man_dir" do
137
- expect(subject.man_dir).to eq('set/in/subclass')
153
+ expect(subject.man_dir).to eq(File.expand_path('set/in/subclass'))
138
154
  end
139
155
 
140
156
  it "must not change the superclass'es man_dir" do
141
- expect(super_subject.man_dir).to eq('set/in/baseclass')
157
+ expect(super_subject.man_dir).to eq(File.expand_path('set/in/baseclass'))
142
158
  end
143
159
  end
144
160
  end
@@ -235,15 +251,6 @@ describe CommandKit::Help::Man do
235
251
  subject { command_class.new }
236
252
 
237
253
  describe "#help_man" do
238
- context "when .man_dir is not set" do
239
- let(:command_class) { TestHelpMan::EmptyCommand }
240
- subject { command_class.new }
241
-
242
- it do
243
- expect { subject.help_man }.to raise_error(NotImplementedError)
244
- end
245
- end
246
-
247
254
  let(:man_page_path) do
248
255
  File.join(subject.class.man_dir,subject.class.man_page)
249
256
  end
@@ -286,21 +293,33 @@ describe CommandKit::Help::Man do
286
293
  expect(subject.stdout).to receive(:tty?).and_return(true)
287
294
  end
288
295
 
289
- it "must open the command's man-page" do
290
- expect(subject).to receive(:help_man).and_return(true)
296
+ context "and man_dir is set" do
297
+ it "must open the command's man-page" do
298
+ expect(subject).to receive(:help_man).and_return(true)
291
299
 
292
- subject.help
293
- end
300
+ subject.help
301
+ end
294
302
 
295
- context "but when the man command is not installed" do
296
- before do
297
- expect(subject).to receive(:help_man).and_return(nil)
303
+ context "but when the man command is not installed" do
304
+ before do
305
+ expect(subject).to receive(:help_man).and_return(nil)
306
+ end
307
+
308
+ it "must call the super help() method" do
309
+ subject.help
310
+
311
+ expect(subject.stdout.string).to eq(normal_help_output)
312
+ end
298
313
  end
314
+ end
315
+
316
+ context "but the man_dir is not set" do
317
+ let(:command_class) { TestHelpMan::TestCommandWithManDirNotSet }
299
318
 
300
- it "must call the super help() method" do
319
+ it "must call the super help() mehtod" do
301
320
  subject.help
302
321
 
303
- expect(subject.stdout.string).to eq(normal_help_output)
322
+ expect(stdout.string).to eq(normal_help_output)
304
323
  end
305
324
  end
306
325
  end
@@ -7,6 +7,61 @@ describe CommandKit::Options::OptionValue do
7
7
  let(:required) { true }
8
8
  let(:default) { 1 }
9
9
 
10
+ describe ".default_usage" do
11
+ subject { described_class }
12
+
13
+ context "when given a Hash" do
14
+ let(:type) do
15
+ {'foo' => :foo, "bar" => :bar}
16
+ end
17
+
18
+ it "must join the Hash's keys with a '|' character" do
19
+ expect(subject.default_usage(type)).to eq(type.keys.join('|'))
20
+ end
21
+ end
22
+
23
+ context "when given an Array" do
24
+ let(:type) do
25
+ ['foo', 'bar', 'baz']
26
+ end
27
+
28
+ it "must join the Array's elements with a '|' character" do
29
+ expect(subject.default_usage(type)).to eq(type.join('|'))
30
+ end
31
+ end
32
+
33
+ context "when given a Regexp" do
34
+ let(:type) { /[0-9a-f]+/ }
35
+
36
+ it "must return the Regexp's source" do
37
+ expect(subject.default_usage(type)).to eq(type.source)
38
+ end
39
+ end
40
+
41
+ context "when given a Class" do
42
+ module TestOptionValue
43
+ class FooBarBaz
44
+ end
45
+ end
46
+
47
+ let(:type) { TestOptionValue::FooBarBaz }
48
+
49
+ it "must return the uppercase and underscored version of it's name" do
50
+ expect(subject.default_usage(type)).to eq("FOO_BAR_BAZ")
51
+ end
52
+ end
53
+
54
+ context "when given another kind of Object" do
55
+ let(:type) { Object.new }
56
+
57
+ it do
58
+ expect {
59
+ subject.default_usage(type)
60
+ }.to raise_error(TypeError,"unsupported option type: #{type.inspect}")
61
+ end
62
+ end
63
+ end
64
+
10
65
  describe "#initialize" do
11
66
  context "when the type: keyword is given" do
12
67
  let(:type) { Integer }