filepath 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ /announcement.txt
2
+ /coverage/
3
+ /doc/
4
+ /pkg/
5
+ /.yardoc/
6
+ /spec/fixtures/
@@ -0,0 +1,3 @@
1
+ -m none
2
+ -
3
+ UNLICENSE
@@ -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
+
@@ -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'
@@ -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/>
@@ -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