gli 2.9.0 → 2.20.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 (94) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -0
  3. data/.gitignore +3 -3
  4. data/.tool-versions +1 -0
  5. data/Gemfile +0 -2
  6. data/README.rdoc +29 -18
  7. data/Rakefile +15 -37
  8. data/bin/ci +29 -0
  9. data/bin/gli +24 -54
  10. data/bin/rake +29 -0
  11. data/bin/setup +5 -0
  12. data/exe/gli +68 -0
  13. data/gli.gemspec +20 -24
  14. data/gli.rdoc +9 -9
  15. data/lib/gli/app.rb +42 -8
  16. data/lib/gli/app_support.rb +17 -5
  17. data/lib/gli/argument.rb +20 -0
  18. data/lib/gli/command.rb +27 -2
  19. data/lib/gli/command_finder.rb +42 -25
  20. data/lib/gli/command_support.rb +13 -7
  21. data/lib/gli/commands/doc.rb +9 -3
  22. data/lib/gli/commands/help.rb +2 -1
  23. data/lib/gli/commands/help_modules/arg_name_formatter.rb +29 -2
  24. data/lib/gli/commands/help_modules/command_help_format.rb +19 -1
  25. data/lib/gli/commands/help_modules/full_synopsis_formatter.rb +5 -4
  26. data/lib/gli/commands/help_modules/global_help_format.rb +1 -1
  27. data/lib/gli/commands/help_modules/options_formatter.rb +4 -6
  28. data/lib/gli/commands/initconfig.rb +3 -6
  29. data/lib/gli/commands/rdoc_document_listener.rb +2 -1
  30. data/lib/gli/commands/scaffold.rb +71 -142
  31. data/lib/gli/dsl.rb +25 -1
  32. data/lib/gli/exceptions.rb +26 -0
  33. data/lib/gli/flag.rb +23 -2
  34. data/lib/gli/gli_option_parser.rb +73 -21
  35. data/lib/gli/option_parser_factory.rb +10 -3
  36. data/lib/gli/options.rb +2 -2
  37. data/lib/gli/switch.rb +4 -0
  38. data/lib/gli/terminal.rb +6 -2
  39. data/lib/gli/version.rb +1 -1
  40. data/lib/gli.rb +2 -0
  41. data/object-model.dot +29 -0
  42. data/object-model.png +0 -0
  43. data/test/apps/todo/Gemfile +1 -1
  44. data/test/apps/todo/bin/todo +16 -6
  45. data/test/apps/todo/lib/todo/commands/create.rb +48 -18
  46. data/test/apps/todo/lib/todo/commands/list.rb +48 -35
  47. data/test/apps/todo/lib/todo/commands/ls.rb +25 -24
  48. data/test/apps/todo/lib/todo/commands/make.rb +42 -39
  49. data/test/apps/todo/todo.gemspec +1 -2
  50. data/test/apps/todo_legacy/todo.gemspec +1 -2
  51. data/test/apps/todo_plugins/commands/third.rb +2 -0
  52. data/test/integration/gli_cli_test.rb +69 -0
  53. data/test/integration/gli_powered_app_test.rb +52 -0
  54. data/test/integration/scaffold_test.rb +30 -0
  55. data/test/integration/test_helper.rb +52 -0
  56. data/test/unit/command_finder_test.rb +54 -0
  57. data/test/{tc_command.rb → unit/command_test.rb} +20 -7
  58. data/test/unit/compound_command_test.rb +17 -0
  59. data/test/{tc_doc.rb → unit/doc_test.rb} +38 -51
  60. data/test/{tc_flag.rb → unit/flag_test.rb} +19 -25
  61. data/test/{tc_gli.rb → unit/gli_test.rb} +92 -49
  62. data/test/{tc_help.rb → unit/help_test.rb} +54 -113
  63. data/test/{init_simplecov.rb → unit/init_simplecov.rb} +0 -0
  64. data/test/{tc_options.rb → unit/options_test.rb} +4 -4
  65. data/test/unit/subcommand_parsing_test.rb +263 -0
  66. data/test/unit/subcommands_test.rb +245 -0
  67. data/test/{fake_std_out.rb → unit/support/fake_std_out.rb} +0 -0
  68. data/test/{config.yaml → unit/support/gli_test_config.yml} +1 -0
  69. data/test/unit/switch_test.rb +49 -0
  70. data/test/{tc_terminal.rb → unit/terminal_test.rb} +28 -3
  71. data/test/unit/test_helper.rb +13 -0
  72. data/test/unit/verbatim_wrapper_test.rb +24 -0
  73. metadata +86 -141
  74. data/.ruby-gemset +0 -1
  75. data/.ruby-version +0 -1
  76. data/.travis.yml +0 -12
  77. data/ObjectModel.graffle +0 -1191
  78. data/bin/report_on_rake_results +0 -10
  79. data/bin/test_all_rubies.sh +0 -6
  80. data/features/gli_executable.feature +0 -90
  81. data/features/gli_init.feature +0 -232
  82. data/features/step_definitions/gli_executable_steps.rb +0 -18
  83. data/features/step_definitions/gli_init_steps.rb +0 -11
  84. data/features/step_definitions/todo_steps.rb +0 -96
  85. data/features/support/env.rb +0 -54
  86. data/features/todo.feature +0 -449
  87. data/features/todo_legacy.feature +0 -128
  88. data/test/option_test_helper.rb +0 -13
  89. data/test/tc_compound_command.rb +0 -22
  90. data/test/tc_subcommand_parsing.rb +0 -104
  91. data/test/tc_subcommands.rb +0 -259
  92. data/test/tc_switch.rb +0 -55
  93. data/test/tc_verbatim_wrapper.rb +0 -36
  94. data/test/test_helper.rb +0 -20
@@ -31,6 +31,7 @@ module GLI
31
31
  end
32
32
  puts "Created #{rvmrc}"
33
33
  end
34
+ init_git(root_dir, project_name)
34
35
  end
35
36
  end
36
37
 
@@ -44,7 +45,7 @@ module GLI
44
45
  puts "Created #{root_dir}/#{project_name}/README.rdoc"
45
46
  File.open("#{root_dir}/#{project_name}/#{project_name}.rdoc",'w') do |file|
46
47
  file << "= #{project_name}\n\n"
47
- file << "Generate this with\n #{project_name} rdoc\nAfter you have described your command line interface"
48
+ file << "Generate this with\n #{project_name} _doc\nAfter you have described your command line interface"
48
49
  end
49
50
  puts "Created #{root_dir}/#{project_name}/#{project_name}.rdoc"
50
51
  end
@@ -55,7 +56,7 @@ module GLI
55
56
  file.puts <<EOS
56
57
  # Ensure we require the local version and not one we might have installed already
57
58
  require File.join([File.dirname(__FILE__),'lib','#{project_name}','version.rb'])
58
- spec = Gem::Specification.new do |s|
59
+ spec = Gem::Specification.new do |s|
59
60
  s.name = '#{project_name}'
60
61
  s.version = #{project_name_as_module_name(project_name)}::VERSION
61
62
  s.author = 'Your Name Here'
@@ -65,15 +66,14 @@ spec = Gem::Specification.new do |s|
65
66
  s.summary = 'A description of your project'
66
67
  s.files = `git ls-files`.split("\n")
67
68
  s.require_paths << 'lib'
68
- s.has_rdoc = true
69
69
  s.extra_rdoc_files = ['README.rdoc','#{project_name}.rdoc']
70
70
  s.rdoc_options << '--title' << '#{project_name}' << '--main' << 'README.rdoc' << '-ri'
71
71
  s.bindir = 'bin'
72
72
  s.executables << '#{project_name}'
73
- s.add_development_dependency('rake')
74
- s.add_development_dependency('rdoc')
75
- s.add_development_dependency('aruba')
76
- s.add_runtime_dependency('gli','#{GLI::VERSION}')
73
+ s.add_development_dependency('rake','~> 0.9.2')
74
+ s.add_development_dependency('rdoc', '~> 4.3')
75
+ s.add_development_dependency('minitest', '~> 5.14')
76
+ s.add_runtime_dependency('gli','~> #{GLI::VERSION}')
77
77
  end
78
78
  EOS
79
79
  end
@@ -114,12 +114,6 @@ require 'rubygems'
114
114
  require 'rubygems/package_task'
115
115
  require 'rdoc/task'
116
116
  EOS
117
- if create_test_dir
118
- file.puts <<EOS
119
- require 'cucumber'
120
- require 'cucumber/rake/task'
121
- EOS
122
- end
123
117
  file.puts <<EOS
124
118
  Rake::RDocTask.new do |rd|
125
119
  rd.main = "README.rdoc"
@@ -134,44 +128,19 @@ end
134
128
  EOS
135
129
  if create_test_dir
136
130
  file.puts <<EOS
137
- CUKE_RESULTS = 'results.html'
138
- CLEAN << CUKE_RESULTS
139
- desc 'Run features'
140
- Cucumber::Rake::Task.new(:features) do |t|
141
- opts = "features --format html -o \#{CUKE_RESULTS} --format progress -x"
142
- opts += " --tags \#{ENV['TAGS']}" if ENV['TAGS']
143
- t.cucumber_opts = opts
144
- t.fork = false
145
- end
146
-
147
- desc 'Run features tagged as work-in-progress (@wip)'
148
- Cucumber::Rake::Task.new('features:wip') do |t|
149
- tag_opts = ' --tags ~@pending'
150
- tag_opts = ' --tags @wip'
151
- t.cucumber_opts = "features --format html -o \#{CUKE_RESULTS} --format pretty -x -s\#{tag_opts}"
152
- t.fork = false
153
- end
154
-
155
- task :cucumber => :features
156
- task 'cucumber:wip' => 'features:wip'
157
- task :wip => 'features:wip'
158
- EOS
159
- end
160
- if create_test_dir
161
- file.puts <<EOS
162
131
  require 'rake/testtask'
163
132
  Rake::TestTask.new do |t|
164
133
  t.libs << "test"
165
134
  t.test_files = FileList['test/*_test.rb']
166
135
  end
167
136
 
168
- task :default => [:test,:features]
137
+ task :default => :test
169
138
  EOS
170
139
  File.open("#{root_dir}/#{project_name}/test/default_test.rb",'w') do |test_file|
171
140
  test_file.puts <<EOS
172
- require 'test_helper'
141
+ require_relative "test_helper"
173
142
 
174
- class DefaultTest < Test::Unit::TestCase
143
+ class DefaultTest < Minitest::Test
175
144
 
176
145
  def setup
177
146
  end
@@ -188,15 +157,10 @@ EOS
188
157
  puts "Created #{root_dir}/#{project_name}/test/default_test.rb"
189
158
  File.open("#{root_dir}/#{project_name}/test/test_helper.rb",'w') do |test_file|
190
159
  test_file.puts <<EOS
191
- require 'test/unit'
160
+ require "minitest/autorun"
192
161
 
193
162
  # Add test libraries you want to use here, e.g. mocha
194
-
195
- class Test::Unit::TestCase
196
-
197
- # Add global extensions to the test case class here
198
-
199
- end
163
+ # Add helper classes or methods here, too
200
164
  EOS
201
165
  end
202
166
  puts "Created #{root_dir}/#{project_name}/test/test_helper.rb"
@@ -210,54 +174,6 @@ EOS
210
174
  bundler_file.puts "gemspec"
211
175
  end
212
176
  puts "Created #{root_dir}/#{project_name}/Gemfile"
213
- if create_test_dir
214
- features_dir = File.join(root_dir,project_name,'features')
215
- FileUtils.mkdir features_dir
216
- FileUtils.mkdir File.join(features_dir,"step_definitions")
217
- FileUtils.mkdir File.join(features_dir,"support")
218
- File.open(File.join(features_dir,"#{project_name}.feature"),'w') do |file|
219
- file.puts <<EOS
220
- Feature: My bootstrapped app kinda works
221
- In order to get going on coding my awesome app
222
- I want to have aruba and cucumber setup
223
- So I don't have to do it myself
224
-
225
- Scenario: App just runs
226
- When I get help for "#{project_name}"
227
- Then the exit status should be 0
228
- EOS
229
- end
230
- File.open(File.join(features_dir,"step_definitions","#{project_name}_steps.rb"),'w') do |file|
231
- file.puts <<EOS
232
- When /^I get help for "([^"]*)"$/ do |app_name|
233
- @app_name = app_name
234
- step %(I run `\#{app_name} help`)
235
- end
236
-
237
- # Add more step definitions here
238
- EOS
239
- end
240
- File.open(File.join(features_dir,"support","env.rb"),'w') do |file|
241
- file.puts <<EOS
242
- require 'aruba/cucumber'
243
-
244
- ENV['PATH'] = "\#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}\#{File::PATH_SEPARATOR}\#{ENV['PATH']}"
245
- LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
246
-
247
- Before do
248
- # Using "announce" causes massive warnings on 1.9.2
249
- @puts = true
250
- @original_rubylib = ENV['RUBYLIB']
251
- ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
252
- end
253
-
254
- After do
255
- ENV['RUBYLIB'] = @original_rubylib
256
- end
257
- EOS
258
- end
259
- puts "Created #{features_dir}"
260
- end
261
177
  end
262
178
 
263
179
  def self.mk_binfile(root_dir,create_ext_dir,force,dry_run,project_name,commands)
@@ -278,82 +194,87 @@ rescue LoadError
278
194
  exit 64
279
195
  end
280
196
 
281
- include GLI::App
197
+ class App
198
+ extend GLI::App
199
+
200
+ program_desc 'Describe your application here'
282
201
 
283
- program_desc 'Describe your application here'
202
+ version #{project_name_as_module_name(project_name)}::VERSION
284
203
 
285
- version #{project_name_as_module_name(project_name)}::VERSION
204
+ subcommand_option_handling :normal
205
+ arguments :strict
286
206
 
287
- desc 'Describe some switch here'
288
- switch [:s,:switch]
207
+ desc 'Describe some switch here'
208
+ switch [:s,:switch]
289
209
 
290
- desc 'Describe some flag here'
291
- default_value 'the default'
292
- arg_name 'The name of the argument'
293
- flag [:f,:flagname]
210
+ desc 'Describe some flag here'
211
+ default_value 'the default'
212
+ arg_name 'The name of the argument'
213
+ flag [:f,:flagname]
294
214
  EOS
295
215
  first = true
296
216
  commands.each do |command|
297
217
  file.puts <<EOS
298
218
 
299
- desc 'Describe #{command} here'
300
- arg_name 'Describe arguments to #{command} here'
219
+ desc 'Describe #{command} here'
220
+ arg_name 'Describe arguments to #{command} here'
301
221
  EOS
302
222
  if first
303
223
  file.puts <<EOS
304
- command :#{command} do |c|
305
- c.desc 'Describe a switch to #{command}'
306
- c.switch :s
224
+ command :#{command} do |c|
225
+ c.desc 'Describe a switch to #{command}'
226
+ c.switch :s
307
227
 
308
- c.desc 'Describe a flag to #{command}'
309
- c.default_value 'default'
310
- c.flag :f
311
- c.action do |global_options,options,args|
228
+ c.desc 'Describe a flag to #{command}'
229
+ c.default_value 'default'
230
+ c.flag :f
231
+ c.action do |global_options,options,args|
312
232
 
313
- # Your command logic here
314
-
315
- # If you have any errors, just raise them
316
- # raise "that command made no sense"
233
+ # Your command logic here
317
234
 
318
- puts "#{command} command ran"
235
+ # If you have any errors, just raise them
236
+ # raise "that command made no sense"
237
+
238
+ puts "#{command} command ran"
239
+ end
319
240
  end
320
- end
321
241
  EOS
322
242
  else
323
243
  file.puts <<EOS
324
- command :#{command} do |c|
325
- c.action do |global_options,options,args|
326
- puts "#{command} command ran"
244
+ command :#{command} do |c|
245
+ c.action do |global_options,options,args|
246
+ puts "#{command} command ran"
247
+ end
327
248
  end
328
- end
329
249
  EOS
330
250
  end
331
251
  first = false
332
252
  end
333
253
  file.puts <<EOS
334
254
 
335
- pre do |global,command,options,args|
336
- # Pre logic here
337
- # Return true to proceed; false to abort and not call the
338
- # chosen command
339
- # Use skips_pre before a command to skip this block
340
- # on that command only
341
- true
342
- end
255
+ pre do |global,command,options,args|
256
+ # Pre logic here
257
+ # Return true to proceed; false to abort and not call the
258
+ # chosen command
259
+ # Use skips_pre before a command to skip this block
260
+ # on that command only
261
+ true
262
+ end
343
263
 
344
- post do |global,command,options,args|
345
- # Post logic here
346
- # Use skips_post before a command to skip this
347
- # block on that command only
348
- end
264
+ post do |global,command,options,args|
265
+ # Post logic here
266
+ # Use skips_post before a command to skip this
267
+ # block on that command only
268
+ end
349
269
 
350
- on_error do |exception|
351
- # Error logic here
352
- # return false to skip default error handling
353
- true
270
+ on_error do |exception|
271
+ # Error logic here
272
+ # return false to skip default error handling
273
+ true
274
+ end
354
275
  end
355
276
 
356
- exit run(ARGV)
277
+ exit App.run(ARGV)
357
278
  EOS
358
279
  puts "Created #{bin_file}"
359
280
  end
@@ -365,6 +286,14 @@ EOS
365
286
  true
366
287
  end
367
288
 
289
+ def self.init_git(root_dir, project_name)
290
+ project_dir = "#{root_dir}/#{project_name}"
291
+
292
+ unless system("git", "init", "--quiet", project_dir)
293
+ puts "There was a problem initializing Git. Your gemspec may not work until you have done a successful `git init`"
294
+ end
295
+ end
296
+
368
297
  def self.mkdirs(dirs,force,dry_run)
369
298
  exists = false
370
299
  if !force
data/lib/gli/dsl.rb CHANGED
@@ -36,6 +36,26 @@ module GLI
36
36
  @next_arg_options = options
37
37
  end
38
38
 
39
+ # Describes one of the arguments of the next command
40
+ #
41
+ # +name+:: A String that *briefly* describes the argument given to the following command.
42
+ # +options+:: Symbol or array of symbols to annotate this argument. This doesn't affect parsing, just
43
+ # the help output. Values recognized are:
44
+ # +:optional+:: indicates this argument is optional; will format it with square brackets
45
+ # +:multiple+:: indicates multiple values are accepted; will format appropriately
46
+ #
47
+ # Example:
48
+ # arg :output
49
+ # arg :input, :multiple
50
+ # command :pack do ...
51
+ #
52
+ # Produces the synopsis:
53
+ # app.rb [global options] pack output input...
54
+ def arg(name, options=[])
55
+ @next_arguments ||= []
56
+ @next_arguments << Argument.new(name, Array(options).flatten)
57
+ end
58
+
39
59
  # set the default value of the next flag or switch
40
60
  #
41
61
  # +val+:: The default value to be used for the following flag if the user doesn't specify one
@@ -53,8 +73,9 @@ module GLI
53
73
  # +:long_desc+:: the long_description, instead of using #long_desc
54
74
  # +:default_value+:: the default value, instead of using #default_value
55
75
  # +:arg_name+:: the arg name, instead of using #arg_name
56
- # +:must_match+:: A regexp that the flag's value must match
76
+ # +:must_match+:: A regexp that the flag's value must match or an array of allowable values
57
77
  # +:type+:: A Class (or object you passed to GLI::App#accept) to trigger type coversion
78
+ # +:multiple+:: if true, flag may be used multiple times and values are stored in an array
58
79
  #
59
80
  # Example:
60
81
  #
@@ -152,10 +173,12 @@ module GLI
152
173
  :description => @next_desc,
153
174
  :arguments_name => @next_arg_name,
154
175
  :arguments_options => @next_arg_options,
176
+ :arguments => @next_arguments,
155
177
  :long_desc => @next_long_desc,
156
178
  :skips_pre => @skips_pre,
157
179
  :skips_post => @skips_post,
158
180
  :skips_around => @skips_around,
181
+ :hide_commands_without_desc => @hide_commands_without_desc,
159
182
  }
160
183
  @commands_declaration_order ||= []
161
184
  if names.first.kind_of? Hash
@@ -177,6 +200,7 @@ module GLI
177
200
  yield command
178
201
  end
179
202
  clear_nexts
203
+ @next_arguments = []
180
204
  end
181
205
  alias :c :command
182
206
 
@@ -5,6 +5,21 @@ module GLI
5
5
  module StandardException
6
6
  def exit_code; 1; end
7
7
  end
8
+
9
+ # Hack to request help from within a command
10
+ # Will *not* be rethrown when GLI_DEBUG is ON
11
+ class RequestHelp < StandardError
12
+ include StandardException
13
+ def exit_code; 0; end
14
+
15
+ # The command for which the argument was unknown
16
+ attr_reader :command_in_context
17
+
18
+ def initialize(command_in_context)
19
+ @command_in_context = command_in_context
20
+ end
21
+ end
22
+
8
23
  # Indicates that the command line invocation was bad
9
24
  class BadCommandLine < StandardError
10
25
  include StandardException
@@ -44,6 +59,17 @@ module GLI
44
59
  end
45
60
  end
46
61
 
62
+ class MissingRequiredArgumentsException < BadCommandLine
63
+ # The command for which the argument was unknown
64
+ attr_reader :command_in_context
65
+ # +message+:: the error message to show the user
66
+ # +command+:: the command we were using to parse command-specific options
67
+ def initialize(message,command)
68
+ super(message)
69
+ @command_in_context = command
70
+ end
71
+ end
72
+
47
73
  # Indicates the bad command line was an unknown command argument
48
74
  class UnknownCommandArgument < CommandException
49
75
  end
data/lib/gli/flag.rb CHANGED
@@ -24,17 +24,18 @@ module GLI
24
24
  # :must_match:: a regexp that the flag's value must match
25
25
  # :type:: a class to convert the value to
26
26
  # :required:: if true, this flag must be specified on the command line
27
+ # :multiple:: if true, flag may be used multiple times and values are stored in an array
27
28
  # :mask:: if true, the default value of this flag will not be output in the help.
28
29
  # This is useful for password flags where you might not want to show it
29
30
  # on the command-line.
30
31
  def initialize(names,options)
31
32
  super(names,options)
32
33
  @argument_name = options[:arg_name] || "arg"
33
- @default_value = options[:default_value]
34
34
  @must_match = options[:must_match]
35
35
  @type = options[:type]
36
36
  @mask = options[:mask]
37
37
  @required = options[:required]
38
+ @multiple = options[:multiple]
38
39
  end
39
40
 
40
41
  # True if this flag is required on the command line
@@ -42,12 +43,32 @@ module GLI
42
43
  @required
43
44
  end
44
45
 
46
+ # True if the flag may be used multiple times.
47
+ def multiple?
48
+ @multiple
49
+ end
45
50
 
46
51
  def safe_default_value
47
52
  if @mask
48
53
  "********"
49
54
  else
50
- default_value
55
+ # This uses @default_value instead of the `default_value` method because
56
+ # this method is only used for display, and for flags that may be passed
57
+ # multiple times, we want to display whatever is set in the code as the
58
+ # the default, or the string "none" rather than displaying an empty
59
+ # array.
60
+ @default_value
61
+ end
62
+ end
63
+
64
+ # The default value for this flag. Uses the value passed if one is set;
65
+ # otherwise uses `[]` if the flag support multiple arguments and `nil` if
66
+ # it does not.
67
+ def default_value
68
+ if @default_value
69
+ @default_value
70
+ elsif @multiple
71
+ []
51
72
  end
52
73
  end
53
74
 
@@ -1,20 +1,35 @@
1
1
  module GLI
2
2
  # Parses the command-line options using an actual +OptionParser+
3
3
  class GLIOptionParser
4
- def initialize(commands,flags,switches,accepts,default_command = nil,subcommand_option_handling_strategy=:legacy)
5
- command_finder = CommandFinder.new(commands,default_command || "help")
4
+ attr_accessor :options
5
+
6
+ DEFAULT_OPTIONS = {
7
+ :default_command => nil,
8
+ :autocomplete => true,
9
+ :subcommand_option_handling_strategy => :legacy,
10
+ :argument_handling_strategy => :loose
11
+ }
12
+
13
+ def initialize(commands,flags,switches,accepts, options={})
14
+ self.options = DEFAULT_OPTIONS.merge(options)
15
+
16
+ command_finder = CommandFinder.new(commands,
17
+ :default_command => (options[:default_command] || :help),
18
+ :autocomplete => options[:autocomplete])
6
19
  @global_option_parser = GlobalOptionParser.new(OptionParserFactory.new(flags,switches,accepts),command_finder,flags)
7
20
  @accepts = accepts
8
- @subcommand_option_handling_strategy = subcommand_option_handling_strategy
21
+ if options[:argument_handling_strategy] == :strict && options[:subcommand_option_handling_strategy] != :normal
22
+ raise ArgumentError, "To use strict argument handling, you must enable normal subcommand_option_handling, e.g. subcommand_option_handling :normal"
23
+ end
9
24
  end
10
25
 
11
26
  # Given the command-line argument array, returns an OptionParsingResult
12
27
  def parse_options(args) # :nodoc:
13
- option_parser_class = self.class.const_get("#{@subcommand_option_handling_strategy.to_s.capitalize}CommandOptionParser")
28
+ option_parser_class = self.class.const_get("#{options[:subcommand_option_handling_strategy].to_s.capitalize}CommandOptionParser")
14
29
  OptionParsingResult.new.tap { |parsing_result|
15
30
  parsing_result.arguments = args
16
31
  parsing_result = @global_option_parser.parse!(parsing_result)
17
- option_parser_class.new(@accepts).parse!(parsing_result)
32
+ option_parser_class.new(@accepts).parse!(parsing_result, options[:argument_handling_strategy], options[:autocomplete])
18
33
  }
19
34
  end
20
35
 
@@ -30,30 +45,63 @@ module GLI
30
45
  def parse!(parsing_result)
31
46
  parsing_result.arguments = GLIOptionBlockParser.new(@option_parser_factory,UnknownGlobalArgument).parse!(parsing_result.arguments)
32
47
  parsing_result.global_options = @option_parser_factory.options_hash_with_defaults_set!
33
- command_name = if parsing_result.global_options[:help]
48
+ command_name = if parsing_result.global_options[:help] || parsing_result.global_options[:version]
34
49
  "help"
35
50
  else
36
51
  parsing_result.arguments.shift
37
52
  end
38
53
  parsing_result.command = @command_finder.find_command(command_name)
39
54
  unless command_name == 'help'
40
- verify_required_options!(@flags,parsing_result.global_options)
55
+ verify_required_options!(@flags, parsing_result.command, parsing_result.global_options)
41
56
  end
42
57
  parsing_result
43
58
  end
44
59
 
45
60
  protected
61
+ def verify_arguments!(arguments, command)
62
+ # Lets assume that if the user sets a 'arg_name' for the command it is for a complex scenario
63
+ # and we should not validate the arguments
64
+ return unless command.arguments_description.empty?
65
+
66
+ # Go through all declared arguments for the command, counting the min and max number
67
+ # of arguments
68
+ min_number_of_arguments = 0
69
+ max_number_of_arguments = 0
70
+ command.arguments.each do |arg|
71
+ if arg.optional?
72
+ max_number_of_arguments = max_number_of_arguments + 1
73
+ else
74
+ min_number_of_arguments = min_number_of_arguments + 1
75
+ max_number_of_arguments = max_number_of_arguments + 1
76
+ end
77
+
78
+ # Special case, as soon as we have a 'multiple' arguments, all bets are off for the
79
+ # maximum number of arguments !
80
+ if arg.multiple?
81
+ max_number_of_arguments = 99999
82
+ end
83
+ end
46
84
 
47
- def verify_required_options!(flags,options)
85
+ # Now validate the number of arguments
86
+ if arguments.size < min_number_of_arguments
87
+ raise MissingRequiredArgumentsException.new("Not enough arguments for command", command)
88
+ end
89
+ if arguments.size > max_number_of_arguments
90
+ raise MissingRequiredArgumentsException.new("Too many arguments for command", command)
91
+ end
92
+ end
93
+
94
+ def verify_required_options!(flags, command, options)
48
95
  missing_required_options = flags.values.
49
96
  select(&:required?).
50
97
  reject { |option|
51
98
  options[option.name] != nil
52
99
  }
53
100
  unless missing_required_options.empty?
54
- raise BadCommandLine, missing_required_options.map { |option|
101
+ missing_required_options.sort!
102
+ raise MissingRequiredArgumentsException.new(missing_required_options.map { |option|
55
103
  "#{option.name} is required"
56
- }.join(' ')
104
+ }.join(', '), command)
57
105
  end
58
106
  end
59
107
  end
@@ -64,12 +112,12 @@ module GLI
64
112
  end
65
113
 
66
114
  def error_handler
67
- lambda { |message,extra_error_context|
115
+ lambda { |message,extra_error_context|
68
116
  raise UnknownCommandArgument.new(message,extra_error_context)
69
117
  }
70
118
  end
71
119
 
72
- def parse!(parsing_result)
120
+ def parse!(parsing_result,argument_handling_strategy,autocomplete)
73
121
  parsed_command_options = {}
74
122
  command = parsing_result.command
75
123
  arguments = nil
@@ -83,10 +131,10 @@ module GLI
83
131
  arguments = option_block_parser.parse!(arguments)
84
132
 
85
133
  parsed_command_options[command] = option_parser_factory.options_hash_with_defaults_set!
86
- command_finder = CommandFinder.new(command.commands,command.get_default_command)
134
+ command_finder = CommandFinder.new(command.commands, :default_command => command.get_default_command, :autocomplete => autocomplete)
87
135
  next_command_name = arguments.shift
88
136
 
89
- verify_required_options!(command.flags,parsed_command_options[command])
137
+ verify_required_options!(command.flags, command, parsed_command_options[command])
90
138
 
91
139
  begin
92
140
  command = command_finder.find_command(next_command_name)
@@ -120,13 +168,17 @@ module GLI
120
168
  parsing_result.command_options = command_options
121
169
  parsing_result.command = command
122
170
  parsing_result.arguments = Array(arguments.compact)
171
+
172
+ # Lets validate the arguments now that we know for sure the command that is invoked
173
+ verify_arguments!(parsing_result.arguments, parsing_result.command) if argument_handling_strategy == :strict
174
+
123
175
  parsing_result
124
176
  end
125
177
 
126
178
  end
127
179
 
128
180
  class LegacyCommandOptionParser < NormalCommandOptionParser
129
- def parse!(parsing_result)
181
+ def parse!(parsing_result,argument_handling_strategy,autocomplete)
130
182
  command = parsing_result.command
131
183
  option_parser_factory = OptionParserFactory.for_command(command,@accepts)
132
184
  option_block_parser = LegacyCommandOptionBlockParser.new(option_parser_factory, self.error_handler)
@@ -135,15 +187,15 @@ module GLI
135
187
  parsing_result.arguments = option_block_parser.parse!(parsing_result.arguments)
136
188
  parsing_result.command_options = option_parser_factory.options_hash_with_defaults_set!
137
189
 
138
- subcommand,args = find_subcommand(command,parsing_result.arguments)
190
+ subcommand,args = find_subcommand(command,parsing_result.arguments,autocomplete)
139
191
  parsing_result.command = subcommand
140
192
  parsing_result.arguments = args
141
- verify_required_options!(command.flags,parsing_result.command_options)
193
+ verify_required_options!(command.flags, parsing_result.command, parsing_result.command_options)
142
194
  end
143
195
 
144
196
  private
145
197
 
146
- def find_subcommand(command,arguments)
198
+ def find_subcommand(command,arguments,autocomplete)
147
199
  arguments = Array(arguments)
148
200
  command_name = if arguments.empty?
149
201
  nil
@@ -152,15 +204,15 @@ module GLI
152
204
  end
153
205
 
154
206
  default_command = command.get_default_command
155
- finder = CommandFinder.new(command.commands,default_command.to_s)
207
+ finder = CommandFinder.new(command.commands, :default_command => default_command.to_s, :autocomplete => autocomplete)
156
208
 
157
209
  begin
158
210
  results = [finder.find_command(command_name),arguments[1..-1]]
159
- find_subcommand(results[0],results[1])
211
+ find_subcommand(results[0],results[1],autocomplete)
160
212
  rescue UnknownCommand, AmbiguousCommand
161
213
  begin
162
214
  results = [finder.find_command(default_command.to_s),arguments]
163
- find_subcommand(results[0],results[1])
215
+ find_subcommand(results[0],results[1],autocomplete)
164
216
  rescue UnknownCommand, AmbiguousCommand
165
217
  [command,arguments]
166
218
  end