clio 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +19 -1
- data/MANIFEST +2 -1
- data/README +5 -6
- data/RELEASE +10 -5
- data/VERSION +1 -1
- data/lib/clio/commandline.rb +2 -9
- data/lib/clio/string.rb +91 -32
- data/lib/clio/usage.rb +2 -2
- data/lib/clio/usage/argument.rb +21 -19
- data/lib/clio/usage/command.rb +76 -351
- data/lib/clio/usage/option.rb +1 -1
- data/lib/clio/usage/subcommand.rb +455 -0
- data/spec/commandline/autousage.rd +0 -2
- data/spec/commandline/bracket.rd +0 -1
- data/spec/commandline/completion.rd +0 -2
- data/spec/commandline/define.rd +0 -1
- data/spec/commandline/method.rd +0 -1
- data/spec/commandline/parse.rd +0 -1
- data/spec/commandline/scenario.rd +0 -1
- data/spec/commandline/subclass.rd +3 -1
- data/spec/string/unit.rd +16 -1
- data/spec/usage/define.rd +85 -17
- data/spec/usage/parse.rd +0 -2
- metadata +4 -4
- data/lib/clio/usage/main.rb +0 -165
data/lib/clio/usage/option.rb
CHANGED
@@ -0,0 +1,455 @@
|
|
1
|
+
require 'clio/usage/option'
|
2
|
+
require 'clio/usage/argument'
|
3
|
+
|
4
|
+
module Clio
|
5
|
+
|
6
|
+
module Usage #:nodoc:
|
7
|
+
|
8
|
+
# = Commandline Usage Command
|
9
|
+
#
|
10
|
+
# This is the heart of usage; subclassed by Main and
|
11
|
+
# containing together Options and Arguments.
|
12
|
+
#
|
13
|
+
# usage = Usage.new
|
14
|
+
#
|
15
|
+
class Subcommand
|
16
|
+
|
17
|
+
# Parent command. This is needed
|
18
|
+
# to support cascading options.
|
19
|
+
#--
|
20
|
+
# NOTE: If it were possible to have this it would be better.
|
21
|
+
#++
|
22
|
+
attr :parent
|
23
|
+
|
24
|
+
# Name of the command.
|
25
|
+
attr :name
|
26
|
+
|
27
|
+
# Array of subcommands.
|
28
|
+
attr :subcommands
|
29
|
+
|
30
|
+
# Array of arguments. Arguments and subcommands
|
31
|
+
# are mutually exclusive, ie. either @arguments
|
32
|
+
# or @subcommands will be empty.
|
33
|
+
#
|
34
|
+
# TODO: Could use single attribute for both subcommands
|
35
|
+
# and arguments and use a flag to designate which type.
|
36
|
+
attr :arguments
|
37
|
+
|
38
|
+
# Array of options.
|
39
|
+
attr :options
|
40
|
+
|
41
|
+
# Help text.
|
42
|
+
attr :help
|
43
|
+
|
44
|
+
# Widely accepted alternate term for options.
|
45
|
+
alias_method :switches, :options
|
46
|
+
|
47
|
+
#
|
48
|
+
def initialize(name, parent=nil, &block)
|
49
|
+
@name = name.to_s
|
50
|
+
@parent = parent
|
51
|
+
@subcommands = []
|
52
|
+
@options = []
|
53
|
+
@arguments = []
|
54
|
+
@help = ''
|
55
|
+
instance_eval(&block) if block
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
def initialize_copy(c)
|
60
|
+
@parent = c.parent
|
61
|
+
@name = c.name.dup
|
62
|
+
@options = c.options.dup
|
63
|
+
@arguments = c.arguments.dup
|
64
|
+
@subcommands = c.subcommands.dup
|
65
|
+
@help = c.help.dup
|
66
|
+
end
|
67
|
+
|
68
|
+
def key ; @name.to_sym ; end
|
69
|
+
|
70
|
+
# METHOD MISSING
|
71
|
+
#-------------------------------------------------------------
|
72
|
+
|
73
|
+
def method_missing(key, *args, &blk)
|
74
|
+
key = key.to_s
|
75
|
+
case key
|
76
|
+
when /\?$/
|
77
|
+
option(key.chomp('?'), *args, &blk)
|
78
|
+
else
|
79
|
+
#k = full_name ? "#{full_name} #{key}" : "#{key}"
|
80
|
+
c = command(key, &blk)
|
81
|
+
args.each{ |a| c[a] }
|
82
|
+
c
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
def help!(*args)
|
88
|
+
Hash[*args].each do |key, desc|
|
89
|
+
self[key, desc]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Define or retrieve a command.
|
94
|
+
#
|
95
|
+
# subcommand('remote')
|
96
|
+
#
|
97
|
+
# A shortcut to accessing subcommands of subcommands, the following
|
98
|
+
# statements are equivalent:
|
99
|
+
#
|
100
|
+
# subcommand('remote').subcommand('add')
|
101
|
+
#
|
102
|
+
# subcommand('remote add')
|
103
|
+
#
|
104
|
+
def subcommand(name, help=nil, &block)
|
105
|
+
name, names = *name.to_s.strip.split(/\s+/)
|
106
|
+
if names
|
107
|
+
names = [name, *names]
|
108
|
+
cmd = names.inject(self) do |c, n|
|
109
|
+
c.subcommand(n)
|
110
|
+
end
|
111
|
+
else
|
112
|
+
cmd = subcommands.find{ |c| c === name }
|
113
|
+
unless cmd
|
114
|
+
cmd = Subcommand.new(name, self)
|
115
|
+
subcommands << cmd
|
116
|
+
end
|
117
|
+
end
|
118
|
+
cmd.help(help) if help
|
119
|
+
cmd.instance_eval(&block) if block
|
120
|
+
cmd
|
121
|
+
end
|
122
|
+
|
123
|
+
alias_method :cmd, :subcommand
|
124
|
+
alias_method :command, :subcommand
|
125
|
+
|
126
|
+
alias_method :commands, :subcommands
|
127
|
+
|
128
|
+
# Define an option.
|
129
|
+
#
|
130
|
+
# option(:output, :o)
|
131
|
+
#
|
132
|
+
def option(name, *aliases, &block)
|
133
|
+
opt = options.find{|o| o === name}
|
134
|
+
if not opt
|
135
|
+
opt = Option.new(name) #, self)
|
136
|
+
#opt.aliases(*aliases)
|
137
|
+
@options << opt
|
138
|
+
end
|
139
|
+
opt.aliases(*aliases) unless aliases.empty?
|
140
|
+
opt.instance_eval(&block) if block
|
141
|
+
opt
|
142
|
+
end
|
143
|
+
|
144
|
+
alias_method :switch, :option
|
145
|
+
|
146
|
+
# Option shorthand.
|
147
|
+
#
|
148
|
+
# opt('--output=FILE -o', 'output directory')
|
149
|
+
#
|
150
|
+
def opt(name, help=nil)
|
151
|
+
name, *aliases = name.split(/\s+/)
|
152
|
+
name, type = *name.split('=')
|
153
|
+
mult = false
|
154
|
+
if type && type[0,1] == '*'
|
155
|
+
mult = true
|
156
|
+
type = type[1..-1]
|
157
|
+
end
|
158
|
+
name = option_name(name).to_sym
|
159
|
+
o = option(name, *aliases)
|
160
|
+
o.help(help) if help
|
161
|
+
o.argument(type) if type
|
162
|
+
o.multiple(mult)
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
alias_method :swt, :opt
|
167
|
+
|
168
|
+
# A switch is like an option, but it is greedy.
|
169
|
+
# When parsed it will pick-up any match subsequent
|
170
|
+
# the switch's parent command. In other words,
|
171
|
+
# switches are consumed by a command even if they
|
172
|
+
# appear in a subcommand's arguments.
|
173
|
+
#
|
174
|
+
#def switch(name, *aliases, &block)
|
175
|
+
# if opt = @switches.find{|o| o === name}
|
176
|
+
# else
|
177
|
+
# opt = Option.new(name, self)
|
178
|
+
# opt.greedy = true
|
179
|
+
# opt.aliases(*aliases)
|
180
|
+
# @switches << opt
|
181
|
+
# end
|
182
|
+
# opt.instance_eval(&block) if block
|
183
|
+
# opt
|
184
|
+
#end
|
185
|
+
|
186
|
+
# Switch shorthand.
|
187
|
+
#
|
188
|
+
# swt('--output=FILE -o', 'output directory')
|
189
|
+
#
|
190
|
+
#def swt(name, help=nil)
|
191
|
+
# name, *aliases = name.split(/\s+/)
|
192
|
+
# name, type = *name.split('=')
|
193
|
+
# mult = false
|
194
|
+
# if type && type[0,1] == '*'
|
195
|
+
# mult = true
|
196
|
+
# type = type[1..-1]
|
197
|
+
# end
|
198
|
+
# name = clean_name(name)
|
199
|
+
# o = switch(name, *aliases)
|
200
|
+
# o.help(help) if help
|
201
|
+
# o.argument(type) if type
|
202
|
+
# o.multiple(mult)
|
203
|
+
# self
|
204
|
+
#end
|
205
|
+
|
206
|
+
# Define an argument.
|
207
|
+
# Takes a name, optional index and block.
|
208
|
+
#
|
209
|
+
# Indexing of arguments starts at 1, not 0.
|
210
|
+
#
|
211
|
+
# Examples
|
212
|
+
#
|
213
|
+
# argument(:path)
|
214
|
+
# argument(1, :path)
|
215
|
+
#
|
216
|
+
def argument(*n_type, &block)
|
217
|
+
index = Integer===n_type[0] ? n_type.shift : @arguments.size + 1
|
218
|
+
type = n_type.shift
|
219
|
+
help = n_type.shift
|
220
|
+
|
221
|
+
index = index - 1
|
222
|
+
type = type.to_s.sub(/^\</,'').chomp('>')
|
223
|
+
|
224
|
+
if type[0,1] == '*'
|
225
|
+
type.sub!('*', '')
|
226
|
+
splat = true
|
227
|
+
elsif type[-1,1] == '*'
|
228
|
+
type.sub!(/[*]$/, '')
|
229
|
+
splat = true
|
230
|
+
else
|
231
|
+
splat = false
|
232
|
+
end
|
233
|
+
|
234
|
+
#if type.index(':')
|
235
|
+
# name, type = *type.split(':')
|
236
|
+
# name = name.downcase
|
237
|
+
# type = type.upcase
|
238
|
+
#else
|
239
|
+
# if type.upcase == type
|
240
|
+
# name = nil
|
241
|
+
# else
|
242
|
+
# name = type
|
243
|
+
# type = type.upcase
|
244
|
+
# end
|
245
|
+
#end
|
246
|
+
|
247
|
+
raise ArgumentError, "Command cannot have both arguments (eg. #{type}) and subcommands." unless subcommands.empty?
|
248
|
+
|
249
|
+
if arg = @arguments[index]
|
250
|
+
arg.type(type) if type
|
251
|
+
#arg.name(name) if name
|
252
|
+
arg.help(help) if help
|
253
|
+
arg.splat(splat) if splat
|
254
|
+
arg.instance_eval(&block) if block
|
255
|
+
else
|
256
|
+
if type || block
|
257
|
+
arg = Argument.new(type, &block) #self, &block)
|
258
|
+
#arg.name(name) if name
|
259
|
+
arg.help(help) if help
|
260
|
+
arg.splat(splat) if splat
|
261
|
+
@arguments[index] = arg
|
262
|
+
end
|
263
|
+
end
|
264
|
+
return arg
|
265
|
+
end
|
266
|
+
|
267
|
+
alias_method :arg, :argument
|
268
|
+
|
269
|
+
# Argument shorthand.
|
270
|
+
#
|
271
|
+
# arg('PIN', 'pin number')
|
272
|
+
#
|
273
|
+
#def arg(type=nil, help=nil)
|
274
|
+
# type = type.to_s.sub(/^\</,'').chomp('>')
|
275
|
+
# argument(type).help(help)
|
276
|
+
# self
|
277
|
+
#end
|
278
|
+
|
279
|
+
#
|
280
|
+
def help(string=nil)
|
281
|
+
@help.replace(string.to_s) if string
|
282
|
+
@help
|
283
|
+
end
|
284
|
+
|
285
|
+
# SHORTHAND NOTATION
|
286
|
+
#-------------------------------------------------------------
|
287
|
+
|
288
|
+
# Super shorthand notation.
|
289
|
+
#
|
290
|
+
# cli['document']['--output=FILE -o']['<files>']
|
291
|
+
#
|
292
|
+
def [](*x)
|
293
|
+
case x[0].to_s[0,1]
|
294
|
+
when '-'
|
295
|
+
opt(*x)
|
296
|
+
when '<'
|
297
|
+
arg(*x)
|
298
|
+
else
|
299
|
+
subcommand(*x)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# QUERY METHODS
|
304
|
+
#-------------------------------------------------------------
|
305
|
+
|
306
|
+
#
|
307
|
+
def completion
|
308
|
+
if subcommands.empty?
|
309
|
+
options.collect{|o| o.to_s.strip } +
|
310
|
+
arguments.collect{|c| c.name}
|
311
|
+
else
|
312
|
+
options.collect{|o| o.to_s.strip } +
|
313
|
+
subcommands.collect{|c| c.name}
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Option defined?
|
318
|
+
#
|
319
|
+
def option?(name)
|
320
|
+
opt = options.find{|o| o === name}
|
321
|
+
if parent && !opt
|
322
|
+
opt = parent.option?(name)
|
323
|
+
end
|
324
|
+
opt
|
325
|
+
#return opt if opt
|
326
|
+
#options.each do |o|
|
327
|
+
# return o if o.aliases.include?(key)
|
328
|
+
#end
|
329
|
+
#nil
|
330
|
+
end
|
331
|
+
|
332
|
+
alias_method :switch?, :option
|
333
|
+
|
334
|
+
# Greedy Option defined?
|
335
|
+
#
|
336
|
+
#def greedy_option?(key)
|
337
|
+
# switches.find{|o| o === key}
|
338
|
+
#end
|
339
|
+
|
340
|
+
#
|
341
|
+
def ===(other_name)
|
342
|
+
name == other_name.to_s
|
343
|
+
end
|
344
|
+
|
345
|
+
#
|
346
|
+
def inspect
|
347
|
+
s = ''
|
348
|
+
s << "#<#{self.class}:#{object_id} #{@name}"
|
349
|
+
s << " @arguments=#{@arguments.inspect} " unless @arguments.empty?
|
350
|
+
s << " @options=#{@options.inspect} " unless @options.empty?
|
351
|
+
#s << "@switches=#{@switches.inspect} " unless @switches.empty?
|
352
|
+
s << " @help=#{@help.inspect}" unless @help.empty?
|
353
|
+
#s << "@commands=#{@commands.inspect} " unless @commands.empty?
|
354
|
+
s << ">"
|
355
|
+
s
|
356
|
+
end
|
357
|
+
|
358
|
+
# Full callable command name.
|
359
|
+
def full_name
|
360
|
+
if parent && parent.full_name
|
361
|
+
"#{parent.full_name} #{name}"
|
362
|
+
else
|
363
|
+
"#{name}"
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
# Usage text.
|
368
|
+
#
|
369
|
+
def to_s
|
370
|
+
#s = [full_name]
|
371
|
+
s = [name]
|
372
|
+
|
373
|
+
case options.size
|
374
|
+
when 0
|
375
|
+
when 1, 2, 3
|
376
|
+
s.concat(options.collect{ |o| "[#{o.to_s.strip}]" })
|
377
|
+
else
|
378
|
+
s << "[switches]" # switches? vs. options
|
379
|
+
end
|
380
|
+
|
381
|
+
s << arguments.join(' ') unless arguments.empty?
|
382
|
+
|
383
|
+
case subcommands.size
|
384
|
+
when 0
|
385
|
+
when 1
|
386
|
+
s << subcommands.join('')
|
387
|
+
when 2, 3
|
388
|
+
s << '[' + subcommands.join(' | ') + ']'
|
389
|
+
else
|
390
|
+
s << 'command'
|
391
|
+
end
|
392
|
+
|
393
|
+
s.flatten.join(' ')
|
394
|
+
end
|
395
|
+
|
396
|
+
# Help text.
|
397
|
+
#
|
398
|
+
def to_s_help
|
399
|
+
s = []
|
400
|
+
unless help.empty?
|
401
|
+
s << help
|
402
|
+
s << ''
|
403
|
+
end
|
404
|
+
s << "Usage:"
|
405
|
+
s << " " + to_s
|
406
|
+
unless subcommands.empty?
|
407
|
+
s << ''
|
408
|
+
s << 'Commands:'
|
409
|
+
s.concat(subcommands.collect{ |x| " %-20s %s" % [x.name, x.help] }.sort)
|
410
|
+
end
|
411
|
+
unless arguments.empty?
|
412
|
+
s << ''
|
413
|
+
s << "Arguments:"
|
414
|
+
s.concat(arguments.collect{ |x| " %-20s %s" % [x, x.help] })
|
415
|
+
end
|
416
|
+
unless options.empty?
|
417
|
+
s << ''
|
418
|
+
s << 'Switches:'
|
419
|
+
s.concat(options.collect{ |x| " %-20s %s" % [x, x.help] })
|
420
|
+
end
|
421
|
+
s.flatten.join("\n")
|
422
|
+
end
|
423
|
+
|
424
|
+
# PARSE
|
425
|
+
#-------------------------------------------------------------
|
426
|
+
|
427
|
+
# Parse usage.
|
428
|
+
def parse(argv, index=0)
|
429
|
+
@parser ||= Parser.new(self, argv, index)
|
430
|
+
@parser.parse
|
431
|
+
end
|
432
|
+
|
433
|
+
private
|
434
|
+
|
435
|
+
def option_key(key)
|
436
|
+
name = option_name(key) #.to_s
|
437
|
+
if name.size == 1
|
438
|
+
"-#{name}".to_sym
|
439
|
+
else
|
440
|
+
"--#{name}".to_sym
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def option_name(name)
|
445
|
+
name = name.to_s
|
446
|
+
name = name.gsub(/^[-]+/, '')
|
447
|
+
return name.chomp('?') #.to_sym
|
448
|
+
end
|
449
|
+
|
450
|
+
end #class Command
|
451
|
+
|
452
|
+
end #module Usage
|
453
|
+
|
454
|
+
end #module Clio
|
455
|
+
|