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
@@ -0,0 +1,46 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
# See the `UNLICENSE` file or <http://unlicense.org/> for more details.
|
3
|
+
|
4
|
+
|
5
|
+
class Array
|
6
|
+
# Generates a path using the elements of an array as path segments.
|
7
|
+
#
|
8
|
+
# `[a, b, c].as_path` is equivalent to `FilePath.join(a, b, c)`.
|
9
|
+
#
|
10
|
+
# @example FilePath from an array of strings
|
11
|
+
#
|
12
|
+
# ["/", "foo", "bar"].as_path #=> </foo/bar>
|
13
|
+
#
|
14
|
+
# @example FilePath from an array of strings and other FilePaths
|
15
|
+
#
|
16
|
+
# server_dir = config["root_dir"] / "server"
|
17
|
+
# ["..", config_dir, "secret"].as_path #=> <../config/server/secret>
|
18
|
+
#
|
19
|
+
# @return [FilePath] a new path generated using the element as path
|
20
|
+
# segments
|
21
|
+
#
|
22
|
+
# @note FIXME: `#as_path` should be `#to_path` but that method name
|
23
|
+
# is already used
|
24
|
+
|
25
|
+
def as_path
|
26
|
+
FilePath.join(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Generates a path list from an array of paths.
|
31
|
+
#
|
32
|
+
# The elements of the array must respond to `#as_path`.
|
33
|
+
#
|
34
|
+
# `ary.as_path` is equivalent to `FilePathList.new(ary)`.
|
35
|
+
#
|
36
|
+
# @return [FilePathList] a new path list containing the elements of
|
37
|
+
# the array as FilePaths
|
38
|
+
#
|
39
|
+
# @see String#as_path
|
40
|
+
# @see Array#as_path
|
41
|
+
# @see FilePath#as_path
|
42
|
+
|
43
|
+
def as_path_list
|
44
|
+
FilePathList.new(self)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
# See the `UNLICENSE` file or <http://unlicense.org/> for more details.
|
3
|
+
|
4
|
+
|
5
|
+
class String
|
6
|
+
# Generates a path from a String.
|
7
|
+
#
|
8
|
+
# `"/a/b/c".as_path` is equivalent to `FilePath.new("/a/b/c")`.
|
9
|
+
#
|
10
|
+
# @example FilePath from a string
|
11
|
+
#
|
12
|
+
# "/etc/ssl/certs".as_path #=> </etc/ssl/certs>
|
13
|
+
#
|
14
|
+
# @return [FilePath] a new path generated from the string
|
15
|
+
#
|
16
|
+
# @note FIXME: `#as_path` should be `#to_path` but that method name
|
17
|
+
# is already used
|
18
|
+
|
19
|
+
def as_path
|
20
|
+
FilePath.new(self)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,802 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
# See the `UNLICENSE` file or <http://unlicense.org/> for more details.
|
3
|
+
|
4
|
+
|
5
|
+
class FilePath
|
6
|
+
SEPARATOR = '/'.freeze
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
if path.is_a? FilePath
|
10
|
+
@segments = path.segments
|
11
|
+
elsif path.is_a? Array
|
12
|
+
@segments = path
|
13
|
+
else
|
14
|
+
@segments = split_path_string(path.to_str)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @private
|
19
|
+
attr_reader :segments
|
20
|
+
|
21
|
+
|
22
|
+
# Creates a FilePath joining the given segments.
|
23
|
+
#
|
24
|
+
# @return [FilePath] a FilePath created joining the given segments
|
25
|
+
|
26
|
+
def FilePath.join(*raw_paths)
|
27
|
+
if (raw_paths.count == 1) && (raw_paths.first.is_a? Array)
|
28
|
+
raw_paths = raw_paths.first
|
29
|
+
end
|
30
|
+
|
31
|
+
paths = raw_paths.map { |p| p.as_path }
|
32
|
+
|
33
|
+
segs = []
|
34
|
+
paths.each { |path| segs += path.segments }
|
35
|
+
|
36
|
+
return FilePath.new(segs)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Appends another path to the current path.
|
41
|
+
#
|
42
|
+
# @example Append a string
|
43
|
+
#
|
44
|
+
# "a/b".as_path / "c" #=> <a/b/c>
|
45
|
+
#
|
46
|
+
# @example Append another FilePath
|
47
|
+
#
|
48
|
+
# home = (ENV["HOME"] || "/root").as_path
|
49
|
+
# conf_dir = '.config'.as_path
|
50
|
+
#
|
51
|
+
# home / conf_dir #=> </home/user/.config>
|
52
|
+
#
|
53
|
+
# @param [FilePath, String] extra_path the path to be appended to the
|
54
|
+
# current path
|
55
|
+
#
|
56
|
+
# @return [FilePath] a new path with the given path appended
|
57
|
+
|
58
|
+
def /(extra_path)
|
59
|
+
return FilePath.join(self, extra_path)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Append multiple paths to the current path.
|
64
|
+
#
|
65
|
+
# @return [FilePath] a new path with all the paths appended
|
66
|
+
|
67
|
+
def join(*extra_paths)
|
68
|
+
return FilePath.join(self, *extra_paths)
|
69
|
+
end
|
70
|
+
|
71
|
+
alias :append :join
|
72
|
+
|
73
|
+
|
74
|
+
# An alias for {FilePath#/}.
|
75
|
+
#
|
76
|
+
# @deprecated Use the {FilePath#/} (slash) method instead. This method
|
77
|
+
# does not show clearly if a path is being added or if a
|
78
|
+
# string should be added to the filename
|
79
|
+
|
80
|
+
def +(extra_path)
|
81
|
+
warn "FilePath#+ is deprecated, use FilePath#/ instead."
|
82
|
+
return self / extra_path
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Calculates the relative path from a given directory.
|
87
|
+
#
|
88
|
+
# @example relative paths between relative paths
|
89
|
+
#
|
90
|
+
# posts_dir = "posts".as_path
|
91
|
+
# images_dir = "static/images".as_path
|
92
|
+
#
|
93
|
+
# logo = images_dir / 'logo.png'
|
94
|
+
#
|
95
|
+
# logo.relative_to(posts_dir) #=> <../static/images/logo.png>
|
96
|
+
#
|
97
|
+
# @example relative paths between absolute paths
|
98
|
+
#
|
99
|
+
# home_dir = "/home/gioele".as_path
|
100
|
+
# docs_dir = "/home/gioele/Documents".as_path
|
101
|
+
# tmp_dir = "/tmp".as_path
|
102
|
+
#
|
103
|
+
# docs_dir.relative_to(home_dir) #=> <Documents>
|
104
|
+
# home_dir.relative_to(docs_dir) #=> <..>
|
105
|
+
#
|
106
|
+
# tmp_dir.relative_to(home_dir) #=> <../../tmp>
|
107
|
+
#
|
108
|
+
# @param [FilePath, String] base the directory to use as base for the
|
109
|
+
# relative path
|
110
|
+
#
|
111
|
+
# @return [FilePath] the relative path
|
112
|
+
#
|
113
|
+
# @note this method operates on the normalized paths
|
114
|
+
#
|
115
|
+
# @see #relative_to_file
|
116
|
+
|
117
|
+
def relative_to(base)
|
118
|
+
base = base.as_path
|
119
|
+
|
120
|
+
if self.absolute? != base.absolute?
|
121
|
+
self_abs = self.absolute? ? "absolute" : "relative"
|
122
|
+
base_abs = base.absolute? ? "absolute" : "relative"
|
123
|
+
msg = "cannot compare: "
|
124
|
+
msg += "`#{self}` is #{self_abs} while "
|
125
|
+
msg += "`#{base}` is #{base_abs}"
|
126
|
+
raise ArgumentError, msg
|
127
|
+
end
|
128
|
+
|
129
|
+
self_segs = self.normalized_segments
|
130
|
+
base_segs = base.normalized_segments
|
131
|
+
|
132
|
+
base_segs_tmp = base_segs.dup
|
133
|
+
num_same = self_segs.find_index do |seg|
|
134
|
+
base_segs_tmp.delete_at(0) != seg
|
135
|
+
end
|
136
|
+
|
137
|
+
# find_index returns nil if `self` is a subset of `base`
|
138
|
+
num_same ||= self_segs.length
|
139
|
+
|
140
|
+
num_parent_dirs = base_segs.length - num_same
|
141
|
+
left_in_self = self_segs[num_same..-1]
|
142
|
+
|
143
|
+
segs = [".."] * num_parent_dirs + left_in_self
|
144
|
+
normalized_segs = normalized_relative_segs(segs)
|
145
|
+
|
146
|
+
return FilePath.join(normalized_segs)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Calculates the relative path from a given file.
|
150
|
+
#
|
151
|
+
# @example relative paths between relative paths
|
152
|
+
#
|
153
|
+
# post = "posts/2012-02-14-hello.html".as_path
|
154
|
+
# images_dir = "static/images".as_path
|
155
|
+
#
|
156
|
+
# rel_img_dir = images_dir.relative_to_file(post)
|
157
|
+
# rel_img_dir.to_s #=> "../static/images"
|
158
|
+
#
|
159
|
+
# logo = rel_img_dir / 'logo.png' #=> <../static/images/logo.png>
|
160
|
+
#
|
161
|
+
# @example relative paths between absolute paths
|
162
|
+
#
|
163
|
+
# rc_file = "/home/gioele/.bashrc".as_path
|
164
|
+
# tmp_dir = "/tmp".as_path
|
165
|
+
#
|
166
|
+
# tmp_dir.relative_to_file(rc_file) #=> <../../tmp>
|
167
|
+
#
|
168
|
+
# @param [FilePath, String] base the file to use as base for the
|
169
|
+
# relative path
|
170
|
+
#
|
171
|
+
# @return [FilePath] the relative path
|
172
|
+
#
|
173
|
+
# @see #relative_to
|
174
|
+
|
175
|
+
def relative_to_file(base_file)
|
176
|
+
return relative_to(base_file.as_path.parent_dir)
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# The filename component of the path.
|
181
|
+
#
|
182
|
+
# The filename is the component of a path that appears after the last
|
183
|
+
# path separator.
|
184
|
+
#
|
185
|
+
# @return [FilePath] the filename
|
186
|
+
|
187
|
+
def filename
|
188
|
+
if self.root?
|
189
|
+
return ''.as_path
|
190
|
+
end
|
191
|
+
|
192
|
+
filename = self.normalized_segments.last
|
193
|
+
return filename.as_path
|
194
|
+
end
|
195
|
+
|
196
|
+
alias :basename :filename
|
197
|
+
|
198
|
+
|
199
|
+
# The dir that contains the file
|
200
|
+
#
|
201
|
+
# @return [FilePath] the path of the parent dir
|
202
|
+
|
203
|
+
def parent_dir
|
204
|
+
return self / '..'
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
# Replace the path filename with the supplied path.
|
209
|
+
#
|
210
|
+
# @example
|
211
|
+
#
|
212
|
+
# post = "posts/2012-02-16-hello-world/index.md".as_path
|
213
|
+
# style = post.replace_filename("style.css")
|
214
|
+
# style.to_s #=> "posts/2012-02-16-hello-world/style.css"
|
215
|
+
#
|
216
|
+
# @param [FilePath, String] new_path the path to be put in place of
|
217
|
+
# the current filename
|
218
|
+
#
|
219
|
+
# @return [FilePath] a path with the supplied path instead of the
|
220
|
+
# current filename
|
221
|
+
#
|
222
|
+
# @see #filename
|
223
|
+
# @see #replace_extension
|
224
|
+
|
225
|
+
def replace_filename(new_path)
|
226
|
+
dir = self.parent_dir
|
227
|
+
return dir / new_path
|
228
|
+
end
|
229
|
+
|
230
|
+
alias :replace_basename :replace_filename
|
231
|
+
|
232
|
+
|
233
|
+
# The extension of the file.
|
234
|
+
#
|
235
|
+
# The extension of a file are the characters after the last dot.
|
236
|
+
#
|
237
|
+
# @return [String] the extension of the file or nil if the file has no
|
238
|
+
# extension
|
239
|
+
#
|
240
|
+
# @see #extension?
|
241
|
+
|
242
|
+
def extension
|
243
|
+
filename = @segments.last
|
244
|
+
|
245
|
+
num_dots = filename.count('.')
|
246
|
+
|
247
|
+
if num_dots.zero?
|
248
|
+
ext = nil
|
249
|
+
elsif filename.start_with?('.') && num_dots == 1
|
250
|
+
ext = nil
|
251
|
+
elsif filename.end_with?('.')
|
252
|
+
ext = ''
|
253
|
+
else
|
254
|
+
ext = filename.split('.').last
|
255
|
+
end
|
256
|
+
|
257
|
+
return ext
|
258
|
+
end
|
259
|
+
|
260
|
+
alias :ext :extension
|
261
|
+
|
262
|
+
|
263
|
+
# @overload extension?(ext)
|
264
|
+
# @param [String, Regexp] ext the extension to be matched
|
265
|
+
#
|
266
|
+
# @return whether the file extension matches the given extension
|
267
|
+
#
|
268
|
+
# @overload extension?
|
269
|
+
# @return whether the file has an extension
|
270
|
+
|
271
|
+
def extension?(ext = nil)
|
272
|
+
cur_ext = self.extension
|
273
|
+
|
274
|
+
if ext.nil?
|
275
|
+
return !cur_ext.nil?
|
276
|
+
else
|
277
|
+
if ext.is_a? Regexp
|
278
|
+
return !cur_ext.match(ext).nil?
|
279
|
+
else
|
280
|
+
return cur_ext == ext
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
alias :ext? :extension?
|
286
|
+
|
287
|
+
|
288
|
+
# Replaces or removes the file extension.
|
289
|
+
#
|
290
|
+
# @see #extension
|
291
|
+
# @see #extension?
|
292
|
+
# @see #remove_extension
|
293
|
+
# @see #replace_filename
|
294
|
+
#
|
295
|
+
# @overload replace_extension(new_ext)
|
296
|
+
# Replaces the file extension with the supplied one. If the file
|
297
|
+
# has no extension it is added to the file name together with a dot.
|
298
|
+
#
|
299
|
+
# @example Extension replacement
|
300
|
+
#
|
301
|
+
# src_path = "pages/about.markdown".as_path
|
302
|
+
# html_path = src_path.replace_extension("html")
|
303
|
+
# html_path.to_s #=> "pages/about.html"
|
304
|
+
#
|
305
|
+
# @example Extension addition
|
306
|
+
#
|
307
|
+
# base = "style/main-style".as_path
|
308
|
+
# sass_style = base.replace_extension("sass")
|
309
|
+
# sass_style.to_s #=> "style/main-style.sass"
|
310
|
+
#
|
311
|
+
# @param [String] new_ext the new extension
|
312
|
+
#
|
313
|
+
# @return [FilePath] a new path with the replaced extension
|
314
|
+
#
|
315
|
+
# @overload replace_extension
|
316
|
+
# Removes the file extension if present.
|
317
|
+
#
|
318
|
+
# The {#remove_extension} method provides the same functionality
|
319
|
+
# but has a more meaningful name.
|
320
|
+
#
|
321
|
+
# @example
|
322
|
+
#
|
323
|
+
# post_file = "post/welcome.html"
|
324
|
+
# post_url = post_file.replace_extension(nil)
|
325
|
+
# post_url.to_s #=> "post/welcome"
|
326
|
+
#
|
327
|
+
# @return [FilePath] a new path without the extension
|
328
|
+
|
329
|
+
def replace_extension(new_ext) # FIXME: accept block
|
330
|
+
orig_filename = filename.to_s
|
331
|
+
|
332
|
+
if !self.extension?
|
333
|
+
if new_ext.nil?
|
334
|
+
new_filename = orig_filename
|
335
|
+
else
|
336
|
+
new_filename = orig_filename + '.' + new_ext
|
337
|
+
end
|
338
|
+
else
|
339
|
+
if new_ext.nil?
|
340
|
+
pattern = /\.[^.]*?\Z/
|
341
|
+
new_filename = orig_filename.sub(pattern, '')
|
342
|
+
else
|
343
|
+
pattern = Regexp.new('.' + extension + '\\Z')
|
344
|
+
new_filename = orig_filename.sub(pattern, '.' + new_ext)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
segs = @segments[0..-2]
|
349
|
+
segs << new_filename
|
350
|
+
|
351
|
+
return FilePath.new(segs)
|
352
|
+
end
|
353
|
+
|
354
|
+
alias :replace_ext :replace_extension
|
355
|
+
alias :sub_ext :replace_extension
|
356
|
+
|
357
|
+
|
358
|
+
# Removes the file extension if present.
|
359
|
+
#
|
360
|
+
# @example
|
361
|
+
#
|
362
|
+
# post_file = "post/welcome.html"
|
363
|
+
# post_url = post_file.remove_extension
|
364
|
+
# post_url.to_s #=> "post/welcome"
|
365
|
+
#
|
366
|
+
# @return [FilePath] a new path without the extension
|
367
|
+
#
|
368
|
+
# @see #replace_extension
|
369
|
+
|
370
|
+
def remove_extension
|
371
|
+
return replace_ext(nil)
|
372
|
+
end
|
373
|
+
|
374
|
+
alias :remove_ext :remove_extension
|
375
|
+
|
376
|
+
|
377
|
+
# Matches a pattern against this path.
|
378
|
+
#
|
379
|
+
# @param [Regexp, Object] pattern the pattern to match against
|
380
|
+
# this path
|
381
|
+
#
|
382
|
+
# @return [Fixnum, nil] the position of the pattern in the path, or
|
383
|
+
# nil if there is no match
|
384
|
+
#
|
385
|
+
# @note this method operates on the normalized path
|
386
|
+
|
387
|
+
def =~(pattern)
|
388
|
+
return self.to_s =~ pattern
|
389
|
+
end
|
390
|
+
|
391
|
+
|
392
|
+
# Is this path pointing to the root directory?
|
393
|
+
#
|
394
|
+
# @return whether the path points to the root directory
|
395
|
+
#
|
396
|
+
# @note this method operates on the normalized paths
|
397
|
+
|
398
|
+
def root?
|
399
|
+
return self.normalized_segments == [SEPARATOR] # FIXME: windows, mac
|
400
|
+
end
|
401
|
+
|
402
|
+
|
403
|
+
# Is this path absolute?
|
404
|
+
#
|
405
|
+
# @example
|
406
|
+
#
|
407
|
+
# "/tmp".absolute? #=> true
|
408
|
+
# "tmp".absolute? #=> false
|
409
|
+
# "../tmp".absolute? #=> false
|
410
|
+
#
|
411
|
+
# FIXME: document what an absolute path is.
|
412
|
+
#
|
413
|
+
# @return whether the current path is absolute
|
414
|
+
#
|
415
|
+
# @see #relative?
|
416
|
+
|
417
|
+
def absolute?
|
418
|
+
return @segments.first == SEPARATOR # FIXME: windows, mac
|
419
|
+
end
|
420
|
+
|
421
|
+
|
422
|
+
# Is this path relative?
|
423
|
+
#
|
424
|
+
# @example
|
425
|
+
#
|
426
|
+
# "/tmp".relative? #=> false
|
427
|
+
# "tmp".relative? #=> true
|
428
|
+
# "../tmp".relative? #=> true
|
429
|
+
#
|
430
|
+
# FIXME: document what a relative path is.
|
431
|
+
#
|
432
|
+
# @return whether the current path is relative
|
433
|
+
#
|
434
|
+
# @see #absolute?
|
435
|
+
|
436
|
+
def relative?
|
437
|
+
return !self.absolute?
|
438
|
+
end
|
439
|
+
|
440
|
+
|
441
|
+
# Simplify paths that contain `.` and `..`.
|
442
|
+
#
|
443
|
+
# The resulting path will be in normal form.
|
444
|
+
#
|
445
|
+
# @example
|
446
|
+
#
|
447
|
+
# path = $ENV["HOME"] / ".." / "jack" / "."
|
448
|
+
#
|
449
|
+
# path #=> </home/gioele/../jack/.>
|
450
|
+
# path.normalized #=> </home/jack>
|
451
|
+
#
|
452
|
+
# FIXME: document what normal form is.
|
453
|
+
#
|
454
|
+
# @return [FilePath] a new path that does not contain `.` or `..`
|
455
|
+
# segments.
|
456
|
+
|
457
|
+
def normalized
|
458
|
+
return FilePath.join(self.normalized_segments)
|
459
|
+
end
|
460
|
+
|
461
|
+
alias :normalised :normalized
|
462
|
+
|
463
|
+
|
464
|
+
# Iterates over all the path directories, from the current path to
|
465
|
+
# the root.
|
466
|
+
#
|
467
|
+
# @example
|
468
|
+
#
|
469
|
+
# web_dir = "/srv/example.org/web/html/".as_path
|
470
|
+
# web_dir.ascend do |path|
|
471
|
+
# is = path.readable? ? "is" : "is NOT"
|
472
|
+
#
|
473
|
+
# puts "#{path} #{is} readable"
|
474
|
+
# end
|
475
|
+
#
|
476
|
+
# # produces
|
477
|
+
# #
|
478
|
+
# # /srv/example.org/web/html is NOT redable
|
479
|
+
# # /srv/example.org/web is NOT readable
|
480
|
+
# # /srv/example.org is readable
|
481
|
+
# # /srv is readable
|
482
|
+
# # / is readable
|
483
|
+
#
|
484
|
+
# @param max_depth the maximum depth to ascend to, nil to ascend
|
485
|
+
# without limits.
|
486
|
+
#
|
487
|
+
# @yield [path] TODO
|
488
|
+
#
|
489
|
+
# @return [FilePath] the path itself.
|
490
|
+
#
|
491
|
+
# @see #descend
|
492
|
+
|
493
|
+
def ascend(max_depth = nil, &block)
|
494
|
+
iterate(max_depth, :reverse_each, &block)
|
495
|
+
end
|
496
|
+
|
497
|
+
|
498
|
+
# Iterates over all the directory that lead to the current path.
|
499
|
+
#
|
500
|
+
# @example
|
501
|
+
#
|
502
|
+
# web_dir = "/srv/example.org/web/html/".as_path
|
503
|
+
# web_dir.descend do |path|
|
504
|
+
# is = path.readable? ? "is" : "is NOT"
|
505
|
+
#
|
506
|
+
# puts "#{path} #{is} readable"
|
507
|
+
# end
|
508
|
+
#
|
509
|
+
# # produces
|
510
|
+
# #
|
511
|
+
# # / is readable
|
512
|
+
# # /srv is readable
|
513
|
+
# # /srv/example.org is readable
|
514
|
+
# # /srv/example.org/web is NOT readable
|
515
|
+
# # /srv/example.org/web/html is NOT redable
|
516
|
+
#
|
517
|
+
# @param max_depth the maximum depth to descent to, nil to descend
|
518
|
+
# without limits.
|
519
|
+
#
|
520
|
+
# @yield [path] TODO
|
521
|
+
#
|
522
|
+
# @return [FilePath] the path itself.
|
523
|
+
#
|
524
|
+
# @see #ascend
|
525
|
+
|
526
|
+
def descend(max_depth = nil, &block)
|
527
|
+
iterate(max_depth, :each, &block)
|
528
|
+
end
|
529
|
+
|
530
|
+
|
531
|
+
# @private
|
532
|
+
def iterate(max_depth, method, &block)
|
533
|
+
max_depth ||= @segments.length
|
534
|
+
(1..max_depth).send(method) do |limit|
|
535
|
+
segs = @segments.take(limit)
|
536
|
+
yield FilePath.join(segs)
|
537
|
+
end
|
538
|
+
|
539
|
+
return self
|
540
|
+
end
|
541
|
+
|
542
|
+
|
543
|
+
# This path converted to a String.
|
544
|
+
#
|
545
|
+
# @example differences between #to_raw_string and #to_s
|
546
|
+
#
|
547
|
+
# path = "/home/gioele/.config".as_path / ".." / ".cache"
|
548
|
+
# path.to_raw_string #=> "/home/gioele/config/../.cache"
|
549
|
+
# path.to_s #=> "/home/gioele/.cache"
|
550
|
+
#
|
551
|
+
# @return [String] this path converted to a String
|
552
|
+
#
|
553
|
+
# @see #to_s
|
554
|
+
|
555
|
+
def to_raw_string
|
556
|
+
@to_raw_string ||= join_segments(@segments)
|
557
|
+
end
|
558
|
+
|
559
|
+
alias :to_raw_str :to_raw_string
|
560
|
+
|
561
|
+
|
562
|
+
# @return [String] this path converted to a String
|
563
|
+
#
|
564
|
+
# @note this method operates on the normalized path
|
565
|
+
|
566
|
+
def to_s
|
567
|
+
to_str
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
# @private
|
572
|
+
def to_str
|
573
|
+
@to_str ||= join_segments(self.normalized_segments)
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
# @return [FilePath] the path itself.
|
578
|
+
def as_path
|
579
|
+
self
|
580
|
+
end
|
581
|
+
|
582
|
+
|
583
|
+
# @private
|
584
|
+
def inspect
|
585
|
+
return '<' + self.to_raw_string + '>'
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
# Checks whether two paths are equivalent.
|
590
|
+
#
|
591
|
+
# Two paths are equivalent when they have the same normalized segments.
|
592
|
+
#
|
593
|
+
# A relative and an absolute path will always be considered different.
|
594
|
+
# To compare relative paths to absolute path, expand first the relative
|
595
|
+
# path using {#absolute_path} or {#real_path}.
|
596
|
+
#
|
597
|
+
# @example
|
598
|
+
#
|
599
|
+
# path1 = "foo/bar".as_path
|
600
|
+
# path2 = "foo/bar/baz".as_path
|
601
|
+
# path3 = "foo/bar/baz/../../bar".as_path
|
602
|
+
#
|
603
|
+
# path1 == path2 #=> false
|
604
|
+
# path1 == path2.parent_dir #=> true
|
605
|
+
# path1 == path3 #=> true
|
606
|
+
#
|
607
|
+
# @param [FilePath, String] other the other path to compare
|
608
|
+
#
|
609
|
+
# @return [boolean] whether the other path is equivalent to the current path
|
610
|
+
#
|
611
|
+
# @note this method compares the normalized versions of the paths
|
612
|
+
|
613
|
+
def ==(other)
|
614
|
+
return self.normalized_segments == other.as_path.normalized_segments
|
615
|
+
end
|
616
|
+
|
617
|
+
|
618
|
+
# @private
|
619
|
+
def eql?(other)
|
620
|
+
if self.equal?(other)
|
621
|
+
return true
|
622
|
+
elsif self.class != other.class
|
623
|
+
return false
|
624
|
+
end
|
625
|
+
|
626
|
+
return @segments == other.segments
|
627
|
+
end
|
628
|
+
|
629
|
+
# @private
|
630
|
+
def hash
|
631
|
+
return @segments.hash
|
632
|
+
end
|
633
|
+
|
634
|
+
# @private
|
635
|
+
def split_path_string(raw_path)
|
636
|
+
segments = raw_path.split(SEPARATOR) # FIXME: windows, mac
|
637
|
+
|
638
|
+
if raw_path == SEPARATOR
|
639
|
+
segments << SEPARATOR
|
640
|
+
end
|
641
|
+
|
642
|
+
if !segments.empty? && segments.first.empty?
|
643
|
+
segments[0] = SEPARATOR
|
644
|
+
end
|
645
|
+
|
646
|
+
return segments
|
647
|
+
end
|
648
|
+
|
649
|
+
# @private
|
650
|
+
def normalized_segments
|
651
|
+
@normalized_segments ||= normalized_relative_segs(@segments)
|
652
|
+
end
|
653
|
+
|
654
|
+
# @private
|
655
|
+
def normalized_relative_segs(orig_segs)
|
656
|
+
segs = orig_segs.dup
|
657
|
+
|
658
|
+
# remove "current dir" markers
|
659
|
+
segs.delete('.')
|
660
|
+
|
661
|
+
i = 0
|
662
|
+
while (i < segs.length)
|
663
|
+
if segs[i] == '..' && segs[i-1] == SEPARATOR
|
664
|
+
# remove '..' segments following a root delimiter
|
665
|
+
segs.delete_at(i)
|
666
|
+
i -= 1
|
667
|
+
elsif segs[i] == '..' && segs[i-1] != '..' && i >= 1
|
668
|
+
# remove every segment followed by a ".." marker
|
669
|
+
segs.delete_at(i)
|
670
|
+
segs.delete_at(i-1)
|
671
|
+
i -= 2
|
672
|
+
end
|
673
|
+
i += 1
|
674
|
+
end
|
675
|
+
|
676
|
+
return segs
|
677
|
+
end
|
678
|
+
|
679
|
+
# @private
|
680
|
+
def join_segments(segs)
|
681
|
+
# FIXME: windows, mac
|
682
|
+
# FIXME: avoid string substitutions and regexen
|
683
|
+
return segs.join(SEPARATOR).sub(%r{^//}, SEPARATOR).sub(/\A\Z/, '.')
|
684
|
+
end
|
685
|
+
|
686
|
+
module PathResolution
|
687
|
+
def absolute_path(base_dir = Dir.pwd) # FIXME: rename to `#absolute`?
|
688
|
+
if self.absolute?
|
689
|
+
return self
|
690
|
+
end
|
691
|
+
|
692
|
+
return base_dir.as_path / self
|
693
|
+
end
|
694
|
+
|
695
|
+
def real_path(base_dir = Dir.pwd)
|
696
|
+
path = absolute_path(base_dir)
|
697
|
+
|
698
|
+
return path.resolve_link
|
699
|
+
end
|
700
|
+
|
701
|
+
alias :realpath :real_path
|
702
|
+
|
703
|
+
def resolve_link
|
704
|
+
return File.readlink(self).as_path
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
module FileInfo
|
709
|
+
# @private
|
710
|
+
def self.define_filetest_method(filepath_method, filetest_method = nil)
|
711
|
+
filetest_method ||= filepath_method
|
712
|
+
define_method(filepath_method) do
|
713
|
+
return FileTest.send(filetest_method, self)
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
define_filetest_method :file?
|
718
|
+
|
719
|
+
define_filetest_method :link?, :symlink?
|
720
|
+
alias :symlink? :link?
|
721
|
+
|
722
|
+
define_filetest_method :directory?
|
723
|
+
|
724
|
+
define_filetest_method :exists?
|
725
|
+
alias :exist? :exists?
|
726
|
+
|
727
|
+
define_filetest_method :readable?
|
728
|
+
|
729
|
+
define_filetest_method :writeable?
|
730
|
+
|
731
|
+
define_filetest_method :executable?
|
732
|
+
|
733
|
+
define_filetest_method :setgid?
|
734
|
+
|
735
|
+
define_filetest_method :setuid?
|
736
|
+
|
737
|
+
define_filetest_method :empty?, :zero?
|
738
|
+
alias :zero? :empty?
|
739
|
+
|
740
|
+
def hidden?
|
741
|
+
@segments.last.start_with?('.') # FIXME: windows, mac
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
module FileManipulationMethods
|
746
|
+
def open(*args, &block)
|
747
|
+
File.open(self, *args, &block)
|
748
|
+
end
|
749
|
+
|
750
|
+
def touch
|
751
|
+
self.open('a') do ; end
|
752
|
+
File.utime(File.atime(self), Time.now, self)
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
module DirectoryMethods
|
757
|
+
def entries(pattern = '*', recursive = false)
|
758
|
+
if !self.directory?
|
759
|
+
raise Errno::ENOTDIR.new(self)
|
760
|
+
end
|
761
|
+
|
762
|
+
glob = self
|
763
|
+
glob /= '**' if recursive
|
764
|
+
glob /= pattern
|
765
|
+
|
766
|
+
raw_entries = Dir.glob(glob)
|
767
|
+
entries = FilePathList.new(raw_entries)
|
768
|
+
|
769
|
+
return entries
|
770
|
+
end
|
771
|
+
alias :glob :entries
|
772
|
+
|
773
|
+
def find(pattern = nil, &block)
|
774
|
+
if pattern.respond_to? :to_str
|
775
|
+
return entries(pattern, true)
|
776
|
+
end
|
777
|
+
|
778
|
+
if !block_given?
|
779
|
+
block = proc { |e| e =~ pattern }
|
780
|
+
end
|
781
|
+
|
782
|
+
return entries('*', true).select { |e| block.call(e) }
|
783
|
+
end
|
784
|
+
|
785
|
+
def files(recursive = false)
|
786
|
+
entries('*', recursive).select_entries(:file)
|
787
|
+
end
|
788
|
+
|
789
|
+
def links(recursive = false)
|
790
|
+
entries('*', recursive).select_entries(:link)
|
791
|
+
end
|
792
|
+
|
793
|
+
def directories(recursive = false)
|
794
|
+
entries('*', recursive).select_entries(:directory)
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
include PathResolution
|
799
|
+
include FileInfo
|
800
|
+
include FileManipulationMethods
|
801
|
+
include DirectoryMethods
|
802
|
+
end
|