path 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,294 @@
1
+ # Path's low-level implementation based on Pathname
2
+
3
+ class Path
4
+ # @private
5
+ SAME_PATHS = if File::FNM_SYSCASE.nonzero?
6
+ lambda { |a,b| a.casecmp(b).zero? }
7
+ else
8
+ lambda { |a,b| a == b }
9
+ end
10
+
11
+ # Returns a cleaned version of +self+ with consecutive slashes and useless dots removed.
12
+ # The filesystem is not accessed.
13
+ #
14
+ # If +consider_symlink+ is +true+, then a more conservative algorithm is used
15
+ # to avoid breaking symbolic linkages. This may retain more +..+
16
+ # entries than absolutely necessary, but without accessing the filesystem,
17
+ # this can't be avoided. See {#realpath}.
18
+ def clean(consider_symlink = false)
19
+ consider_symlink ? cleanpath_conservative : cleanpath_aggressive
20
+ end
21
+ alias :cleanpath :clean
22
+
23
+ # #parent returns the parent directory.
24
+ # This can be chained.
25
+ def parent
26
+ self / '..'
27
+ end
28
+
29
+ # Path#/ appends a path fragment to this one to produce a new Path.
30
+ #
31
+ # p = Path.new("/usr") # => #<Path /usr>
32
+ # p / "bin/ruby" # => #<Path /usr/bin/ruby>
33
+ # p / "/etc/passwd" # => #<Path /etc/passwd>
34
+ #
35
+ # This method doesn't access the file system, it is pure string manipulation.
36
+ def /(other)
37
+ Path.new(plus(@path, other.to_s))
38
+ end
39
+
40
+ # Configures the behavior of {Path#+}. The default is +:warning+.
41
+ #
42
+ # Path + :defined # aliased to Path#/
43
+ # Path + :warning # calls Path#/ but warns
44
+ # Path + :error # not defined
45
+ # Path + :string # like String#+. Warns if $VERBOSE (-w)
46
+ #
47
+ # @param config [:defined, :warning, :error, :string] the configuration value
48
+ def Path.+(config)
49
+ unless [:defined, :warning, :error, :string].include? config
50
+ raise ArgumentError, "Invalid configuration: #{config.inspect}"
51
+ end
52
+ if @plus_configured
53
+ raise "Path.+ has already been called: #{@plus_configured}"
54
+ end
55
+ remove_method :+ if method_defined? :+
56
+ case config
57
+ when :defined
58
+ alias :+ :/
59
+ when :warning
60
+ def +(other)
61
+ warn 'Warning: use of deprecated Path#+ as Path#/: ' <<
62
+ "#{inspect} + #{other.inspect}\n#{caller.first}"
63
+ self / other
64
+ end
65
+ when :error
66
+ # nothing to do, the method has been removed
67
+ when :string
68
+ def +(other)
69
+ warn 'Warning: use of deprecated Path#+ as String#+: ' <<
70
+ "#{inspect} + #{other.inspect}\n#{caller.first}" if $VERBOSE
71
+ Path(to_s + other.to_s)
72
+ end
73
+ end
74
+ @plus_configured = caller.first
75
+ end
76
+
77
+ @plus_configured = nil # Initialization
78
+ Path + :warning
79
+ @plus_configured = nil # Let the user overrides this default configuration
80
+
81
+ # @!method +(other)
82
+ # The behavior depends on the configuration with Path.{Path.+}.
83
+ # It might behave as {Path#/}, String#+, give warnings,
84
+ # or not be defined at all.
85
+
86
+ # Joins paths.
87
+ #
88
+ # path0.join(path1, ..., pathN)
89
+ # # is the same as
90
+ # path0 / path1 / ... / pathN
91
+ def join(*paths)
92
+ result = nil
93
+ paths.reverse_each { |path|
94
+ result = Path.new(path) / result
95
+ return result if result.absolute?
96
+ }
97
+ self / result
98
+ end
99
+
100
+ # #relative_path_from returns a relative path from the argument to the
101
+ # receiver. They must be both relative or both absolute.
102
+ #
103
+ # #relative_path_from doesn't access the filesystem. It assumes no symlinks.
104
+ #
105
+ # @raise [ArgumentError] if it cannot find a relative path:
106
+ # Either the base is relative and contains '..' (in that case you can expand
107
+ # both paths) or the paths are absolutes and on different drives (Windows).
108
+ def relative_path_from(base_directory)
109
+ dest = clean.path
110
+ base = Path.new(base_directory).clean.path
111
+ dest_prefix, dest_names = split_names(dest)
112
+ base_prefix, base_names = split_names(base)
113
+
114
+ unless SAME_PATHS[dest_prefix, base_prefix]
115
+ raise ArgumentError, "different prefix: #{self.inspect} and #{base_directory.inspect}"
116
+ end
117
+ while d = dest_names.first and b = base_names.first and SAME_PATHS[d, b]
118
+ dest_names.shift
119
+ base_names.shift
120
+ end
121
+ raise ArgumentError, "base_directory has ..: #{base_directory.inspect}" if base_names.include? '..'
122
+ # the number of names left in base is the ones we have to climb
123
+ names = base_names.fill('..').concat(dest_names)
124
+ return Path.new('.') if names.empty?
125
+ Path.new(*names)
126
+ end
127
+ alias :relative_to :relative_path_from
128
+ alias :% :relative_path_from
129
+
130
+ # @private
131
+ module Helpers
132
+ private
133
+
134
+ # remove the leading . of +ext+ if present.
135
+ def pure_ext(ext)
136
+ ext = ext.to_s and ext.start_with?('.') ? ext[1..-1] : ext
137
+ end
138
+
139
+ # add a leading . to +ext+ if missing. Returns '' if +ext+ is empty.
140
+ def dotted_ext(ext)
141
+ ext = ext.to_s and (ext.empty? or ext.start_with?('.')) ? ext : ".#{ext}"
142
+ end
143
+ end
144
+
145
+ include Helpers
146
+ extend Helpers
147
+
148
+ private
149
+
150
+ def init
151
+ @path = validate(@path)
152
+
153
+ taint if @path.tainted?
154
+ @path.freeze
155
+ freeze
156
+ end
157
+
158
+ def validate(path)
159
+ raise ArgumentError, "path contains a null byte: #{path.inspect}" if path.include? "\0"
160
+ path.gsub!(File::ALT_SEPARATOR, '/') if File::ALT_SEPARATOR
161
+ path = File.expand_path(path) if path.start_with? '~'
162
+ path
163
+ end
164
+
165
+ # chop_basename(path) -> [pre-basename, basename] or nil
166
+ def chop_basename(path)
167
+ base = File.basename(path)
168
+ if base.empty? or base == '/'
169
+ return nil
170
+ else
171
+ return path[0, path.rindex(base)], base
172
+ end
173
+ end
174
+
175
+ def is_absolute?(path)
176
+ path.start_with?('/') or (path =~ /\A[a-zA-Z]:\// and is_root?($&))
177
+ end
178
+
179
+ def is_root?(path)
180
+ chop_basename(path) == nil and path.include?('/')
181
+ end
182
+
183
+ # split_names(path) -> prefix, [name, ...]
184
+ def split_names(path)
185
+ names = []
186
+ while r = chop_basename(path)
187
+ path, basename = r
188
+ names.unshift basename if basename != '.'
189
+ end
190
+ return path, names
191
+ end
192
+
193
+ def prepend_prefix(prefix, relnames)
194
+ relpath = File.join(*relnames)
195
+ if relpath.empty?
196
+ File.dirname(prefix)
197
+ elsif prefix.include? '/'
198
+ # safe because File.dirname returns a new String
199
+ add_trailing_separator(File.dirname(prefix)) << relpath
200
+ else
201
+ prefix + relpath
202
+ end
203
+ end
204
+
205
+ def has_trailing_separator?(path)
206
+ !is_root?(path) and path.end_with?('/')
207
+ end
208
+
209
+ def add_trailing_separator(path) # mutates path
210
+ path << '/' unless path.end_with? '/'
211
+ path
212
+ end
213
+
214
+ def del_trailing_separator(path)
215
+ if r = chop_basename(path)
216
+ pre, basename = r
217
+ pre + basename
218
+ elsif %r{/+\z} =~ path
219
+ $` + File.dirname(path)[%r{/*\z}]
220
+ else
221
+ path
222
+ end
223
+ end
224
+
225
+ # remove '..' segments since root's parent is root
226
+ def remove_root_parents(prefix, names)
227
+ names.shift while names.first == '..' if is_root?(prefix)
228
+ end
229
+
230
+ # Clean the path simply by resolving and removing excess "." and ".." entries.
231
+ # Nothing more, nothing less.
232
+ def cleanpath_aggressive
233
+ pre = @path
234
+ names = []
235
+ while r = chop_basename(pre)
236
+ pre, base = r
237
+ if base == '.'
238
+ # do nothing, it can be ignored
239
+ elsif names.first == '..' and base != '..'
240
+ # base can be ignored as we go back to its parent
241
+ names.shift
242
+ else
243
+ names.unshift base
244
+ end
245
+ end
246
+ remove_root_parents(pre, names)
247
+ Path.new(prepend_prefix(pre, names))
248
+ end
249
+
250
+ def cleanpath_conservative
251
+ path = @path
252
+ pre, names = split_names(path)
253
+ remove_root_parents(pre, names)
254
+ if names.empty?
255
+ Path.new(File.dirname(pre))
256
+ else
257
+ names << '.' if names.last != '..' and File.basename(path) == '.'
258
+ result = prepend_prefix(pre, names)
259
+ if names.last != '.' and names.last != '..' and has_trailing_separator?(path)
260
+ Path.new(add_trailing_separator(result))
261
+ else
262
+ Path.new(result)
263
+ end
264
+ end
265
+ end
266
+
267
+ def plus(prefix, rel)
268
+ return rel if is_absolute?(rel)
269
+ _, names = split_names(rel)
270
+
271
+ loop do
272
+ # break if that was the last segment
273
+ break unless r = chop_basename(prefix)
274
+ prefix, name = r
275
+ next if name == '.'
276
+
277
+ # break if we can't resolve anymore
278
+ if name == '..' or names.first != '..'
279
+ prefix << name
280
+ break
281
+ end
282
+ names.shift
283
+ end
284
+
285
+ remove_root_parents(prefix, names)
286
+ has_prefix = chop_basename(prefix)
287
+ if names.empty?
288
+ has_prefix ? prefix : File.dirname(prefix)
289
+ else
290
+ suffix = File.join(*names)
291
+ has_prefix ? File.join(prefix, suffix) : prefix + suffix
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,104 @@
1
+ class Path
2
+ # @!group IO
3
+
4
+ # Opens the file for reading or writing. See +File.open+.
5
+ # @yieldparam [File] file
6
+ def open(*args, &block)
7
+ File.open(@path, *args, &block)
8
+ end
9
+
10
+ # Iterates over the lines in the file. See +IO.foreach+.
11
+ # @yieldparam [String] line
12
+ def each_line(*args, &block)
13
+ IO.foreach(@path, *args, &block)
14
+ end
15
+ alias :lines :each_line
16
+
17
+ # Returns all data from the file, or the first +bytes+ bytes if specified.
18
+ # See +IO.read+.
19
+ def read(*args)
20
+ IO.read(@path, *args)
21
+ end
22
+
23
+ if IO.respond_to? :binread
24
+ # Returns all the bytes from the file, or the first +N+ if specified.
25
+ # See +IO.binread+.
26
+ def binread(*args)
27
+ IO.binread(@path, *args)
28
+ end
29
+ else
30
+ def binread(*args)
31
+ open('rb', &:read)
32
+ end
33
+ end
34
+
35
+ # Returns all the lines from the file. See +IO.readlines+.
36
+ def readlines(*args)
37
+ IO.readlines(@path, *args)
38
+ end
39
+
40
+ # See +IO.sysopen+.
41
+ def sysopen(*args)
42
+ IO.sysopen(@path, *args)
43
+ end
44
+
45
+ if IO.respond_to? :write
46
+ # Writes +contents+ to +self+. See +IO.write+ or +IO#write+.
47
+ def write(contents, *open_args)
48
+ IO.write(@path, contents, *open_args)
49
+ end
50
+ else
51
+ def write(contents, *open_args)
52
+ open('w', *open_args) { |f| f.write(contents) }
53
+ end
54
+ end
55
+
56
+ if IO.respond_to? :binwrite
57
+ # Writes +contents+ to +self+. See +IO.binwrite+.
58
+ def binwrite(contents, *open_args)
59
+ IO.binwrite(@path, contents, *open_args)
60
+ end
61
+ else
62
+ def binwrite(contents, *open_args)
63
+ open('wb', *open_args) { |f| f.write(contents) }
64
+ end
65
+ end
66
+
67
+ if IO.respond_to? :write and !RUBY_DESCRIPTION.start_with?('jruby')
68
+ # Appends +contents+ to +self+. See +IO.write+ or +IO#write+.
69
+ def append(contents, open_args = {})
70
+ open_args[:mode] = 'a'
71
+ IO.write(@path, contents, open_args)
72
+ end
73
+ else
74
+ def append(contents, *open_args)
75
+ open('a', *open_args) { |f| f.write(contents) }
76
+ end
77
+ end
78
+
79
+ # Rewrites contents of +self+.
80
+ #
81
+ # Path('file').rewrite { |contents| contents.reverse }
82
+ #
83
+ # @yieldparam [String] contents
84
+ # @yieldreturn [String] contents to write
85
+ def rewrite
86
+ write yield read
87
+ end
88
+
89
+ # Returns the first +bytes+ bytes of the file.
90
+ # If the file size is smaller than +bytes+, return the whole contents.
91
+ def head(bytes)
92
+ read(bytes)
93
+ end
94
+
95
+ # Returns the last +bytes+ bytes of the file.
96
+ # If the file size is smaller than +bytes+, return the whole contents.
97
+ def tail(bytes)
98
+ return read if size < bytes
99
+ open { |f|
100
+ f.seek(-bytes, IO::SEEK_END)
101
+ f.read
102
+ }
103
+ end
104
+ end
@@ -0,0 +1,29 @@
1
+ class Path
2
+ # @!group Loading
3
+
4
+ # The list of loaders. See {Path.register_loader}.
5
+ LOADERS = {}
6
+
7
+ # Registers a new loader (a block which will be called with the Path to load)
8
+ # for the given extensions (either with the leading dot or not)
9
+ #
10
+ # Path.register_loader('.marshal') { |file| Marshal.load file.read }
11
+ #
12
+ # @yieldparam [Path] path
13
+ def self.register_loader(*extensions, &loader)
14
+ extensions.each { |ext|
15
+ LOADERS[pure_ext(ext)] = loader
16
+ }
17
+ end
18
+
19
+ # Path#load helps loading data from various files.
20
+ # JSON and YAML loaders are provided by default.
21
+ # See {Path.register_loader}.
22
+ def load
23
+ if LOADERS.key? ext
24
+ LOADERS[ext].call(self)
25
+ else
26
+ raise "Unable to load #{self} (unrecognized extension)"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,136 @@
1
+ class Path
2
+ # @!group Path parts
3
+
4
+ # Returns the last component of the path. See +File.basename+.
5
+ def basename(*args)
6
+ Path.new(File.basename(@path, *args))
7
+ end
8
+
9
+ # basename(extname)
10
+ def base
11
+ basename(extname)
12
+ end
13
+
14
+ # Returns all but the last component of the path.
15
+ #
16
+ # Don't chain this when the path is relative:
17
+ # Path('.').dir # => #<Path .>
18
+ # Use #parent instead.
19
+ # See +File.dirname+.
20
+ def dirname
21
+ Path.new(File.dirname(@path))
22
+ end
23
+ alias :dir :dirname
24
+
25
+ # Returns the extension, with a leading dot. See +File.extname+.
26
+ def extname
27
+ File.extname(@path)
28
+ end
29
+
30
+ # {#extname} without leading dot.
31
+ def ext
32
+ ext = extname
33
+ ext.empty? ? ext : ext[1..-1]
34
+ end
35
+
36
+ # Returns the #dirname and the #basename in an Array. See +File.split+.
37
+ def split
38
+ File.split(@path).map(&Path)
39
+ end
40
+
41
+ # Adds +ext+ as an extension to +path+.
42
+ # Handle both extensions with or without leading dot.
43
+ # No-op if +ext+ is +empty?+.
44
+ #
45
+ # Path('file').add_extension('txt') # => #<Path file.txt>
46
+ def add_extension(ext)
47
+ return self if ext.to_s.empty?
48
+ Path.new @path+dotted_ext(ext)
49
+ end
50
+ alias :add_ext :add_extension
51
+
52
+ # Removes the last extension of +path+.
53
+ #
54
+ # Path('script.rb').without_extension # => #<Path script>
55
+ # Path('archive.tar.gz').without_extension # => #<Path archive.tar>
56
+ def without_extension
57
+ Path.new @path[0..-extname.size-1]
58
+ end
59
+ alias :rm_ext :without_extension
60
+
61
+ # Replaces the last extension of +path+ with +ext+.
62
+ # Handle both extensions with or without leading dot.
63
+ # Removes last extension if +ext+ is +empty?+.
64
+ #
65
+ # Path('main.c++').replace_extension('cc') # => #<Path main.cc>
66
+ def replace_extension(ext)
67
+ return without_extension if ext.to_s.empty?
68
+ Path.new(@path[0..-extname.size-1] << dotted_ext(ext))
69
+ end
70
+ alias :sub_ext :replace_extension
71
+
72
+ # Iterates over each component of the path.
73
+ #
74
+ # Path.new("/usr/bin/ruby").each_filename { |filename| ... }
75
+ # # yields "usr", "bin", and "ruby".
76
+ #
77
+ # @yieldparam [String] filename
78
+ def each_filename
79
+ return to_enum(__method__) unless block_given?
80
+ _, names = split_names(@path)
81
+ names.each { |filename| yield filename }
82
+ nil
83
+ end
84
+
85
+ # Iterates over each element in the given path in descending order.
86
+ #
87
+ # Path.new('/path/to/some/file.rb').descend { |v| p v }
88
+ # #<Path />
89
+ # #<Path /path>
90
+ # #<Path /path/to>
91
+ # #<Path /path/to/some>
92
+ # #<Path /path/to/some/file.rb>
93
+ #
94
+ # Path.new('path/to/some/file.rb').descend { |v| p v }
95
+ # #<Path path>
96
+ # #<Path path/to>
97
+ # #<Path path/to/some>
98
+ # #<Path path/to/some/file.rb>
99
+ #
100
+ # It doesn't access actual filesystem.
101
+ # @yieldparam [Path] path
102
+ def descend
103
+ return to_enum(:descend) unless block_given?
104
+ ascend.reverse_each { |v| yield v }
105
+ nil
106
+ end
107
+
108
+ # Iterates over each element in the given path in ascending order.
109
+ #
110
+ # Path.new('/path/to/some/file.rb').ascend { |v| p v }
111
+ # #<Path /path/to/some/file.rb>
112
+ # #<Path /path/to/some>
113
+ # #<Path /path/to>
114
+ # #<Path /path>
115
+ # #<Path />
116
+ #
117
+ # Path.new('path/to/some/file.rb').ascend { |v| p v }
118
+ # #<Path path/to/some/file.rb>
119
+ # #<Path path/to/some>
120
+ # #<Path path/to>
121
+ # #<Path path>
122
+ #
123
+ # It doesn't access actual filesystem.
124
+ # @yieldparam [Path] path
125
+ def ascend
126
+ return to_enum(:ascend) unless block_given?
127
+ path = @path
128
+ yield self
129
+ while r = chop_basename(path)
130
+ path, = r
131
+ break if path.empty?
132
+ yield Path.new(del_trailing_separator(path))
133
+ end
134
+ end
135
+ alias :ancestors :ascend
136
+ end