path 1.3.0

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