Class: Tap::Root
- Extended by:
- Support::Versions
- Includes:
- Support::Configurable, Support::Versions
- Defined in:
- lib/tap/root.rb
Overview
Root allows you to define a root directory and alias subdirectories, 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 subdirectories
r = Root.new '/root_dir', :input => 'in', :output => 'out'
# work with directories
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 stores expanded paths 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 ‘directories’ hash mapping aliases to their subdirectory 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 ‘directories’, whereas subdirectory paths have aliases in both.
These features may be important to note when subclassing Root:
-
root and all filepaths in ‘paths’ are expanded
-
subdirectory paths are stored in ‘directories’
-
absolute paths are present in ‘paths’ but not in ‘directories’
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 aliased subdirectories and absolute paths.
Attributes included from Support::Configurable
Class Method Summary collapse
-
.chdir(dir, mkdir = false, &block) ⇒ Object
Like Dir.chdir but makes the directory, if necessary, when mkdir is specified.
-
.expanded_path?(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).
-
.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.
-
.vglob(path, *vpatterns) ⇒ Object
Lists all unique versions of path matching the glob version patterns.
Instance Method Summary collapse
-
#[](dir) ⇒ Object
Returns the expanded path for the specified alias.
-
#[]=(dir, path, absolute = false) ⇒ Object
Sets an alias for the subdirectory 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(dir, mkdir = false, &block) ⇒ Object
chdirs to the specified directory using Root.chdir.
-
#directories=(dirs) ⇒ Object
Sets the directories to those provided.
-
#filepath(dir, *filename) ⇒ Object
Constructs expanded filepaths relative to the path of the specified alias.
-
#glob(dir, *patterns) ⇒ Object
Lists all files in the aliased dir matching the input patterns.
-
#initialize(root = Dir.pwd, directories = {}, absolute_paths = {}) ⇒ Root
constructor
Creates a new Root with the given root directory, aliased directories and absolute paths.
-
#relative_filepath(dir, filepath) ⇒ Object
Retrieves the filepath relative to the path of the specified alias.
-
#root=(path) ⇒ Object
Sets the root directory.
-
#translate(filepath, source_dir, target_dir) ⇒ Object
Generates a target filepath translated from the aliased source_dir to the aliased target_dir.
-
#vglob(dir, filename, *vpatterns) ⇒ Object
Lists all versions of filename in the aliased dir matching the version patterns.
Methods included from Support::Versions
compare_versions, deversion, increment, version
Methods included from Support::Configurable
included, #initialize_copy, #reconfigure
Constructor Details
#initialize(root = Dir.pwd, directories = {}, absolute_paths = {}) ⇒ Root
Creates a new Root with the given root directory, aliased directories and absolute paths. By default root is the present working directory and no aliased directories or absolute paths are specified.
433 434 435 436 |
# File 'lib/tap/root.rb', line 433 def initialize(root=Dir.pwd, directories={}, absolute_paths={}) assign_paths(root, directories, absolute_paths) @config = self.class.configurations.instance_config(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).
428 429 430 |
# File 'lib/tap/root.rb', line 428 def path_root @path_root end |
#paths ⇒ Object (readonly)
A hash of (alias, expanded path) pairs for aliased subdirectories and absolute paths.
424 425 426 |
# File 'lib/tap/root.rb', line 424 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.
128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/tap/root.rb', line 128 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 |
.expanded_path?(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_path? always returns nil.
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/tap/root.rb', line 170 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.
97 98 99 100 101 |
# File 'lib/tap/root.rb', line 97 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
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/tap/root.rb', line 297 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.
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/tap/root.rb', line 210 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).
144 145 146 147 148 149 150 |
# File 'lib/tap/root.rb', line 144 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 |
.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"
71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/tap/root.rb', line 71 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.
118 119 120 121 122 123 |
# File 'lib/tap/root.rb', line 118 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"]
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/tap/root.rb', line 343 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'
89 90 91 92 93 94 |
# File 'lib/tap/root.rb', line 89 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 |
.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.
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/tap/root.rb', line 105 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
#[](dir) ⇒ Object
Returns the expanded path for the specified alias. If the alias has not been set, then the path is inferred to be ‘root/dir’ unless the path is relative to path_root. These 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'
526 527 528 529 530 531 532 |
# File 'lib/tap/root.rb', line 526 def [](dir) path = self.paths[dir] return path unless path == nil dir = dir.to_s Root.(dir) ? dir : File.(File.join(root, dir)) end |
#[]=(dir, path, absolute = false) ⇒ Object
Sets an alias for the subdirectory 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 Notes: 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. ++
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 |
# File 'lib/tap/root.rb', line 491 def []=(dir, path, absolute=false) raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.to_s == 'root' # switch the paths if absolute was provided unless absolute == false switch = path path = absolute absolute = switch end case when path.nil? @directories.delete(dir) @paths.delete(dir) when absolute @directories.delete(dir) @paths[dir] = File.(path) else @directories[dir] = path @paths[dir] = File.(File.join(root, path)) end end |
#absolute_paths ⇒ Object
Returns the absolute paths registered with self.
464 465 466 467 468 469 470 |
# File 'lib/tap/root.rb', line 464 def absolute_paths abs_paths = {} paths.each do |da, path| abs_paths[da] = path unless directories.include?(da) || da.to_s == 'root' end abs_paths end |
#absolute_paths=(paths) ⇒ Object
459 460 461 |
# File 'lib/tap/root.rb', line 459 def absolute_paths=(paths) assign_paths(root, directories, paths) end |
#chdir(dir, mkdir = false, &block) ⇒ Object
chdirs to the specified directory using Root.chdir.
571 572 573 |
# File 'lib/tap/root.rb', line 571 def chdir(dir, mkdir=false, &block) Root.chdir(self[dir], mkdir, &block) end |
#directories=(dirs) ⇒ Object
449 450 451 |
# File 'lib/tap/root.rb', line 449 def directories=(dirs) assign_paths(root, dirs, absolute_paths) end |
#filepath(dir, *filename) ⇒ Object
Constructs expanded filepaths relative to the path of the specified alias.
535 536 537 538 |
# File 'lib/tap/root.rb', line 535 def filepath(dir, *filename) # TODO - consider filename.compact so nils will not raise errors File.(File.join(self[dir], *filename)) end |
#glob(dir, *patterns) ⇒ Object
Lists all files in the aliased dir matching the input patterns. Patterns should be valid inputs for Dir.glob
. If no patterns are specified, lists all files/folders matching ‘*/’.
558 559 560 561 562 |
# File 'lib/tap/root.rb', line 558 def glob(dir, *patterns) patterns << "**/*" if patterns.empty? patterns.collect! {|pattern| filepath(dir, pattern)} Root.glob(*patterns) end |
#relative_filepath(dir, filepath) ⇒ Object
Retrieves the filepath relative to the path of the specified alias.
541 542 543 |
# File 'lib/tap/root.rb', line 541 def relative_filepath(dir, filepath) Root.relative_filepath(self[dir], filepath) end |
#root=(path) ⇒ Object
Sets the root directory. All paths are reassigned accordingly.
439 440 441 |
# File 'lib/tap/root.rb', line 439 def root=(path) assign_paths(path, directories, absolute_paths) end |
#translate(filepath, source_dir, target_dir) ⇒ Object
Generates a target filepath translated from the aliased source_dir to the aliased target_dir. Raises an error if the filepath is not relative to the aliased source_dir.
fp = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
r.translate(fp, :in, :out) # => '/root_dir/out/path/to/file.txt'
551 552 553 |
# File 'lib/tap/root.rb', line 551 def translate(filepath, source_dir, target_dir) Root.translate(filepath, self[source_dir], self[target_dir]) end |
#vglob(dir, filename, *vpatterns) ⇒ Object
Lists all versions of filename in the aliased dir matching the version patterns. If no patterns are specified, then all versions of filename will be returned.
566 567 568 |
# File 'lib/tap/root.rb', line 566 def vglob(dir, filename, *vpatterns) Root.vglob(filepath(dir, filename), *vpatterns) end |