gli 2.9.0 → 2.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +28 -0
- data/.gitignore +3 -3
- data/.tool-versions +1 -0
- data/Gemfile +0 -2
- data/README.rdoc +29 -18
- data/Rakefile +15 -37
- data/bin/ci +29 -0
- data/bin/gli +24 -54
- data/bin/rake +29 -0
- data/bin/setup +5 -0
- data/exe/gli +68 -0
- data/gli.gemspec +20 -24
- data/gli.rdoc +9 -9
- data/lib/gli/app.rb +42 -8
- data/lib/gli/app_support.rb +17 -5
- data/lib/gli/argument.rb +20 -0
- data/lib/gli/command.rb +27 -2
- data/lib/gli/command_finder.rb +42 -25
- data/lib/gli/command_support.rb +13 -7
- data/lib/gli/commands/doc.rb +9 -3
- data/lib/gli/commands/help.rb +2 -1
- data/lib/gli/commands/help_modules/arg_name_formatter.rb +29 -2
- data/lib/gli/commands/help_modules/command_help_format.rb +19 -1
- data/lib/gli/commands/help_modules/full_synopsis_formatter.rb +5 -4
- data/lib/gli/commands/help_modules/global_help_format.rb +1 -1
- data/lib/gli/commands/help_modules/options_formatter.rb +4 -6
- data/lib/gli/commands/initconfig.rb +3 -6
- data/lib/gli/commands/rdoc_document_listener.rb +2 -1
- data/lib/gli/commands/scaffold.rb +71 -142
- data/lib/gli/dsl.rb +25 -1
- data/lib/gli/exceptions.rb +26 -0
- data/lib/gli/flag.rb +23 -2
- data/lib/gli/gli_option_parser.rb +73 -21
- data/lib/gli/option_parser_factory.rb +10 -3
- data/lib/gli/options.rb +2 -2
- data/lib/gli/switch.rb +4 -0
- data/lib/gli/terminal.rb +6 -2
- data/lib/gli/version.rb +1 -1
- data/lib/gli.rb +2 -0
- data/object-model.dot +29 -0
- data/object-model.png +0 -0
- data/test/apps/todo/Gemfile +1 -1
- data/test/apps/todo/bin/todo +16 -6
- data/test/apps/todo/lib/todo/commands/create.rb +48 -18
- data/test/apps/todo/lib/todo/commands/list.rb +48 -35
- data/test/apps/todo/lib/todo/commands/ls.rb +25 -24
- data/test/apps/todo/lib/todo/commands/make.rb +42 -39
- data/test/apps/todo/todo.gemspec +1 -2
- data/test/apps/todo_legacy/todo.gemspec +1 -2
- data/test/apps/todo_plugins/commands/third.rb +2 -0
- data/test/integration/gli_cli_test.rb +69 -0
- data/test/integration/gli_powered_app_test.rb +52 -0
- data/test/integration/scaffold_test.rb +30 -0
- data/test/integration/test_helper.rb +52 -0
- data/test/unit/command_finder_test.rb +54 -0
- data/test/{tc_command.rb → unit/command_test.rb} +20 -7
- data/test/unit/compound_command_test.rb +17 -0
- data/test/{tc_doc.rb → unit/doc_test.rb} +38 -51
- data/test/{tc_flag.rb → unit/flag_test.rb} +19 -25
- data/test/{tc_gli.rb → unit/gli_test.rb} +92 -49
- data/test/{tc_help.rb → unit/help_test.rb} +54 -113
- data/test/{init_simplecov.rb → unit/init_simplecov.rb} +0 -0
- data/test/{tc_options.rb → unit/options_test.rb} +4 -4
- data/test/unit/subcommand_parsing_test.rb +263 -0
- data/test/unit/subcommands_test.rb +245 -0
- data/test/{fake_std_out.rb → unit/support/fake_std_out.rb} +0 -0
- data/test/{config.yaml → unit/support/gli_test_config.yml} +1 -0
- data/test/unit/switch_test.rb +49 -0
- data/test/{tc_terminal.rb → unit/terminal_test.rb} +28 -3
- data/test/unit/test_helper.rb +13 -0
- data/test/unit/verbatim_wrapper_test.rb +24 -0
- metadata +86 -141
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -12
- data/ObjectModel.graffle +0 -1191
- data/bin/report_on_rake_results +0 -10
- data/bin/test_all_rubies.sh +0 -6
- data/features/gli_executable.feature +0 -90
- data/features/gli_init.feature +0 -232
- data/features/step_definitions/gli_executable_steps.rb +0 -18
- data/features/step_definitions/gli_init_steps.rb +0 -11
- data/features/step_definitions/todo_steps.rb +0 -96
- data/features/support/env.rb +0 -54
- data/features/todo.feature +0 -449
- data/features/todo_legacy.feature +0 -128
- data/test/option_test_helper.rb +0 -13
- data/test/tc_compound_command.rb +0 -22
- data/test/tc_subcommand_parsing.rb +0 -104
- data/test/tc_subcommands.rb +0 -259
- data/test/tc_switch.rb +0 -55
- data/test/tc_verbatim_wrapper.rb +0 -36
- 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}
|
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('
|
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 =>
|
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
|
-
|
141
|
+
require_relative "test_helper"
|
173
142
|
|
174
|
-
class DefaultTest < Test
|
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
|
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
|
-
|
197
|
+
class App
|
198
|
+
extend GLI::App
|
199
|
+
|
200
|
+
program_desc 'Describe your application here'
|
282
201
|
|
283
|
-
|
202
|
+
version #{project_name_as_module_name(project_name)}::VERSION
|
284
203
|
|
285
|
-
|
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
|
-
|
306
|
-
|
224
|
+
command :#{command} do |c|
|
225
|
+
c.desc 'Describe a switch to #{command}'
|
226
|
+
c.switch :s
|
307
227
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
326
|
-
|
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
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
346
|
-
|
347
|
-
|
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
|
-
|
352
|
-
|
353
|
-
|
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
|
|
data/lib/gli/exceptions.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
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
|
-
|
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("#{
|
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
|
-
|
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
|
-
|
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
|