command-set 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,547 @@
1
+ require 'command-set/dsl'
2
+
3
+ module Command
4
+ #An Argument has a name and a value. They're used to to validate input, provide input prompts (like
5
+ #tab-completion or pop-up menus.
6
+ class Argument
7
+
8
+ #Used for new Argument subclasses to register the types they can be based on, and the explicit names of the
9
+ # arguments
10
+ def self.register(shorthand, type=nil)
11
+ DSL::Argument::register_argument(self, shorthand, type)
12
+ end
13
+
14
+ def initialize(name)
15
+ @name = name.to_s
16
+ @value = nil
17
+ end
18
+
19
+ attr_reader :name, :value
20
+
21
+ #Provides a list of completion options based on a string prefix and the subject
22
+ #The completion should be an array of completion options. If the completions have a common
23
+ #prefix, completion will enter it for the user. As a clever trick for providing hints:
24
+ # [ "This is a hint", "" ]
25
+ def complete(prefix, subject)
26
+ raise NotImplementedError, "complete not implemented in #{self.class.name}"
27
+ end
28
+
29
+ #Validates the input string against the type of the argument. Returns true if the input is valid, or else
30
+ #false
31
+ def validate(term, subject)
32
+ raise NotImplementedError, "validate not implemented in #{self.class.name}"
33
+ end
34
+
35
+ #Pulls strings from an ordered list of inputs and provides the parsed data to the host command
36
+ #Returns the parsed data or raises ArgumentInvalidException
37
+ def consume(subject, arguments)
38
+ term = arguments.shift
39
+ unless validate(term, subject)
40
+ raise ArgumentInvalidException, {@name => term}
41
+ end
42
+ return {@name => parse(subject, term)}
43
+ end
44
+
45
+ def consume_hash(subject, hash)
46
+ unless((term = hash[@name]).nil?)
47
+ if validate(term, subject)
48
+ return {@name => parse(subject, term)}
49
+ else
50
+ raise ArgumentInvalidException, {@name => term}
51
+ end
52
+ end
53
+ return {}
54
+ end
55
+
56
+ def check_present(keys)
57
+ unless keys.include?(@name)
58
+ raise OutOfArgumentsException, "Missing argument: #@name!"
59
+ end
60
+ end
61
+
62
+ #Used for completion, to skip along terms and arguments until we're at a place where completion begins.
63
+ #Both the terms and arguments arrays are modified by this method, so a number of clever tricks can be played
64
+ #to make completion work.
65
+ def match_terms(subject, terms, arguments)
66
+ arguments.shift
67
+ validate(terms.shift, subject)
68
+ end
69
+
70
+ #Used in completion to recognize that some arguments can be skipped
71
+ def omittable?
72
+ false
73
+ end
74
+
75
+ #Used in validation to require some arguments, and allow others to be optional
76
+ def required?
77
+ true
78
+ end
79
+
80
+ #Returns the parsed data equivalent of the string input
81
+ def parse(subject, term)
82
+ return term
83
+ end
84
+ end
85
+
86
+ class ArgumentDecorator < Argument
87
+ def self.register(name)
88
+ DSL::Argument::register_decorator(self, name)
89
+ end
90
+
91
+ class << self
92
+ alias register_as register
93
+ end
94
+
95
+ def self.decoration(&block)
96
+ @decor = block
97
+ end
98
+
99
+ def self.decor
100
+ @decor
101
+ end
102
+
103
+ def decor
104
+ self.class.decor
105
+ end
106
+
107
+ def initialize(up)
108
+ @wrapping_decorator = up
109
+ end
110
+
111
+ def embed_argument(argument)
112
+ @wrapping_decorator.embed_argument(decorate(argument))
113
+ end
114
+
115
+ def decorate(argument)
116
+ block = decor
117
+ (class << argument; self; end).class_eval &block
118
+ return argument
119
+ end
120
+ end
121
+
122
+ class NumberArgument < Argument
123
+ register "number", Range
124
+
125
+ def initialize(name, range=nil)
126
+ super(name)
127
+ @range = range
128
+ end
129
+
130
+ attr_accessor :range
131
+
132
+ def complete(prefix, subject)
133
+ return [] unless validate(prefix, subject)
134
+ return [prefix]
135
+ end
136
+
137
+ def validate(term, subject)
138
+ value = parse(subject, term)
139
+ return false if not range.nil? and not range.include?(value)
140
+ return true if %r{^0(\D.*)?} =~ term
141
+ return value != 0
142
+ end
143
+
144
+ def parse(subject, term)
145
+ @value = term.to_i
146
+ end
147
+ end
148
+
149
+ class RegexpArgument < Argument
150
+ register "regexp", Regexp
151
+ register "regex"
152
+
153
+ def initialize(name, regex)
154
+ @name = name
155
+ @regex = regex
156
+ end
157
+
158
+ def complete(prefix, subject)
159
+ return [prefix]
160
+ end
161
+
162
+ def validate(term, subject)
163
+ return @regex =~ term
164
+ end
165
+ end
166
+
167
+ class FileArgument < Argument
168
+ register "file"
169
+ Defaults = {
170
+ :prune_patterns => [/^\./],
171
+ :dirs => [ENV['PWD']],
172
+ :acceptor => proc do |path|
173
+ return (File.exists?(path) and not File.directory?(path))
174
+ end
175
+ }
176
+
177
+ def initialize(name, options={})
178
+ super(name)
179
+ if Hash === options
180
+ @options = Defaults.merge(options)
181
+ @acceptor = @options[:acceptor]
182
+ elsif Proc === options
183
+ @options = Defaults.dup
184
+ @acceptor = options
185
+ else
186
+ raise "File argument needs hash or proc!"
187
+ end
188
+ end
189
+
190
+ def complete(prefix, subject)
191
+ list = []
192
+ search_path = @options[:dirs].dup
193
+ match = %r{^#{Regexp.escape(prefix)}.*}
194
+ infix = ""
195
+
196
+ if( not (m = %r{^/([^/]*)$}.match(prefix)).nil? )
197
+ infix = "/"
198
+ search_path = ["/"]
199
+ match = /^#{m[1]}.*/
200
+ elsif( not (m = %r{^/(.*/)([^/]*)$}.match(prefix)).nil? )
201
+ infix = "/#{m[1]}"
202
+ match = /^#{m[2]}.*/
203
+ search_path = [infix]
204
+ elsif( %r{/$} =~ prefix )
205
+ infix = prefix
206
+ search_path.map! {|path| File.join(path, prefix)}
207
+ match = /.*/
208
+ elsif( %r{/} =~ prefix )
209
+ infix = File.dirname(prefix) + "/"
210
+ search_path.map! {|path| File.join(path, infix)}
211
+ match = %r{^#{Regexp.escape(File.basename(prefix))}.*}
212
+ end
213
+
214
+ search_path.each do |dir|
215
+ begin
216
+ Dir.foreach(dir) do |path|
217
+ catch(:bad_path) do
218
+ next unless match =~ path
219
+
220
+ @options[:prune_patterns].each do |pat|
221
+ if pat =~ path
222
+ throw :bad_path
223
+ end
224
+ end
225
+
226
+ candidate = File.join(dir,path)
227
+ if(File.file?(candidate))
228
+ throw :bad_path unless @acceptor[candidate]
229
+ end
230
+
231
+ if(File.directory?(candidate))
232
+ path += "/"
233
+ end
234
+
235
+ list << infix + path
236
+ end
237
+ end
238
+ rescue Errno::ENOENT
239
+ next
240
+ end
241
+ end
242
+
243
+ if(list.length == 1 && list[0] =~ /\/$/)
244
+ list << list[0] + "."
245
+ end
246
+
247
+ list
248
+ end
249
+
250
+ def validate(term, subject)
251
+ if(%r{^/} =~ term)
252
+ return @acceptor[term]
253
+ end
254
+
255
+ @options[:dirs].each do |dir|
256
+ path = File.join(dir, term)
257
+ if(File.exists?(path))
258
+ return @acceptor[path]
259
+ end
260
+ end
261
+
262
+ return @acceptor[File.join(@options[:dirs].first, term)]
263
+ end
264
+ end
265
+
266
+ class FiddlyArgument < Argument
267
+ def initialize(name, &block)
268
+ super(name)
269
+
270
+ (class << self; self; end).class_eval &block
271
+ end
272
+
273
+ def self.completion(&block)
274
+ raise TypeError unless block.arity == 2
275
+ define_method :complete, &block
276
+ end
277
+
278
+
279
+ def self.validation(&block)
280
+ raise TypeError unless block.arity == 2
281
+ define_method :validate, &block
282
+ end
283
+
284
+ def self.parser(&block)
285
+ raise TypeError unless block.arity == 2
286
+ define_method :parse, &block
287
+ end
288
+ end
289
+
290
+ class ArrayArgument < Argument
291
+ register "array", Array
292
+ register "choose"
293
+
294
+ def initialize(name, array)
295
+ super(name)
296
+ @options = array
297
+ end
298
+
299
+ def complete(prefix, subject)
300
+ return @options.grep(%r{^#{prefix}.*})
301
+ end
302
+
303
+ def validate(term, subject)
304
+ return @options.include?(term)
305
+ end
306
+ end
307
+
308
+ class StringArgument < Argument
309
+ register "string", String
310
+ register "any"
311
+
312
+ def initialize(name, string)
313
+ super(name)
314
+ @description = string
315
+ end
316
+
317
+ def complete(prefix, subject)
318
+ return [@description, ""]
319
+ end
320
+
321
+ def validate(term, subject)
322
+ return true
323
+ end
324
+ end
325
+
326
+ class ProcArgument < Argument
327
+ register "proc", Proc
328
+
329
+ def initialize(name, prok)
330
+ raise TypeError, "Block not arity 2: #{prok.arity}" unless prok.arity == 2
331
+ super(name)
332
+ @process = proc &prok
333
+ end
334
+
335
+ def complete(prefix, subject)
336
+ return @process.call(prefix, subject)
337
+ end
338
+
339
+ def validate(term, subject)
340
+ return @process.call(term, subject).include?(term)
341
+ end
342
+ end
343
+
344
+ class NoValidateProcArgument < ProcArgument
345
+ register "nonvalidating_proc"
346
+
347
+ def validate(term, subject)
348
+ return true
349
+ end
350
+ end
351
+
352
+ #Most common decorator. Tags a argument as omitable. Otherwise, the
353
+ #interpreter will return an error to the user if they leave out an
354
+ #argument. Optional arguments that aren't provided are set to nil.
355
+ class Optional < ArgumentDecorator
356
+ register_as "optional"
357
+
358
+ decoration do
359
+ def required?
360
+ false
361
+ end
362
+ end
363
+ end
364
+
365
+ #Indicated that the name of the argument has to appear on the command line
366
+ #before it will be recognized. Useful for optional or alternating arguments
367
+ class Named < ArgumentDecorator
368
+ register_as "named"
369
+
370
+ decoration do
371
+ alias_method(:old_consume, :consume)
372
+ alias_method(:old_complete, :complete)
373
+ alias_method(:old_match_terms, :match_terms)
374
+ def consume(subject, arguments)
375
+ if arguments.first == @name
376
+ arguments.shift
377
+ return old_consume(subject,arguments)
378
+ else
379
+ raise ArgumentInvalidException, "Name \"#{@name}\" required."
380
+ end
381
+ end
382
+
383
+ def match_terms(subject, terms, arguments)
384
+ if terms.first == @name.to_s
385
+ terms.shift
386
+ else
387
+ old_match_terms(subject, terms, arguments)
388
+ end
389
+ end
390
+
391
+ def complete(prefix, subject)
392
+ if %r{^#{prefix.to_s}.*} =~ @name.to_s
393
+ return [@name.to_s]
394
+ else
395
+ return old_complete(prefix, subject)
396
+ end
397
+ end
398
+ end
399
+ end
400
+
401
+
402
+ class MultiArgument < Argument
403
+ register "multiword"
404
+
405
+ def initialize(name, block)
406
+ raise TypeError, "Block arity is #{prok.arity}, not 2" unless block.arity == 2
407
+ super(name)
408
+ @process = proc &block
409
+ end
410
+
411
+ def complete(prefix, subject)
412
+ return @process[[*prefix], subject]
413
+ end
414
+
415
+ def validate(terms, subject)
416
+ return (not @process[[*terms], subject].empty?)
417
+ end
418
+
419
+ def consume(subject, arguments)
420
+ value = []
421
+ until arguments.empty? do
422
+ trying = arguments.shift
423
+ if(validate(value + [trying], subject))
424
+ value << trying
425
+ else
426
+ arguments.unshift(trying)
427
+ break
428
+ end
429
+ end
430
+ return {@name => value}
431
+ end
432
+
433
+ def omittable?
434
+ true
435
+ end
436
+
437
+ def match_terms(subject, terms, arguments)
438
+ validated = validate(terms.first, subject)
439
+ if(validated)
440
+ terms.shift
441
+ else
442
+ arguments.shift
443
+ end
444
+ return true
445
+ end
446
+ end
447
+
448
+ #Allows several arguments to share a position. Pass a block to the
449
+ #"decorator" method with the argument declarations inside. The first
450
+ #argument that can parse the input will be assigned - others will get nil.
451
+ class AlternatingArgument < ArgumentDecorator
452
+ register "alternating"
453
+ register "multi"
454
+ # initialize(embed_in){ yield if block_given? }
455
+ def initialize(embed_in, &block)
456
+ super(embed_in)
457
+ @names = []
458
+ @sub_arguments = []
459
+ self.instance_eval &block
460
+ @wrapping_decorator.embed_argument(self)
461
+ end
462
+
463
+ def name(name = nil)
464
+ if name.nil?
465
+ return @names
466
+ else
467
+ name = name.to_s
468
+ @names << name
469
+ @name = name
470
+ end
471
+ end
472
+
473
+ def complete(prefix, subject)
474
+ return sub_arguments.inject([]) do |list, sub_arg|
475
+ list.concat sub_arg.complete(prefix, subject)
476
+ end
477
+ end
478
+
479
+ def validate(term, subject)
480
+ begin
481
+ first_catch(term, subject)
482
+ return true
483
+ rescue CommandError
484
+ return false
485
+ end
486
+ end
487
+
488
+ def consume(subject, arguments)
489
+ term = arguments.first
490
+ catcher = first_catch(term, subject)
491
+ result = catcher.consume(subject, arguments)
492
+ @value = catcher.value
493
+ return result.merge({@name => result[catcher.name]})
494
+ end
495
+
496
+ #If a hash is used for arguments that includes more than one of alternating argument's sub-arguments, the behavior is undefined
497
+ def consume_hash(subject, hash)
498
+ begin
499
+ return super(subject, hash)
500
+ rescue OutOfArgumentsException; end
501
+ @sub_arguments.each do |arg|
502
+ unless hash[arg.name].nil?
503
+ result = arg.consume_hash(subject, hash)
504
+ return result.merge({@name => result[arg.name]})
505
+ end
506
+ end
507
+ raise OutOfArgumentsException, "Missing argument: #@name!"
508
+ end
509
+
510
+ def match_terms(subject, terms, arguments)
511
+ first_catch(terms.first, subject).match_terms(subject, terms, arguments)
512
+ end
513
+
514
+ def omittable?
515
+ return sub_arguments.inject(false) do |can_omit, sub_arg|
516
+ can_omit || sub_arg.omittable?
517
+ end
518
+ end
519
+
520
+ def parse(subject, term)
521
+ catcher = first_catch(term, subject)
522
+ return catcher.parse(subject, term)
523
+ end
524
+
525
+ def embed_argument(arg)
526
+ @names << arg.name
527
+ @sub_arguments << optional.decorate(arg)
528
+ end
529
+ private
530
+
531
+ attr_reader :sub_arguments
532
+
533
+ def first_catch(term, subject)
534
+ catcher = sub_arguments.find do |sub_arg|
535
+ sub_arg.validate(term, subject)
536
+ end
537
+
538
+ if catcher.nil?
539
+ raise ArgumentInvalidException, {@name => term}
540
+ end
541
+
542
+ return catcher
543
+ end
544
+
545
+ include DSL::Argument
546
+ end
547
+ end
File without changes