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.
- data/doc/README +2 -0
- data/doc/Specifications +219 -0
- data/doc/argumentDSL +36 -0
- data/lib/command-set/arguments.rb +547 -0
- data/lib/command-set/batch-interpreter.rb +0 -0
- data/lib/command-set/command-set.rb +282 -0
- data/lib/command-set/command.rb +456 -0
- data/lib/command-set/dsl.rb +526 -0
- data/lib/command-set/interpreter.rb +196 -0
- data/lib/command-set/og.rb +615 -0
- data/lib/command-set/quick-interpreter.rb +91 -0
- data/lib/command-set/result-list.rb +300 -0
- data/lib/command-set/results.rb +754 -0
- data/lib/command-set/standard-commands.rb +243 -0
- data/lib/command-set/subject.rb +91 -0
- data/lib/command-set/text-interpreter.rb +171 -0
- data/lib/command-set.rb +3 -0
- metadata +70 -0
@@ -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
|