filepath 0.3.1 → 0.4

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