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.
@@ -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