filepath 0.3.1 → 0.4
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/.yardopts +1 -0
- data/Rakefile +1 -1
- data/lib/filepath.rb +4 -608
- data/lib/filepath/core_ext/array.rb +46 -0
- data/lib/filepath/core_ext/string.rb +22 -0
- data/lib/filepath/filepath.rb +802 -0
- data/lib/{filepathlist.rb → filepath/filepathlist.rb} +31 -31
- data/spec/filepath_spec.rb +63 -4
- data/spec/filepathlist_spec.rb +45 -12
- data/spec/spec_helper.rb +0 -1
- metadata +10 -7
data/.yardopts
CHANGED
data/Rakefile
CHANGED
data/lib/filepath.rb
CHANGED
@@ -1,612 +1,8 @@
|
|
1
1
|
# This is free and unencumbered software released into the public domain.
|
2
2
|
# See the `UNLICENSE` file or <http://unlicense.org/> for more details.
|
3
3
|
|
4
|
-
require '
|
4
|
+
require 'filepath/filepath.rb'
|
5
|
+
require 'filepath/filepathlist.rb'
|
6
|
+
require 'filepath/core_ext/array.rb'
|
7
|
+
require 'filepath/core_ext/string.rb'
|
5
8
|
|
6
|
-
class FilePath
|
7
|
-
SEPARATOR = '/'.freeze
|
8
|
-
|
9
|
-
def initialize(path)
|
10
|
-
if path.is_a? FilePath
|
11
|
-
@fragments = path.fragments
|
12
|
-
elsif path.is_a? Array
|
13
|
-
@fragments = path
|
14
|
-
else
|
15
|
-
@fragments = split_path_string(path.to_str)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_reader :fragments
|
20
|
-
|
21
|
-
# Creates a FilePath joining the given fragments.
|
22
|
-
#
|
23
|
-
# @return [FilePath] a FilePath created joining the given fragments
|
24
|
-
|
25
|
-
def FilePath.join(*raw_paths)
|
26
|
-
if (raw_paths.count == 1) && (raw_paths.first.is_a? Array)
|
27
|
-
raw_paths = raw_paths.first
|
28
|
-
end
|
29
|
-
|
30
|
-
paths = raw_paths.map { |p| p.as_path }
|
31
|
-
|
32
|
-
frags = []
|
33
|
-
paths.each { |path| frags += path.fragments }
|
34
|
-
|
35
|
-
return FilePath.new(frags)
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
# Appends another path to the current path.
|
40
|
-
#
|
41
|
-
# @example Append a string
|
42
|
-
#
|
43
|
-
# "a/b".as_path / "c" #=> <a/b/c>
|
44
|
-
#
|
45
|
-
# @example Append another FilePath
|
46
|
-
#
|
47
|
-
# home = (ENV["HOME"] || "/root").as_path
|
48
|
-
# conf_dir = '.config'.as_path
|
49
|
-
#
|
50
|
-
# home / conf_dir #=> </home/user/.config>
|
51
|
-
#
|
52
|
-
# @param [FilePath, String] extra_path the path to be appended to the
|
53
|
-
# current path
|
54
|
-
#
|
55
|
-
# @return [FilePath] a new path with the given path appended
|
56
|
-
|
57
|
-
def /(extra_path)
|
58
|
-
return FilePath.join(self, extra_path)
|
59
|
-
end
|
60
|
-
|
61
|
-
|
62
|
-
# Append multiple paths to the current path.
|
63
|
-
#
|
64
|
-
# @return [FilePath] a new path with all the paths appended
|
65
|
-
|
66
|
-
def join(*extra_paths)
|
67
|
-
return FilePath.join(self, *extra_paths)
|
68
|
-
end
|
69
|
-
|
70
|
-
alias :append :join
|
71
|
-
|
72
|
-
|
73
|
-
# An alias for {FilePath#/}.
|
74
|
-
#
|
75
|
-
# @deprecated Use the {FilePath#/} (slash) method instead. This method
|
76
|
-
# does not show clearly if a path is being added or if a
|
77
|
-
# string should be added to the filename
|
78
|
-
|
79
|
-
def +(extra_path)
|
80
|
-
warn "FilePath#+ is deprecated, use FilePath#/ instead."
|
81
|
-
return self / extra_path
|
82
|
-
end
|
83
|
-
|
84
|
-
|
85
|
-
# Calculates the relative path from a given directory.
|
86
|
-
#
|
87
|
-
# @param [FilePath, String] base the directory to use as base for the
|
88
|
-
# relative path
|
89
|
-
#
|
90
|
-
# @return [FilePath] the relative path
|
91
|
-
#
|
92
|
-
# @note this method operates on the normalized paths
|
93
|
-
|
94
|
-
def relative_to(base)
|
95
|
-
base = base.as_path
|
96
|
-
|
97
|
-
if self.absolute? != base.absolute?
|
98
|
-
self_abs = self.absolute? ? "absolute" : "relative"
|
99
|
-
base_abs = base.absolute? ? "absolute" : "relative"
|
100
|
-
msg = "cannot compare: "
|
101
|
-
msg += "`#{self}` is #{self_abs} while "
|
102
|
-
msg += "`#{base}` is #{base_abs}"
|
103
|
-
raise ArgumentError, msg
|
104
|
-
end
|
105
|
-
|
106
|
-
self_frags = self.normalized_fragments
|
107
|
-
base_frags = base.normalized_fragments
|
108
|
-
|
109
|
-
base_frags_tmp = base_frags.dup
|
110
|
-
num_same = self_frags.find_index do |frag|
|
111
|
-
base_frags_tmp.delete_at(0) != frag
|
112
|
-
end
|
113
|
-
|
114
|
-
# find_index returns nil if `self` is a subset of `base`
|
115
|
-
num_same ||= self_frags.length
|
116
|
-
|
117
|
-
num_parent_dirs = base_frags.length - num_same
|
118
|
-
left_in_self = self_frags[num_same..-1]
|
119
|
-
|
120
|
-
frags = [".."] * num_parent_dirs + left_in_self
|
121
|
-
normalized_frags = normalized_relative_frags(frags)
|
122
|
-
|
123
|
-
return FilePath.join(normalized_frags)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Calculates the relative path from a given file.
|
127
|
-
#
|
128
|
-
# @param [FilePath, String] base the file to use as base for the
|
129
|
-
# relative path
|
130
|
-
#
|
131
|
-
# @return [FilePath] the relative path
|
132
|
-
#
|
133
|
-
# @see #relative_to
|
134
|
-
|
135
|
-
def relative_to_file(base_file)
|
136
|
-
return relative_to(base_file.as_path.parent_dir)
|
137
|
-
end
|
138
|
-
|
139
|
-
|
140
|
-
# The filename component of the path.
|
141
|
-
#
|
142
|
-
# The filename is the component of a path that appears after the last
|
143
|
-
# path separator.
|
144
|
-
#
|
145
|
-
# @return [FilePath] the filename
|
146
|
-
|
147
|
-
def filename
|
148
|
-
if self.root?
|
149
|
-
return ''.as_path
|
150
|
-
end
|
151
|
-
|
152
|
-
filename = self.normalized_fragments.last
|
153
|
-
return filename.as_path
|
154
|
-
end
|
155
|
-
|
156
|
-
alias :basename :filename
|
157
|
-
|
158
|
-
|
159
|
-
# The dir that contains the file
|
160
|
-
#
|
161
|
-
# @return [FilePath] the path of the parent dir
|
162
|
-
|
163
|
-
def parent_dir
|
164
|
-
return self / '..'
|
165
|
-
end
|
166
|
-
|
167
|
-
|
168
|
-
# Replace the path filename with the supplied path.
|
169
|
-
#
|
170
|
-
# @param [FilePath, String] new_path the path to be put in place of
|
171
|
-
# the current filename
|
172
|
-
#
|
173
|
-
# @return [FilePath] a path with the supplied path instead of the
|
174
|
-
# current filename
|
175
|
-
|
176
|
-
def replace_filename(new_path)
|
177
|
-
dir = self.parent_dir
|
178
|
-
return dir / new_path
|
179
|
-
end
|
180
|
-
|
181
|
-
alias :replace_basename :replace_filename
|
182
|
-
|
183
|
-
|
184
|
-
# The extension of the file.
|
185
|
-
#
|
186
|
-
# The extension of a file are the characters after the last dot.
|
187
|
-
#
|
188
|
-
# @return [String] the extension of the file or nil if the file has no
|
189
|
-
# extension
|
190
|
-
|
191
|
-
def extension
|
192
|
-
filename = @fragments.last
|
193
|
-
|
194
|
-
num_dots = filename.count('.')
|
195
|
-
|
196
|
-
if num_dots.zero?
|
197
|
-
ext = nil
|
198
|
-
elsif filename.start_with?('.') && num_dots == 1
|
199
|
-
ext = nil
|
200
|
-
elsif filename.end_with?('.')
|
201
|
-
ext = ''
|
202
|
-
else
|
203
|
-
ext = filename.split('.').last
|
204
|
-
end
|
205
|
-
|
206
|
-
return ext
|
207
|
-
end
|
208
|
-
|
209
|
-
alias :ext :extension
|
210
|
-
|
211
|
-
|
212
|
-
# @overload extension?(ext)
|
213
|
-
# @param [String, Regexp] ext the extension to be matched
|
214
|
-
#
|
215
|
-
# @return whether the file extension matches the given extension
|
216
|
-
#
|
217
|
-
# @overload extension?
|
218
|
-
# @return whether the file has an extension
|
219
|
-
|
220
|
-
def extension?(ext = nil)
|
221
|
-
cur_ext = self.extension
|
222
|
-
|
223
|
-
if ext.nil?
|
224
|
-
return !cur_ext.nil?
|
225
|
-
else
|
226
|
-
if ext.is_a? Regexp
|
227
|
-
return !cur_ext.match(ext).nil?
|
228
|
-
else
|
229
|
-
return cur_ext == ext
|
230
|
-
end
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
alias :ext? :extension?
|
235
|
-
|
236
|
-
|
237
|
-
# @overload replace_extension(new_ext)
|
238
|
-
# Replaces the file extension with the supplied one. If the file
|
239
|
-
# has no extension it is added to the file name together with a dot.
|
240
|
-
#
|
241
|
-
# @param [String] new_ext the new extension
|
242
|
-
#
|
243
|
-
# @return [FilePath] a new path with the replaced extension
|
244
|
-
#
|
245
|
-
# @overload replace_extension
|
246
|
-
# Removes the file extension if present.
|
247
|
-
#
|
248
|
-
# @return [FilePath] a new path without the extension
|
249
|
-
|
250
|
-
def replace_extension(new_ext) # FIXME: accept block
|
251
|
-
if !self.extension?
|
252
|
-
if new_ext.nil?
|
253
|
-
new_filename = filename
|
254
|
-
else
|
255
|
-
new_filename = filename.to_s + '.' + new_ext
|
256
|
-
end
|
257
|
-
else
|
258
|
-
if new_ext.nil?
|
259
|
-
pattern = /\.[^.]*?\Z/
|
260
|
-
new_filename = filename.to_s.sub(pattern, '')
|
261
|
-
else
|
262
|
-
pattern = Regexp.new('.' + extension + '\\Z')
|
263
|
-
new_filename = filename.to_s.sub(pattern, '.' + new_ext)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
frags = @fragments[0..-2]
|
268
|
-
frags << new_filename
|
269
|
-
|
270
|
-
return FilePath.join(frags)
|
271
|
-
end
|
272
|
-
|
273
|
-
alias :replace_ext :replace_extension
|
274
|
-
alias :sub_ext :replace_extension
|
275
|
-
|
276
|
-
|
277
|
-
# Removes the file extension if present.
|
278
|
-
#
|
279
|
-
# @return [FilePath] a new path without the extension
|
280
|
-
|
281
|
-
def remove_extension
|
282
|
-
return replace_ext(nil)
|
283
|
-
end
|
284
|
-
|
285
|
-
alias :remove_ext :remove_extension
|
286
|
-
|
287
|
-
|
288
|
-
# Matches a pattern against this path.
|
289
|
-
#
|
290
|
-
# @param [Regexp, Object] pattern the pattern to match against
|
291
|
-
# this path
|
292
|
-
#
|
293
|
-
# @return [Fixnum, nil] the position of the pattern in the path, or
|
294
|
-
# nil if there is no match
|
295
|
-
#
|
296
|
-
# @note this method operates on the normalized path
|
297
|
-
|
298
|
-
def =~(pattern)
|
299
|
-
return self.to_s =~ pattern
|
300
|
-
end
|
301
|
-
|
302
|
-
def root?
|
303
|
-
return @fragments == [SEPARATOR] # FIXME: windows, mac
|
304
|
-
end
|
305
|
-
|
306
|
-
|
307
|
-
# Is this path absolute?
|
308
|
-
#
|
309
|
-
# FIXME: document what an absolute path is.
|
310
|
-
#
|
311
|
-
# @return whether the current path is absolute
|
312
|
-
|
313
|
-
def absolute?
|
314
|
-
return @fragments.first == SEPARATOR # FIXME: windows, mac
|
315
|
-
end
|
316
|
-
|
317
|
-
|
318
|
-
# Is this path relative?
|
319
|
-
#
|
320
|
-
# FIXME: document what a relative path is.
|
321
|
-
#
|
322
|
-
# @return whether the current path is relative
|
323
|
-
|
324
|
-
def relative?
|
325
|
-
return !self.absolute?
|
326
|
-
end
|
327
|
-
|
328
|
-
|
329
|
-
# Simplify paths that contain `.` and `..`.
|
330
|
-
#
|
331
|
-
# The resulting path will be in normal form.
|
332
|
-
#
|
333
|
-
# FIXME: document what normal form is.
|
334
|
-
#
|
335
|
-
# @return [FilePath] a new path that does not contain `.` or `..`
|
336
|
-
# fragments.
|
337
|
-
|
338
|
-
def normalized
|
339
|
-
return FilePath.join(self.normalized_fragments)
|
340
|
-
end
|
341
|
-
|
342
|
-
alias :normalised :normalized
|
343
|
-
|
344
|
-
|
345
|
-
# Iterates over all the path directories, from the current path to
|
346
|
-
# the root.
|
347
|
-
#
|
348
|
-
# @param max_depth the maximum depth to ascend to, nil to ascend
|
349
|
-
# without limits.
|
350
|
-
#
|
351
|
-
# @yield [path] TODO
|
352
|
-
|
353
|
-
def ascend(max_depth = nil, &block)
|
354
|
-
iterate(max_depth, :reverse_each, &block)
|
355
|
-
end
|
356
|
-
|
357
|
-
# Iterates over all the directory that lead to the current path.
|
358
|
-
#
|
359
|
-
# @param max_depth the maximum depth to descent to, nil to descend
|
360
|
-
# without limits.
|
361
|
-
#
|
362
|
-
# @yield [path] TODO
|
363
|
-
|
364
|
-
def descend(max_depth = nil, &block)
|
365
|
-
iterate(max_depth, :each, &block)
|
366
|
-
end
|
367
|
-
|
368
|
-
# @private
|
369
|
-
def iterate(max_depth, method, &block)
|
370
|
-
max_depth ||= @fragments.length
|
371
|
-
(1..max_depth).send(method) do |limit|
|
372
|
-
frags = @fragments.take(limit)
|
373
|
-
yield FilePath.join(frags)
|
374
|
-
end
|
375
|
-
end
|
376
|
-
|
377
|
-
|
378
|
-
# This path converted to a String
|
379
|
-
#
|
380
|
-
# @return [String] this path converted to a String
|
381
|
-
|
382
|
-
def to_raw_string
|
383
|
-
@to_raw_string ||= join_fragments(@fragments)
|
384
|
-
end
|
385
|
-
|
386
|
-
alias :to_raw_str :to_raw_string
|
387
|
-
|
388
|
-
|
389
|
-
# @return [String] this path converted to a String
|
390
|
-
#
|
391
|
-
# @note this method operates on the normalized path
|
392
|
-
|
393
|
-
def to_s
|
394
|
-
to_str
|
395
|
-
end
|
396
|
-
|
397
|
-
|
398
|
-
def to_str
|
399
|
-
@to_str ||= join_fragments(self.normalized_fragments)
|
400
|
-
end
|
401
|
-
|
402
|
-
|
403
|
-
# @return [FilePath] the path itself.
|
404
|
-
def as_path
|
405
|
-
self
|
406
|
-
end
|
407
|
-
|
408
|
-
|
409
|
-
def inspect
|
410
|
-
return '<' + self.to_raw_string + '>'
|
411
|
-
end
|
412
|
-
|
413
|
-
def ==(other)
|
414
|
-
return self.normalized_fragments == other.as_path.normalized_fragments
|
415
|
-
end
|
416
|
-
|
417
|
-
def eql?(other)
|
418
|
-
if self.equal?(other)
|
419
|
-
return true
|
420
|
-
elsif self.class != other.class
|
421
|
-
return false
|
422
|
-
end
|
423
|
-
|
424
|
-
return self.fragments == other.fragments
|
425
|
-
end
|
426
|
-
|
427
|
-
def hash
|
428
|
-
return self.fragments.hash
|
429
|
-
end
|
430
|
-
|
431
|
-
# @private
|
432
|
-
def split_path_string(raw_path)
|
433
|
-
fragments = raw_path.split(SEPARATOR) # FIXME: windows, mac
|
434
|
-
|
435
|
-
if raw_path == SEPARATOR
|
436
|
-
fragments << SEPARATOR
|
437
|
-
end
|
438
|
-
|
439
|
-
if !fragments.empty? && fragments.first.empty?
|
440
|
-
fragments[0] = SEPARATOR
|
441
|
-
end
|
442
|
-
|
443
|
-
return fragments
|
444
|
-
end
|
445
|
-
|
446
|
-
# @private
|
447
|
-
def normalized_fragments
|
448
|
-
@normalized_fragments ||= normalized_relative_frags(self.fragments)
|
449
|
-
end
|
450
|
-
|
451
|
-
# @private
|
452
|
-
def normalized_relative_frags(orig_frags)
|
453
|
-
frags = orig_frags.dup
|
454
|
-
|
455
|
-
# remove "current dir" markers
|
456
|
-
frags.delete('.')
|
457
|
-
|
458
|
-
i = 0
|
459
|
-
while (i < frags.length)
|
460
|
-
if frags[i] == '..' && frags[i-1] == SEPARATOR
|
461
|
-
# remove '..' fragments following a root delimiter
|
462
|
-
frags.delete_at(i)
|
463
|
-
i -= 1
|
464
|
-
elsif frags[i] == '..' && frags[i-1] != '..' && i >= 1
|
465
|
-
# remove every fragment followed by a ".." marker
|
466
|
-
frags.delete_at(i)
|
467
|
-
frags.delete_at(i-1)
|
468
|
-
i -= 2
|
469
|
-
end
|
470
|
-
i += 1
|
471
|
-
end
|
472
|
-
|
473
|
-
return frags
|
474
|
-
end
|
475
|
-
|
476
|
-
# @private
|
477
|
-
def join_fragments(frags)
|
478
|
-
# FIXME: windows, mac
|
479
|
-
# FIXME: avoid string substitutions and regexen
|
480
|
-
return frags.join(SEPARATOR).sub(%r{^//}, SEPARATOR).sub(/\A\Z/, '.')
|
481
|
-
end
|
482
|
-
|
483
|
-
module PathResolution
|
484
|
-
def absolute_path(base_dir = Dir.pwd) # FIXME: rename to `#absolute`?
|
485
|
-
if self.absolute?
|
486
|
-
return self
|
487
|
-
end
|
488
|
-
|
489
|
-
return base_dir.as_path / self
|
490
|
-
end
|
491
|
-
|
492
|
-
def real_path(base_dir = Dir.pwd)
|
493
|
-
path = absolute_path(base_dir)
|
494
|
-
|
495
|
-
return path.resolve_link
|
496
|
-
end
|
497
|
-
|
498
|
-
alias :realpath :real_path
|
499
|
-
|
500
|
-
def resolve_link
|
501
|
-
return File.readlink(self).as_path
|
502
|
-
end
|
503
|
-
end
|
504
|
-
|
505
|
-
module FileInfo
|
506
|
-
# @private
|
507
|
-
def self.define_filetest_method(filepath_method, filetest_method = nil)
|
508
|
-
filetest_method ||= filepath_method
|
509
|
-
define_method(filepath_method) do
|
510
|
-
return FileTest.send(filetest_method, self)
|
511
|
-
end
|
512
|
-
end
|
513
|
-
|
514
|
-
define_filetest_method :file?
|
515
|
-
|
516
|
-
define_filetest_method :link?, :symlink?
|
517
|
-
alias :symlink? :link?
|
518
|
-
|
519
|
-
define_filetest_method :directory?
|
520
|
-
|
521
|
-
define_filetest_method :exists?
|
522
|
-
alias :exist? :exists?
|
523
|
-
|
524
|
-
define_filetest_method :readable?
|
525
|
-
|
526
|
-
define_filetest_method :writeable?
|
527
|
-
|
528
|
-
define_filetest_method :executable?
|
529
|
-
|
530
|
-
define_filetest_method :setgid?
|
531
|
-
|
532
|
-
define_filetest_method :setuid?
|
533
|
-
|
534
|
-
define_filetest_method :empty?, :zero?
|
535
|
-
alias :zero? :empty?
|
536
|
-
|
537
|
-
def hidden?
|
538
|
-
@fragments.last.start_with?('.') # FIXME: windows, mac
|
539
|
-
end
|
540
|
-
end
|
541
|
-
|
542
|
-
module FileManipulationMethods
|
543
|
-
def open(*args, &block)
|
544
|
-
File.open(self, *args, &block)
|
545
|
-
end
|
546
|
-
|
547
|
-
def touch
|
548
|
-
self.open('a') do ; end
|
549
|
-
File.utime(File.atime(self), Time.now, self)
|
550
|
-
end
|
551
|
-
end
|
552
|
-
|
553
|
-
module DirectoryMethods
|
554
|
-
def entries(pattern = '*')
|
555
|
-
if !self.directory?
|
556
|
-
raise Errno::ENOTDIR.new(self)
|
557
|
-
end
|
558
|
-
|
559
|
-
raw_entries = Dir.glob((self / pattern))
|
560
|
-
entries = FilePathList.new(raw_entries)
|
561
|
-
|
562
|
-
return entries
|
563
|
-
end
|
564
|
-
alias :glob :entries
|
565
|
-
|
566
|
-
def files
|
567
|
-
entries.select_entries(:file)
|
568
|
-
end
|
569
|
-
|
570
|
-
def links
|
571
|
-
entries.select_entries(:link)
|
572
|
-
end
|
573
|
-
|
574
|
-
def directories
|
575
|
-
entries.select_entries(:directory)
|
576
|
-
end
|
577
|
-
end
|
578
|
-
|
579
|
-
include PathResolution
|
580
|
-
include FileInfo
|
581
|
-
include FileManipulationMethods
|
582
|
-
include DirectoryMethods
|
583
|
-
end
|
584
|
-
|
585
|
-
class String
|
586
|
-
# Generates a path from a String.
|
587
|
-
#
|
588
|
-
# `"/a/b/c".as_path` is equivalent to `FilePath.new("/a/b/c")`.
|
589
|
-
#
|
590
|
-
# @return [FilePath] a new path generated from the string
|
591
|
-
#
|
592
|
-
# @note FIXME: `#as_path` should be `#to_path` but that method name
|
593
|
-
# is already used
|
594
|
-
def as_path
|
595
|
-
FilePath.new(self)
|
596
|
-
end
|
597
|
-
end
|
598
|
-
|
599
|
-
class Array
|
600
|
-
# Generates a path using the elements of an Array as path fragments.
|
601
|
-
#
|
602
|
-
# `%w{a b c}.as_path` is equivalent to `FilePath.join('a', 'b', 'c')`.
|
603
|
-
#
|
604
|
-
# @return [FilePath] a new path generated using the element as path
|
605
|
-
# fragments
|
606
|
-
#
|
607
|
-
# @note FIXME: `#as_path` should be `#to_path` but that method name
|
608
|
-
# is already used
|
609
|
-
def as_path
|
610
|
-
FilePath.join(self)
|
611
|
-
end
|
612
|
-
end
|