Module: Root::Utils

Includes:
Versions
Included in:
Root
Defined in:
lib/root/utils.rb

Constant Summary collapse

WIN_ROOT_PATTERN =

Regexp to match a windows-style root filepath.

/^[A-z]:\//

Class Method Summary collapse

Methods included from Versions

#compare_versions, #deversion, #increment, #version, #vniq

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.



86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/root/utils.rb', line 86

def chdir(dir, mkdir=false, &block)
  dir = File.expand_path(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

.dirname_or_array(path) ⇒ Object

utility method for minimize – returns the dirname of path, or an array if the dirname is effectively empty.



360
361
362
363
364
365
366
# File 'lib/root/utils.rb', line 360

def dirname_or_array(path) # :nodoc:
  dir = File.dirname(path)
  case dir
  when path, '.' then []
  else dir
  end
end

.empty?(dir) ⇒ Boolean

Empty returns true when dir is an existing directory that has no files.

Returns:

  • (Boolean)


157
158
159
# File 'lib/root/utils.rb', line 157

def empty?(dir)
  File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
end

.exchange(path, extname) ⇒ Object

Returns the path, exchanging the extension with extname. Extname may optionally omit the leading period.

exchange('path/to/file.txt', '.html')  # => 'path/to/file.html'
exchange('path/to/file.txt', 'rb')     # => 'path/to/file.rb'


50
51
52
# File 'lib/root/utils.rb', line 50

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 path_root_type.

If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.

expanded?('C:/path')  # => true
expanded?('c:/path')  # => true
expanded?('D:/path')  # => true
expanded?('path')     # => false

If root_type == :nix, then expanded? returns true if the path begins with ‘/’.

expanded?('/path')  # => true
expanded?('path')   # => false

Otherwise expanded? always returns nil.

Returns:

  • (Boolean)


139
140
141
142
143
144
145
146
147
148
# File 'lib/root/utils.rb', line 139

def expanded?(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.



55
56
57
58
59
# File 'lib/root/utils.rb', line 55

def glob(*patterns)
  patterns.collect do |pattern| 
    Dir.glob(pattern)
  end.flatten.uniq
end

.just_one?(splits, index, base) ⇒ Boolean

utility method for minimize – determines if there is just one of the base in splits, while flagging all matching entries.

Returns:

  • (Boolean)


371
372
373
374
375
376
377
378
379
380
381
# File 'lib/root/utils.rb', line 371

def just_one?(splits, index, base) # :nodoc:
  just_one = true
  index.upto(splits.length-1) do |i|
    if splits[i][1] == base
      splits[i][4] = true
      just_one = false
    end
  end
  
  just_one
end

.min_join(dir, path) ⇒ Object

utility method for minimize – joins the dir and path, preventing results like:

"./path"
"//path"


349
350
351
352
353
354
355
# File 'lib/root/utils.rb', line 349

def min_join(dir, path) # :nodoc:
  case dir
  when "." then path
  when "/" then "/#{path}"
  else "#{dir}/#{path}"
  end
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:

minimal_match?('dir/file-0.1.0.rb', 'file')           # => true
minimal_match?('dir/file-0.1.0.rb', 'dir/file')       # => true
minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0')     # => true
minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb')  # => true

minimal_match?('dir/file-0.1.0.rb', 'file.rb')        # => false
minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0')     # => false
minimal_match?('dir/file-0.1.0.rb', 'another')        # => false

In matching, partial basenames are not allowed but partial directories are allowed. Hence:

minimal_match?('dir/file-0.1.0.txt', 'file')          # => true
minimal_match?('dir/file-0.1.0.txt', 'ile')           # => false
minimal_match?('dir/file-0.1.0.txt', 'r/file')        # => true

Returns:

  • (Boolean)


278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/root/utils.rb', line 278

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:

minimize ['path/to/a.rb', 'path/to/b.rb']
# => ['a', 'b']

minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
# => ['a', 'b']

minimize ['path/to/file.rb', 'path/to/file.txt']
# => ['file.rb', 'file.txt']

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.

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']

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.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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
# File 'lib/root/utils.rb', line 190

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

.non_version_extname(path) ⇒ Object

utility method for minimal_match – returns a non-version extname, or an empty string if the path ends in a version.



385
386
387
388
# File 'lib/root/utils.rb', line 385

def non_version_extname(path) # :nodoc:
  extname = File.extname(path)
  extname =~ /^\.\d+$/ ? '' : extname
end

.path_root_typeObject

The path root type indicating windows, *nix, or some unknown style of filepaths (:win, :nix, :unknown).



114
115
116
117
118
119
120
# File 'lib/root/utils.rb', line 114

def path_root_type
  @path_root_type ||= case
  when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win 
  when File.expand_path(".")[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.



105
106
107
108
109
110
# File 'lib/root/utils.rb', line 105

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.

relative_filepath('dir', "dir/path/to/file.txt")  # => "path/to/file.txt"


19
20
21
22
23
24
25
26
27
28
29
# File 'lib/root/utils.rb', line 19

def relative_filepath(dir, path, dir_string=Dir.pwd)
  expanded_dir = File.expand_path(dir, dir_string)
  expanded_path = File.expand_path(path, dir_string)

  return nil unless expanded_path.index(expanded_dir) == 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
  expanded_path[(expanded_dir.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.



76
77
78
79
80
81
# File 'lib/root/utils.rb', line 76

def sglob(suffix_pattern, *base_paths)
  base_paths.collect do |base|
    base = File.expand_path(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     '\'        split('C:\path\to\file')  # => ["C:", "path", "to", "file"]
*nix        '/'        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:/'
split('path\to\..\.\to\file')                    # => ["C:", "path", "to", "file"]
split('path/to/.././to/file', false)             # => ["path", "to", "file"]

On *nix (or more generally systems with ‘/’ roots):

Dir.pwd                                               # => '/'
split('path/to/.././to/file')                    # => ["", "path", "to", "file"]
split('path/to/.././to/file', false)             # => ["path", "to", "file"]


325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/root/utils.rb', line 325

def split(path, expand_path=true, expand_dir=Dir.pwd)
  path = if expand_path
    File.expand_path(path, expand_dir)
  else
    # normalize the path by expanding it, then
    # work back to the relative filepath as needed
    expanded_dir = File.expand_path(expand_dir)
    expanded_path = File.expand_path(path, expand_dir)
    expanded_path.index(expanded_dir) != 0 ? expanded_path : relative_filepath(expanded_dir, expanded_path)
  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.

translate("/path/to/file.txt", "/path", "/another/path")  # => '/another/path/to/file.txt'


37
38
39
40
41
42
# File 'lib/root/utils.rb', line 37

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.

Returns:

  • (Boolean)


152
153
154
# File 'lib/root/utils.rb', line 152

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.



63
64
65
66
67
68
69
70
71
72
# File 'lib/root/utils.rb', line 63

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