command_kit 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 (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