tap 0.12.4 → 0.17.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/History +34 -0
- data/README +62 -41
- data/bin/tap +36 -40
- data/cmd/console.rb +14 -6
- data/cmd/manifest.rb +62 -58
- data/cmd/run.rb +49 -31
- data/doc/API +84 -0
- data/doc/Class Reference +83 -115
- data/doc/Examples/Command Line +36 -0
- data/doc/Examples/Workflow +40 -0
- data/lib/tap/app.rb +293 -214
- data/lib/tap/app/node.rb +43 -0
- data/lib/tap/app/queue.rb +77 -0
- data/lib/tap/app/stack.rb +16 -0
- data/lib/tap/app/state.rb +22 -0
- data/lib/tap/constants.rb +2 -2
- data/lib/tap/env.rb +400 -314
- data/lib/tap/env/constant.rb +227 -0
- data/lib/tap/env/gems.rb +63 -0
- data/lib/tap/env/manifest.rb +89 -0
- data/lib/tap/env/minimap.rb +292 -0
- data/lib/tap/{support → env}/string_ext.rb +2 -2
- data/lib/tap/exe.rb +113 -125
- data/lib/tap/join.rb +175 -0
- data/lib/tap/joins.rb +9 -0
- data/lib/tap/joins/switch.rb +44 -0
- data/lib/tap/joins/sync.rb +99 -0
- data/lib/tap/root.rb +100 -491
- data/lib/tap/root/utils.rb +220 -0
- data/lib/tap/{support → root}/versions.rb +31 -29
- data/lib/tap/schema.rb +248 -0
- data/lib/tap/schema/parser.rb +413 -0
- data/lib/tap/schema/utils.rb +82 -0
- data/lib/tap/support/intern.rb +19 -6
- data/lib/tap/support/templater.rb +8 -3
- data/lib/tap/task.rb +175 -171
- data/lib/tap/tasks/dump.rb +58 -0
- data/lib/tap/tasks/load.rb +62 -0
- metadata +30 -73
- data/cmd/destroy.rb +0 -27
- data/cmd/generate.rb +0 -27
- data/doc/Command Reference +0 -105
- data/doc/Syntax Reference +0 -234
- data/doc/Tutorial +0 -348
- data/lib/tap/dump.rb +0 -142
- data/lib/tap/file_task.rb +0 -384
- data/lib/tap/generator/arguments.rb +0 -13
- data/lib/tap/generator/base.rb +0 -176
- data/lib/tap/generator/destroy.rb +0 -60
- data/lib/tap/generator/generate.rb +0 -93
- data/lib/tap/generator/generators/command/command_generator.rb +0 -21
- data/lib/tap/generator/generators/command/templates/command.erb +0 -32
- data/lib/tap/generator/generators/config/config_generator.rb +0 -98
- data/lib/tap/generator/generators/generator/generator_generator.rb +0 -37
- data/lib/tap/generator/generators/generator/templates/task.erb +0 -27
- data/lib/tap/generator/generators/generator/templates/test.erb +0 -26
- data/lib/tap/generator/generators/root/root_generator.rb +0 -84
- data/lib/tap/generator/generators/root/templates/MIT-LICENSE +0 -22
- data/lib/tap/generator/generators/root/templates/README +0 -14
- data/lib/tap/generator/generators/root/templates/Rakefile +0 -84
- data/lib/tap/generator/generators/root/templates/Rapfile +0 -11
- data/lib/tap/generator/generators/root/templates/gemspec +0 -27
- data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -3
- data/lib/tap/generator/generators/task/task_generator.rb +0 -25
- data/lib/tap/generator/generators/task/templates/task.erb +0 -14
- data/lib/tap/generator/generators/task/templates/test.erb +0 -19
- data/lib/tap/generator/manifest.rb +0 -20
- data/lib/tap/generator/preview.rb +0 -69
- data/lib/tap/load.rb +0 -64
- data/lib/tap/spec.rb +0 -41
- data/lib/tap/support/aggregator.rb +0 -65
- data/lib/tap/support/audit.rb +0 -333
- data/lib/tap/support/constant.rb +0 -143
- data/lib/tap/support/constant_manifest.rb +0 -126
- data/lib/tap/support/dependencies.rb +0 -54
- data/lib/tap/support/dependency.rb +0 -44
- data/lib/tap/support/executable.rb +0 -198
- data/lib/tap/support/executable_queue.rb +0 -125
- data/lib/tap/support/gems.rb +0 -43
- data/lib/tap/support/join.rb +0 -144
- data/lib/tap/support/joins.rb +0 -12
- data/lib/tap/support/joins/switch.rb +0 -27
- data/lib/tap/support/joins/sync_merge.rb +0 -38
- data/lib/tap/support/manifest.rb +0 -171
- data/lib/tap/support/minimap.rb +0 -90
- data/lib/tap/support/node.rb +0 -176
- data/lib/tap/support/parser.rb +0 -450
- data/lib/tap/support/schema.rb +0 -385
- data/lib/tap/support/shell_utils.rb +0 -67
- data/lib/tap/test.rb +0 -77
- data/lib/tap/test/assertions.rb +0 -38
- data/lib/tap/test/env_vars.rb +0 -29
- data/lib/tap/test/extensions.rb +0 -73
- data/lib/tap/test/file_test.rb +0 -362
- data/lib/tap/test/file_test_class.rb +0 -15
- data/lib/tap/test/regexp_escape.rb +0 -87
- data/lib/tap/test/script_test.rb +0 -46
- data/lib/tap/test/script_tester.rb +0 -115
- data/lib/tap/test/subset_test.rb +0 -260
- data/lib/tap/test/subset_test_class.rb +0 -99
- data/lib/tap/test/tap_test.rb +0 -109
- data/lib/tap/test/utils.rb +0 -231
data/lib/tap/joins.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Tap
|
2
|
+
module Joins
|
3
|
+
|
4
|
+
# A Switch join allows a block to determine which output from an array
|
5
|
+
# of outputs will receive the results of the input.
|
6
|
+
#
|
7
|
+
#--
|
8
|
+
# Note that switch is NOT identified as a join that can be created from
|
9
|
+
# the command line. Switch inherently requires a block to select which
|
10
|
+
# output receives the input, and so cannot be loaded from data alone.
|
11
|
+
#
|
12
|
+
# Switch facilitates in-code switch joins.
|
13
|
+
class Switch < Join
|
14
|
+
|
15
|
+
# An object responding to call that return the index of the output
|
16
|
+
# to that receives the result.
|
17
|
+
attr_accessor :selector
|
18
|
+
|
19
|
+
def initialize(config={}, app=Tap::App.instance, &block)
|
20
|
+
super(config, app)
|
21
|
+
@selector = block
|
22
|
+
end
|
23
|
+
|
24
|
+
def join(inputs, outputs, &block)
|
25
|
+
@selector = block
|
26
|
+
super(inputs, outputs)
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(result)
|
30
|
+
index = selector.call(result)
|
31
|
+
|
32
|
+
unless index && output = outputs[index]
|
33
|
+
raise SwitchError, "no switch target at index: #{index}"
|
34
|
+
end
|
35
|
+
|
36
|
+
dispatch(output, result)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Raised by a Switch join to indicate when a switch index is out of bounds.
|
40
|
+
class SwitchError < RuntimeError # :nodoc:
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Tap
|
2
|
+
module Joins
|
3
|
+
|
4
|
+
# :startdoc::join a synchronized multi-way join
|
5
|
+
#
|
6
|
+
# Sync works the same as Join, but passes the collected results of the
|
7
|
+
# inputs (ie an array) to the outputs. The results will not be passed
|
8
|
+
# until all of inputs have returned. A collision results if a single
|
9
|
+
# input completes twice before the group completes as a whole.
|
10
|
+
#
|
11
|
+
class Sync < Join
|
12
|
+
|
13
|
+
# NIL_VALUE is used to mark empty slots (nil itself cannot be used
|
14
|
+
# because it is a valid result value).
|
15
|
+
NIL_VALUE = Object.new
|
16
|
+
|
17
|
+
# An array holding results until the batch is ready to execute.
|
18
|
+
attr_reader :results
|
19
|
+
|
20
|
+
def initialize(config={}, app=Tap::App.instance)
|
21
|
+
super
|
22
|
+
@results = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Resets results. Normally there is no reason to call this method as it
|
26
|
+
# will shuffle the arguments being passed through self.
|
27
|
+
def reset
|
28
|
+
@results = inputs ? Array.new(inputs.length, NIL_VALUE) : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# A synchronized join sets a Callback as the join of each input. The
|
32
|
+
# callback is responsible for setting the result of each input into the
|
33
|
+
# correct 'results' slot.
|
34
|
+
def join(inputs, outputs)
|
35
|
+
@inputs.each do |input|
|
36
|
+
input.joins.delete(self)
|
37
|
+
end if @inputs
|
38
|
+
|
39
|
+
@inputs = inputs
|
40
|
+
|
41
|
+
index = 0
|
42
|
+
inputs.each do |input|
|
43
|
+
input.joins << Callback.new(self, index)
|
44
|
+
index += 1
|
45
|
+
end if inputs
|
46
|
+
reset
|
47
|
+
|
48
|
+
@outputs = outputs
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Call is called by a Callback and stores the result at the specified
|
53
|
+
# index in results. If the results have all been set, then they are
|
54
|
+
# sent to each output.
|
55
|
+
def call(result, index)
|
56
|
+
if result == NIL_VALUE
|
57
|
+
raise "NIL_VALUE cannot be passed as a result"
|
58
|
+
end
|
59
|
+
|
60
|
+
unless results[index] == NIL_VALUE
|
61
|
+
raise SynchronizeError, "already got a result for: #{inputs[index]}"
|
62
|
+
end
|
63
|
+
results[index] = result
|
64
|
+
|
65
|
+
unless results.include?(NIL_VALUE)
|
66
|
+
outputs.each {|output| dispatch(output, results) }
|
67
|
+
reset
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Callbacks are set as the join for each input to a Sync join, and
|
72
|
+
# allow the result of an input to be stored in the correct slot of
|
73
|
+
# the results array.
|
74
|
+
class Callback
|
75
|
+
|
76
|
+
# A backreference to the parent Sync join.
|
77
|
+
attr_reader :join
|
78
|
+
|
79
|
+
# The results index where result should be stored.
|
80
|
+
attr_reader :index
|
81
|
+
|
82
|
+
def initialize(join, index)
|
83
|
+
@join = join
|
84
|
+
@index = index
|
85
|
+
end
|
86
|
+
|
87
|
+
# Calls back to a Sync join to store the result at index.
|
88
|
+
def call(result)
|
89
|
+
join.call(result, index)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Raised by a Sync join to indicate when an input returns twice before
|
94
|
+
# the group is ready to execute.
|
95
|
+
class SynchronizeError < RuntimeError
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/tap/root.rb
CHANGED
@@ -1,42 +1,37 @@
|
|
1
1
|
require 'configurable'
|
2
|
-
require 'tap/
|
3
|
-
autoload(:FileUtils, 'fileutils')
|
2
|
+
require 'tap/root/utils'
|
4
3
|
|
5
4
|
module Tap
|
6
5
|
|
7
|
-
# Root
|
8
|
-
#
|
9
|
-
# full filepaths. Root also simplifies operations on filepaths.
|
6
|
+
# Root abstracts a directory to standardize access to resources organized
|
7
|
+
# within variable directory structures.
|
10
8
|
#
|
11
9
|
# # define a root directory with aliased relative paths
|
12
|
-
#
|
10
|
+
# root = Root.new(
|
11
|
+
# :root => '/root_dir',
|
12
|
+
# :relative_paths => {:input => 'in', :output => 'out'})
|
13
13
|
#
|
14
|
-
# #
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
14
|
+
# # access aliased paths
|
15
|
+
# root[:input] # => '/root_dir/in'
|
16
|
+
# root[:output] # => '/root_dir/out'
|
17
|
+
# root['implicit'] # => '/root_dir/implicit'
|
18
18
|
#
|
19
|
-
# #
|
20
|
-
#
|
19
|
+
# # absolute paths can also be aliased
|
20
|
+
# root[:abs, true] = "/absolute/path"
|
21
|
+
# root.path(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
|
21
22
|
#
|
22
|
-
# #
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
|
26
|
-
#
|
27
|
-
# # version filepaths
|
28
|
-
# r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
|
29
|
-
# r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
|
30
|
-
# r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
|
23
|
+
# # expanded paths are returned unchanged
|
24
|
+
# path = File.expand_path('expanded')
|
25
|
+
# root[path] # => path
|
31
26
|
#
|
32
|
-
# #
|
33
|
-
#
|
34
|
-
#
|
27
|
+
# # work with paths
|
28
|
+
# path = root.path(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
|
29
|
+
# root.relative_path(:input, path) # => 'path/to/file.txt'
|
30
|
+
# root.translate(path, :input, :output) # => '/root_dir/out/path/to/file.txt'
|
35
31
|
#
|
36
32
|
# By default, Roots are initialized to the present working directory
|
37
33
|
# (Dir.pwd).
|
38
34
|
#
|
39
|
-
#--
|
40
35
|
# === Implementation Notes
|
41
36
|
#
|
42
37
|
# Internally Root expands and stores all aliased paths in the 'paths' hash.
|
@@ -50,436 +45,62 @@ module Tap
|
|
50
45
|
# 'relative_paths', whereas relative paths have aliases in both.
|
51
46
|
#
|
52
47
|
# These features may be important to note when subclassing Root:
|
53
|
-
# - root and all
|
48
|
+
# - root and all paths in 'paths' are expanded
|
54
49
|
# - relative paths are stored in 'relative_paths'
|
55
50
|
# - absolute paths are present in 'paths' but not in 'relative_paths'
|
56
51
|
#
|
57
52
|
class Root
|
58
|
-
# Regexp to match a windows-style root filepath.
|
59
|
-
WIN_ROOT_PATTERN = /^[A-z]:\//
|
60
|
-
|
61
|
-
class << self
|
62
|
-
include Support::Versions
|
63
|
-
|
64
|
-
# Returns the filepath of path relative to dir. Both dir and path are
|
65
|
-
# expanded before the relative filepath is determined. Returns nil if
|
66
|
-
# the path is not relative to dir.
|
67
|
-
#
|
68
|
-
# Root.relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
|
69
|
-
#
|
70
|
-
def relative_filepath(dir, path, dir_string=Dir.pwd)
|
71
|
-
expanded_dir = File.expand_path(dir, dir_string)
|
72
|
-
expanded_path = File.expand_path(path, dir_string)
|
73
|
-
|
74
|
-
return nil unless expanded_path.index(expanded_dir) == 0
|
75
|
-
|
76
|
-
# use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
|
77
|
-
# as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
|
78
|
-
# empty string is returned
|
79
|
-
expanded_path[(expanded_dir.chomp("/").length + 1)..-1] || ""
|
80
|
-
end
|
81
|
-
|
82
|
-
# Generates a target filepath translated from the source_dir to the
|
83
|
-
# target_dir. Raises an error if the filepath is not relative to the
|
84
|
-
# source_dir.
|
85
|
-
#
|
86
|
-
# Root.translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
|
87
|
-
#
|
88
|
-
def translate(path, source_dir, target_dir)
|
89
|
-
unless relative_path = relative_filepath(source_dir, path)
|
90
|
-
raise ArgumentError, "\n#{path}\nis not relative to:\n#{source_dir}"
|
91
|
-
end
|
92
|
-
File.join(target_dir, relative_path)
|
93
|
-
end
|
94
|
-
|
95
|
-
# Returns the path, exchanging the extension with extname. Extname may
|
96
|
-
# optionally omit the leading period.
|
97
|
-
#
|
98
|
-
# Root.exchange('path/to/file.txt', '.html') # => 'path/to/file.html'
|
99
|
-
# Root.exchange('path/to/file.txt', 'rb') # => 'path/to/file.rb'
|
100
|
-
#
|
101
|
-
def exchange(path, extname)
|
102
|
-
"#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
|
103
|
-
end
|
104
|
-
|
105
|
-
# Lists all unique paths matching the input glob patterns.
|
106
|
-
def glob(*patterns)
|
107
|
-
patterns.collect do |pattern|
|
108
|
-
Dir.glob(pattern)
|
109
|
-
end.flatten.uniq
|
110
|
-
end
|
111
|
-
|
112
|
-
# Lists all unique versions of path matching the glob version patterns. If
|
113
|
-
# no patterns are specified, then all versions of path will be returned.
|
114
|
-
def vglob(path, *vpatterns)
|
115
|
-
vpatterns << "*" if vpatterns.empty?
|
116
|
-
vpatterns.collect do |vpattern|
|
117
|
-
results = Dir.glob(version(path, vpattern))
|
118
|
-
|
119
|
-
# extra work to include the default version path for any version
|
120
|
-
results << path if vpattern == "*" && File.exists?(path)
|
121
|
-
results
|
122
|
-
end.flatten.uniq
|
123
|
-
end
|
124
|
-
|
125
|
-
# Path suffix glob. Globs along the base paths for paths that match the
|
126
|
-
# specified suffix pattern.
|
127
|
-
def sglob(suffix_pattern, *base_paths)
|
128
|
-
base_paths.collect do |base|
|
129
|
-
base = File.expand_path(base)
|
130
|
-
Dir.glob(File.join(base, suffix_pattern))
|
131
|
-
end.flatten.uniq
|
132
|
-
end
|
133
|
-
|
134
|
-
# Like Dir.chdir but makes the directory, if necessary, when mkdir is
|
135
|
-
# specified. chdir raises an error for non-existant directories, as well
|
136
|
-
# as non-directory inputs.
|
137
|
-
def chdir(dir, mkdir=false, &block)
|
138
|
-
dir = File.expand_path(dir)
|
139
|
-
|
140
|
-
unless File.directory?(dir)
|
141
|
-
if !File.exists?(dir) && mkdir
|
142
|
-
FileUtils.mkdir_p(dir)
|
143
|
-
else
|
144
|
-
raise ArgumentError, "not a directory: #{dir}"
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
Dir.chdir(dir, &block)
|
149
|
-
end
|
150
|
-
|
151
|
-
# Prepares the input path by making the parent directory for path. If a
|
152
|
-
# block is given, a file is created at path and passed to it; in this
|
153
|
-
# way files with non-existant parent directories are readily made.
|
154
|
-
#
|
155
|
-
# Returns path.
|
156
|
-
def prepare(path, &block)
|
157
|
-
dirname = File.dirname(path)
|
158
|
-
FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
|
159
|
-
File.open(path, "w", &block) if block_given?
|
160
|
-
path
|
161
|
-
end
|
162
|
-
|
163
|
-
# The path root type indicating windows, *nix, or some unknown style of
|
164
|
-
# filepaths (:win, :nix, :unknown).
|
165
|
-
def path_root_type
|
166
|
-
@path_root_type ||= case
|
167
|
-
when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
|
168
|
-
when File.expand_path(".")[0] == ?/ then :nix
|
169
|
-
else :unknown
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
# Returns true if the input path appears to be an expanded path, based on
|
174
|
-
# Root.path_root_type.
|
175
|
-
#
|
176
|
-
# If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
|
177
|
-
#
|
178
|
-
# Root.expanded?('C:/path') # => true
|
179
|
-
# Root.expanded?('c:/path') # => true
|
180
|
-
# Root.expanded?('D:/path') # => true
|
181
|
-
# Root.expanded?('path') # => false
|
182
|
-
#
|
183
|
-
# If root_type == :nix, then expanded? returns true if the path begins
|
184
|
-
# with '/'.
|
185
|
-
#
|
186
|
-
# Root.expanded?('/path') # => true
|
187
|
-
# Root.expanded?('path') # => false
|
188
|
-
#
|
189
|
-
# Otherwise expanded? always returns nil.
|
190
|
-
def expanded?(path, root_type=path_root_type)
|
191
|
-
case root_type
|
192
|
-
when :win
|
193
|
-
path =~ WIN_ROOT_PATTERN ? true : false
|
194
|
-
when :nix
|
195
|
-
path[0] == ?/
|
196
|
-
else
|
197
|
-
nil
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
# Trivial indicates when a path does not have content to load. Returns
|
202
|
-
# true if the file at path is empty, non-existant, a directory, or nil.
|
203
|
-
def trivial?(path)
|
204
|
-
path == nil || !File.file?(path) || File.size(path) == 0
|
205
|
-
end
|
206
|
-
|
207
|
-
# Empty returns true when dir is an existing directory that has no files.
|
208
|
-
def empty?(dir)
|
209
|
-
File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
|
210
|
-
end
|
211
|
-
|
212
|
-
# Minimizes a set of paths to the set of shortest basepaths that unqiuely
|
213
|
-
# identify the paths. The path extension and versions are removed from
|
214
|
-
# the basepath if possible. For example:
|
215
|
-
#
|
216
|
-
# Tap::Root.minimize ['path/to/a.rb', 'path/to/b.rb']
|
217
|
-
# # => ['a', 'b']
|
218
|
-
#
|
219
|
-
# Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
|
220
|
-
# # => ['a', 'b']
|
221
|
-
#
|
222
|
-
# Tap::Root.minimize ['path/to/file.rb', 'path/to/file.txt']
|
223
|
-
# # => ['file.rb', 'file.txt']
|
224
|
-
#
|
225
|
-
# Tap::Root.minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
|
226
|
-
# # => ['path-0.1/to/file', 'path-0.2/to/file']
|
227
|
-
#
|
228
|
-
# Minimized paths that carry their extension will always carry
|
229
|
-
# their version as well, but the converse is not true; paths
|
230
|
-
# can be minimized to carry just the version and not the path
|
231
|
-
# extension.
|
232
|
-
#
|
233
|
-
# Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
|
234
|
-
# # => ['a-0.1.0.rb', 'a-0.1.0.txt']
|
235
|
-
#
|
236
|
-
# Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
|
237
|
-
# # => ['a-0.1.0', 'a-0.2.0']
|
238
|
-
#
|
239
|
-
# If a block is given, each (path, mini-path) pair will be passed
|
240
|
-
# to it after minimization.
|
241
|
-
def minimize(paths) # :yields: path, mini_path
|
242
|
-
unless block_given?
|
243
|
-
mini_paths = []
|
244
|
-
minimize(paths) {|p, mp| mini_paths << mp }
|
245
|
-
return mini_paths
|
246
|
-
end
|
247
|
-
|
248
|
-
splits = paths.uniq.collect do |path|
|
249
|
-
extname = File.extname(path)
|
250
|
-
extname = '' if extname =~ /^\.\d+$/
|
251
|
-
base = File.basename(path.chomp(extname))
|
252
|
-
version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
|
253
|
-
|
254
|
-
[dirname_or_array(path), base.chomp(version), extname, version, false, path]
|
255
|
-
end
|
256
|
-
|
257
|
-
while !splits.empty?
|
258
|
-
index = 0
|
259
|
-
splits = splits.collect do |(dir, base, extname, version, flagged, path)|
|
260
|
-
index += 1
|
261
|
-
case
|
262
|
-
when !flagged && just_one?(splits, index, base)
|
263
|
-
|
264
|
-
# found just one
|
265
|
-
yield(path, base)
|
266
|
-
nil
|
267
|
-
when dir.kind_of?(Array)
|
268
|
-
|
269
|
-
# no more path segments to use, try to add
|
270
|
-
# back version and extname
|
271
|
-
if dir.empty?
|
272
|
-
dir << File.dirname(base)
|
273
|
-
base = File.basename(base)
|
274
|
-
end
|
275
|
-
|
276
|
-
case
|
277
|
-
when !version.empty?
|
278
|
-
# add back version (occurs first)
|
279
|
-
[dir, "#{base}#{version}", extname, '', false, path]
|
280
|
-
|
281
|
-
when !extname.empty?
|
282
|
-
|
283
|
-
# add back extension (occurs second)
|
284
|
-
[dir, "#{base}#{extname}", '', version, false, path]
|
285
|
-
else
|
286
|
-
|
287
|
-
# nothing more to distinguish... path is minimized (occurs third)
|
288
|
-
yield(path, min_join(dir[0], base))
|
289
|
-
nil
|
290
|
-
end
|
291
|
-
else
|
292
|
-
|
293
|
-
# shift path segment. dirname_or_array returns an
|
294
|
-
# array if this is the last path segment to shift.
|
295
|
-
[dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
|
296
|
-
end
|
297
|
-
end.compact
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
# Returns true if the mini_path matches path. Matching logic reverses
|
302
|
-
# that of minimize:
|
303
|
-
#
|
304
|
-
# * a match occurs when path ends with mini_path
|
305
|
-
# * if mini_path doesn't specify an extension, then mini_path
|
306
|
-
# must only match path up to the path extension
|
307
|
-
# * if mini_path doesn't specify a version, then mini_path
|
308
|
-
# must only match path up to the path basename (minus the
|
309
|
-
# version and extname)
|
310
|
-
#
|
311
|
-
# For example:
|
312
|
-
#
|
313
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file') # => true
|
314
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
|
315
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
|
316
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
|
317
|
-
#
|
318
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
|
319
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
|
320
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'another') # => false
|
321
|
-
#
|
322
|
-
# In matching, partial basenames are not allowed but partial directories
|
323
|
-
# are allowed. Hence:
|
324
|
-
#
|
325
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'file') # => true
|
326
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
|
327
|
-
# Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
|
328
|
-
#
|
329
|
-
def minimal_match?(path, mini_path)
|
330
|
-
extname = non_version_extname(mini_path)
|
331
|
-
version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
|
332
|
-
|
333
|
-
match_path = case
|
334
|
-
when !extname.empty?
|
335
|
-
# force full match
|
336
|
-
path
|
337
|
-
when !version.empty?
|
338
|
-
# match up to version
|
339
|
-
path.chomp(non_version_extname(path))
|
340
|
-
else
|
341
|
-
# match up base
|
342
|
-
path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
|
343
|
-
end
|
344
|
-
|
345
|
-
# key ends with pattern AND basenames of each are equal...
|
346
|
-
# the last check ensures that a full path segment has
|
347
|
-
# been specified
|
348
|
-
match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path)
|
349
|
-
end
|
350
|
-
|
351
|
-
# Returns the path segments for the given path, splitting along the path
|
352
|
-
# divider. Root paths are always represented by a string, if only an
|
353
|
-
# empty string.
|
354
|
-
#
|
355
|
-
# os divider example
|
356
|
-
# windows '\' Root.split('C:\path\to\file') # => ["C:", "path", "to", "file"]
|
357
|
-
# *nix '/' Root.split('/path/to/file') # => ["", "path", "to", "file"]
|
358
|
-
#
|
359
|
-
# The path is always expanded relative to the expand_dir; so '.' and
|
360
|
-
# '..' are resolved. However, unless expand_path == true, only the
|
361
|
-
# segments relative to the expand_dir are returned.
|
362
|
-
#
|
363
|
-
# On windows (note that expanding paths allows the use of slashes or
|
364
|
-
# backslashes):
|
365
|
-
#
|
366
|
-
# Dir.pwd # => 'C:/'
|
367
|
-
# Root.split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
|
368
|
-
# Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
|
369
|
-
#
|
370
|
-
# On *nix (or more generally systems with '/' roots):
|
371
|
-
#
|
372
|
-
# Dir.pwd # => '/'
|
373
|
-
# Root.split('path/to/.././to/file') # => ["", "path", "to", "file"]
|
374
|
-
# Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
|
375
|
-
#
|
376
|
-
def split(path, expand_path=true, expand_dir=Dir.pwd)
|
377
|
-
path = if expand_path
|
378
|
-
File.expand_path(path, expand_dir)
|
379
|
-
else
|
380
|
-
# normalize the path by expanding it, then
|
381
|
-
# work back to the relative filepath as needed
|
382
|
-
expanded_dir = File.expand_path(expand_dir)
|
383
|
-
expanded_path = File.expand_path(path, expand_dir)
|
384
|
-
expanded_path.index(expanded_dir) != 0 ? expanded_path : Tap::Root.relative_filepath(expanded_dir, expanded_path)
|
385
|
-
end
|
386
|
-
|
387
|
-
segments = path.scan(/[^\/]+/)
|
388
|
-
|
389
|
-
# add back the root filepath as needed on *nix
|
390
|
-
segments.unshift "" if path[0] == ?/
|
391
|
-
segments
|
392
|
-
end
|
393
|
-
|
394
|
-
private
|
395
|
-
|
396
|
-
# utility method for minimize -- joins the
|
397
|
-
# dir and path, preventing results like:
|
398
|
-
#
|
399
|
-
# "./path"
|
400
|
-
# "//path"
|
401
|
-
#
|
402
|
-
def min_join(dir, path) # :nodoc:
|
403
|
-
case dir
|
404
|
-
when "." then path
|
405
|
-
when "/" then "/#{path}"
|
406
|
-
else "#{dir}/#{path}"
|
407
|
-
end
|
408
|
-
end
|
409
|
-
|
410
|
-
# utility method for minimize -- returns the
|
411
|
-
# dirname of path, or an array if the dirname
|
412
|
-
# is effectively empty.
|
413
|
-
def dirname_or_array(path) # :nodoc:
|
414
|
-
dir = File.dirname(path)
|
415
|
-
case dir
|
416
|
-
when path, '.' then []
|
417
|
-
else dir
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
# utility method for minimize -- determines if there
|
422
|
-
# is just one of the base in splits, while flagging
|
423
|
-
# all matching entries.
|
424
|
-
def just_one?(splits, index, base) # :nodoc:
|
425
|
-
just_one = true
|
426
|
-
index.upto(splits.length-1) do |i|
|
427
|
-
if splits[i][1] == base
|
428
|
-
splits[i][4] = true
|
429
|
-
just_one = false
|
430
|
-
end
|
431
|
-
end
|
432
|
-
|
433
|
-
just_one
|
434
|
-
end
|
435
|
-
|
436
|
-
# utility method for minimal_match -- returns a non-version
|
437
|
-
# extname, or an empty string if the path ends in a version.
|
438
|
-
def non_version_extname(path) # :nodoc:
|
439
|
-
extname = File.extname(path)
|
440
|
-
extname =~ /^\.\d+$/ ? '' : extname
|
441
|
-
end
|
442
|
-
|
443
|
-
end
|
444
|
-
|
445
53
|
include Configurable
|
446
|
-
include
|
447
|
-
|
54
|
+
include Utils
|
55
|
+
|
448
56
|
# The root directory.
|
449
|
-
config_attr(:root, '.', :writer => false)
|
450
|
-
|
57
|
+
config_attr(:root, '.', :writer => false, :set_default => false)
|
58
|
+
|
451
59
|
# A hash of (alias, relative path) pairs for aliased paths relative
|
452
60
|
# to root.
|
453
|
-
config_attr(:relative_paths, {}, :writer => false)
|
454
|
-
|
61
|
+
config_attr(:relative_paths, {}, :writer => false, :set_default => false, :type => :hash)
|
62
|
+
|
455
63
|
# A hash of (alias, relative path) pairs for aliased absolute paths.
|
456
|
-
config_attr(:absolute_paths, {}, :reader => false, :writer => false)
|
457
|
-
|
64
|
+
config_attr(:absolute_paths, {}, :reader => false, :writer => false, :set_default => false, :type => :hash)
|
65
|
+
|
458
66
|
# A hash of (alias, expanded path) pairs for expanded relative and
|
459
67
|
# absolute paths.
|
460
68
|
attr_reader :paths
|
461
|
-
|
69
|
+
|
462
70
|
# The filesystem root, inferred from self.root
|
463
71
|
# (ex '/' on *nix or something like 'C:/' on Windows).
|
464
72
|
attr_reader :path_root
|
73
|
+
|
74
|
+
# Creates a new Root from the specified configurations. A directory may be
|
75
|
+
# provided instead of a configuration hash; in that case no aliased relative
|
76
|
+
# or absolute paths are specified. By default root is the present working
|
77
|
+
# directory.
|
78
|
+
def initialize(config_or_dir=Dir.pwd)
|
79
|
+
# root, relative_paths, and absolute_paths are assigned manually as
|
80
|
+
# an optimization (otherwise assign_paths would get called once for
|
81
|
+
# each configuration)
|
82
|
+
if config_or_dir.kind_of?(String)
|
83
|
+
assign_paths(config_or_dir, {}, {})
|
84
|
+
config_or_dir = {}
|
85
|
+
else
|
86
|
+
root = config_or_dir.delete(:root) || Dir.pwd
|
87
|
+
relative_paths = config_or_dir.delete(:relative_paths) || {}
|
88
|
+
absolute_paths = config_or_dir.delete(:absolute_paths) || {}
|
89
|
+
assign_paths(root, relative_paths, absolute_paths)
|
90
|
+
end
|
465
91
|
|
466
|
-
|
467
|
-
# and absolute paths. By default root is the present working directory
|
468
|
-
# and no aliased relative or absolute paths are specified.
|
469
|
-
def initialize(root=Dir.pwd, relative_paths={}, absolute_paths={})
|
470
|
-
assign_paths(root, relative_paths, absolute_paths)
|
471
|
-
@config = DelegateHash.new(self.class.configurations, {}, self)
|
92
|
+
initialize_config(config_or_dir)
|
472
93
|
end
|
473
|
-
|
94
|
+
|
474
95
|
# Sets the root directory. All paths are reassigned accordingly.
|
475
96
|
def root=(path)
|
476
97
|
assign_paths(path, relative_paths, absolute_paths)
|
477
98
|
end
|
478
|
-
|
99
|
+
|
479
100
|
# Sets the relative_paths to those provided. 'root' and :root are reserved
|
480
101
|
# aliases and cannot be set using this method (use root= instead).
|
481
102
|
#
|
482
|
-
# r =
|
103
|
+
# r = Root.new
|
483
104
|
# r['alt'] # => File.join(r.root, 'alt')
|
484
105
|
# r.relative_paths = {'alt' => 'dir'}
|
485
106
|
# r['alt'] # => File.join(r.root, 'dir')
|
@@ -488,11 +109,11 @@ module Tap
|
|
488
109
|
paths = Validation::HASH[paths]
|
489
110
|
assign_paths(root, paths, absolute_paths)
|
490
111
|
end
|
491
|
-
|
112
|
+
|
492
113
|
# Sets the absolute paths to those provided. 'root' and :root are reserved
|
493
114
|
# aliases and cannot be set using this method (use root= instead).
|
494
115
|
#
|
495
|
-
# r =
|
116
|
+
# r = Root.new
|
496
117
|
# r['abs'] # => File.join(r.root, 'abs')
|
497
118
|
# r.absolute_paths = {'abs' => '/path/to/dir'}
|
498
119
|
# r['abs'] # => '/path/to/dir'
|
@@ -501,7 +122,7 @@ module Tap
|
|
501
122
|
paths = Validation::HASH[paths]
|
502
123
|
assign_paths(root, relative_paths, paths)
|
503
124
|
end
|
504
|
-
|
125
|
+
|
505
126
|
# Returns the absolute paths registered with self.
|
506
127
|
def absolute_paths
|
507
128
|
abs_paths = {}
|
@@ -515,7 +136,7 @@ module Tap
|
|
515
136
|
|
516
137
|
# Sets an alias for the path relative to the root directory. The aliases
|
517
138
|
# 'root' and :root cannot be set with this method (use root= instead).
|
518
|
-
# Absolute
|
139
|
+
# Absolute paths can be set using the second syntax.
|
519
140
|
#
|
520
141
|
# r = Root.new '/root_dir'
|
521
142
|
# r[:dir] = 'path/to/dir'
|
@@ -523,25 +144,23 @@ module Tap
|
|
523
144
|
#
|
524
145
|
# r[:abs, true] = '/abs/path/to/dir'
|
525
146
|
# r[:abs] # => '/abs/path/to/dir'
|
526
|
-
#
|
527
|
-
#--
|
147
|
+
#
|
148
|
+
#--
|
528
149
|
# Implementation Note:
|
529
150
|
#
|
530
|
-
# The syntax for setting an absolute
|
531
|
-
# In fact the method
|
532
|
-
# rather than (:dir, '/abs/path/to/dir', true)
|
533
|
-
# and absolute are switched when setting an absolute
|
151
|
+
# The syntax for setting an absolute path requires an odd use []=.
|
152
|
+
# In fact the method receives the arguments (:dir, true, '/abs/path/to/dir')
|
153
|
+
# rather than (:dir, '/abs/path/to/dir', true) meaning that internally path
|
154
|
+
# and absolute are switched when setting an absolute path.
|
534
155
|
#
|
535
156
|
def []=(als, path, absolute=false)
|
536
157
|
raise ArgumentError, "the alias #{als.inspect} is reserved" if als.to_s == 'root'
|
537
|
-
|
158
|
+
|
538
159
|
# switch the paths if absolute was provided
|
539
160
|
unless absolute == false
|
540
|
-
|
541
|
-
path = absolute
|
542
|
-
absolute = switch
|
161
|
+
path, absolute = absolute, path
|
543
162
|
end
|
544
|
-
|
163
|
+
|
545
164
|
case
|
546
165
|
when path.nil?
|
547
166
|
@relative_paths.delete(als)
|
@@ -554,7 +173,7 @@ module Tap
|
|
554
173
|
@paths[als] = File.expand_path(File.join(root, path))
|
555
174
|
end
|
556
175
|
end
|
557
|
-
|
176
|
+
|
558
177
|
# Returns the expanded path for the specified alias. If the alias has not
|
559
178
|
# been set, then the path is inferred to be 'root/als'. Expanded paths
|
560
179
|
# are returned directly.
|
@@ -569,79 +188,70 @@ module Tap
|
|
569
188
|
def [](als)
|
570
189
|
path = self.paths[als]
|
571
190
|
return path unless path == nil
|
572
|
-
|
191
|
+
|
573
192
|
als = als.to_s
|
574
|
-
|
193
|
+
expanded?(als) ? als : File.expand_path(File.join(root, als))
|
575
194
|
end
|
576
|
-
|
577
|
-
# Resolves the specified alias, joins
|
578
|
-
# resulting
|
579
|
-
def
|
195
|
+
|
196
|
+
# Resolves the specified alias, joins paths together, and expands the
|
197
|
+
# resulting path.
|
198
|
+
def path(als, *paths)
|
580
199
|
File.expand_path(File.join(self[als], *paths))
|
581
200
|
end
|
582
201
|
|
583
|
-
#
|
584
|
-
def
|
585
|
-
|
202
|
+
# Returns true if the path is relative to the specified alias.
|
203
|
+
def relative?(als, path)
|
204
|
+
super(self[als], path)
|
586
205
|
end
|
587
|
-
|
588
|
-
#
|
589
|
-
|
590
|
-
|
591
|
-
dir = self[als]
|
592
|
-
path = filepath(als, *paths)
|
593
|
-
|
594
|
-
if path.rindex(dir, 0) != 0
|
595
|
-
raise "not a subpath: #{path} (#{dir})"
|
596
|
-
end
|
597
|
-
|
598
|
-
path
|
206
|
+
|
207
|
+
# Returns the part of path relative to the specified alias.
|
208
|
+
def relative_path(als, path)
|
209
|
+
super(self[als], path)
|
599
210
|
end
|
600
211
|
|
601
|
-
# Generates a
|
602
|
-
#
|
603
|
-
# to the source dir.
|
212
|
+
# Generates a path translated from the aliased source to the aliased target.
|
213
|
+
# Raises an error if path is not relative to the source.
|
604
214
|
#
|
605
|
-
# r =
|
606
|
-
# path = r.
|
607
|
-
# r.translate(path, :in, :out)
|
215
|
+
# r = Root.new '/root_dir'
|
216
|
+
# path = r.path(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
|
217
|
+
# r.translate(path, :in, :out) # => '/root_dir/out/path/to/file.txt'
|
608
218
|
#
|
609
219
|
def translate(path, source_als, target_als)
|
610
|
-
|
220
|
+
super(path, self[source_als], self[target_als])
|
611
221
|
end
|
612
|
-
|
613
|
-
#
|
222
|
+
|
223
|
+
# Globs for paths along the aliased path matching the input patterns.
|
614
224
|
# Patterns should join with the aliased path make valid globs for
|
615
225
|
# Dir.glob. If no patterns are specified, glob returns all paths
|
616
226
|
# matching 'als/**/*'.
|
617
227
|
def glob(als, *patterns)
|
618
228
|
patterns << "**/*" if patterns.empty?
|
619
|
-
patterns.collect! {|pattern|
|
620
|
-
|
229
|
+
patterns.collect! {|pattern| path(als, pattern)}
|
230
|
+
super(*patterns)
|
621
231
|
end
|
622
|
-
|
232
|
+
|
623
233
|
# Lists all versions of path in the aliased dir matching the version
|
624
234
|
# patterns. If no patterns are specified, then all versions of path
|
625
235
|
# will be returned.
|
626
|
-
def
|
627
|
-
|
236
|
+
def version_glob(als, path, *vpatterns)
|
237
|
+
super(path(als, path), *vpatterns)
|
628
238
|
end
|
629
|
-
|
239
|
+
|
630
240
|
# Changes pwd to the specified directory using Root.chdir.
|
631
241
|
def chdir(als, mkdir=false, &block)
|
632
|
-
|
242
|
+
super(self[als], mkdir, &block)
|
633
243
|
end
|
634
|
-
|
635
|
-
# Constructs a path from the inputs
|
244
|
+
|
245
|
+
# Constructs a path from the inputs and prepares it using
|
636
246
|
# Root.prepare. Returns the path.
|
637
247
|
def prepare(als, *paths, &block)
|
638
|
-
|
248
|
+
super(path(als, *paths), &block)
|
639
249
|
end
|
640
|
-
|
250
|
+
|
641
251
|
private
|
642
252
|
|
643
253
|
# reassigns all paths with the input root, relative_paths, and absolute_paths
|
644
|
-
def assign_paths(root, relative_paths, absolute_paths)
|
254
|
+
def assign_paths(root, relative_paths, absolute_paths) # :nodoc:
|
645
255
|
@root = File.expand_path(root)
|
646
256
|
@relative_paths = {}
|
647
257
|
@paths = {'root' => @root, :root => @root}
|
@@ -650,10 +260,9 @@ module Tap
|
|
650
260
|
while @path_root != (parent = File.dirname(@path_root))
|
651
261
|
@path_root = parent
|
652
262
|
end
|
653
|
-
|
263
|
+
|
654
264
|
relative_paths.each_pair {|dir, path| self[dir] = path }
|
655
265
|
absolute_paths.each_pair {|dir, path| self[dir, true] = path }
|
656
266
|
end
|
657
|
-
|
658
267
|
end
|
659
268
|
end
|