filepath 0.3.1 → 0.4

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,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