command-set 0.8.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.
@@ -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