gli 2.9.0 → 2.20.0

Sign up to get free protection for your applications and to get access to all the features.
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