filepath 0.1
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/.gitignore +6 -0
- data/.yardopts +3 -0
- data/README.md +135 -0
- data/Rakefile +26 -0
- data/UNLICENSE +24 -0
- data/lib/filepath.rb +551 -0
- data/lib/filepathlist.rb +74 -0
- data/spec/filepath_spec.rb +440 -0
- data/spec/filepathlist_spec.rb +65 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/tasks.rb +40 -0
- metadata +72 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
FilePath
|
2
|
+
========
|
3
|
+
|
4
|
+
`FilePath` is a class that helps dealing with files, directories and paths in
|
5
|
+
general; a modern replacement for the standard Pathname.
|
6
|
+
|
7
|
+
`FilePath` instances are immutable objects with dozens of convience methods
|
8
|
+
for common operations such as calculating relative paths, concatenating paths
|
9
|
+
or finding all the files in a directory. There is also a companion class
|
10
|
+
`FilePathList` to perform operations on multiple files at once.
|
11
|
+
|
12
|
+
Features and examples
|
13
|
+
---------------------
|
14
|
+
|
15
|
+
The main purpose of FilePath is to able to write
|
16
|
+
|
17
|
+
require __FILE_.as_path / 'spec' / 'tasks'
|
18
|
+
|
19
|
+
instad of cumbersome code like
|
20
|
+
|
21
|
+
require File.join(File.dirname(__FILE__), ['spec', 'tasks'])
|
22
|
+
|
23
|
+
The main features of FilePath are…
|
24
|
+
|
25
|
+
### Path concatenation
|
26
|
+
|
27
|
+
oauth_conf = ENV['HOME'].as_path / '.config' / 'myapp' / 'oauth.ini'
|
28
|
+
oauth_conf.to_s #=> "/home/gioele/.config/myapp/oauth.ini"
|
29
|
+
|
30
|
+
joe_home = ENV['HOME'].as_path / '..' / 'joe'
|
31
|
+
joe_home.to_raw_string #=> "/home/gioele/../joe"
|
32
|
+
joe_home.to_s #=> "/home/joe"
|
33
|
+
|
34
|
+
rel1 = oauth_conf.relative_to(joe_home)
|
35
|
+
rel1.to_s #=> "../gioele/.config/myapp/oauth.ini"
|
36
|
+
|
37
|
+
rel2 = joe_home.relative_to(oauth_conf)
|
38
|
+
rel2.to_s #=> "../../../joe"
|
39
|
+
|
40
|
+
### Path manipulation
|
41
|
+
|
42
|
+
image = ENV['HOME'].as_path / 'Documents' / 'images' / 'cat.png'
|
43
|
+
image.parent_dir.to_s #=> "/home/gioele/Documents/images"
|
44
|
+
image.filename.to_s #=> "cat.png"
|
45
|
+
image.extension #=> "png"
|
46
|
+
|
47
|
+
converted_img = image.replace_extension("jpeg")
|
48
|
+
converted_img.to_s #=> "/home/gioele/Documents/images/cat.jpeg"
|
49
|
+
convert(image.to_s, converted_img.to_s)
|
50
|
+
|
51
|
+
### Path traversal
|
52
|
+
|
53
|
+
file_dir = FilePath.new("/srv/example.org/web/html/")
|
54
|
+
file_dir.descend do |path|
|
55
|
+
is = path.readable? ? "is" : "is not!"
|
56
|
+
|
57
|
+
puts "#{path} #{is} readable"
|
58
|
+
end
|
59
|
+
|
60
|
+
produces
|
61
|
+
|
62
|
+
/ is readable
|
63
|
+
/srv is readable
|
64
|
+
/srv/example.org is readable
|
65
|
+
/srv/example.org/web is not! readable
|
66
|
+
/srv/example.org/web/html is not! redable
|
67
|
+
|
68
|
+
|
69
|
+
### Shortcuts for file and directory operations
|
70
|
+
|
71
|
+
home_dir = ENV['HOME']
|
72
|
+
|
73
|
+
files = home_dir.files
|
74
|
+
files.count #=> 3
|
75
|
+
files.each { |path| puts path.filename.to_s }
|
76
|
+
|
77
|
+
produces
|
78
|
+
|
79
|
+
# .bashrc
|
80
|
+
# .vimrc
|
81
|
+
# TODO.txt
|
82
|
+
|
83
|
+
Similarly,
|
84
|
+
|
85
|
+
dirs = home_dir.directories
|
86
|
+
dirs.count #=> 2
|
87
|
+
dirs.each { |path| puts path.filename.to_s + "/"}
|
88
|
+
|
89
|
+
produces
|
90
|
+
|
91
|
+
# .ssh/
|
92
|
+
# Documents/
|
93
|
+
|
94
|
+
|
95
|
+
Requirements
|
96
|
+
------------
|
97
|
+
|
98
|
+
The `filepath` library does not require any external library: it relies
|
99
|
+
complitely on functionalities available in the Ruby's core classes.
|
100
|
+
|
101
|
+
The `filepath` library has been tested and found compatible with Ruby 1.8.7,
|
102
|
+
Ruby 1.9.3 and JRuby 1.6.
|
103
|
+
|
104
|
+
|
105
|
+
Installation
|
106
|
+
------------
|
107
|
+
|
108
|
+
gem install filepath
|
109
|
+
|
110
|
+
|
111
|
+
Authors
|
112
|
+
-------
|
113
|
+
|
114
|
+
* Gioele Barabucci <http://svario.it/gioele> (initial author)
|
115
|
+
|
116
|
+
|
117
|
+
Development
|
118
|
+
-----------
|
119
|
+
|
120
|
+
Code
|
121
|
+
: <https://github.com/gioele/filepath>
|
122
|
+
|
123
|
+
Report issues
|
124
|
+
: <https://github.com/gioele/filepath/issues>
|
125
|
+
|
126
|
+
Documentation
|
127
|
+
: <http://rubydoc.info/gems/filepath>
|
128
|
+
|
129
|
+
|
130
|
+
License
|
131
|
+
-------
|
132
|
+
|
133
|
+
This is free and unencumbered software released into the public domain.
|
134
|
+
See the `UNLICENSE` file or <http://unlicense.org/> for more details.
|
135
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
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
|
+
begin
|
5
|
+
require 'bones'
|
6
|
+
rescue LoadError
|
7
|
+
abort '### Please install the "bones" gem ###'
|
8
|
+
end
|
9
|
+
|
10
|
+
Bones {
|
11
|
+
name 'filepath'
|
12
|
+
authors 'Gioele Barabucci'
|
13
|
+
email 'gioele@svario.it'
|
14
|
+
url 'http://github.com/gioele/filepath'
|
15
|
+
|
16
|
+
version '0.1'
|
17
|
+
|
18
|
+
ignore_file '.gitignore'
|
19
|
+
}
|
20
|
+
|
21
|
+
require File.join(File.dirname(__FILE__), 'spec/tasks')
|
22
|
+
|
23
|
+
task :default => 'spec:run'
|
24
|
+
task 'gem:release' => 'spec:run'
|
25
|
+
|
26
|
+
task 'spec:run' => 'spec:fixtures:gen'
|
data/UNLICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
data/lib/filepath.rb
ADDED
@@ -0,0 +1,551 @@
|
|
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
|
+
require 'filepathlist'
|
5
|
+
|
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_s)
|
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| FilePath.new(p) }
|
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
|
+
# FilePath.new("a/b") / "c" #=> <a/b/c>
|
44
|
+
#
|
45
|
+
# @example Append another FilePath
|
46
|
+
#
|
47
|
+
# home = FilePath.new(ENV["HOME"] || "/root")
|
48
|
+
# conf_dir = FilePath.new('.config')
|
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 another path.
|
86
|
+
#
|
87
|
+
# @param [FilePath, String] base the path to use as a base for the
|
88
|
+
# relative path
|
89
|
+
#
|
90
|
+
# @return [FilePath] the relative path
|
91
|
+
|
92
|
+
def relative_to(base)
|
93
|
+
base = FilePath.new(base) unless base.is_a? FilePath
|
94
|
+
|
95
|
+
if self.absolute? != base.absolute?
|
96
|
+
self_abs = self.absolute? ? "absolute" : "relative"
|
97
|
+
base_abs = base.absolute? ? "absolute" : "relative"
|
98
|
+
msg = "cannot compare: "
|
99
|
+
msg += "`#{self}` is #{self_abs} while "
|
100
|
+
msg += "`#{base}` is #{base_abs}"
|
101
|
+
raise msg # FIXME: argerror error class
|
102
|
+
end
|
103
|
+
|
104
|
+
self_frags = self.fragments
|
105
|
+
base_frags = base.fragments.dup
|
106
|
+
num_same = self_frags.find_index do |frag|
|
107
|
+
base_frags.delete_at(0) != frag
|
108
|
+
end
|
109
|
+
|
110
|
+
# find_index returns nil if `self` is a subset of `base`
|
111
|
+
num_same ||= self.fragments.length
|
112
|
+
|
113
|
+
num_parent_dirs = base.fragments.length - num_same
|
114
|
+
left_in_self = self.fragments[num_same..-1]
|
115
|
+
|
116
|
+
frags = [".."] * num_parent_dirs + left_in_self
|
117
|
+
|
118
|
+
return FilePath.join(frags)
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# The filename component of the path.
|
123
|
+
#
|
124
|
+
# The filename is the component of a path that appears after the last
|
125
|
+
# path separator.
|
126
|
+
#
|
127
|
+
# @return [FilePath] the filename
|
128
|
+
|
129
|
+
def filename
|
130
|
+
if self.root?
|
131
|
+
return FilePath.new('')
|
132
|
+
end
|
133
|
+
|
134
|
+
filename = self.normalized_fragments.last
|
135
|
+
return FilePath.new(filename)
|
136
|
+
end
|
137
|
+
|
138
|
+
alias :basename :filename
|
139
|
+
|
140
|
+
|
141
|
+
# The dir that contains the file
|
142
|
+
#
|
143
|
+
# @return [FilePath] the path of the parent dir
|
144
|
+
|
145
|
+
def parent_dir
|
146
|
+
return self / '..'
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# Replace the path filename with the supplied path.
|
151
|
+
#
|
152
|
+
# @param [FilePath, String] new_path the path to be put in place of
|
153
|
+
# the current filename
|
154
|
+
#
|
155
|
+
# @return [FilePath] a path with the supplied path instead of the
|
156
|
+
# current filename
|
157
|
+
|
158
|
+
def replace_filename(new_path)
|
159
|
+
dir = self.parent_dir
|
160
|
+
return dir / FilePath.new(new_path)
|
161
|
+
end
|
162
|
+
|
163
|
+
alias :replace_basename :replace_filename
|
164
|
+
|
165
|
+
|
166
|
+
# The extension of the file.
|
167
|
+
#
|
168
|
+
# The extension of a file are the characters after the last dot.
|
169
|
+
#
|
170
|
+
# @return [String] the extension of the file or nil if the file has no
|
171
|
+
# extension
|
172
|
+
|
173
|
+
def extension
|
174
|
+
filename = @fragments.last
|
175
|
+
|
176
|
+
num_dots = filename.count('.')
|
177
|
+
|
178
|
+
if num_dots.zero?
|
179
|
+
ext = nil
|
180
|
+
elsif filename.start_with?('.') && num_dots == 1
|
181
|
+
ext = nil
|
182
|
+
elsif filename.end_with?('.')
|
183
|
+
ext = ''
|
184
|
+
else
|
185
|
+
ext = filename.split('.').last
|
186
|
+
end
|
187
|
+
|
188
|
+
return ext
|
189
|
+
end
|
190
|
+
|
191
|
+
alias :ext :extension
|
192
|
+
|
193
|
+
|
194
|
+
# @overload extension?(ext)
|
195
|
+
# @param [String, Regexp] ext the extension to be matched
|
196
|
+
#
|
197
|
+
# @return whether the file extension matches the given extension
|
198
|
+
#
|
199
|
+
# @overload extension?
|
200
|
+
# @return whether the file has an extension
|
201
|
+
|
202
|
+
def extension?(ext = nil)
|
203
|
+
cur_ext = self.extension
|
204
|
+
|
205
|
+
if ext.nil?
|
206
|
+
return !cur_ext.nil?
|
207
|
+
else
|
208
|
+
if ext.is_a? Regexp
|
209
|
+
return !cur_ext.match(ext).nil?
|
210
|
+
else
|
211
|
+
return cur_ext == ext
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
alias ext? extension?
|
217
|
+
|
218
|
+
|
219
|
+
# @overload replace_extension(new_ext)
|
220
|
+
# Replaces the file extension with the supplied one. If the file
|
221
|
+
# has no extension it is added to the file name together with a dot.
|
222
|
+
#
|
223
|
+
# @param [String] new_ext the new extension
|
224
|
+
#
|
225
|
+
# @return [FilePath] a new path with the replaced extension
|
226
|
+
#
|
227
|
+
# @overload replace_extension
|
228
|
+
# Removes the file extension if present.
|
229
|
+
#
|
230
|
+
# @return [FilePath] a new path without the extension
|
231
|
+
|
232
|
+
def replace_extension(new_ext) # FIXME: accept block
|
233
|
+
if !self.extension?
|
234
|
+
if new_ext.nil?
|
235
|
+
path = self.to_s
|
236
|
+
else
|
237
|
+
path = self.to_s + '.' + new_ext
|
238
|
+
end
|
239
|
+
else
|
240
|
+
if new_ext.nil?
|
241
|
+
pattern = /\.[^.]*?\Z/
|
242
|
+
path = self.to_s.sub(pattern, '')
|
243
|
+
else
|
244
|
+
pattern = '.' + extension
|
245
|
+
path = self.to_s.sub(pattern, '.' + new_ext)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
return FilePath.new(path)
|
250
|
+
end
|
251
|
+
|
252
|
+
alias :replace_ext :replace_extension
|
253
|
+
alias :sub_ext :replace_extension
|
254
|
+
|
255
|
+
|
256
|
+
# Removes the file extension if present.
|
257
|
+
#
|
258
|
+
# @return [FilePath] a new path without the extension
|
259
|
+
|
260
|
+
def remove_extension
|
261
|
+
return replace_ext(nil)
|
262
|
+
end
|
263
|
+
|
264
|
+
alias :remove_ext :remove_extension
|
265
|
+
|
266
|
+
|
267
|
+
def =~(pattern)
|
268
|
+
return self.to_s =~ pattern
|
269
|
+
end
|
270
|
+
|
271
|
+
def root?
|
272
|
+
return @fragments == [SEPARATOR] # FIXME: windows, mac
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
# Is this path absolute?
|
277
|
+
#
|
278
|
+
# FIXME: document what an absolute path is.
|
279
|
+
#
|
280
|
+
# @return whether the current path is absolute
|
281
|
+
|
282
|
+
def absolute?
|
283
|
+
return @fragments.first == SEPARATOR # FIXME: windows, mac
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
# Is this path relative?
|
288
|
+
#
|
289
|
+
# FIXME: document what a relative path is.
|
290
|
+
#
|
291
|
+
# @return whether the current path is relative
|
292
|
+
|
293
|
+
def relative?
|
294
|
+
return !self.absolute?
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
# Simplify paths that contain `.` and `..`.
|
299
|
+
#
|
300
|
+
# The resulting path will be in normal form.
|
301
|
+
#
|
302
|
+
# FIXME: document what normal form is.
|
303
|
+
#
|
304
|
+
# @return [FilePath] a new path that does not contain `.` or `..`
|
305
|
+
# fragments.
|
306
|
+
|
307
|
+
def normalized
|
308
|
+
return FilePath.join(self.normalized_fragments)
|
309
|
+
end
|
310
|
+
alias :normalised :normalized
|
311
|
+
|
312
|
+
|
313
|
+
# Iterates over all the path directories, from the current path to
|
314
|
+
# the root.
|
315
|
+
#
|
316
|
+
# @param max_depth the maximum depth to ascend to, nil to ascend
|
317
|
+
# without limits.
|
318
|
+
#
|
319
|
+
# @yield [path] TODO
|
320
|
+
|
321
|
+
def ascend(max_depth = nil, &block)
|
322
|
+
max_depth ||= @fragments.length
|
323
|
+
(1..max_depth).reverse_each do |limit|
|
324
|
+
frags = @fragments.take(limit)
|
325
|
+
yield FilePath.join(frags)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Iterates over all the directory that lead to the current path.
|
330
|
+
#
|
331
|
+
# @param max_depth the maximum depth to descent to, nil to descend
|
332
|
+
# without limits.
|
333
|
+
#
|
334
|
+
# @yield [path] TODO
|
335
|
+
|
336
|
+
def descend(max_depth = nil, &block)
|
337
|
+
max_depth ||= @fragments.length
|
338
|
+
(1..max_depth).each do |limit|
|
339
|
+
frags = @fragments.take(limit)
|
340
|
+
yield FilePath.join(frags)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
|
345
|
+
# This path converted to a String
|
346
|
+
#
|
347
|
+
# @return [String] this path converted to a String
|
348
|
+
|
349
|
+
def to_raw_string
|
350
|
+
return @fragments.join(SEPARATOR).sub(%r{^//}, SEPARATOR) # FIXME: windows, mac
|
351
|
+
end
|
352
|
+
|
353
|
+
alias :to_raw_str :to_raw_string
|
354
|
+
|
355
|
+
|
356
|
+
# @return [String] this path converted to a String
|
357
|
+
#
|
358
|
+
# @note this method operates on the normalized the path
|
359
|
+
|
360
|
+
def to_s
|
361
|
+
return self.normalized_fragments.join(SEPARATOR).sub(%r{^//}, SEPARATOR)
|
362
|
+
end
|
363
|
+
|
364
|
+
|
365
|
+
def inspect
|
366
|
+
return '<' + self.to_raw_string + '>'
|
367
|
+
end
|
368
|
+
|
369
|
+
def ==(other)
|
370
|
+
return self.to_s == FilePath.new(other).to_s
|
371
|
+
end
|
372
|
+
|
373
|
+
# @private
|
374
|
+
def split_path_string(raw_path)
|
375
|
+
fragments = raw_path.split(SEPARATOR) # FIXME: windows, mac
|
376
|
+
|
377
|
+
if raw_path == SEPARATOR
|
378
|
+
fragments << SEPARATOR
|
379
|
+
end
|
380
|
+
|
381
|
+
if !fragments.empty? && fragments.first.empty?
|
382
|
+
fragments[0] = SEPARATOR
|
383
|
+
end
|
384
|
+
|
385
|
+
return fragments
|
386
|
+
end
|
387
|
+
|
388
|
+
# @private
|
389
|
+
def normalized_fragments
|
390
|
+
normalized_relative_frags(self.fragments)
|
391
|
+
end
|
392
|
+
|
393
|
+
# @private
|
394
|
+
def normalized_relative_frags(orig_frags)
|
395
|
+
frags = orig_frags.dup
|
396
|
+
|
397
|
+
# remove "current dir" markers
|
398
|
+
frags.delete('.')
|
399
|
+
|
400
|
+
i = 0
|
401
|
+
while (i < frags.length)
|
402
|
+
if frags[i] == '..' && frags[i-1] == SEPARATOR
|
403
|
+
# remove '..' fragments following a root delimiter
|
404
|
+
frags.delete_at(i)
|
405
|
+
i -= 1
|
406
|
+
elsif frags[i] == '..' && frags[i-1] != '..'
|
407
|
+
# remove every fragment followed by a ".." marker
|
408
|
+
frags.delete_at(i)
|
409
|
+
frags.delete_at(i-1)
|
410
|
+
i -= 2
|
411
|
+
end
|
412
|
+
i += 1
|
413
|
+
end
|
414
|
+
|
415
|
+
return frags
|
416
|
+
end
|
417
|
+
|
418
|
+
module PathResolution
|
419
|
+
def absolute_path(base_dir = Dir.pwd) # FIXME: rename to `#absolute`?
|
420
|
+
path = if !self.absolute?
|
421
|
+
self
|
422
|
+
else
|
423
|
+
FilePath.new(base_dir) / self
|
424
|
+
end
|
425
|
+
|
426
|
+
return path.resolve_link
|
427
|
+
end
|
428
|
+
|
429
|
+
def resolve_link
|
430
|
+
return FilePath.new(File.readlink(self.to_s))
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
module FileInfo
|
435
|
+
def file?
|
436
|
+
FileTest.file?(self.to_s)
|
437
|
+
end
|
438
|
+
|
439
|
+
def link?
|
440
|
+
FileTest.symlink?(self.to_s)
|
441
|
+
end
|
442
|
+
alias symlink? link?
|
443
|
+
|
444
|
+
def directory?
|
445
|
+
FileTest.directory?(self.to_s)
|
446
|
+
end
|
447
|
+
|
448
|
+
def exists?
|
449
|
+
FileTest.exists?(self.to_s)
|
450
|
+
end
|
451
|
+
alias exist? exists?
|
452
|
+
|
453
|
+
def readable?
|
454
|
+
FileTest.readable?(self.to_s)
|
455
|
+
end
|
456
|
+
|
457
|
+
def writable?
|
458
|
+
FileTest.writable?(self.to_s)
|
459
|
+
end
|
460
|
+
|
461
|
+
def executable?
|
462
|
+
FileTest.executable?(self.to_s)
|
463
|
+
end
|
464
|
+
|
465
|
+
def setgid?
|
466
|
+
FileTest.setgid?(self.to_s)
|
467
|
+
end
|
468
|
+
|
469
|
+
def setuid?
|
470
|
+
FileTest.setuid?(self.to_s)
|
471
|
+
end
|
472
|
+
|
473
|
+
def hidden?
|
474
|
+
@fragments.last.start_with('.') # FIXME: windows, mac
|
475
|
+
end
|
476
|
+
|
477
|
+
def empty?
|
478
|
+
FileTest.zero?(self.to_s)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
module FileManipulationMethods
|
483
|
+
def open(*args, &block)
|
484
|
+
File.open(self.to_s, *args, &block)
|
485
|
+
end
|
486
|
+
|
487
|
+
def touch
|
488
|
+
self.open do ; end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
module DirectoyMethods
|
493
|
+
def entries(pattern = '*')
|
494
|
+
if !self.directory?
|
495
|
+
raise Errno::ENOTDIR.new(self.to_s)
|
496
|
+
end
|
497
|
+
|
498
|
+
raw_entries = Dir.glob((self / pattern).to_s)
|
499
|
+
entries = FilePathList.new(raw_entries)
|
500
|
+
|
501
|
+
return entries
|
502
|
+
end
|
503
|
+
alias :glob :entries
|
504
|
+
|
505
|
+
def files
|
506
|
+
entries.select_entries(:file)
|
507
|
+
end
|
508
|
+
|
509
|
+
def links
|
510
|
+
entries.select_entries(:link)
|
511
|
+
end
|
512
|
+
|
513
|
+
def directories
|
514
|
+
entries.select_entries(:directory)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
include PathResolution
|
519
|
+
include FileInfo
|
520
|
+
include FileManipulationMethods
|
521
|
+
include DirectoyMethods
|
522
|
+
end
|
523
|
+
|
524
|
+
class String
|
525
|
+
# Generates a path from a String.
|
526
|
+
#
|
527
|
+
# `"/a/b/c".as_path` is equivalent to `FilePath.new("/a/b/c")`.
|
528
|
+
#
|
529
|
+
# @return [FilePath] a new path generated from the string
|
530
|
+
#
|
531
|
+
# @note FIXME: `#as_path` should be `#to_path` but that method name
|
532
|
+
# is already used
|
533
|
+
def as_path
|
534
|
+
FilePath.new(self)
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
class Array
|
539
|
+
# Generates a path using the elements of an Array as path fragments.
|
540
|
+
#
|
541
|
+
# `%w{a b c}.as_path` is equivalent to `FilePath.join('a', 'b', 'c')`.
|
542
|
+
#
|
543
|
+
# @return [FilePath] a new path generated using the element as path
|
544
|
+
# fragments
|
545
|
+
#
|
546
|
+
# @note FIXME: `#as_path` should be `#to_path` but that method name
|
547
|
+
# is already used
|
548
|
+
def as_path
|
549
|
+
FilePath.join(self)
|
550
|
+
end
|
551
|
+
end
|