bahuvrihi-tap 0.10.3 → 0.10.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tap/constants.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Tap
2
2
  MAJOR = 0
3
3
  MINOR = 10
4
- TINY = 3
4
+ TINY = 4
5
5
 
6
6
  VERSION="#{MAJOR}.#{MINOR}.#{TINY}"
7
7
  WEBSITE="http://tap.rubyforge.org"
data/lib/tap/exe.rb CHANGED
@@ -1,9 +1,13 @@
1
+ require 'tap/env'
2
+ require 'tap/app'
3
+ require 'tap/parser'
4
+
1
5
  module Tap
2
6
  class Exe < Env
3
7
 
4
8
  class << self
5
- def instantiate
6
- app = Tap::App.instance
9
+ def instantiate(path=Dir.pwd, logger=Tap::App::DEFAULT_LOGGER, &block)
10
+ app = Tap::App.instance = Tap::App.new({:root => path}, logger)
7
11
  exe = super(app, load_config(Tap::Env::GLOBAL_CONFIG_FILE), app.logger)
8
12
 
9
13
  # add all gems if no gems are specified (Note this is VERY SLOW ~ 1/3 the overhead for tap)
@@ -43,7 +47,7 @@ module Tap
43
47
  end
44
48
  end
45
49
 
46
- def launch(argv=ARGV)
50
+ def execute(argv=ARGV)
47
51
  command = argv.shift.to_s
48
52
 
49
53
  if aliases && aliases.has_key?(command)
@@ -63,5 +67,46 @@ module Tap
63
67
  end
64
68
  end
65
69
  end
70
+
71
+ def build(argv=ARGV)
72
+ parser = Parser.new(argv)
73
+
74
+ # attempt lookup and instantiate the task class
75
+ tasks = parser.tasks.collect do |args|
76
+ task = args.shift
77
+
78
+ const = search(:tasks, task) or raise ArgumentError, "unknown task: #{task}"
79
+ task_class = const.constantize or raise ArgumentError, "unknown task: #{task}"
80
+ task_class.instantiate(args, app)
81
+ end
82
+
83
+ # build the workflow
84
+ parser.workflow.each_with_index do |(type, target_indicies), source_index|
85
+ next if type == nil
86
+
87
+ tasks[source_index].send(type, *target_indicies.collect {|i| tasks[i] })
88
+ end
89
+
90
+ # build queues
91
+ queues = parser.rounds.collect do |round|
92
+ round.each do |index|
93
+ task, args = tasks[index]
94
+ task.enq(*args)
95
+ end
96
+
97
+ app.queue.clear
98
+ end
99
+ queues.delete_if {|queue| queue.empty? }
100
+
101
+ queues
102
+ end
103
+
104
+ def run(queues)
105
+ queues.each_with_index do |queue, i|
106
+ app.queue.concat(queue)
107
+ app.run
108
+ end
109
+ end
110
+
66
111
  end
67
112
  end
@@ -60,6 +60,10 @@ module Tap
60
60
  raise NotImplementedError
61
61
  end
62
62
 
63
+ def prepare(target, options={})
64
+ raise NotImplementedError
65
+ end
66
+
63
67
  def directories(root, targets, options={})
64
68
  directory(root)
65
69
  targets.each do |target|
@@ -19,8 +19,12 @@ module Tap
19
19
  file_task.rmdir(target) unless pretend
20
20
  end
21
21
  end
22
-
22
+
23
23
  def file(target, options={})
24
+ prepare(target, options)
25
+ end
26
+
27
+ def prepare(target, options={})
24
28
  target = File.expand_path(target, target_dir)
25
29
 
26
30
  if File.exists?(target)
@@ -31,7 +35,7 @@ module Tap
31
35
  log_relative :missing, target
32
36
  end
33
37
  end
38
+
34
39
  end
35
-
36
40
  end
37
41
  end
@@ -17,23 +17,29 @@ module Tap
17
17
  end
18
18
 
19
19
  def file(target, options={})
20
- target = File.expand_path(target, target_dir)
21
-
22
- case
23
- when !File.exists?(target)
24
- log_relative :create, target
25
- # should check for identical...
26
- when force_file_collision?(target)
27
- log_relative :force, target
28
- else
29
- log_relative :skip, target
30
- return
31
- end
32
-
33
- unless pretend
34
- file_task.prepare(target)
35
- File.open(target, "wb") {|file| yield(file) if block_given? }
20
+ prepare(target, options) do |path|
21
+ File.open(path, "wb") {|file| yield(file) if block_given? }
36
22
  end
23
+ end
24
+
25
+ def prepare(target, options={})
26
+ target = File.expand_path(target, target_dir)
27
+
28
+ case
29
+ when !File.exists?(target)
30
+ log_relative :create, target
31
+ # should check for identical...
32
+ when force_file_collision?(target)
33
+ log_relative :force, target
34
+ else
35
+ log_relative :skip, target
36
+ return
37
+ end
38
+
39
+ unless pretend
40
+ file_task.prepare(target)
41
+ yield(target)
42
+ end
37
43
  end
38
44
 
39
45
  # Ask the user interactively whether to force collision.
@@ -4,7 +4,8 @@ module Tap::Generator::Generators
4
4
 
5
5
  # :startdoc::generator a basic tap directory structure
6
6
  #
7
- # Generates a tap root directory structure:
7
+ # Generates a tap root directory structure. Use the switches to
8
+ # generate a tapfile and/or a tap config file:
8
9
  #
9
10
  # root
10
11
  # |- Rakefile
@@ -17,11 +18,10 @@ module Tap::Generator::Generators
17
18
  # |- tap_test_suite.rb
18
19
  # `- tapfile_test.rb
19
20
  #
20
- # By default a tapfile will be created, but not a config file.
21
21
  class RootGenerator < Tap::Generator::Base
22
22
 
23
23
  config :config_file, false, &c.switch # create a tap.yml file
24
- config :tapfile, true, &c.switch # create a tapfile
24
+ config :tapfile, false, &c.switch # create a tapfile
25
25
 
26
26
  # ::args ROOT, PROJECT_NAME=basename(ROOT)
27
27
  def manifest(m, root, project_name=nil)
data/lib/tap/parser.rb ADDED
@@ -0,0 +1,435 @@
1
+ autoload(:Shellwords, 'shellwords')
2
+
3
+ module Tap
4
+ class Parser
5
+ module Utils
6
+ module_function
7
+
8
+ # Parses the input string as YAML, if the string matches the YAML document
9
+ # specifier (ie it begins with "---\s*\n"). Otherwise returns the string.
10
+ #
11
+ # str = {'key' => 'value'}.to_yaml # => "--- \nkey: value\n"
12
+ # Tap::Script.parse_yaml(str) # => {'key' => 'value'}
13
+ # Tap::Script.parse_yaml("str") # => "str"
14
+ def parse_yaml(str)
15
+ str =~ /\A---\s*\n/ ? YAML.load(str) : str
16
+ end
17
+
18
+ # Shell quotes the input string by enclosing in quotes if
19
+ # str has no quotes, or double quotes if str has no double
20
+ # quotes. Returns the str if it has not whitespace, quotes
21
+ # or double quotes.
22
+ #
23
+ # Raises an ArgumentError if str has both quotes and double
24
+ # quotes.
25
+ def shell_quote(str)
26
+ return str unless str =~ /[\s'"]/
27
+
28
+ quote = str.include?("'")
29
+ double_quote = str.include?('"')
30
+
31
+ case
32
+ when !quote then "'#{str}'"
33
+ when !double_quote then "\"#{str}\""
34
+ else raise ArgumentError, "cannot shell quote: #{str}"
35
+ end
36
+ end
37
+
38
+ # Defines a break regexp that matches a bracketed-pairs
39
+ # break. The left and right brackets are specified as
40
+ # inputs. After a match:
41
+ #
42
+ # $2:: The source string after the break.
43
+ # (ex: '--[]' => '', '--1[]' => '1')
44
+ # $3:: The target string, or nil.
45
+ # (ex: '--[]' => '', '--1[1,2,3]' => '1,2,3')
46
+ #
47
+ def bracket_regexp(l, r)
48
+ /\A(--)?(\d*)#{Regexp.escape(l)}([\d,]*)#{Regexp.escape(r)}\z/
49
+ end
50
+
51
+ # Matches any breaking arg (ex: '--', '--+', '--1:2')
52
+ BREAK = /\A--(\z|[\+\d\:\*\[\{\(])/
53
+
54
+ # Matches the start of any workflow regex (ex: '+', '2', '[', '{')
55
+ WORKFLOW = /\A[\+\d\:\*\[\{\(]/
56
+
57
+ # Matches an execution-round break. After the match:
58
+ #
59
+ # $3:: The round string after the break, or nil.
60
+ # (ex: '--' => nil, '--++' => '++', '--+1' => '+1')
61
+ # $6:: The target string, or nil.
62
+ # (ex: '--+' => nil, '--+[1,2,3]' => '1,2,3')
63
+ #
64
+ ROUND = /\A(--\z|(--)?(\+(\d*|\+*))(\[([\d,]*)\])?\z)/
65
+
66
+ # Matches a sequence break. After the match:
67
+ #
68
+ # $2:: The sequence string after the break.
69
+ # (ex: '--:' => ':', '--1:2' => '1:2', '--1:' => '1:', '--:2' => ':2')
70
+ #
71
+ SEQUENCE = /\A(--)?(\d*(:\d*)+)\z/
72
+
73
+ # Matches an instance break. After the match:
74
+ #
75
+ # $2:: The index string after the break.
76
+ # (ex: '--*' => '', '--*1' => '1')
77
+ #
78
+ INSTANCE = /\A(--)?\*(\d*)\z/
79
+
80
+ # A break regexp using "[]"
81
+ FORK = bracket_regexp("[", "]")
82
+
83
+ # A break regexp using "{}"
84
+ MERGE = bracket_regexp("{", "}")
85
+
86
+ # A break regexp using "()"
87
+ SYNC_MERGE = bracket_regexp("(", ")")
88
+
89
+ # Parses an indicies str along commas, and collects the indicies
90
+ # as integers. Ex:
91
+ #
92
+ # parse_indicies('') # => []
93
+ # parse_indicies('1') # => [1]
94
+ # parse_indicies('1,2,3') # => [1,2,3]
95
+ #
96
+ def parse_indicies(str, regexp=/,+/)
97
+ indicies = []
98
+ str.split(regexp).each do |n|
99
+ indicies << n.to_i unless n.empty?
100
+ end
101
+ indicies
102
+ end
103
+
104
+ # Parses the match of a ROUND regexp into a round index
105
+ # and an array of task indicies that should be added to the
106
+ # round. The inputs correspond to $3 and $6 for the match.
107
+ #
108
+ # If $3 is nil, a round index of zero is assumed; if $6 is
109
+ # nil, then indicies of [] are assumed.
110
+ #
111
+ # parse_round("+", "") # => [1, []]
112
+ # parse_round("+2", "1,2,3") # => [2, [1,2,3]]
113
+ # parse_round(nil, nil) # => [0, []]
114
+ #
115
+ def parse_round(three, six)
116
+ index = case three
117
+ when nil then 0
118
+ when /\d/ then three[1, three.length-1].to_i
119
+ else three.length
120
+ end
121
+ [index, six == nil ? [] : parse_indicies(six)]
122
+ end
123
+
124
+ # Parses the match of a SEQUENCE regexp into an array of task
125
+ # indicies. The input corresponds to $2 for the match. The
126
+ # current and next index are assumed if $2 starts and/or ends
127
+ # with a semi-colon.
128
+ #
129
+ # parse_sequence("1:2:3") # => [1,2,3]
130
+ # parse_sequence(":1:2:") # => [:current_index,1,2,:next_index]
131
+ #
132
+ def parse_sequence(two)
133
+ seq = parse_indicies(two, /:+/)
134
+ seq.unshift current_index if two[0] == ?:
135
+ seq << next_index if two[-1] == ?:
136
+ seq
137
+ end
138
+
139
+ # Parses the match of an INSTANCE regexp into an index.
140
+ # The input corresponds to $2 for the match. The next
141
+ # index is assumed if $2 is empty.
142
+ #
143
+ # parse_instance("1") # => 1
144
+ # parse_instance("") # => :next_index
145
+ #
146
+ def parse_instance(two)
147
+ two.empty? ? next_index : two.to_i
148
+ end
149
+
150
+ # Parses the match of an bracket_regexp into a [source_index,
151
+ # target_indicies] array. The inputs corresponds to $2 and
152
+ # $3 for the match. The current and next index are assumed
153
+ # if $2 and/or $3 is empty.
154
+ #
155
+ # parse_bracket("1", "2,3") # => [1, [2,3]]
156
+ # parse_bracket("", "") # => [:current_index, [:next_index]]
157
+ # parse_bracket("1", "") # => [1, [:next_index]]
158
+ # parse_bracket("", "2,3") # => [:current_index, [2,3]]
159
+ #
160
+ def parse_bracket(two, three)
161
+ targets = parse_indicies(three)
162
+ targets << next_index if targets.empty?
163
+ [two.empty? ? current_index : two.to_i, targets]
164
+ end
165
+ end
166
+
167
+ class << self
168
+ def load(task_argv)
169
+ task_argv = YAML.load(task_argv) if task_argv.kind_of?(String)
170
+
171
+ tasks, argv = task_argv.partition {|obj| obj.kind_of?(Array) }
172
+ parser = new
173
+ parser.tasks.concat(tasks)
174
+ parser.parse(argv)
175
+ parser
176
+ end
177
+ end
178
+
179
+ include Utils
180
+
181
+ # An array of task declarations.
182
+ attr_reader :tasks
183
+
184
+ # The internal rounds data; an array of integers signifying the
185
+ # round a task should be assigned to, at a given index.
186
+ attr_reader :round_indicies
187
+
188
+ # The internal workflow data; an array of [type, targets] pairs
189
+ # signifying the workflow type and targets assigned to the task
190
+ # at a given index. Differs from the return from workflow in
191
+ # that reversed-workflows (ex merge, sync_merge) will be organized
192
+ # by target rather than by source.
193
+ attr_reader :workflows
194
+
195
+ def initialize(argv=nil)
196
+ @tasks = []
197
+ @round_indicies = []
198
+ @workflows = []
199
+
200
+ case argv
201
+ when String, Array
202
+ parse(argv)
203
+ end
204
+ end
205
+
206
+ # Iterates through the argv splitting out task and workflow definitions.
207
+ # Task definitions are split out (with configurations) along round and/or
208
+ # workflow break lines. Rounds and workflows are dynamically parsed;
209
+ # tasks may be reassigned to rounds, or have their workflow reassigned
210
+ # by later arguments, perhaps in later calls to parse.
211
+ #
212
+ # === Examples
213
+ #
214
+ # Parse two tasks, with inputs and configs at separate times. Both
215
+ # are assigned to round 0.
216
+ #
217
+ # p = Parser.new
218
+ # p.parse(["a", "b", "--config", "c"])
219
+ # p.tasks
220
+ # # => [
221
+ # # ["a", "b", "--config", "c"]]
222
+ #
223
+ # p.parse(["x", "y", "z"])
224
+ # p.tasks
225
+ # # => [
226
+ # # ["a", "b", "--config", "c"],
227
+ # # ["x", "y", "z"]]
228
+ #
229
+ # p.rounds # => [[0,1]]
230
+ #
231
+ # Parse two simple tasks at the same time into different rounds.
232
+ #
233
+ # p = Parser.new ["a", "--+", "b"]
234
+ # p.tasks # => [["a"], ["b"]]
235
+ # p.rounds # => [[0], [1]]
236
+ #
237
+ # Rounds can be declared multiple ways:
238
+ #
239
+ # p = Parser.new ["--+", "a", "--", "b", "--", "c", "--", "d"]
240
+ # p.tasks # => [["a"], ["b"], ["c"], ["d"]]
241
+ # p.rounds # => [[1,2,3], [0]]
242
+ #
243
+ # p.parse ["+3[2,3]"]
244
+ # p.rounds # => [[1], [0], nil, [2,3]]
245
+ #
246
+ # Note the rounds were re-assigned using the second parse. Very
247
+ # similar things may be done with workflows (note also that this
248
+ # example shows how parse splits a string input into an argv
249
+ # using Shellwords):
250
+ #
251
+ # p = Parser.new "a --: b --: c --: d"
252
+ # p.tasks # => [["a"], ["b"], ["c"], ["d"]]
253
+ # p.workflow(:sequence) # => [[0,1],[1,2],[2,3]]
254
+ #
255
+ # p.parse "1[2,3]"
256
+ # p.workflow(:sequence) # => [[0,1],[2,3]]
257
+ # p.workflow(:fork) # => [[1,[2,3]]]
258
+ #
259
+ # p.parse "e --{2,3}"
260
+ # p.tasks # => [["a"], ["b"], ["c"], ["d"], ["e"]]
261
+ # p.workflow(:sequence) # => [[0,1]]
262
+ # p.workflow(:fork) # => [[1,[2,3]]]
263
+ # p.workflow(:merge) # => [[4,[2,3]]]
264
+ #
265
+ def parse(argv)
266
+ if argv.kind_of?(String)
267
+ argv = Shellwords.shellwords(argv)
268
+ end
269
+
270
+ current_round_index = @round_indicies[next_index]
271
+ current = []
272
+ argv.each do |arg|
273
+ # add all non-breaking args to the
274
+ # current argv array. this should
275
+ # include all lookups, inputs, and
276
+ # configurations
277
+ unless arg =~ BREAK || (current.empty? && arg =~ WORKFLOW)
278
+ current << arg
279
+ next
280
+ end
281
+
282
+ # unless the current argv is empty,
283
+ # append and start a new argv
284
+ unless current.empty?
285
+ current_round_index = add_task(current, current_round_index)
286
+ current = []
287
+ end
288
+
289
+ # determine the type of break, parse,
290
+ # and add to the appropriate collection
291
+ case arg
292
+ when ROUND
293
+ current_round_index, indicies = parse_round($3, $6)
294
+ indicies.each {|index| @round_indicies[index] = current_round_index }
295
+
296
+ when SEQUENCE
297
+ indicies = parse_sequence($2)
298
+ while indicies.length > 1
299
+ source_index = indicies.shift
300
+ set_workflow(:sequence, source_index, indicies[0])
301
+ end
302
+
303
+ # when INSTANCE then @instances << parse_instance($2)
304
+ when FORK then set_workflow(:fork, *parse_bracket($2, $3))
305
+ when MERGE then set_reverse_workflow(:merge, *parse_bracket($2, $3))
306
+ when SYNC_MERGE then set_reverse_workflow(:sync_merge, *parse_bracket($2, $3))
307
+ else raise ArgumentError, "invalid break argument: #{arg}"
308
+ end
309
+ end
310
+
311
+ unless current.empty?
312
+ add_task(current, current_round_index)
313
+ end
314
+ end
315
+
316
+ # Returns an array of [type, targets] objects; the index of
317
+ # each entry corresponds to the task on which to build the
318
+ # workflow of the specified type.
319
+ #
320
+ # If a type is specified, the output is ordered differently;
321
+ # The return is an array of [source, targets] for the
322
+ # specified workflow type. In this case the order of the
323
+ # returned array is meaningless.
324
+ #
325
+ def workflow(type=nil)
326
+ # recollect reverse types
327
+
328
+ workflows = []
329
+ @workflows.each_with_index do |entry, source|
330
+ next if entry == nil
331
+
332
+ workflows[source] = case entry[0]
333
+ when :merge, :sync_merge
334
+ workflow_type, target = entry
335
+ (workflows[target] ||= [workflow_type, []])[1] << source
336
+ nil
337
+ else entry
338
+ end
339
+ end
340
+
341
+ return workflows if type == nil
342
+
343
+ declarations = []
344
+ workflows.each_with_index do |(workflow_type, targets), source|
345
+ declarations << [source, targets] if workflow_type == type
346
+ end
347
+
348
+ declarations
349
+ end
350
+
351
+ # Returns an array task indicies; the index of each entry
352
+ # corresponds to the round the tasks should be assigned to.
353
+ #
354
+ def rounds
355
+ collected_rounds = []
356
+ @round_indicies.each_with_index do |round_index, index|
357
+ (collected_rounds[round_index] ||= []) << index unless round_index == nil
358
+ end
359
+
360
+ collected_rounds.each {|round| round.uniq! unless round.nil? }
361
+ end
362
+
363
+ def to_s
364
+ segments = tasks.collect do |argv|
365
+ argv.collect {|arg| shell_quote(arg) }.join(' ')
366
+ end
367
+ each_round_str {|str| segments << str }
368
+ each_workflow_str {|str| segments << str }
369
+
370
+ segments.join(" -- ")
371
+ end
372
+
373
+ def dump
374
+ segments = tasks.dup
375
+ each_round_str {|str| segments << str }
376
+ each_workflow_str {|str| segments << str }
377
+
378
+ segments
379
+ end
380
+
381
+ protected
382
+
383
+ # Returns the index of the next argv to be parsed.
384
+ def next_index
385
+ tasks.length
386
+ end
387
+
388
+ # Returns the index of the last argv parsed.
389
+ def current_index
390
+ tasks.length - 1
391
+ end
392
+
393
+ # Sets the targets to the source in workflows, tracking the
394
+ # workflow type.
395
+ def set_workflow(type, source, targets) # :nodoc
396
+ # warn if workflows[source] is already set
397
+ @workflows[source] = [type, targets]
398
+ end
399
+
400
+ # Sets a reverse workflow... ie the source is set to
401
+ # each target index.
402
+ def set_reverse_workflow(type, source, targets) # :nodoc
403
+ targets.each {|target| set_workflow(type, target, source) }
404
+ end
405
+
406
+ def add_task(definition, round_index=@round_indicies[next_index]) # :nodoc
407
+ @tasks << definition
408
+ @round_indicies[current_index] = (round_index || 0)
409
+ @round_indicies[next_index]
410
+ end
411
+
412
+ # Yields each round formatted as a string.
413
+ def each_round_str # :nodoc
414
+ rounds.each_with_index do |indicies, round_index|
415
+ unless indicies == nil
416
+ yield "+#{round_index}[#{indicies.join(',')}]"
417
+ end
418
+ end
419
+ end
420
+
421
+ # Yields each workflow element formatted as a string.
422
+ def each_workflow_str # :nodoc
423
+ workflow.each_with_index do |(type, targets), source|
424
+ next if type == nil
425
+
426
+ yield case type
427
+ when :sequence then [source, *targets].join(":")
428
+ when :fork then "#{source}[#{targets.join(',')}]"
429
+ when :merge then "#{source}{#{targets.join(',')}}"
430
+ when :sync_merge then "#{source}(#{targets.join(',')})"
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end