command_kit 0.1.0 → 0.3.0

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +18 -3
  3. data/.rubocop.yml +141 -0
  4. data/ChangeLog.md +165 -0
  5. data/Gemfile +3 -0
  6. data/README.md +186 -118
  7. data/Rakefile +3 -2
  8. data/command_kit.gemspec +4 -4
  9. data/examples/command.rb +1 -1
  10. data/gemspec.yml +7 -0
  11. data/lib/command_kit/arguments/argument.rb +2 -2
  12. data/lib/command_kit/arguments.rb +36 -7
  13. data/lib/command_kit/colors.rb +702 -53
  14. data/lib/command_kit/command.rb +2 -3
  15. data/lib/command_kit/commands/auto_load.rb +8 -1
  16. data/lib/command_kit/commands/help.rb +3 -2
  17. data/lib/command_kit/commands/subcommand.rb +1 -1
  18. data/lib/command_kit/commands.rb +24 -9
  19. data/lib/command_kit/env/path.rb +1 -1
  20. data/lib/command_kit/file_utils.rb +46 -0
  21. data/lib/command_kit/help/man.rb +17 -33
  22. data/lib/command_kit/inflector.rb +47 -17
  23. data/lib/command_kit/interactive.rb +9 -0
  24. data/lib/command_kit/main.rb +7 -9
  25. data/lib/command_kit/man.rb +44 -0
  26. data/lib/command_kit/open_app.rb +69 -0
  27. data/lib/command_kit/options/option.rb +41 -27
  28. data/lib/command_kit/options/option_value.rb +3 -2
  29. data/lib/command_kit/options/parser.rb +17 -22
  30. data/lib/command_kit/options.rb +102 -14
  31. data/lib/command_kit/os/linux.rb +157 -0
  32. data/lib/command_kit/os.rb +159 -11
  33. data/lib/command_kit/package_manager.rb +200 -0
  34. data/lib/command_kit/pager.rb +46 -4
  35. data/lib/command_kit/printing/indent.rb +4 -4
  36. data/lib/command_kit/printing.rb +14 -3
  37. data/lib/command_kit/program_name.rb +9 -0
  38. data/lib/command_kit/sudo.rb +40 -0
  39. data/lib/command_kit/terminal.rb +5 -0
  40. data/lib/command_kit/version.rb +1 -1
  41. data/spec/arguments/argument_spec.rb +1 -1
  42. data/spec/arguments_spec.rb +84 -1
  43. data/spec/colors_spec.rb +357 -70
  44. data/spec/command_spec.rb +77 -6
  45. data/spec/commands/auto_load_spec.rb +33 -2
  46. data/spec/commands_spec.rb +101 -29
  47. data/spec/env/path_spec.rb +6 -0
  48. data/spec/exception_handler_spec.rb +1 -1
  49. data/spec/file_utils_spec.rb +59 -0
  50. data/spec/fixtures/template.erb +5 -0
  51. data/spec/help/man_spec.rb +54 -57
  52. data/spec/inflector_spec.rb +70 -8
  53. data/spec/man_spec.rb +46 -0
  54. data/spec/open_app_spec.rb +85 -0
  55. data/spec/options/option_spec.rb +38 -2
  56. data/spec/options/option_value_spec.rb +55 -0
  57. data/spec/options/parser_spec.rb +0 -10
  58. data/spec/options_spec.rb +328 -0
  59. data/spec/os/linux_spec.rb +164 -0
  60. data/spec/os_spec.rb +200 -13
  61. data/spec/package_manager_spec.rb +806 -0
  62. data/spec/pager_spec.rb +71 -6
  63. data/spec/printing/indent_spec.rb +7 -5
  64. data/spec/printing_spec.rb +23 -1
  65. data/spec/program_name_spec.rb +8 -0
  66. data/spec/sudo_spec.rb +51 -0
  67. data/spec/terminal_spec.rb +30 -0
  68. data/spec/usage_spec.rb +1 -1
  69. metadata +23 -4
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'command_kit/open_app'
3
+
4
+ describe CommandKit::OpenApp do
5
+ module TestOpenApp
6
+ class TestCommand
7
+ include CommandKit::OpenApp
8
+ end
9
+ end
10
+
11
+ let(:command_class) { TestOpenApp::TestCommand }
12
+
13
+ subject { command_class.new }
14
+
15
+ describe "#initialize" do
16
+ context "when the OS is macOS" do
17
+ subject { command_class.new(os: :macos) }
18
+
19
+ it "must set @open_command to \"open\"" do
20
+ expect(subject.instance_variable_get("@open_command")).to eq("open")
21
+ end
22
+ end
23
+
24
+ context "when the OS is Linux" do
25
+ subject { command_class.new(os: :linux) }
26
+
27
+ it "must set @open_command to \"xdg-open\"" do
28
+ expect(subject.instance_variable_get("@open_command")).to eq("xdg-open")
29
+ end
30
+ end
31
+
32
+ context "when the OS is FreeBSD" do
33
+ subject { command_class.new(os: :freebsd) }
34
+
35
+ it "must set @open_command to \"xdg-open\"" do
36
+ expect(subject.instance_variable_get("@open_command")).to eq("xdg-open")
37
+ end
38
+ end
39
+
40
+ context "when the OS is OpenBSD" do
41
+ subject { command_class.new(os: :openbsd) }
42
+
43
+ it "must set @open_command to \"xdg-open\"" do
44
+ expect(subject.instance_variable_get("@open_command")).to eq("xdg-open")
45
+ end
46
+ end
47
+
48
+ context "when the OS is NetBSD" do
49
+ subject { command_class.new(os: :openbsd) }
50
+
51
+ it "must set @open_command to \"xdg-open\"" do
52
+ expect(subject.instance_variable_get("@open_command")).to eq("xdg-open")
53
+ end
54
+ end
55
+
56
+ context "when the OS is Windows" do
57
+ subject { command_class.new(os: :windows) }
58
+
59
+ it "must set @open_command to \"start\"" do
60
+ expect(subject.instance_variable_get("@open_command")).to eq("start")
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "#open_app_for" do
66
+ context "when @open_command is set" do
67
+ let(:file_or_uri) { "foo" }
68
+ let(:status) { true }
69
+
70
+ it "must execute the @open_command with the given URI or file" do
71
+ expect(subject).to receive(:system).with(subject.instance_variable_get("@open_command"),file_or_uri).and_return(status)
72
+
73
+ expect(subject.open_app_for(file_or_uri)).to be(status)
74
+ end
75
+ end
76
+
77
+ context "when @open_command is not set" do
78
+ before do
79
+ subject.instance_variable_set("@open_command",nil)
80
+ end
81
+
82
+ it { expect(subject.open_app_for("foo")).to be(nil) }
83
+ end
84
+ end
85
+ end
@@ -18,12 +18,12 @@ describe CommandKit::Options::Option do
18
18
  end
19
19
 
20
20
  subject do
21
- described_class.new name, short: short,
21
+ described_class.new(name, short: short,
22
22
  long: long,
23
23
  equals: equals,
24
24
  desc: desc,
25
25
  value: value,
26
- &block
26
+ &block)
27
27
  end
28
28
 
29
29
  describe "#initialize" do
@@ -145,6 +145,20 @@ describe CommandKit::Options::Option do
145
145
  end
146
146
  end
147
147
 
148
+ it "must default #category to nil" do
149
+ expect(subject.category).to be(nil)
150
+ end
151
+
152
+ context "when the category: keyword is given" do
153
+ let(:category) { 'Other Options' }
154
+
155
+ subject { described_class.new(name, desc: desc, category: category) }
156
+
157
+ it "must set #category" do
158
+ expect(subject.category).to eq(category)
159
+ end
160
+ end
161
+
148
162
  context "when a block is given" do
149
163
  subject { described_class.new(name, desc: desc, &block) }
150
164
 
@@ -293,5 +307,27 @@ describe CommandKit::Options::Option do
293
307
  expect(subject.desc).to eq("#{desc} (Default: #{default})")
294
308
  end
295
309
  end
310
+
311
+ context "when #desc was initialized with an Array" do
312
+ let(:desc) do
313
+ [
314
+ 'Line 1',
315
+ 'Line 2'
316
+ ]
317
+ end
318
+
319
+ it "must return the desc: value" do
320
+ expect(subject.desc).to eq(desc)
321
+ end
322
+
323
+ context "when #value has been initialized with a default value" do
324
+ let(:default) { "foo" }
325
+ let(:value) { {default: default} }
326
+
327
+ it "should append '(Default: ...)' to the desc Array" do
328
+ expect(subject.desc).to eq([*desc, "(Default: #{default})"])
329
+ end
330
+ end
331
+ end
296
332
  end
297
333
  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 }
@@ -34,16 +34,6 @@ describe CommandKit::Options::Parser do
34
34
  expect(subject.option_parser.banner).to eq("Usage: #{subject.usage}")
35
35
  end
36
36
 
37
- it "must include a 'Options:' separator" do
38
- expect(subject.option_parser.to_s).to include(
39
- [
40
- '',
41
- 'Options:',
42
- ''
43
- ].join($/)
44
- )
45
- end
46
-
47
37
  it "must define a default --help option" do
48
38
  expect(subject.option_parser.to_s).to include(
49
39
  [
data/spec/options_spec.rb CHANGED
@@ -133,5 +133,333 @@ describe CommandKit::Options do
133
133
  it "must initialize #options" do
134
134
  expect(subject.options).to eq({})
135
135
  end
136
+
137
+ context "when options have default values" do
138
+ module TestOptions
139
+ class TestCommandWithDefaultValues
140
+
141
+ include CommandKit::Options
142
+
143
+ option :option1, value: {
144
+ required: true,
145
+ type: String
146
+ },
147
+ desc: 'Option 1'
148
+
149
+ option :option2, value: {
150
+ required: false,
151
+ type: String,
152
+ default: "foo"
153
+ },
154
+ desc: 'Option 2'
155
+ end
156
+ end
157
+
158
+ let(:command_class) { TestOptions::TestCommandWithDefaultValues }
159
+
160
+ it "must pre-populate #options with the default values" do
161
+ expect(subject.options).to_not have_key(:option1)
162
+ expect(subject.options).to have_key(:option2)
163
+ expect(subject.options[:option2]).to eq("foo")
164
+ end
165
+ end
166
+ end
167
+
168
+ module TestOptions
169
+ class TestCommandWithOptionsAndArguments
170
+
171
+ include CommandKit::Options
172
+
173
+ usage '[OPTIONS] ARG1 [ARG2]'
174
+
175
+ option :option1, short: '-a',
176
+ value: {
177
+ type: Integer,
178
+ default: 1
179
+ },
180
+ desc: "Option 1"
181
+
182
+ option :option2, short: '-b',
183
+ value: {
184
+ type: String,
185
+ usage: 'FILE'
186
+ },
187
+ desc: "Option 2"
188
+
189
+ argument :argument1, required: true,
190
+ usage: 'ARG1',
191
+ desc: "Argument 1"
192
+
193
+ argument :argument2, required: false,
194
+ usage: 'ARG2',
195
+ desc: "Argument 2"
196
+
197
+ end
198
+ end
199
+
200
+ describe "#option_parser" do
201
+ context "when an option does not accept a value" do
202
+ module TestOptions
203
+ class TestCommandWithOptionWithoutValue
204
+
205
+ include CommandKit::Options
206
+
207
+ option :opt, desc: "Option without a value"
208
+
209
+ end
210
+ end
211
+
212
+ let(:command_class) { TestOptions::TestCommandWithOptionWithoutValue }
213
+
214
+ context "but the option flag was not given" do
215
+ let(:argv) { [] }
216
+
217
+ before { subject.option_parser.parse(argv) }
218
+
219
+ it "must not populate #options with a value" do
220
+ expect(subject.options).to be_empty
221
+ end
222
+ end
223
+
224
+ context "and the option flag was given" do
225
+ let(:argv) { %w[--opt] }
226
+
227
+ before { subject.option_parser.parse(argv) }
228
+
229
+ it "must set a key in #options to true" do
230
+ expect(subject.options[:opt]).to be(true)
231
+ end
232
+ end
233
+ end
234
+
235
+ context "when an option requires a value" do
236
+ module TestOptions
237
+ class TestCommandWithOptionWithRequiredValue
238
+
239
+ include CommandKit::Options
240
+
241
+ option :opt, value: {
242
+ required: true,
243
+ type: String
244
+ },
245
+ desc: "Option without a value"
246
+
247
+ end
248
+ end
249
+
250
+ let(:command_class) do
251
+ TestOptions::TestCommandWithOptionWithRequiredValue
252
+ end
253
+
254
+ context "but the option flag was not given" do
255
+ let(:argv) { [] }
256
+
257
+ before { subject.option_parser.parse(argv) }
258
+
259
+ it "must not populate #options with a value" do
260
+ expect(subject.options).to be_empty
261
+ end
262
+ end
263
+
264
+ context "and the option flag and value were given" do
265
+ let(:value) { 'foo' }
266
+ let(:argv) { ['--opt', value] }
267
+
268
+ before { subject.option_parser.parse(argv) }
269
+
270
+ it "must set a key in #options to the value" do
271
+ expect(subject.options[:opt]).to eq(value)
272
+ end
273
+ end
274
+ end
275
+
276
+ context "when an option does not require a value" do
277
+ module TestOptions
278
+ class TestCommandWithOptionWithOptionalValue
279
+
280
+ include CommandKit::Options
281
+
282
+ option :opt, value: {
283
+ required: false,
284
+ type: String
285
+ },
286
+ desc: "Option without a value"
287
+
288
+ end
289
+ end
290
+
291
+ let(:command_class) do
292
+ TestOptions::TestCommandWithOptionWithOptionalValue
293
+ end
294
+
295
+ context "but the option flag was not given" do
296
+ let(:argv) { [] }
297
+
298
+ before { subject.option_parser.parse(argv) }
299
+
300
+ it "must not populate #options with a value" do
301
+ expect(subject.options).to be_empty
302
+ end
303
+ end
304
+
305
+ context "and the option flag and value were given" do
306
+ let(:value) { 'foo' }
307
+ let(:argv) { ['--opt', value] }
308
+
309
+ before { subject.option_parser.parse(argv) }
310
+
311
+ it "must set a key in #options to the value" do
312
+ expect(subject.options[:opt]).to eq(value)
313
+ end
314
+ end
315
+
316
+ context "and the option has a default value" do
317
+ module TestOptions
318
+ class TestCommandWithOptionWithOptionalValueAndDefaultValue
319
+
320
+ include CommandKit::Options
321
+
322
+ option :opt, value: {
323
+ required: false,
324
+ type: String,
325
+ default: "bar"
326
+ },
327
+ desc: "Option without a value"
328
+
329
+ end
330
+ end
331
+
332
+ let(:command_class) do
333
+ TestOptions::TestCommandWithOptionWithOptionalValueAndDefaultValue
334
+ end
335
+
336
+ context "but the option flag was not given" do
337
+ let(:argv) { [] }
338
+
339
+ before { subject.option_parser.parse(argv) }
340
+
341
+ it "must set a key in #options to the default value" do
342
+ expect(subject.options[:opt]).to eq("bar")
343
+ end
344
+ end
345
+
346
+ context "and the option flag and value were given" do
347
+ let(:value) { 'foo' }
348
+ let(:argv) { ['--opt', value] }
349
+
350
+ before { subject.option_parser.parse(argv) }
351
+
352
+ it "must set a key in #options to the value" do
353
+ expect(subject.options[:opt]).to eq(value)
354
+ end
355
+ end
356
+
357
+ context "and the option flag but not the value are given" do
358
+ let(:argv) { ['--opt'] }
359
+
360
+ before { subject.option_parser.parse(argv) }
361
+
362
+ it "must set a key in #options to nil" do
363
+ expect(subject.options).to have_key(:opt)
364
+ expect(subject.options[:opt]).to be(nil)
365
+ end
366
+ end
367
+ end
368
+ end
369
+ end
370
+
371
+ describe "#main" do
372
+ let(:command_class) { TestOptions::TestCommandWithOptionsAndArguments }
373
+
374
+ let(:argv) { %w[-a 42 -b foo.txt arg1 arg2] }
375
+
376
+ it "must parse options before validating the number of arguments" do
377
+ expect {
378
+ expect(subject.main(argv)).to eq(0)
379
+ }.to_not output.to_stderr
380
+ end
381
+
382
+ context "but the wrong number of arguments are given" do
383
+ let(:argv) { %w[-a 42 -b foo.txt] }
384
+
385
+ it "must still validate the number of arguments" do
386
+ expect {
387
+ expect(subject.main(argv)).to eq(1)
388
+ }.to output("#{subject.command_name}: insufficient number of arguments.#{$/}").to_stderr
389
+ end
390
+ end
391
+ end
392
+
393
+ describe "#help" do
394
+ let(:command_class) { TestOptions::TestCommandWithOptionsAndArguments }
395
+
396
+ let(:option1) { command_class.options[:option1] }
397
+ let(:option2) { command_class.options[:option2] }
398
+ let(:argument1) { command_class.arguments[:argument1] }
399
+ let(:argument2) { command_class.arguments[:argument2] }
400
+
401
+ it "must print the usage, options and arguments" do
402
+ expect { subject.help }.to output(
403
+ [
404
+ "Usage: #{subject.usage}",
405
+ '',
406
+ 'Options:',
407
+ " #{option1.usage.join(', ').ljust(33 - 1)} #{option1.desc}",
408
+ " #{option2.usage.join(', ').ljust(33 - 1)} #{option2.desc}",
409
+ ' -h, --help Print help information',
410
+ '',
411
+ "Arguments:",
412
+ " #{argument1.usage.ljust(33)}#{argument1.desc}",
413
+ " #{argument2.usage.ljust(33)}#{argument2.desc}",
414
+ ''
415
+ ].join($/)
416
+ ).to_stdout
417
+ end
418
+
419
+ context "but when the options are have categories" do
420
+ module TestOptions
421
+ class TestCommandWithOptionsAndCategories
422
+
423
+ include CommandKit::Options
424
+
425
+ option :opt1, short: '-a',
426
+ desc: "Option 1"
427
+ option :opt2, short: '-b',
428
+ desc: "Option 2"
429
+
430
+ option :opt3, short: '-c',
431
+ desc: "Option 3",
432
+ category: 'Other Options'
433
+ option :opt4, short: '-d',
434
+ desc: "Option 4",
435
+ category: 'Other Options'
436
+ end
437
+ end
438
+
439
+ let(:command_class) { TestOptions::TestCommandWithOptionsAndCategories }
440
+
441
+ let(:option1) { command_class.options[:opt1] }
442
+ let(:option2) { command_class.options[:opt2] }
443
+ let(:option3) { command_class.options[:opt3] }
444
+ let(:option4) { command_class.options[:opt4] }
445
+
446
+ it "must group the options by category" do
447
+ expect { subject.help }.to output(
448
+ [
449
+ "Usage: #{subject.usage}",
450
+ '',
451
+ 'Other Options:',
452
+ " #{option3.usage.join(', ').ljust(33 - 1)} #{option3.desc}",
453
+ " #{option4.usage.join(', ').ljust(33 - 1)} #{option4.desc}",
454
+ '',
455
+ 'Options:',
456
+ " #{option1.usage.join(', ').ljust(33 - 1)} #{option1.desc}",
457
+ " #{option2.usage.join(', ').ljust(33 - 1)} #{option2.desc}",
458
+ ' -h, --help Print help information',
459
+ ''
460
+ ].join($/)
461
+ ).to_stdout
462
+ end
463
+ end
136
464
  end
137
465
  end