Class: Tap::Root
- Extended by:
- Support::Versions
- Includes:
- Configurable, Support::Versions
- Defined in:
- lib/tap/root.rb
Overview
Root allows you to define a root directory and alias relative paths, so that you can conceptualize what filepaths you need without predefining the full filepaths. Root also simplifies operations on filepaths.
# define a root directory with aliased relative paths
r = Root.new '/root_dir', :input => 'in', :output => 'out'
# work with aliases
r[:input] # => '/root_dir/in'
r[:output] # => '/root_dir/out'
r['implicit'] # => '/root_dir/implicit'
# expanded paths are returned unchanged
r[File.('expanded')] # => File.expand_path('expanded')
# work with filepaths
fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
r.relative_filepath(:input, fp) # => 'path/to/file.txt'
r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
# version filepaths
r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
# absolute paths can also be aliased
r[:abs, true] = "/absolute/path"
r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
By default, Roots are initialized to the present working directory (Dir.pwd). As in the ‘implicit’ example, Root infers a path relative to the root directory whenever it needs to resolve an alias that is not explicitly set. The only exceptions to this are fully expanded paths. These are returned unchanged.
–
Implementation Notes
Internally Root expands and stores all aliased paths in the ‘paths’ hash. Expanding paths ensures they remain constant even when the present working directory (Dir.pwd) changes.
Root keeps a separate ‘relative_paths’ hash mapping aliases to their relative paths. This hash allow reassignment if and when the root directory changes. By contrast, there is no separate data structure storing the absolute paths. An absolute path thus has an alias in ‘paths’ but not ‘relative_paths’, whereas relative paths have aliases in both.
These features may be important to note when subclassing Root:
-
root and all filepaths in ‘paths’ are expanded
-
relative paths are stored in ‘relative_paths’
-
absolute paths are present in ‘paths’ but not in ‘relative_paths’
Direct Known Subclasses
Constant Summary collapse
- WIN_ROOT_PATTERN =
Regexp to match a windows-style root filepath.
/^[A-z]:\//
Instance Attribute Summary collapse
-
#path_root ⇒ Object
readonly
The filesystem root, inferred from self.root (ex ‘/’ on *nix or something like ‘C:/’ on Windows).
-
#paths ⇒ Object
readonly
A hash of (alias, expanded path) pairs for expanded relative and absolute paths.
Class Method Summary collapse
-
.chdir(dir, mkdir = false, &block) ⇒ Object
Like Dir.chdir but makes the directory, if necessary, when mkdir is specified.
-
.empty?(dir) ⇒ Boolean
Empty returns true when dir is an existing directory that has no files.
-
.exchange(path, extname) ⇒ Object
Returns the path, exchanging the extension with extname.
-
.expanded?(path, root_type = path_root_type) ⇒ Boolean
Returns true if the input path appears to be an expanded path, based on Root.path_root_type.
-
.glob(*patterns) ⇒ Object
Lists all unique paths matching the input glob patterns.
-
.minimal_match?(path, mini_path) ⇒ Boolean
Returns true if the mini_path matches path.
-
.minimize(paths) ⇒ Object
Minimizes a set of paths to the set of shortest basepaths that unqiuely identify the paths.
-
.path_root_type ⇒ Object
The path root type indicating windows, *nix, or some unknown style of filepaths (:win, :nix, :unknown).
-
.prepare(path, &block) ⇒ Object
Prepares the input path by making the parent directory for path.
-
.relative_filepath(dir, path, dir_string = Dir.pwd) ⇒ Object
Returns the filepath of path relative to dir.
-
.sglob(suffix_pattern, *base_paths) ⇒ Object
Path suffix glob.
-
.split(path, expand_path = true, expand_dir = Dir.pwd) ⇒ Object
Returns the path segments for the given path, splitting along the path divider.
-
.translate(path, source_dir, target_dir) ⇒ Object
Generates a target filepath translated from the source_dir to the target_dir.
-
.trivial?(path) ⇒ Boolean
Trivial indicates when a path does not have content to load.
-
.vglob(path, *vpatterns) ⇒ Object
Lists all unique versions of path matching the glob version patterns.
Instance Method Summary collapse
-
#[](als) ⇒ Object
Returns the expanded path for the specified alias.
-
#[]=(als, path, absolute = false) ⇒ Object
Sets an alias for the path relative to the root directory.
-
#absolute_paths ⇒ Object
Returns the absolute paths registered with self.
-
#absolute_paths=(paths) ⇒ Object
Sets the absolute paths to those provided.
-
#chdir(als, mkdir = false, &block) ⇒ Object
Changes pwd to the specified directory using Root.chdir.
-
#filepath(als, *paths) ⇒ Object
Resolves the specified alias, joins the paths together, and expands the resulting filepath.
-
#glob(als, *patterns) ⇒ Object
Lists all files along the aliased path matching the input patterns.
-
#initialize(root = Dir.pwd, relative_paths = {}, absolute_paths = {}) ⇒ Root
constructor
Creates a new Root with the given root directory, aliased relative paths and absolute paths.
-
#prepare(als, *paths, &block) ⇒ Object
Constructs a path from the inputs (using filepath) and prepares it using Root.prepare.
-
#relative_filepath(als, path) ⇒ Object
Retrieves the filepath relative to the path of the specified alias.
-
#relative_paths=(paths) ⇒ Object
Sets the relative_paths to those provided.
-
#root=(path) ⇒ Object
Sets the root directory.
-
#translate(path, source_als, target_als) ⇒ Object
Generates a filepath translated from the aliased source dir to the aliased target dir.
-
#vglob(als, path, *vpatterns) ⇒ Object
Lists all versions of path in the aliased dir matching the version patterns.
Methods included from Support::Versions
compare_versions, deversion, increment, version
Constructor Details
#initialize(root = Dir.pwd, relative_paths = {}, absolute_paths = {}) ⇒ Root
Creates a new Root with the given root directory, aliased relative paths and absolute paths. By default root is the present working directory and no aliased relative or absolute paths are specified.
473 474 475 476 |
# File 'lib/tap/root.rb', line 473 def initialize(root=Dir.pwd, relative_paths={}, absolute_paths={}) assign_paths(root, relative_paths, absolute_paths) @config = DelegateHash.new(self.class.configurations, {}, self) end |
Instance Attribute Details
#path_root ⇒ Object (readonly)
The filesystem root, inferred from self.root (ex ‘/’ on *nix or something like ‘C:/’ on Windows).
468 469 470 |
# File 'lib/tap/root.rb', line 468 def path_root @path_root end |
#paths ⇒ Object (readonly)
A hash of (alias, expanded path) pairs for expanded relative and absolute paths.
464 465 466 |
# File 'lib/tap/root.rb', line 464 def paths @paths end |
Class Method Details
.chdir(dir, mkdir = false, &block) ⇒ Object
Like Dir.chdir but makes the directory, if necessary, when mkdir is specified. chdir raises an error for non-existant directories, as well as non-directory inputs.
141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/tap/root.rb', line 141 def chdir(dir, mkdir=false, &block) dir = File.(dir) unless File.directory?(dir) if !File.exists?(dir) && mkdir FileUtils.mkdir_p(dir) else raise ArgumentError, "not a directory: #{dir}" end end Dir.chdir(dir, &block) end |
.empty?(dir) ⇒ Boolean
Empty returns true when dir is an existing directory that has no files.
212 213 214 |
# File 'lib/tap/root.rb', line 212 def empty?(dir) File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty? end |
.exchange(path, extname) ⇒ Object
105 106 107 |
# File 'lib/tap/root.rb', line 105 def exchange(path, extname) "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}" end |
.expanded?(path, root_type = path_root_type) ⇒ Boolean
Returns true if the input path appears to be an expanded path, based on Root.path_root_type.
If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
Root.('C:/path') # => true
Root.('c:/path') # => true
Root.('D:/path') # => true
Root.('path') # => false
If root_type == :nix, then expanded? returns true if the path begins with ‘/’.
Root.('/path') # => true
Root.('path') # => false
Otherwise expanded? always returns nil.
194 195 196 197 198 199 200 201 202 203 |
# File 'lib/tap/root.rb', line 194 def (path, root_type=path_root_type) case root_type when :win path =~ WIN_ROOT_PATTERN ? true : false when :nix path[0] == ?/ else nil end end |
.glob(*patterns) ⇒ Object
Lists all unique paths matching the input glob patterns.
110 111 112 113 114 |
# File 'lib/tap/root.rb', line 110 def glob(*patterns) patterns.collect do |pattern| Dir.glob(pattern) end.flatten.uniq end |
.minimal_match?(path, mini_path) ⇒ Boolean
Returns true if the mini_path matches path. Matching logic reverses that of minimize:
-
a match occurs when path ends with mini_path
-
if mini_path doesn’t specify an extension, then mini_path must only match path up to the path extension
-
if mini_path doesn’t specify a version, then mini_path must only match path up to the path basename (minus the version and extname)
For example:
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file') # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'another') # => false
In matching, partial basenames are not allowed but partial directories are allowed. Hence:
Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'file') # => true
Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/tap/root.rb', line 333 def minimal_match?(path, mini_path) extname = non_version_extname(mini_path) version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : '' match_path = case when !extname.empty? # force full match path when !version.empty? # match up to version path.chomp(non_version_extname(path)) else # match up base path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '') end # key ends with pattern AND basenames of each are equal... # the last check ensures that a full path segment has # been specified match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path) end |
.minimize(paths) ⇒ Object
Minimizes a set of paths to the set of shortest basepaths that unqiuely identify the paths. The path extension and versions are removed from the basepath if possible. For example:
Tap::Root.minimize ['path/to/a.rb', 'path/to/b.rb']
# => ['a', 'b']
Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
# => ['a', 'b']
Tap::Root.minimize ['path/to/file.rb', 'path/to/file.txt']
# => ['file.rb', 'file.txt']
Tap::Root.minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
# => ['path-0.1/to/file', 'path-0.2/to/file']
Minimized paths that carry their extension will always carry their version as well, but the converse is not true; paths can be minimized to carry just the version and not the path extension.
Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
# => ['a-0.1.0.rb', 'a-0.1.0.txt']
Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
# => ['a-0.1.0', 'a-0.2.0']
If a block is given, each (path, mini-path) pair will be passed to it after minimization.
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/tap/root.rb', line 245 def minimize(paths) # :yields: path, mini_path unless block_given? mini_paths = [] minimize(paths) {|p, mp| mini_paths << mp } return mini_paths end splits = paths.uniq.collect do |path| extname = File.extname(path) extname = '' if extname =~ /^\.\d+$/ base = File.basename(path.chomp(extname)) version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : '' [dirname_or_array(path), base.chomp(version), extname, version, false, path] end while !splits.empty? index = 0 splits = splits.collect do |(dir, base, extname, version, flagged, path)| index += 1 case when !flagged && just_one?(splits, index, base) # found just one yield(path, base) nil when dir.kind_of?(Array) # no more path segments to use, try to add # back version and extname if dir.empty? dir << File.dirname(base) base = File.basename(base) end case when !version.empty? # add back version (occurs first) [dir, "#{base}#{version}", extname, '', false, path] when !extname.empty? # add back extension (occurs second) [dir, "#{base}#{extname}", '', version, false, path] else # nothing more to distinguish... path is minimized (occurs third) yield(path, min_join(dir[0], base)) nil end else # shift path segment. dirname_or_array returns an # array if this is the last path segment to shift. [dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path] end end.compact end end |
.path_root_type ⇒ Object
The path root type indicating windows, *nix, or some unknown style of filepaths (:win, :nix, :unknown).
169 170 171 172 173 174 175 |
# File 'lib/tap/root.rb', line 169 def path_root_type @path_root_type ||= case when RUBY_PLATFORM =~ /mswin/ && File.(".") =~ WIN_ROOT_PATTERN then :win when File.(".")[0] == ?/ then :nix else :unknown end end |
.prepare(path, &block) ⇒ Object
Prepares the input path by making the parent directory for path. If a block is given, a file is created at path and passed to it; in this way files with non-existant parent directories are readily made.
Returns path.
160 161 162 163 164 165 |
# File 'lib/tap/root.rb', line 160 def prepare(path, &block) dirname = File.dirname(path) FileUtils.mkdir_p(dirname) unless File.exists?(dirname) File.open(path, "w", &block) if block_given? path end |
.relative_filepath(dir, path, dir_string = Dir.pwd) ⇒ Object
Returns the filepath of path relative to dir. Both dir and path are expanded before the relative filepath is determined. Returns nil if the path is not relative to dir.
Root.relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/tap/root.rb', line 74 def relative_filepath(dir, path, dir_string=Dir.pwd) = File.(dir, dir_string) = File.(path, dir_string) return nil unless .index() == 0 # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length # as in: relative_filepath('/path', '/path') then the first arg returns nil, and an # empty string is returned [(.chomp("/").length + 1)..-1] || "" end |
.sglob(suffix_pattern, *base_paths) ⇒ Object
Path suffix glob. Globs along the base paths for paths that match the specified suffix pattern.
131 132 133 134 135 136 |
# File 'lib/tap/root.rb', line 131 def sglob(suffix_pattern, *base_paths) base_paths.collect do |base| base = File.(base) Dir.glob(File.join(base, suffix_pattern)) end.flatten.uniq end |
.split(path, expand_path = true, expand_dir = Dir.pwd) ⇒ Object
Returns the path segments for the given path, splitting along the path divider. Root paths are always represented by a string, if only an empty string.
os divider example
windows '\' Root.split('C:\path\to\file') # => ["C:", "path", "to", "file"]
*nix '/' Root.split('/path/to/file') # => ["", "path", "to", "file"]
The path is always expanded relative to the expand_dir; so ‘.’ and ‘..’ are resolved. However, unless expand_path == true, only the segments relative to the expand_dir are returned.
On windows (note that expanding paths allows the use of slashes or backslashes):
Dir.pwd # => 'C:/'
Root.split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
On *nix (or more generally systems with ‘/’ roots):
Dir.pwd # => '/'
Root.split('path/to/.././to/file') # => ["", "path", "to", "file"]
Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 |
# File 'lib/tap/root.rb', line 380 def split(path, =true, =Dir.pwd) path = if File.(path, ) else # normalize the path by expanding it, then # work back to the relative filepath as needed = File.() = File.(path, ) .index() != 0 ? : Tap::Root.relative_filepath(, ) end segments = path.scan(/[^\/]+/) # add back the root filepath as needed on *nix segments.unshift "" if path[0] == ?/ segments end |
.translate(path, source_dir, target_dir) ⇒ Object
Generates a target filepath translated from the source_dir to the target_dir. Raises an error if the filepath is not relative to the source_dir.
Root.translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
92 93 94 95 96 97 |
# File 'lib/tap/root.rb', line 92 def translate(path, source_dir, target_dir) unless relative_path = relative_filepath(source_dir, path) raise ArgumentError, "\n#{path}\nis not relative to:\n#{source_dir}" end File.join(target_dir, relative_path) end |
.trivial?(path) ⇒ Boolean
Trivial indicates when a path does not have content to load. Returns true if the file at path is empty, non-existant, a directory, or nil.
207 208 209 |
# File 'lib/tap/root.rb', line 207 def trivial?(path) path == nil || !File.file?(path) || File.size(path) == 0 end |
.vglob(path, *vpatterns) ⇒ Object
Lists all unique versions of path matching the glob version patterns. If no patterns are specified, then all versions of path will be returned.
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/tap/root.rb', line 118 def vglob(path, *vpatterns) vpatterns << "*" if vpatterns.empty? vpatterns.collect do |vpattern| results = Dir.glob(version(path, vpattern)) # extra work to include the default version path for any version results << path if vpattern == "*" && File.exists?(path) results end.flatten.uniq end |
Instance Method Details
#[](als) ⇒ Object
Returns the expanded path for the specified alias. If the alias has not been set, then the path is inferred to be ‘root/als’. Expanded paths are returned directly.
r = Root.new '/root_dir', :dir => 'path/to/dir'
r[:dir] # => '/root_dir/path/to/dir'
r.path_root # => '/'
r['relative/path'] # => '/root_dir/relative/path'
r['/expanded/path'] # => '/expanded/path'
571 572 573 574 575 576 577 |
# File 'lib/tap/root.rb', line 571 def [](als) path = self.paths[als] return path unless path == nil als = als.to_s Root.(als) ? als : File.(File.join(root, als)) end |
#[]=(als, path, absolute = false) ⇒ Object
Sets an alias for the path relative to the root directory. The aliases ‘root’ and :root cannot be set with this method (use root= instead). Absolute filepaths can be set using the second syntax.
r = Root.new '/root_dir'
r[:dir] = 'path/to/dir'
r[:dir] # => '/root_dir/path/to/dir'
r[:abs, true] = '/abs/path/to/dir'
r[:abs] # => '/abs/path/to/dir'
– Implementation Note:
The syntax for setting an absolute filepath requires an odd use []=.
In fact the method recieves the arguments (:dir, true, ‘/abs/path/to/dir’) rather than (:dir, ‘/abs/path/to/dir’, true), meaning that internally path and absolute are switched when setting an absolute filepath.
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/tap/root.rb', line 537 def []=(als, path, absolute=false) raise ArgumentError, "the alias #{als.inspect} is reserved" if als.to_s == 'root' # switch the paths if absolute was provided unless absolute == false switch = path path = absolute absolute = switch end case when path.nil? @relative_paths.delete(als) @paths.delete(als) when absolute @relative_paths.delete(als) @paths[als] = File.(path) else @relative_paths[als] = path @paths[als] = File.(File.join(root, path)) end end |
#absolute_paths ⇒ Object
Returns the absolute paths registered with self.
508 509 510 511 512 513 514 515 516 |
# File 'lib/tap/root.rb', line 508 def absolute_paths abs_paths = {} paths.each do |als, path| unless relative_paths.include?(als) || als.to_s == 'root' abs_paths[als] = path end end abs_paths end |
#absolute_paths=(paths) ⇒ Object
503 504 505 |
# File 'lib/tap/root.rb', line 503 def absolute_paths=(paths) assign_paths(root, relative_paths, paths) end |
#chdir(als, mkdir = false, &block) ⇒ Object
Changes pwd to the specified directory using Root.chdir.
620 621 622 |
# File 'lib/tap/root.rb', line 620 def chdir(als, mkdir=false, &block) Root.chdir(self[als], mkdir, &block) end |
#filepath(als, *paths) ⇒ Object
Resolves the specified alias, joins the paths together, and expands the resulting filepath. Analagous to File#expand_path(File#join).
581 582 583 |
# File 'lib/tap/root.rb', line 581 def filepath(als, *paths) File.(File.join(self[als], *paths)) end |
#glob(als, *patterns) ⇒ Object
Lists all files along the aliased path matching the input patterns. Patterns should join with the aliased path make valid globs for Dir.glob. If no patterns are specified, glob returns all paths matching ‘als/*/’.
606 607 608 609 610 |
# File 'lib/tap/root.rb', line 606 def glob(als, *patterns) patterns << "**/*" if patterns.empty? patterns.collect! {|pattern| filepath(als, pattern)} Root.glob(*patterns) end |
#prepare(als, *paths, &block) ⇒ Object
Constructs a path from the inputs (using filepath) and prepares it using Root.prepare. Returns the path.
626 627 628 |
# File 'lib/tap/root.rb', line 626 def prepare(als, *paths, &block) Root.prepare(filepath(als, *paths), &block) end |
#relative_filepath(als, path) ⇒ Object
Retrieves the filepath relative to the path of the specified alias.
586 587 588 |
# File 'lib/tap/root.rb', line 586 def relative_filepath(als, path) Root.relative_filepath(self[als], path) end |
#relative_paths=(paths) ⇒ Object
491 492 493 |
# File 'lib/tap/root.rb', line 491 def relative_paths=(paths) assign_paths(root, paths, absolute_paths) end |
#root=(path) ⇒ Object
Sets the root directory. All paths are reassigned accordingly.
479 480 481 |
# File 'lib/tap/root.rb', line 479 def root=(path) assign_paths(path, relative_paths, absolute_paths) end |
#translate(path, source_als, target_als) ⇒ Object
Generates a filepath translated from the aliased source dir to the aliased target dir. Raises an error if the filepath is not relative to the source dir.
r = Tap::Root.new '/root_dir'
path = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
r.translate(path, :in, :out) # => '/root_dir/out/path/to/file.txt'
598 599 600 |
# File 'lib/tap/root.rb', line 598 def translate(path, source_als, target_als) Root.translate(path, self[source_als], self[target_als]) end |
#vglob(als, path, *vpatterns) ⇒ Object
Lists all versions of path in the aliased dir matching the version patterns. If no patterns are specified, then all versions of path will be returned.
615 616 617 |
# File 'lib/tap/root.rb', line 615 def vglob(als, path, *vpatterns) Root.vglob(filepath(als, path), *vpatterns) end |