filepath 0.3.1 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|