command_kit 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.github/workflows/ruby.yml +29 -0
  4. data/.gitignore +7 -0
  5. data/.rspec +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.md +29 -0
  8. data/Gemfile +14 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +283 -0
  11. data/Rakefile +23 -0
  12. data/command_kit.gemspec +60 -0
  13. data/gemspec.yml +14 -0
  14. data/lib/command_kit.rb +1 -0
  15. data/lib/command_kit/arguments.rb +161 -0
  16. data/lib/command_kit/arguments/argument.rb +111 -0
  17. data/lib/command_kit/arguments/argument_value.rb +81 -0
  18. data/lib/command_kit/arguments/usage.rb +6 -0
  19. data/lib/command_kit/colors.rb +355 -0
  20. data/lib/command_kit/command.rb +42 -0
  21. data/lib/command_kit/command_name.rb +95 -0
  22. data/lib/command_kit/commands.rb +299 -0
  23. data/lib/command_kit/commands/auto_load.rb +153 -0
  24. data/lib/command_kit/commands/auto_load/subcommand.rb +90 -0
  25. data/lib/command_kit/commands/auto_require.rb +138 -0
  26. data/lib/command_kit/commands/command.rb +12 -0
  27. data/lib/command_kit/commands/help.rb +43 -0
  28. data/lib/command_kit/commands/parent_command.rb +21 -0
  29. data/lib/command_kit/commands/subcommand.rb +51 -0
  30. data/lib/command_kit/console.rb +141 -0
  31. data/lib/command_kit/description.rb +89 -0
  32. data/lib/command_kit/env.rb +43 -0
  33. data/lib/command_kit/env/home.rb +71 -0
  34. data/lib/command_kit/env/path.rb +71 -0
  35. data/lib/command_kit/examples.rb +99 -0
  36. data/lib/command_kit/exception_handler.rb +55 -0
  37. data/lib/command_kit/help.rb +62 -0
  38. data/lib/command_kit/help/man.rb +125 -0
  39. data/lib/command_kit/inflector.rb +84 -0
  40. data/lib/command_kit/main.rb +103 -0
  41. data/lib/command_kit/options.rb +179 -0
  42. data/lib/command_kit/options/option.rb +171 -0
  43. data/lib/command_kit/options/option_value.rb +90 -0
  44. data/lib/command_kit/options/parser.rb +227 -0
  45. data/lib/command_kit/options/quiet.rb +53 -0
  46. data/lib/command_kit/options/usage.rb +6 -0
  47. data/lib/command_kit/options/verbose.rb +55 -0
  48. data/lib/command_kit/options/version.rb +62 -0
  49. data/lib/command_kit/os.rb +47 -0
  50. data/lib/command_kit/pager.rb +115 -0
  51. data/lib/command_kit/printing.rb +32 -0
  52. data/lib/command_kit/printing/indent.rb +78 -0
  53. data/lib/command_kit/program_name.rb +57 -0
  54. data/lib/command_kit/stdio.rb +138 -0
  55. data/lib/command_kit/usage.rb +102 -0
  56. data/lib/command_kit/version.rb +4 -0
  57. data/lib/command_kit/xdg.rb +138 -0
  58. data/spec/arguments/argument_spec.rb +169 -0
  59. data/spec/arguments/argument_value_spec.rb +126 -0
  60. data/spec/arguments_spec.rb +213 -0
  61. data/spec/colors_spec.rb +470 -0
  62. data/spec/command_kit_spec.rb +8 -0
  63. data/spec/command_name_spec.rb +130 -0
  64. data/spec/command_spec.rb +49 -0
  65. data/spec/commands/auto_load/subcommand_spec.rb +82 -0
  66. data/spec/commands/auto_load_spec.rb +128 -0
  67. data/spec/commands/auto_require_spec.rb +142 -0
  68. data/spec/commands/fixtures/test_auto_load/cli/commands/test1.rb +10 -0
  69. data/spec/commands/fixtures/test_auto_load/cli/commands/test2.rb +10 -0
  70. data/spec/commands/fixtures/test_auto_require/lib/test_auto_require/cli/commands/test1.rb +10 -0
  71. data/spec/commands/help_spec.rb +66 -0
  72. data/spec/commands/parent_command_spec.rb +40 -0
  73. data/spec/commands/subcommand_spec.rb +99 -0
  74. data/spec/commands_spec.rb +767 -0
  75. data/spec/console_spec.rb +201 -0
  76. data/spec/description_spec.rb +203 -0
  77. data/spec/env/home_spec.rb +46 -0
  78. data/spec/env/path_spec.rb +78 -0
  79. data/spec/env_spec.rb +123 -0
  80. data/spec/examples_spec.rb +235 -0
  81. data/spec/exception_handler_spec.rb +103 -0
  82. data/spec/help_spec.rb +119 -0
  83. data/spec/inflector_spec.rb +104 -0
  84. data/spec/main_spec.rb +179 -0
  85. data/spec/options/option_spec.rb +258 -0
  86. data/spec/options/option_value_spec.rb +67 -0
  87. data/spec/options/parser_spec.rb +265 -0
  88. data/spec/options_spec.rb +137 -0
  89. data/spec/os_spec.rb +46 -0
  90. data/spec/pager_spec.rb +154 -0
  91. data/spec/printing/indent_spec.rb +130 -0
  92. data/spec/printing_spec.rb +76 -0
  93. data/spec/program_name_spec.rb +62 -0
  94. data/spec/spec_helper.rb +6 -0
  95. data/spec/stdio_spec.rb +264 -0
  96. data/spec/usage_spec.rb +237 -0
  97. data/spec/xdg_spec.rb +191 -0
  98. metadata +156 -0
@@ -0,0 +1,10 @@
1
+ require 'command_kit/command'
2
+
3
+ module TestAutoLoad
4
+ class CLI
5
+ module Commands
6
+ class Test1 < CommandKit::Command
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'command_kit/command'
2
+
3
+ module TestAutoLoad
4
+ class CLI
5
+ module Commands
6
+ class Test2 < CommandKit::Command
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'command_kit/command'
2
+
3
+ module TestAutoRequire
4
+ class CLI
5
+ module Commands
6
+ class Test1 < CommandKit::Command
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/commands/help'
3
+ require 'command_kit/commands'
4
+
5
+ describe Commands::Help do
6
+ module TestHelpCommand
7
+ class CLI
8
+ include CommandKit::Commands
9
+
10
+ class Test < CommandKit::Command
11
+ end
12
+
13
+ class TestWithoutHelp
14
+ end
15
+
16
+ command Test
17
+ command 'test-without-help', TestWithoutHelp
18
+ end
19
+ end
20
+
21
+ let(:parent_command_class) { TestHelpCommand::CLI }
22
+ let(:parent_command) { parent_command_class.new }
23
+ let(:command_class) { CommandKit::Commands::Help }
24
+
25
+ subject { command_class.new(parent_command: parent_command) }
26
+
27
+ describe ".run" do
28
+ context "when giving no arguments" do
29
+ it "must call the parent command's #help method" do
30
+ expect(parent_command).to receive(:help).with(no_args)
31
+
32
+ subject.run
33
+ end
34
+ end
35
+
36
+ context "when given a command name" do
37
+ let(:command) { 'test' }
38
+
39
+ it "must lookup the command and call it's #help method" do
40
+ expect_any_instance_of(parent_command_class::Test).to receive(:help)
41
+
42
+ subject.run(command)
43
+ end
44
+
45
+ context "but the command does not define a #help method" do
46
+ let(:command) { 'test-without-help' }
47
+
48
+ it do
49
+ expect { subject.run(command) }.to raise_error(TypeError)
50
+ end
51
+ end
52
+
53
+ context "but the command name is invalid" do
54
+ let(:command) { 'xxx' }
55
+
56
+ it "must print an error message and exit with 1" do
57
+ expect(subject).to receive(:exit).with(1)
58
+
59
+ expect { subject.run(command) }.to output(
60
+ "#{subject.command_name}: unknown command: #{command}#{$/}"
61
+ ).to_stderr
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/commands'
3
+ require 'command_kit/commands/parent_command'
4
+
5
+ describe Commands::ParentCommand do
6
+ module TestParentCommand
7
+ class TestCommands
8
+
9
+ include CommandKit::Commands
10
+
11
+ class Test < CommandKit::Command
12
+ include CommandKit::Commands::ParentCommand
13
+ end
14
+
15
+ command Test
16
+
17
+ end
18
+ end
19
+
20
+ let(:parent_command_class) { TestParentCommand::TestCommands }
21
+ let(:command_class) { TestParentCommand::TestCommands::Test }
22
+
23
+ describe "#initialize" do
24
+ context "when given a parent_command: keyword argument" do
25
+ let(:parent_command) { parent_command_class.new }
26
+
27
+ subject { command_class.new(parent_command: parent_command) }
28
+
29
+ it "must initialize #parent_command" do
30
+ expect(subject.parent_command).to be(parent_command)
31
+ end
32
+ end
33
+
34
+ context "when the parent_command: keyword argument is not given" do
35
+ it do
36
+ expect { command_class.new }.to raise_error(ArgumentError)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/commands/subcommand'
3
+ require 'command_kit/command'
4
+
5
+ describe Commands::Subcommand do
6
+ module TestSubcommands
7
+ class TestCommand < CommandKit::Command
8
+ end
9
+
10
+ class TestCommandWithoutDescription
11
+ end
12
+
13
+ class TestCommandWithASentenceFragmentDescription
14
+ include CommandKit::Description
15
+
16
+ description "The quick brown fox"
17
+ end
18
+
19
+ class TestCommandWithASingleSentenceDescription
20
+ include CommandKit::Description
21
+
22
+ description "The quick brown fox jumps over the lazy dog."
23
+ end
24
+
25
+ class TestCommandWithAMultiSentenceDescription
26
+ include CommandKit::Description
27
+
28
+ description "The quick brown fox jumps over the lazy dog. Foo bar baz."
29
+ end
30
+ end
31
+
32
+ describe "#initialize" do
33
+ let(:command_class) { TestSubcommands::TestCommand }
34
+
35
+ subject { described_class.new(command_class) }
36
+
37
+ it "must initialize the subcommand with a given command class" do
38
+ expect(subject.command).to be(command_class)
39
+ end
40
+
41
+ context "when the command class has no description" do
42
+ it "#summary must be nil" do
43
+ expect(subject.summary).to be(nil)
44
+ end
45
+ end
46
+
47
+ context "when the command class have a description" do
48
+ let(:command_class) { TestSubcommands::TestCommandWithAMultiSentenceDescription }
49
+
50
+ it "must extract the summary using .summary" do
51
+ expect(subject.summary).to eq(described_class.summary(command_class))
52
+ end
53
+ end
54
+
55
+ context "when given aliases:" do
56
+ let(:aliases) { %w[test t] }
57
+
58
+ subject { described_class.new(command_class, aliases: aliases) }
59
+
60
+ it "must initialize #aliases" do
61
+ expect(subject.aliases).to eq(aliases)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe ".summary" do
67
+ subject { described_class.summary(command_class) }
68
+
69
+ context "when the command class does respond_to?(:description)" do
70
+ let(:command_class) { TestSubcommands::TestCommandWithoutDescription }
71
+
72
+ it { expect(subject).to be(nil) }
73
+ end
74
+
75
+ context "when the command class has a sentence fragment description" do
76
+ let(:command_class) { TestSubcommands::TestCommandWithASentenceFragmentDescription }
77
+
78
+ it "must return the sentence fragment" do
79
+ expect(subject).to eq(command_class.description)
80
+ end
81
+ end
82
+
83
+ context "when the command class has a single sentence description" do
84
+ let(:command_class) { TestSubcommands::TestCommandWithASingleSentenceDescription }
85
+
86
+ it "must return the single sentence without the terminating period" do
87
+ expect(subject).to eq(command_class.description.chomp('.'))
88
+ end
89
+ end
90
+
91
+ context "when the command class has a multi-sentence description" do
92
+ let(:command_class) { TestSubcommands::TestCommandWithAMultiSentenceDescription }
93
+
94
+ it "must extract the first sentence, without the terminating period" do
95
+ expect(subject).to eq(command_class.description.split(/\.\s*/).first)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,767 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/commands'
3
+
4
+ describe Commands do
5
+ module TestCommands
6
+ class TestEmptyCommands
7
+
8
+ include CommandKit::Commands
9
+
10
+ end
11
+
12
+ class TestCommands
13
+
14
+ include CommandKit::Commands
15
+
16
+ class Test1 < CommandKit::Command
17
+ end
18
+
19
+ class Test2 < CommandKit::Command
20
+ end
21
+
22
+ p method(:command).source_location
23
+ command Test1
24
+ command Test2
25
+
26
+ end
27
+
28
+ class TestCommandsWithAliases
29
+
30
+ include CommandKit::Commands
31
+
32
+ class Test1 < CommandKit::Command
33
+ end
34
+
35
+ class Test2 < CommandKit::Command
36
+ end
37
+
38
+ command Test1, aliases: %w[t1]
39
+ command Test2, aliases: %w[t2]
40
+
41
+ end
42
+
43
+ class TestCommandsWithExplicitNames
44
+
45
+ include CommandKit::Commands
46
+
47
+ class Test1 < CommandKit::Command
48
+ end
49
+
50
+ class Test2 < CommandKit::Command
51
+ end
52
+
53
+ command 'command-name-1', Test1
54
+ command 'command-name-2', Test2
55
+
56
+ end
57
+
58
+ class TestCommandsWithExplicitNamesAndAliases
59
+
60
+ include CommandKit::Commands
61
+
62
+ class Test1 < CommandKit::Command
63
+ end
64
+
65
+ class Test2 < CommandKit::Command
66
+ end
67
+
68
+ command 'command-name-1', Test1, aliases: %w[t1]
69
+ command 'command-name-2', Test2, aliases: %w[t2]
70
+
71
+ end
72
+
73
+ class TestCommandsWithExplicitSummaries
74
+
75
+ include CommandKit::Commands
76
+
77
+ class Test1 < CommandKit::Command
78
+ end
79
+
80
+ class Test2 < CommandKit::Command
81
+ end
82
+
83
+ command Test1, summary: 'Explicit summary 1'
84
+ command Test2, summary: 'Explicit summary 2'
85
+
86
+ end
87
+
88
+ class TestCommandsWithCustomExitStatus
89
+
90
+ include CommandKit::Commands
91
+
92
+ class Test < CommandKit::Command
93
+
94
+ def run(*argv)
95
+ exit(2)
96
+ end
97
+
98
+ end
99
+
100
+ command Test
101
+
102
+ end
103
+
104
+ class TestCommandsWithGlobalOptions
105
+
106
+ include CommandKit::Commands
107
+
108
+ class Test1 < CommandKit::Command
109
+ end
110
+
111
+ class Test2 < CommandKit::Command
112
+ end
113
+
114
+ option :foo, short: '-f',
115
+ desc: "Global --foo option"
116
+
117
+ option :bar, short: '-b',
118
+ value: {
119
+ required: true,
120
+ type: String,
121
+ usage: 'BAR'
122
+ },
123
+ desc: "Global --bar option"
124
+
125
+ command Test1
126
+ command Test2
127
+
128
+ end
129
+ end
130
+
131
+ let(:command_class) { TestCommands::TestCommands }
132
+
133
+ describe ".commands" do
134
+ subject { command_class }
135
+
136
+ it "must return a Hash" do
137
+ expect(subject.commands).to be_kind_of(Hash)
138
+ end
139
+
140
+ it "must provide a default 'help' command" do
141
+ expect(subject.commands['help']).to_not be_nil
142
+ expect(subject.commands['help'].command).to eq(CommandKit::Commands::Help)
143
+ end
144
+
145
+ context "when additional commands are defined in a superclass" do
146
+ module TestCommands
147
+ class TestInheritedCommands < TestCommands
148
+
149
+ class Test3 < CommandKit::Command
150
+
151
+ def run
152
+ puts 'test command three'
153
+ end
154
+
155
+ end
156
+
157
+ command :test3, Test3
158
+
159
+ end
160
+ end
161
+
162
+ let(:command_superclass) { TestCommands::TestCommands }
163
+ let(:command_class) { TestCommands::TestInheritedCommands }
164
+
165
+ it "must inherit the superclass'es commands" do
166
+ expect(subject.commands['test1']).to eq(command_superclass.commands['test1'])
167
+ expect(subject.commands['test2']).to eq(command_superclass.commands['test2'])
168
+ end
169
+
170
+ it "must allow defining additional commands in the subclass" do
171
+ expect(subject.commands['test3']).to_not be_nil
172
+ expect(subject.commands['test3'].command).to eq(command_class::Test3)
173
+ end
174
+
175
+ it "must not change the superclass'es commands" do
176
+ expect(command_superclass.commands['test3']).to be(nil)
177
+ end
178
+ end
179
+ end
180
+
181
+ describe ".command_aliases" do
182
+ subject { command_class }
183
+
184
+ it "must return an empty Hash by default" do
185
+ expect(subject.command_aliases).to eq({})
186
+ end
187
+
188
+ context "when commands have aliases" do
189
+ let(:command_class) { TestCommands::TestCommandsWithAliases }
190
+
191
+ it "must contain the mapping of aliases to command names" do
192
+ expect(subject.command_aliases).to eq({
193
+ 't1' => 'test1',
194
+ 't2' => 'test2',
195
+ })
196
+ end
197
+ end
198
+
199
+ context "when additional command aliases are defined in a superclass" do
200
+ module TestCommands
201
+ class TestInheritedCommandsWithAliases < TestCommandsWithAliases
202
+
203
+ class Test3 < CommandKit::Command
204
+
205
+ def run
206
+ puts 'test command three'
207
+ end
208
+
209
+ end
210
+
211
+ command :test3, Test3, aliases: %w[t3]
212
+
213
+ end
214
+ end
215
+
216
+ let(:command_superclass) { TestCommands::TestCommandsWithAliases }
217
+ let(:command_class) { TestCommands::TestInheritedCommandsWithAliases }
218
+
219
+ it "must inherit the superclass'es command aliases" do
220
+ expect(subject.command_aliases['t1']).to eq(command_superclass.command_aliases['t1'])
221
+ expect(subject.command_aliases['t2']).to eq(command_superclass.command_aliases['t2'])
222
+ end
223
+
224
+ it "must allow defining additional command aliases in the subclass" do
225
+ expect(subject.command_aliases['t3']).to eq('test3')
226
+ end
227
+
228
+ it "must not change the superclass'es command aliases" do
229
+ expect(command_superclass.command_aliases['test3']).to be(nil)
230
+ end
231
+ end
232
+ end
233
+
234
+ describe ".command" do
235
+ subject { command_class }
236
+
237
+ context "when given only a command class" do
238
+ module TestCommands
239
+ class TestCommandWithOnlyACommandClass
240
+ include CommandKit::Commands
241
+
242
+ class Test < CommandKit::Command
243
+ end
244
+
245
+ command Test
246
+ end
247
+ end
248
+
249
+ let(:command_class) { TestCommands::TestCommandWithOnlyACommandClass }
250
+
251
+ it "must default the command name to the command class'es command_name" do
252
+ expect(subject.commands['test']).to_not be_nil
253
+ expect(subject.commands['test'].command).to eq(command_class::Test)
254
+ end
255
+
256
+ context "and aliases:" do
257
+ let(:command_class) { TestCommands::TestCommandsWithAliases }
258
+
259
+ it "must populate aliases with the aliases and the command names" do
260
+ expect(subject.command_aliases['t1']).to eq(command_class::Test1.command_name)
261
+ expect(subject.command_aliases['t2']).to eq(command_class::Test2.command_name)
262
+ end
263
+ end
264
+ end
265
+
266
+ context "when given a command name and a command class" do
267
+ let(:command_class) { TestCommands::TestCommandsWithExplicitNames }
268
+
269
+ it "must not add an entry for the command class'es command_name" do
270
+ expect(subject.commands['test1']).to be_nil
271
+ expect(subject.commands['test2']).to be_nil
272
+ end
273
+
274
+ it "must add an entry for the command name" do
275
+ expect(subject.commands['command-name-1']).to_not be_nil
276
+ expect(subject.commands['command-name-1'].command).to eq(command_class::Test1)
277
+
278
+ expect(subject.commands['command-name-2']).to_not be_nil
279
+ expect(subject.commands['command-name-2'].command).to eq(command_class::Test2)
280
+ end
281
+
282
+ context "and aliases:" do
283
+ let(:command_class) { TestCommands::TestCommandsWithExplicitNamesAndAliases }
284
+
285
+ it "must populate aliases with the aliases and the explicit command names" do
286
+ expect(subject.command_aliases['t1']).to eq('command-name-1')
287
+ expect(subject.command_aliases['t2']).to eq('command-name-2')
288
+ end
289
+ end
290
+
291
+ context "when the command name is a Symbol" do
292
+ module TestCommands
293
+ class TestCommandWithASymbolCommandName
294
+ include CommandKit::Commands
295
+
296
+ class Test < CommandKit::Command
297
+ end
298
+
299
+ command :test_sym, Test
300
+ end
301
+ end
302
+
303
+ let(:command_class) { TestCommands::TestCommandWithASymbolCommandName }
304
+
305
+ it "must not add an entry for the command class'es command_name" do
306
+ expect(subject.commands['test']).to be_nil
307
+ end
308
+
309
+ it "must not add entries with Symbol keys" do
310
+ expect(subject.commands.keys).to all(be_kind_of(String))
311
+ end
312
+
313
+ it "must convert the command name to a String" do
314
+ expect(subject.commands['test_sym']).to_not be_nil
315
+ expect(subject.commands['test_sym'].command).to eq(command_class::Test)
316
+ end
317
+ end
318
+ end
319
+
320
+ context "when given an explicit summary: keyword argument" do
321
+ let(:command_class) { TestCommands::TestCommandsWithExplicitSummaries }
322
+
323
+ it "must initialize the Subcommand#summary" do
324
+ expect(subject.commands['test1'].summary).to eq('Explicit summary 1')
325
+ expect(subject.commands['test2'].summary).to eq('Explicit summary 2')
326
+ end
327
+ end
328
+ end
329
+
330
+ describe ".get_command" do
331
+ subject { command_class }
332
+
333
+ context "when given a command name" do
334
+ let(:command_class) { TestCommands::TestCommands }
335
+
336
+ it "must return the command's class" do
337
+ expect(subject.get_command('test1')).to eq(command_class::Test1)
338
+ expect(subject.get_command('test2')).to eq(command_class::Test2)
339
+ end
340
+ end
341
+
342
+ context "when given a command's alias name" do
343
+ let(:command_class) { TestCommands::TestCommandsWithAliases }
344
+
345
+ it "must return the command's class associated with the alias" do
346
+ expect(subject.get_command('t1')).to eq(command_class::Test1)
347
+ expect(subject.get_command('t2')).to eq(command_class::Test2)
348
+ end
349
+ end
350
+
351
+ context "when given an unknown command name" do
352
+ it "must return nil" do
353
+ expect(subject.get_command('foo')).to be(nil)
354
+ end
355
+ end
356
+ end
357
+
358
+ subject { command_class.new }
359
+
360
+ describe "#command" do
361
+ module TestCommands
362
+ class TestSubCommandInitialization
363
+ include CommandKit::Commands
364
+
365
+ class Test < CommandKit::Command
366
+ end
367
+
368
+ command 'test', Test
369
+ end
370
+ end
371
+
372
+ let(:command_class) { TestCommands::TestSubCommandInitialization }
373
+ let(:subcommand_class) { command_class::Test }
374
+
375
+ context "when given a valid command name" do
376
+ it "must lookup the command and initialize it" do
377
+ expect(subject.command(subcommand_class.command_name)).to be_kind_of(subcommand_class)
378
+ end
379
+ end
380
+
381
+ context "when given a command alias" do
382
+ let(:command_class) { TestCommands::TestCommandsWithAliases }
383
+
384
+ it "must lookup the command and initialize it" do
385
+ expect(subject.command('t1')).to be_kind_of(command_class::Test1)
386
+ expect(subject.command('t2')).to be_kind_of(command_class::Test2)
387
+ end
388
+ end
389
+
390
+ context "when given an unknown command name" do
391
+ it "must return nil" do
392
+ expect(subject.command('foo')).to be_nil
393
+ end
394
+ end
395
+
396
+ context "when the command includes CommandKit::Commands::ParentCommand" do
397
+ module TestCommands
398
+ class TestSubCommandInitializationWithParentCommand
399
+ include CommandKit::Commands
400
+
401
+ class Test
402
+ include CommandKit::Commands::ParentCommand
403
+ end
404
+
405
+ command 'test', Test
406
+ end
407
+ end
408
+
409
+ let(:command_class) { TestCommands::TestSubCommandInitializationWithParentCommand }
410
+ let(:subcommand_class) { command_class::Test }
411
+
412
+ it "must initialize the sub-command with a parent_command value" do
413
+ subcommand = subject.command('test')
414
+
415
+ expect(subcommand.parent_command).to be(subject)
416
+ end
417
+ end
418
+
419
+ context "when the command includes CommandKit::CommandName" do
420
+ module TestCommands
421
+ class TestSubCommandInitializationWithCommandName
422
+ include CommandKit::Commands
423
+
424
+ class Test
425
+ include CommandKit::CommandName
426
+ end
427
+
428
+ command 'test', Test
429
+ end
430
+ end
431
+
432
+ let(:command_class) { TestCommands::TestSubCommandInitializationWithCommandName }
433
+ let(:subcommand_class) { command_class::Test }
434
+ let(:expected_subcommand_name) do
435
+ "#{command_class.command_name} #{subcommand_class.command_name}"
436
+ end
437
+
438
+ it "must initialize the sub-command with the command and subcommand name" do
439
+ subcommand = subject.command('test')
440
+
441
+ expect(subcommand.command_name).to eq(expected_subcommand_name)
442
+ end
443
+ end
444
+
445
+ context "when the command includes CommandKit::Stdio" do
446
+ module TestCommands
447
+ class TestSubCommandInitializationWithStdio
448
+ include CommandKit::Commands
449
+
450
+ class Test
451
+ include CommandKit::Stdio
452
+ end
453
+
454
+ command 'test', Test
455
+ end
456
+ end
457
+
458
+ let(:command_class) { TestCommands::TestSubCommandInitializationWithStdio }
459
+ let(:subcommand_class) { command_class::Test }
460
+
461
+ let(:stdin) { StringIO.new }
462
+ let(:stdout) { StringIO.new }
463
+ let(:stderr) { StringIO.new }
464
+
465
+ subject do
466
+ command_class.new(stdin: stdin, stdout: stdout, stderr: stderr)
467
+ end
468
+
469
+ it "must initialize the sub-command with the command's #stdin, #stdout, #stderr" do
470
+ subcommand = subject.command('test')
471
+
472
+ expect(subcommand.stdin).to be(stdin)
473
+ expect(subcommand.stdout).to be(stdout)
474
+ expect(subcommand.stderr).to be(stderr)
475
+ end
476
+ end
477
+
478
+ context "when the command includes CommandKit::Env" do
479
+ module TestCommands
480
+ class TestSubCommandInitializationWithEnv
481
+ include CommandKit::Commands
482
+
483
+ class Test
484
+ include CommandKit::Env
485
+ end
486
+
487
+ command 'test', Test
488
+ end
489
+ end
490
+
491
+ let(:command_class) { TestCommands::TestSubCommandInitializationWithEnv }
492
+ let(:subcommand_class) { command_class::Test }
493
+
494
+ let(:env) { {'FOO' => 'bar'} }
495
+ subject { command_class.new(env: env) }
496
+
497
+ it "must initialize the sub-command with a copy of the command's #env" do
498
+ subcommand = subject.command('test')
499
+
500
+ expect(subcommand.env).to eq(env)
501
+ expect(subcommand.env).to_not be(env)
502
+ end
503
+ end
504
+
505
+ context "when the command includes CommandKit::Options" do
506
+ module TestCommands
507
+ class TestSubCommandInitializationWithOptions
508
+ include CommandKit::Commands
509
+
510
+ class Test
511
+ include CommandKit::Options
512
+ end
513
+
514
+ command 'test', Test
515
+ end
516
+ end
517
+
518
+ let(:command_class) { TestCommands::TestSubCommandInitializationWithOptions }
519
+ let(:subcommand_class) { command_class::Test }
520
+
521
+ let(:options) { {foo: 'bar'} }
522
+ subject { command_class.new(options: options) }
523
+
524
+ it "must initialize the sub-command with a copy of the command's #options Hash" do
525
+ subcommand = subject.command('test')
526
+
527
+ expect(subcommand.options).to eq(options)
528
+ expect(subcommand.options).to_not be(options)
529
+ end
530
+ end
531
+ end
532
+
533
+ describe "#invoke" do
534
+ context "when given a valid command name" do
535
+ let(:command_name) { 'test2' }
536
+ let(:argv) { %w[--opt arg1 arg2] }
537
+
538
+ it "must call the command's #main method with the given argv" do
539
+ expect_any_instance_of(command_class::Test2).to receive(:main).with(argv)
540
+
541
+ subject.invoke(command_name,*argv)
542
+ end
543
+
544
+ context "when the command returns a custom exit code" do
545
+ let(:command_class) { TestCommands::TestCommandsWithCustomExitStatus }
546
+
547
+ it "must return the exit code" do
548
+ expect(subject.invoke('test')).to eq(2)
549
+ end
550
+ end
551
+ end
552
+
553
+ context "when given an unknown command name" do
554
+ let(:command_name) { 'xxx' }
555
+ let(:argv) { %w[--opt arg1 arg2] }
556
+
557
+ it "must call #on_unknown_command with the given name and argv" do
558
+ expect(subject).to receive(:on_unknown_command).with(command_name,argv)
559
+
560
+ subject.invoke(command_name,*argv)
561
+ end
562
+ end
563
+ end
564
+
565
+ let(:unknown_command) { 'xxx' }
566
+
567
+ describe "#command_not_found" do
568
+ it "must print an error message to stderr and exit with 1" do
569
+ expect(subject).to receive(:exit).with(1)
570
+
571
+ expect { subject.command_not_found(unknown_command) }.to output(
572
+ "'#{unknown_command}' is not a #{subject.command_name} command. See `#{subject.command_name} help`" + $/
573
+ ).to_stderr
574
+ end
575
+ end
576
+
577
+ describe "#on_unknown_command" do
578
+ it "must call #command_not_found with the given command name by default" do
579
+ expect(subject).to receive(:command_not_found).with(unknown_command)
580
+
581
+ subject.on_unknown_command(unknown_command)
582
+ end
583
+ end
584
+
585
+ describe "#run" do
586
+ context "when a command name is the first argument" do
587
+ let(:command) { 'test1' }
588
+ let(:exit_status) { 2 }
589
+
590
+ it "must invoke the command and exit with it's status" do
591
+ expect(subject).to receive(:invoke).with(command).and_return(exit_status)
592
+ expect(subject).to receive(:exit).with(exit_status)
593
+
594
+ subject.run(command)
595
+ end
596
+
597
+ context "when additional argv is given after the command name" do
598
+ let(:argv) { %w[--opt arg1 arg2] }
599
+
600
+ it "must pass the additional argv to the command" do
601
+ expect(subject).to receive(:invoke).with(command,*argv).and_return(exit_status)
602
+ expect(subject).to receive(:exit).with(exit_status)
603
+
604
+ subject.run(command,*argv)
605
+ end
606
+ end
607
+ end
608
+
609
+ context "when given no arguments" do
610
+ it "must default to calling #help" do
611
+ expect(subject).to receive(:help)
612
+
613
+ subject.run()
614
+ end
615
+ end
616
+ end
617
+
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] }
622
+
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
+ )
627
+ end
628
+
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] }
632
+
633
+ it "must stop before the first non-option argument" do
634
+ expect(subject.option_parser.parse(argv)).to eq(argv)
635
+ end
636
+ end
637
+
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
644
+
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
+ )
649
+
650
+ expect(subject.options[:foo]).to be(true)
651
+ expect(subject.options[:bar]).to eq(bar)
652
+ end
653
+ end
654
+ end
655
+
656
+ describe "#help_commands" do
657
+ it "must print usage and the list of available commands" do
658
+ expect { subject.help_commands }.to output(
659
+ [
660
+ "",
661
+ "Commands:",
662
+ " help",
663
+ " test1",
664
+ " test2",
665
+ ""
666
+ ].join($/)
667
+ ).to_stdout
668
+ end
669
+
670
+ context "when the commands have custom names" do
671
+ let(:command_class) { TestCommands::TestCommandsWithExplicitNames }
672
+
673
+ it "must print the command names, not the command class'es #command_name" do
674
+ expect { subject.help_commands }.to output(
675
+ [
676
+ "",
677
+ "Commands:",
678
+ " command-name-1",
679
+ " command-name-2",
680
+ " help",
681
+ ""
682
+ ].join($/)
683
+ ).to_stdout
684
+ end
685
+ end
686
+
687
+ context "when the commands have summaries" do
688
+ let(:command_class) { TestCommands::TestCommandsWithExplicitSummaries }
689
+
690
+ it "must print the command names and their summaries" do
691
+ expect { subject.help_commands }.to output(
692
+ [
693
+ "",
694
+ "Commands:",
695
+ " help",
696
+ " test1\t#{command_class.commands['test1'].summary}",
697
+ " test2\t#{command_class.commands['test2'].summary}",
698
+ ""
699
+ ].join($/)
700
+ ).to_stdout
701
+ end
702
+ end
703
+ end
704
+
705
+ describe "#help" do
706
+ it "must print the list of available commands after other help output" do
707
+ expect { subject.help }.to output(
708
+ [
709
+ "Usage: #{subject.command_name} [options] [COMMAND [ARGS...]]",
710
+ "",
711
+ "Options:",
712
+ " -h, --help Print help information",
713
+ "",
714
+ "Commands:",
715
+ " help",
716
+ " test1",
717
+ " test2",
718
+ ""
719
+ ].join($/)
720
+ ).to_stdout
721
+ end
722
+
723
+ context "when the command has command alises" do
724
+ let(:command_class) { TestCommands::TestCommandsWithAliases }
725
+
726
+ it "must print the command names, along with their command aliases" do
727
+ expect { subject.help }.to output(
728
+ [
729
+ "Usage: #{subject.command_name} [options] [COMMAND [ARGS...]]",
730
+ "",
731
+ "Options:",
732
+ " -h, --help Print help information",
733
+ "",
734
+ "Commands:",
735
+ " help",
736
+ " test1, t1",
737
+ " test2, t2",
738
+ ""
739
+ ].join($/)
740
+ ).to_stdout
741
+ end
742
+ end
743
+
744
+ context "when the command defines additional global options" do
745
+ let(:command_class) { TestCommands::TestCommandsWithGlobalOptions }
746
+
747
+ it "must print any global options" do
748
+ expect { subject.help }.to output(
749
+ [
750
+ "Usage: #{subject.command_name} [options] [COMMAND [ARGS...]]",
751
+ "",
752
+ "Options:",
753
+ " -f, --foo Global --foo option",
754
+ " -b, --bar BAR Global --bar option",
755
+ " -h, --help Print help information",
756
+ "",
757
+ "Commands:",
758
+ " help",
759
+ " test1",
760
+ " test2",
761
+ ""
762
+ ].join($/)
763
+ ).to_stdout
764
+ end
765
+ end
766
+ end
767
+ end