benry-cli 0.1.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.
- checksums.yaml +7 -0
- data/README.md +428 -0
- data/Rakefile +108 -0
- data/benry-cli.gemspec +30 -0
- data/lib/benry/cli.rb +595 -0
- data/test/cli_test.rb +1025 -0
- metadata +79 -0
data/benry-cli.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'benry-cli'
|
5
|
+
spec.version = '$Release: 0.1.0 $'.split()[1]
|
6
|
+
spec.author = 'kwatch'
|
7
|
+
spec.email = 'kwatch@gmail.com'
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
9
|
+
spec.homepage = 'https://github.com/kwatch/benry/tree/ruby/benry-cli'
|
10
|
+
spec.summary = "MVC-like framework for command-line application"
|
11
|
+
spec.description = <<-'END'
|
12
|
+
MVC-like framework for command-line application such as Git or SVN.
|
13
|
+
END
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.files = Dir[
|
16
|
+
'README.md', 'CHANGES.md', 'MIT-LICENSE',
|
17
|
+
'Rakefile', 'benry-cli.gemspec',
|
18
|
+
'bin/*',
|
19
|
+
'lib/**/*.rb',
|
20
|
+
'test/**/*.rb',
|
21
|
+
]
|
22
|
+
#spec.executables = ['benry-cli']
|
23
|
+
spec.bindir = 'bin'
|
24
|
+
spec.require_path = 'lib'
|
25
|
+
spec.test_files = Dir['test/**/*_test.rb']
|
26
|
+
#spec.extra_rdoc_files = ['README.rdoc', 'CHANGES.md']
|
27
|
+
|
28
|
+
spec.add_development_dependency 'minitest' , '~> 5.8'
|
29
|
+
spec.add_development_dependency 'minitest-ok' , '~> 0.2'
|
30
|
+
end
|
data/lib/benry/cli.rb
ADDED
@@ -0,0 +1,595 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
###
|
4
|
+
### $Release: 0.1.0 $
|
5
|
+
### $Copyright: copyright(c) 2016 kuwata-lab.com all rights reserved $
|
6
|
+
### $License: MIT License $
|
7
|
+
###
|
8
|
+
|
9
|
+
|
10
|
+
module Benry
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
module Benry::CLI
|
15
|
+
|
16
|
+
|
17
|
+
class OptionDefinitionError < StandardError
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class OptionError < StandardError
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
class OptionSchema
|
26
|
+
|
27
|
+
def initialize(name, short, long, argname, argflag, desc, &callback)
|
28
|
+
@name = name
|
29
|
+
@short = short
|
30
|
+
@long = long
|
31
|
+
@argname = argname
|
32
|
+
@argflag = argflag # :required, :optional, or nil
|
33
|
+
@desc = desc
|
34
|
+
@callback = callback
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :name, :short, :long, :argname, :argflag, :desc, :callback
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
return (
|
41
|
+
self.class == other.class \
|
42
|
+
&& @short == other.short \
|
43
|
+
&& @long == other.long \
|
44
|
+
&& @argname == other.argname \
|
45
|
+
&& @argflag == other.argflag \
|
46
|
+
&& @desc == other.desc \
|
47
|
+
&& @callback == other.callback
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def arg_required?
|
52
|
+
return @argflag == :required
|
53
|
+
end
|
54
|
+
|
55
|
+
def arg_optional?
|
56
|
+
return @argflag == :optional
|
57
|
+
end
|
58
|
+
|
59
|
+
def arg_nothing?
|
60
|
+
return @argflag == nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.parse(defstr, desc, name: nil, &callback)
|
64
|
+
#; [!cy1ux] regards canonical name of '-f NAME #file' as 'file'.
|
65
|
+
defstr = defstr.strip()
|
66
|
+
defstr = defstr.sub(/\s+\#(\w+)\z/, '')
|
67
|
+
name ||= $1
|
68
|
+
#; [!fdh36] can parse '-v, --version' (short + long).
|
69
|
+
#; [!jkmee] can parse '-v' (short)
|
70
|
+
#; [!uc2en] can parse '--version' (long).
|
71
|
+
#; [!sy157] can parse '-f, --file=FILE' (short + long + required-arg).
|
72
|
+
#; [!wrjqa] can parse '-f FILE' (short + required-arg).
|
73
|
+
#; [!ip99s] can parse '--file=FILE' (long + required-arg).
|
74
|
+
#; [!9pmv8] can parse '-i, --indent[=N]' (short + long + optional-arg).
|
75
|
+
#; [!ooo42] can parse '-i[N]' (short + optional-arg).
|
76
|
+
#; [!o93c7] can parse '--indent[=N]' (long + optional-arg).
|
77
|
+
#; [!gzuhx] can parse string with extra spaces.
|
78
|
+
case defstr
|
79
|
+
when /\A-(\w),\s*--(\w[-\w]*)(?:=(\S+)|\[=(\S+)\])?\z/ ; arr = [$1, $2, $3, $4]
|
80
|
+
when /\A-(\w)(?:\s+(\S+)|\[(\S+)\])?\z/ ; arr = [$1, nil, $2, $3]
|
81
|
+
when /\A--(\w[-\w]*)(?:=(\S+)|\[=(\S+)\])?\z/ ; arr = [nil, $1, $2, $3]
|
82
|
+
else
|
83
|
+
#; [!1769n] raises error when invalid format.
|
84
|
+
raise OptionDefinitionError.new("'#{defstr}': failed to parse option definition.")
|
85
|
+
end
|
86
|
+
short, long, arg_required, arg_optional = arr
|
87
|
+
#; [!j2wgf] raises error when '-i [N]' specified.
|
88
|
+
defstr !~ /\A-\w\s+\[/ or
|
89
|
+
raise OptionDefinitionError.new("'#{defstr}': failed to parse option definition"+\
|
90
|
+
" due to extra space before '['"+\
|
91
|
+
" (should be '#{defstr.sub(/\s+/, '')}').")
|
92
|
+
#; [!6f4xx] uses long name or short name as option name when option name is not specfied.
|
93
|
+
name = name || long || short
|
94
|
+
#
|
95
|
+
argname = arg_required || arg_optional
|
96
|
+
argflag = arg_required ? :required \
|
97
|
+
: arg_optional ? :optional : nil
|
98
|
+
return self.new(name.to_s, short, long, argname, argflag, desc, &callback)
|
99
|
+
end
|
100
|
+
|
101
|
+
def option_string
|
102
|
+
#; [!pdaz3] builds option definition string.
|
103
|
+
s = ""
|
104
|
+
case
|
105
|
+
when @short && @long ; s << "-#{@short}, --#{@long}"
|
106
|
+
when @short ; s << "-#{@short}"
|
107
|
+
when @long ; s << " --#{@long}"
|
108
|
+
else
|
109
|
+
raise "unreachable"
|
110
|
+
end
|
111
|
+
#
|
112
|
+
case
|
113
|
+
when arg_required? ; s << (@long ? "=#{@argname}" : " #{@argname}")
|
114
|
+
when arg_optional? ; s << (@long ? "[=#{@argname}]" : "[#{@argname}]")
|
115
|
+
else ; nil
|
116
|
+
end
|
117
|
+
#
|
118
|
+
return s
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
class OptionParser
|
125
|
+
|
126
|
+
def initialize(option_schemas=[])
|
127
|
+
#; [!bflls] takes array of option schema.
|
128
|
+
@option_schemas = option_schemas.collect {|x|
|
129
|
+
case x
|
130
|
+
when OptionSchema ; x
|
131
|
+
when Array ; OptionSchema.parse(*x)
|
132
|
+
else
|
133
|
+
raise OptionDefinitionError.new("#{x.inspect}: invalid option schema.")
|
134
|
+
end
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def option(symbol, defstr=nil, desc=nil, &callback)
|
139
|
+
#; [!s59ly] accepts option definition string and description.
|
140
|
+
#; [!2gfnh] recognizes first argument as option name if it is a symbol.
|
141
|
+
unless symbol.is_a?(Symbol)
|
142
|
+
symbol, defstr, desc = nil, symbol, defstr
|
143
|
+
end
|
144
|
+
@option_schemas << OptionSchema.parse(defstr, desc, name: symbol, &callback)
|
145
|
+
#; [!fv5g4] return self in order to chain method call.
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
def each_option_string
|
150
|
+
#; [!luro4] yields each option string and description.
|
151
|
+
@option_schemas.each do |schema|
|
152
|
+
yield schema.option_string, schema.desc
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def each_option_schema(&callback)
|
157
|
+
#; [!ycgdm] yields each option schema.
|
158
|
+
@option_schemas.each(&callback)
|
159
|
+
end
|
160
|
+
|
161
|
+
def err(msg)
|
162
|
+
OptionError.new(msg)
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse(args)
|
166
|
+
#; [!5jfhv] returns command-line options as hash object.
|
167
|
+
#; [!06iq3] removes command-line options from args.
|
168
|
+
#; [!j2fda] stops command-line parsing when '-' found in args.
|
169
|
+
option_values = {}
|
170
|
+
while args[0] && args[0].start_with?('-') && args[0] != '-'
|
171
|
+
argstr = args.shift
|
172
|
+
#; [!31h46] stops parsing when '--' appears in args.
|
173
|
+
if argstr == '--'
|
174
|
+
break
|
175
|
+
#; [!w5dpy] can parse long options.
|
176
|
+
elsif argstr.start_with?('--')
|
177
|
+
parse_long_option(argstr, option_values)
|
178
|
+
#; [!mov8e] can parse short options.
|
179
|
+
else
|
180
|
+
parse_short_option(args, argstr, option_values)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
return option_values
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def parse_long_option(argstr, option_values)
|
189
|
+
argstr =~ /\A--(\w[-\w]*)(?:=(.*))?\z/
|
190
|
+
long, val = $1, $2
|
191
|
+
#; [!w67gl] raises error when long option is unknown.
|
192
|
+
opt = @option_schemas.find {|x| x.long == long } or
|
193
|
+
raise err("--#{long}: unknown option.")
|
194
|
+
#; [!kyd1j] raises error when required argument of long option is missing.
|
195
|
+
if opt.arg_required?
|
196
|
+
val or
|
197
|
+
raise err("#{argstr}: argument required.")
|
198
|
+
#; [!wuyrh] uses true as default value of optional argument of long option.
|
199
|
+
elsif opt.arg_optional?
|
200
|
+
val ||= true
|
201
|
+
#; [!91b2j] raises error when long option takes no argument but specified.
|
202
|
+
else
|
203
|
+
val.nil? or
|
204
|
+
raise err("#{argstr}: unexpected argument.")
|
205
|
+
val = true
|
206
|
+
end
|
207
|
+
#; [!9td8b] invokes callback with long option value if callback exists.
|
208
|
+
#; [!1hak2] invokes callback with long option values as 2nd argument.
|
209
|
+
begin
|
210
|
+
if (pr = opt.callback)
|
211
|
+
val = pr.arity == 2 ? pr.call(val, option_values) : pr.call(val)
|
212
|
+
end
|
213
|
+
rescue => ex
|
214
|
+
#; [!nkqln] regards RuntimeError callback raised as long option error.
|
215
|
+
raise unless ex.class == RuntimeError
|
216
|
+
raise err("#{argstr}: #{ex.message}")
|
217
|
+
end
|
218
|
+
#
|
219
|
+
option_values[opt.name] = val
|
220
|
+
end
|
221
|
+
|
222
|
+
def parse_short_option(args, argstr, option_values)
|
223
|
+
n = argstr.length
|
224
|
+
i = 0
|
225
|
+
while (i += 1) < n
|
226
|
+
char = argstr[i]
|
227
|
+
#; [!wr58v] raises error when unknown short option specified.
|
228
|
+
opt = @option_schemas.find {|x| x.short == char } or
|
229
|
+
raise err("-#{char}: unknown option.")
|
230
|
+
#; [!jzdcr] raises error when requried argument of short option is missing.
|
231
|
+
if opt.arg_required?
|
232
|
+
val = argstr[(i+1)..-1]
|
233
|
+
val = args.shift if val.empty?
|
234
|
+
val or
|
235
|
+
raise err("-#{char}: argument required.")
|
236
|
+
i = n
|
237
|
+
#; [!hnki9] uses true as default value of optional argument of short option.
|
238
|
+
elsif opt.arg_optional?
|
239
|
+
val = argstr[(i+1)..-1]
|
240
|
+
val = true if val.empty?
|
241
|
+
i = n
|
242
|
+
#; [!8gj65] uses true as value of short option which takes no argument.
|
243
|
+
else
|
244
|
+
val = true
|
245
|
+
end
|
246
|
+
#; [!l6gss] invokes callback with short option value if exists.
|
247
|
+
#; [!g4pld] invokes callback with short option values as 2nd argument.
|
248
|
+
begin
|
249
|
+
if (pr = opt.callback)
|
250
|
+
val = pr.arity == 2 ? pr.call(val, option_values) : pr.call(val)
|
251
|
+
end
|
252
|
+
rescue => ex
|
253
|
+
#; [!d4mgr] regards RuntimeError callback raised as short option error.
|
254
|
+
raise unless ex.class == RuntimeError
|
255
|
+
space = opt.arg_required? ? ' ' : ''
|
256
|
+
raise err("-#{char}#{space}#{val}: #{ex.message}")
|
257
|
+
end
|
258
|
+
#
|
259
|
+
option_values[opt.name] = val
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
|
265
|
+
|
266
|
+
class Action
|
267
|
+
|
268
|
+
SUBCLASSES = []
|
269
|
+
|
270
|
+
def self.inherited(subclass)
|
271
|
+
#; [!al5pr] provides @action and @option for subclass.
|
272
|
+
subclass.class_eval do
|
273
|
+
@__mappings = []
|
274
|
+
@__defining = nil
|
275
|
+
@action = proc do |action_name, desc|
|
276
|
+
option_schemas = []
|
277
|
+
option_schemas << OptionSchema.parse("-h, --help", "print help message")
|
278
|
+
method_name = nil
|
279
|
+
@__defining = [action_name, desc, option_schemas, method_name]
|
280
|
+
end
|
281
|
+
#; [!ymtsg] allows block argument to @option.
|
282
|
+
@option = proc do |symbol, defstr, desc, &callback|
|
283
|
+
#; [!v76cf] can take symbol as kwarg name.
|
284
|
+
if ! symbol.is_a?(Symbol)
|
285
|
+
defstr, desc = symbol, defstr
|
286
|
+
symbol = nil
|
287
|
+
end
|
288
|
+
#; [!di9na] raises error when @option.() called without @action.().
|
289
|
+
@__defining or
|
290
|
+
raise OptionDefinitionError.new("@option.(#{defstr.inspect}): @action.() should be called prior to @option.().")
|
291
|
+
option_schemas = @__defining[2]
|
292
|
+
option_schemas << OptionSchema.parse(defstr, desc, name: symbol, &callback)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
#; [!4otr6] registers subclass.
|
296
|
+
SUBCLASSES << subclass
|
297
|
+
end
|
298
|
+
|
299
|
+
def self.method_added(method_name)
|
300
|
+
#; [!syzvc] registers action with method.
|
301
|
+
if @__defining
|
302
|
+
@__defining[-1] = method_name
|
303
|
+
@__mappings << @__defining
|
304
|
+
#; [!m7y8p] clears current action definition.
|
305
|
+
@__defining = nil
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def self.prefix
|
310
|
+
return @prefix
|
311
|
+
end
|
312
|
+
|
313
|
+
def self.prefix=(prefix)
|
314
|
+
@prefix = prefix
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
class ActionInfo
|
321
|
+
|
322
|
+
def initialize(full_name, name, desc, option_schemas, action_class, action_method)
|
323
|
+
@full_name = full_name
|
324
|
+
@name = name
|
325
|
+
@desc = desc
|
326
|
+
@option_schemas = option_schemas
|
327
|
+
@action_class = action_class
|
328
|
+
@action_method = action_method
|
329
|
+
end
|
330
|
+
|
331
|
+
attr_reader :full_name, :name, :desc, :option_schemas, :action_class, :action_method
|
332
|
+
|
333
|
+
def ==(other)
|
334
|
+
return (
|
335
|
+
self.class == other.class \
|
336
|
+
&& @full_name == other.full_name \
|
337
|
+
&& @name == other.name \
|
338
|
+
&& @desc == other.desc \
|
339
|
+
&& @option_schemas == other.option_schemas \
|
340
|
+
&& @action_class == other.action_class \
|
341
|
+
&& @action_method == other.action_method
|
342
|
+
)
|
343
|
+
end
|
344
|
+
|
345
|
+
def help_message(command)
|
346
|
+
#; [!hjq5l] builds help message.
|
347
|
+
meth = @action_class.new.method(@action_method)
|
348
|
+
argstr = ""
|
349
|
+
meth.parameters.each do |kind, name|
|
350
|
+
#; [!7qmnz] replaces '_' in arg names with '-'.
|
351
|
+
#; [!s6p09] converts arg name 'file_or_dir' into 'file|dir'.
|
352
|
+
name_str = name.to_s.gsub('_or_', '|').gsub('_', '-')
|
353
|
+
case kind
|
354
|
+
when :req ; argstr << " <#{name_str}>"
|
355
|
+
when :opt ; argstr << " [<#{name_str}>]"
|
356
|
+
when :rest; argstr << " [<#{name_str}>...]"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
#; [!6m50d] don't show non-described options.
|
360
|
+
pairs = @option_schemas.collect {|opt| [opt.option_string, opt.desc] }
|
361
|
+
pairs = pairs.select {|optstr, desc| desc }
|
362
|
+
#
|
363
|
+
width = pairs.collect {|pair| pair[0].length }.max || 0
|
364
|
+
width = [width, 20].max
|
365
|
+
width = [width, 35].min
|
366
|
+
#
|
367
|
+
msg = ""
|
368
|
+
#msg << "#{command} #{@full_name} -- #{@desc}\n"
|
369
|
+
msg << "#{@desc}\n"
|
370
|
+
msg << "\n"
|
371
|
+
msg << "Usage:\n"
|
372
|
+
msg << " #{command} #{@full_name} [<options>]#{argstr}\n"
|
373
|
+
msg << "\n" unless pairs.empty?
|
374
|
+
msg << "Options:\n" unless pairs.empty?
|
375
|
+
pairs.each do |option_string, desc|
|
376
|
+
msg << " %-#{width}s : %s\n" % [option_string, desc]
|
377
|
+
end
|
378
|
+
return msg
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
383
|
+
|
384
|
+
class Application
|
385
|
+
|
386
|
+
def self._setup_app_class(klass) # :nodoc:
|
387
|
+
klass.class_eval do
|
388
|
+
#; [!8swia] global option '-h' and '--help' are enabled by default.
|
389
|
+
#; [!vh08n] global option '--version' is enabled by defaut.
|
390
|
+
@_global_option_schemas = [
|
391
|
+
OptionSchema.parse("-h, --help", "print help message"),
|
392
|
+
OptionSchema.parse(" --version", "print version"),
|
393
|
+
]
|
394
|
+
#; [!b09pv] provides @global_option in subclass.
|
395
|
+
@global_option = proc do |defstr, desc, &callback|
|
396
|
+
@_global_option_schemas << OptionSchema.parse(defstr, desc, &callback)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
self._setup_app_class(self)
|
402
|
+
|
403
|
+
def self.inherited(subclass) # :nodoc:
|
404
|
+
self._setup_app_class(subclass)
|
405
|
+
end
|
406
|
+
|
407
|
+
def initialize(desc=nil, version: '0.0', script_name: nil, action_classes: nil)
|
408
|
+
@desc = desc
|
409
|
+
@version = version
|
410
|
+
@script_name = script_name || File.basename($0)
|
411
|
+
@action_dict = accept(action_classes || Action::SUBCLASSES)
|
412
|
+
end
|
413
|
+
|
414
|
+
attr_reader :desc, :version, :script_name
|
415
|
+
|
416
|
+
private
|
417
|
+
|
418
|
+
def accept(action_classes)
|
419
|
+
#; [!ue26k] builds action dictionary.
|
420
|
+
action_dict = {}
|
421
|
+
action_classes.each do |klass|
|
422
|
+
prefix = klass.instance_variable_get('@prefix')
|
423
|
+
(klass.instance_variable_get('@__mappings') || []).each do |tuple|
|
424
|
+
action_name, desc, option_schemas, method_name = tuple
|
425
|
+
action_name ||= method_name
|
426
|
+
full_name = prefix ? "#{prefix}:#{action_name}" : action_name.to_s
|
427
|
+
#; [!x6rh1] registers action name replacing '_' with '-'.
|
428
|
+
full_name = full_name.gsub('_', '-')
|
429
|
+
#
|
430
|
+
action_dict[full_name] = ActionInfo.new(full_name, action_name, desc,
|
431
|
+
option_schemas, klass, method_name)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
return action_dict
|
435
|
+
end
|
436
|
+
|
437
|
+
public
|
438
|
+
|
439
|
+
def run(*args)
|
440
|
+
## global options
|
441
|
+
gopt_values = parse_global_options(args)
|
442
|
+
output = handle_global_options(args, gopt_values)
|
443
|
+
return output if output
|
444
|
+
## global help
|
445
|
+
#; [!p5pr6] returns global help message when action is 'help'.
|
446
|
+
#; [!3hyvi] returns help message of action when action is 'help' with action name.
|
447
|
+
action_full_name = args.shift || "help"
|
448
|
+
if action_full_name == "help"
|
449
|
+
return help_message(self.script_name, args[0])
|
450
|
+
end
|
451
|
+
## action and options
|
452
|
+
#; [!mb92l] raises error when action name is unknown.
|
453
|
+
action_info = @action_dict[action_full_name] or
|
454
|
+
raise err("#{action_full_name}: unknown action.")
|
455
|
+
option_values = parse_options(args, action_info.option_schemas)
|
456
|
+
## show help
|
457
|
+
#; [!13m3q] returns help message if '-h' or '--help' specified to action.
|
458
|
+
if option_values['help']
|
459
|
+
return action_info.help_message(self.script_name)
|
460
|
+
end
|
461
|
+
## validation
|
462
|
+
obj = action_info.action_class.new()
|
463
|
+
method_name = action_info.action_method
|
464
|
+
validate_args(obj, method_name, args, action_full_name)
|
465
|
+
## do action
|
466
|
+
ret = kick_action(obj, method_name, args, option_values)
|
467
|
+
return ret
|
468
|
+
end
|
469
|
+
|
470
|
+
def main(argv=ARGV)
|
471
|
+
begin
|
472
|
+
ret = run(*argv)
|
473
|
+
rescue OptionError => ex
|
474
|
+
$stderr.puts "ERROR: #{ex}"
|
475
|
+
exit 1
|
476
|
+
else
|
477
|
+
case ret
|
478
|
+
when String
|
479
|
+
output = ret
|
480
|
+
puts output
|
481
|
+
exit 0
|
482
|
+
when Integer
|
483
|
+
status = ret
|
484
|
+
exit status
|
485
|
+
else
|
486
|
+
exit 0
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
protected
|
492
|
+
|
493
|
+
def parse_global_options(args)
|
494
|
+
gopt_schemas = self.class.instance_variable_get('@_global_option_schemas')
|
495
|
+
gopt_values = parse_options(args, gopt_schemas)
|
496
|
+
return gopt_values
|
497
|
+
end
|
498
|
+
|
499
|
+
def handle_global_options(args, global_option_values)
|
500
|
+
g_opts = global_option_values
|
501
|
+
#; [!b8isy] returns help message when global option '-h' or '--help' is specified.
|
502
|
+
if g_opts['help']
|
503
|
+
return help_message(self.script_name, nil)
|
504
|
+
end
|
505
|
+
#; [!4irzw] returns version string when global option '--version' is specified.
|
506
|
+
if g_opts['version']
|
507
|
+
return @version || '0.0'
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
def validate_args(action_obj, method_name, args, action_full_name)
|
512
|
+
#; [!yhry7] raises error when required argument is missing.
|
513
|
+
meth = action_obj.method(method_name)
|
514
|
+
n_min = meth.parameters.count {|x| x[0] == :req }
|
515
|
+
args.length >= n_min or
|
516
|
+
raise err("too few arguments (at least #{n_min} args expected).\n" +\
|
517
|
+
"(run `#{@script_name} help #{action_full_name}' for details.)")
|
518
|
+
#; [!h5522] raises error when too much arguments specified.
|
519
|
+
#; [!hq8b0] not raise error when many argument specified but method has *args.
|
520
|
+
unless meth.parameters.find {|x| x[0] == :rest }
|
521
|
+
n_max = meth.parameters.count {|x| x[0] == :req || x[0] == :opt }
|
522
|
+
args.length <= n_max or
|
523
|
+
raise err("too many arguments (at most #{n_max} args expected).\n" +\
|
524
|
+
"(run `#{@script_name} help #{action_full_name}' for details.)")
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
def kick_action(action_obj, method_name, args, option_values)
|
529
|
+
#; [!rph9y] converts 'foo-bar' option name into :foo_bar keyword.
|
530
|
+
kwargs = Hash[option_values.map {|k, v| [k.gsub(/-/, '_').intern, v] }]
|
531
|
+
#; [!qwd9x] passes command arguments and options as method arguments and options.
|
532
|
+
method_obj = action_obj.method(method_name)
|
533
|
+
has_kwargs = method_obj.parameters.any? {|x| x[0] == :key }
|
534
|
+
if has_kwargs
|
535
|
+
return method_obj.call(*args, kwargs)
|
536
|
+
else
|
537
|
+
return method_obj.call(*args)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
public
|
542
|
+
|
543
|
+
def help_message(command, action_name=nil)
|
544
|
+
if action_name
|
545
|
+
action_info = @action_dict[action_name] or
|
546
|
+
raise err("#{action_name}: no such action.")
|
547
|
+
return action_info.help_message(command)
|
548
|
+
end
|
549
|
+
#
|
550
|
+
msg = ""
|
551
|
+
#; [!1zpv4] adds command desc if it is specified at initializer.
|
552
|
+
if @desc
|
553
|
+
#msg << "#{command} -- #{@desc}\n"
|
554
|
+
#msg << "\n"
|
555
|
+
msg << @desc << "\n\n"
|
556
|
+
end
|
557
|
+
msg << "Usage:\n"
|
558
|
+
msg << " #{command} [<options>] <action> [<args>...]\n"
|
559
|
+
msg << "\n"
|
560
|
+
msg << "Options:\n"
|
561
|
+
self.class.instance_variable_get('@_global_option_schemas').each do |schema|
|
562
|
+
msg << " %-20s : %s\n" % [schema.option_string, schema.desc]
|
563
|
+
end
|
564
|
+
msg << "\n"
|
565
|
+
msg << "Actions:\n"
|
566
|
+
#msg << " %-20s : %s\n" % ["help", "show this help"]
|
567
|
+
@action_dict.keys.sort.each do |action_full_name|
|
568
|
+
action_info = @action_dict[action_full_name]
|
569
|
+
#; [!m3mry] skips action name when description is not provided.
|
570
|
+
msg << " %-20s : %s\n" % [action_full_name, action_info.desc] if action_info.desc
|
571
|
+
end
|
572
|
+
msg << "\n"
|
573
|
+
msg << "(Run `#{command} help <action>' to show help message of each action.)\n"
|
574
|
+
return msg
|
575
|
+
end
|
576
|
+
|
577
|
+
private
|
578
|
+
|
579
|
+
def parse_options(args, option_schemas)
|
580
|
+
return OptionParser.new(option_schemas).parse(args)
|
581
|
+
end
|
582
|
+
|
583
|
+
def err(msg)
|
584
|
+
return OptionError.new(msg)
|
585
|
+
end
|
586
|
+
|
587
|
+
end
|
588
|
+
|
589
|
+
|
590
|
+
def self.main(argv=nil)
|
591
|
+
Application.new.main(argv || ARGV)
|
592
|
+
end
|
593
|
+
|
594
|
+
|
595
|
+
end
|