rubypath 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/LICENSE.txt +165 -0
- data/lib/rubypath.rb +29 -0
- data/lib/rubypath/backend.rb +96 -0
- data/lib/rubypath/backend/mock.rb +362 -0
- data/lib/rubypath/backend/sys.rb +163 -0
- data/lib/rubypath/comparison.rb +21 -0
- data/lib/rubypath/construction.rb +115 -0
- data/lib/rubypath/dir_operations.rb +161 -0
- data/lib/rubypath/extensions.rb +162 -0
- data/lib/rubypath/file_operations.rb +193 -0
- data/lib/rubypath/file_predicates.rb +34 -0
- data/lib/rubypath/identity.rb +59 -0
- data/lib/rubypath/io_operations.rb +84 -0
- data/lib/rubypath/mock.rb +44 -0
- data/lib/rubypath/path_operations.rb +320 -0
- data/lib/rubypath/path_predicates.rb +63 -0
- data/lib/rubypath/version.rb +15 -0
- data/rubypath.gemspec +33 -0
- metadata +22 -3
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
# @!group IO Operations
|
5
|
+
|
6
|
+
# Write given content to file.
|
7
|
+
#
|
8
|
+
# @overload write(content, [..])
|
9
|
+
# Write given content to file. An existing file will be truncated otherwise
|
10
|
+
# a file will be created.
|
11
|
+
#
|
12
|
+
# Additional arguments will be passed to {::IO.write}.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# Path('/path/to/file.txt').write('CONTENT')
|
16
|
+
# #=> 7
|
17
|
+
#
|
18
|
+
# @param content [String] Content to write to file.
|
19
|
+
#
|
20
|
+
# @overload write(content, offset, [..])
|
21
|
+
# Write content at specific position in file. Content will be replaced
|
22
|
+
# starting at given offset.
|
23
|
+
#
|
24
|
+
# Additional arguments will be passed to {::IO.write}.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# path.write('CONTENT', 4)
|
28
|
+
# #=> 7
|
29
|
+
# path.read
|
30
|
+
# #=> "1234CONTENT2345678"
|
31
|
+
#
|
32
|
+
# @param content [String] Content to write to file.
|
33
|
+
# @param offset [Integer] Offset where to start writing. If nil file will
|
34
|
+
# be truncated.
|
35
|
+
#
|
36
|
+
# @see IO.write
|
37
|
+
# @return [Path] Self.
|
38
|
+
#
|
39
|
+
def write(content, *args)
|
40
|
+
invoke_backend :write, self, content, *args
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Read file content from disk.
|
45
|
+
#
|
46
|
+
# @overload read([..])
|
47
|
+
# Read all content from file.
|
48
|
+
#
|
49
|
+
# Additional arguments will be passed to {::IO.read}.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# Path('file.txt').read
|
53
|
+
# #=> "CONTENT"
|
54
|
+
#
|
55
|
+
# @overload read(length, [..])
|
56
|
+
# Read given amount of bytes from file.
|
57
|
+
#
|
58
|
+
# Additional arguments will be passed to {::IO.read}.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# Path('file.txt').read(4)
|
62
|
+
# #=> "CONT"
|
63
|
+
#
|
64
|
+
# @param length [Integer] Number of bytes to read.
|
65
|
+
#
|
66
|
+
# @overload read(length, offset, [..])
|
67
|
+
# Read given amount of bytes from file starting at given offset.
|
68
|
+
#
|
69
|
+
# Additional arguments will be passed to {::IO.read}.
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# Path('file.txt').read(4, 2)
|
73
|
+
# #=> "NTEN"
|
74
|
+
#
|
75
|
+
# @param length [Integer] Number of bytes to read.
|
76
|
+
# @param offset [Integer] Where to start reading.
|
77
|
+
#
|
78
|
+
# @see IO.read
|
79
|
+
# @return [String] Read content.
|
80
|
+
#
|
81
|
+
def read(*args)
|
82
|
+
invoke_backend :read, self, *args
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
class << self
|
5
|
+
# @!group Mocking / Virtual File System
|
6
|
+
|
7
|
+
# Configure current path backend. Can be used to configure specified
|
8
|
+
# test scenario. If no virtual or scoped path backend is set the default
|
9
|
+
# one will be used.
|
10
|
+
#
|
11
|
+
# Do not forget to use mock file system in your specs:
|
12
|
+
# See more {Backend.mock}.
|
13
|
+
#
|
14
|
+
# around do |example|
|
15
|
+
# Path::Backend.mock &example
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# *Note*: Not all operations are supported.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Path.mock do |root|
|
22
|
+
# root.mkpath '/a/b/c/d/e'
|
23
|
+
# root.touch '/a/b/test.txt'
|
24
|
+
# root.join('/a/c/lorem.yaml').write YAML.dump({'lorem' => 'ipsum'})
|
25
|
+
# #...
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# @example Configure backend (only with virtual file system)
|
29
|
+
# Path.mock do |root, backend|
|
30
|
+
# backend.current_user = 'test'
|
31
|
+
# backend.homes = {'test' => '/path/to/test/home'}
|
32
|
+
# #...
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @yield |root, backend| Yield file system root path and current backend.
|
36
|
+
# @yieldparam root [Path] Root path of current packend.
|
37
|
+
# @yieldparam backend [Backend] Current backend.
|
38
|
+
#
|
39
|
+
def mock(_opts = {})
|
40
|
+
yield Path('/'), Backend.instance.backend if block_given?
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path # rubocop:disable ClassLength
|
4
|
+
# @!group Path Operations
|
5
|
+
|
6
|
+
# Join path with given arguments.
|
7
|
+
#
|
8
|
+
# @overload initialize([[Path, String, #to_path, #path, #to_s], ...]
|
9
|
+
# Join all given arguments to build a new path.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# Path('/').join('test', %w(a b), 5, Pathname.new('file'))
|
13
|
+
# # => <Path:"/test/a/b/5/file">
|
14
|
+
#
|
15
|
+
# @return [Path]
|
16
|
+
#
|
17
|
+
def join(*args)
|
18
|
+
parts = args.flatten
|
19
|
+
case parts.size
|
20
|
+
when 0
|
21
|
+
self
|
22
|
+
when 1
|
23
|
+
join = Path parts.shift
|
24
|
+
join.absolute? ? join : Path(::File.join(path, join.path))
|
25
|
+
else
|
26
|
+
join(parts.shift).join(*parts)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Iterate over all path components.
|
31
|
+
#
|
32
|
+
# @overload each_component
|
33
|
+
# Return a enumerator to iterate over all path components.
|
34
|
+
#
|
35
|
+
# @example Iterate over path components using a enumerator
|
36
|
+
# enum = Path('/path/to/file.txt').each_component
|
37
|
+
# enum.each{|fn| puts fn}
|
38
|
+
# # => "path"
|
39
|
+
# # => "to"
|
40
|
+
# # => "file.txt"
|
41
|
+
#
|
42
|
+
# @example Map each path component and create a new path
|
43
|
+
# path = Path('/path/to/file.txt')
|
44
|
+
# Path path.each_component.map{|fn| fn.length}
|
45
|
+
# # => <Path:"/4/2/8">
|
46
|
+
#
|
47
|
+
# @return [Enumerator] Return a enumerator for all path components.
|
48
|
+
#
|
49
|
+
# @overload each_component(&block)
|
50
|
+
# Yield given block for each path components.
|
51
|
+
#
|
52
|
+
# @example Print each file name
|
53
|
+
# Path('/path/to/file.txt').each_component{|fn| puts fn}
|
54
|
+
# # => "path"
|
55
|
+
# # => "to"
|
56
|
+
# # => "file.txt"
|
57
|
+
#
|
58
|
+
# @param block [Proc] Block to invoke with each path component.
|
59
|
+
# If no block is given an enumerator will returned.
|
60
|
+
# @return [self] Self.
|
61
|
+
#
|
62
|
+
def each_component(opts = {}, &block)
|
63
|
+
rv = if opts[:empty]
|
64
|
+
# split eats leading slashes
|
65
|
+
ary = path.split(Path.separator)
|
66
|
+
# so add an empty string if path ends with slash
|
67
|
+
ary << '' if path[-1] == Path.separator
|
68
|
+
ary.each(&block)
|
69
|
+
else
|
70
|
+
Pathname(path).each_filename(&block)
|
71
|
+
end
|
72
|
+
block ? self : rv
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return an array with all path components.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# Path('path/to/file').components
|
79
|
+
# # => ["path", "to", "file"]
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# Path('/path/to/file').components
|
83
|
+
# # => ["path", "to", "file"]
|
84
|
+
#
|
85
|
+
# @return [Array<String>] File names.
|
86
|
+
#
|
87
|
+
def components(*args)
|
88
|
+
each_component(*args).to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
# Converts a pathname to an absolute pathname. Given arguments will be
|
92
|
+
# joined to current path before expanding path. Relative paths are referenced
|
93
|
+
# from the current working directory of the process unless the `:base` option
|
94
|
+
# is set, which will be used as the starting point.
|
95
|
+
#
|
96
|
+
# The given pathname may start with a "~", which expands to the process
|
97
|
+
# owner's home directory (the environment variable HOME must be set
|
98
|
+
# correctly). "~user" expands to the named user's home directory.
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# Path('path/to/../tmp').expand
|
102
|
+
# #=> <Path:"path/tmp">
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# Path('~/tmp').expand
|
106
|
+
# #=> <Path:"/home/user/tmp">
|
107
|
+
#
|
108
|
+
# @example
|
109
|
+
# Path('~oma/tmp').expand
|
110
|
+
# #=> <Path:"/home/oma/tmp">
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# Path('~/tmp').expand('../file.txt')
|
114
|
+
# #=> <Path:"/home/user/file.txt">
|
115
|
+
#
|
116
|
+
# @return [Path] Expanded path.
|
117
|
+
# @see ::File#expand_path
|
118
|
+
#
|
119
|
+
def expand(*args)
|
120
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
121
|
+
|
122
|
+
with_path(*args) do |path|
|
123
|
+
base = Path.like_path(opts[:base] || Backend.instance.getwd)
|
124
|
+
expanded_path = Backend.instance.expand_path(path, base)
|
125
|
+
if expanded_path != internal_path
|
126
|
+
Path expanded_path
|
127
|
+
else
|
128
|
+
self
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
alias expand_path expand
|
134
|
+
alias absolute expand
|
135
|
+
alias absolute_path expand
|
136
|
+
|
137
|
+
# Check if path consists of only a filename.
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# Path('file.txt').only_filename?
|
141
|
+
# #=> true
|
142
|
+
#
|
143
|
+
# @return [Boolean] True if path consists of only a filename.
|
144
|
+
#
|
145
|
+
def only_filename?
|
146
|
+
internal_path.index(Path.separator).nil?
|
147
|
+
end
|
148
|
+
|
149
|
+
# Return path to parent directory. If path is already an absolute or relative
|
150
|
+
# root nil will be returned.
|
151
|
+
#
|
152
|
+
# @example Get parent directory:
|
153
|
+
# Path.new('/path/to/file').dir.path
|
154
|
+
# #=> '/path/to'
|
155
|
+
#
|
156
|
+
# @example Try to get parent of absolute root:
|
157
|
+
# Path.new('/').dir
|
158
|
+
# #=> nil
|
159
|
+
#
|
160
|
+
# @example Try to get parent of relative root:
|
161
|
+
# Path.new('.').dir
|
162
|
+
# #=> nil
|
163
|
+
#
|
164
|
+
# @return [Path] Parent path or nil if path already points to an absolute
|
165
|
+
# or relative root.
|
166
|
+
#
|
167
|
+
def dirname
|
168
|
+
return nil if %w[. /].include? internal_path
|
169
|
+
|
170
|
+
dir = ::File.dirname internal_path
|
171
|
+
dir.empty? ? nil : self.class.new(dir)
|
172
|
+
end
|
173
|
+
|
174
|
+
alias parent dirname
|
175
|
+
|
176
|
+
# Yield given block for path and each ancestor.
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# Path('/path/to/file.txt').ascend{|path| p path}
|
180
|
+
# #<Path:/path/to/file.txt>
|
181
|
+
# #<Path:/path/to>
|
182
|
+
# #<Path:/path>
|
183
|
+
# #<Path:/>
|
184
|
+
# #=> <Path:/path/to/file.txt>
|
185
|
+
#
|
186
|
+
# @example
|
187
|
+
# Path('path/to/file.txt').ascend{|path| p path}
|
188
|
+
# #<Path:path/to/file.txt>
|
189
|
+
# #<Path:path/to>
|
190
|
+
# #<Path:path>
|
191
|
+
# #<Path:.>
|
192
|
+
# #=> <Path:path/to/file.txt>
|
193
|
+
#
|
194
|
+
# @yield |path| Yield path and ancestors.
|
195
|
+
# @yieldparam path [Path] Path or ancestor.
|
196
|
+
# @return [Path] Self.
|
197
|
+
#
|
198
|
+
def ascend
|
199
|
+
return to_enum(:ascend) unless block_given?
|
200
|
+
|
201
|
+
path = self
|
202
|
+
loop do
|
203
|
+
yield path
|
204
|
+
break unless (path = path.parent)
|
205
|
+
end
|
206
|
+
|
207
|
+
self
|
208
|
+
end
|
209
|
+
|
210
|
+
alias each_ancestors ascend
|
211
|
+
|
212
|
+
# Return an array of all ancestors.
|
213
|
+
#
|
214
|
+
# @example
|
215
|
+
# Path('/path/to/file').ancestors
|
216
|
+
# # => [<Path:/path/to/file.txt>, <Path:/path/to>, <Path:/path>, <Path:/>]
|
217
|
+
#
|
218
|
+
# @return [Array<Path>] All ancestors.
|
219
|
+
#
|
220
|
+
def ancestors
|
221
|
+
each_ancestors.to_a
|
222
|
+
end
|
223
|
+
|
224
|
+
# Return given path as a relative path by just striping leading slashes.
|
225
|
+
#
|
226
|
+
# @example
|
227
|
+
# Path.new('/path/to/file').as_relative
|
228
|
+
# #=> <Path 'path/to/file'>
|
229
|
+
#
|
230
|
+
# @return [Path] Path transformed to relative path.
|
231
|
+
#
|
232
|
+
def as_relative
|
233
|
+
if (rel_path = internal_path.gsub(%r{^/+}, '')) != internal_path
|
234
|
+
Path rel_path
|
235
|
+
else
|
236
|
+
self
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Return given path as a absolute path by just prepending a leading slash.
|
241
|
+
#
|
242
|
+
# @example
|
243
|
+
# Path.new('path/to/file').as_absolute
|
244
|
+
# #=> <Path '/path/to/file'>
|
245
|
+
#
|
246
|
+
# @return [Path] Path transformed to absolute path.
|
247
|
+
#
|
248
|
+
def as_absolute
|
249
|
+
if internal_path[0] != '/'
|
250
|
+
Path "/#{internal_path}"
|
251
|
+
else
|
252
|
+
self
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Return a relative path from the given base path to the receiver path.
|
257
|
+
#
|
258
|
+
# Both paths need to be either absolute or relative otherwise an error
|
259
|
+
# will be raised. The file system will not be accessed and no symlinks are
|
260
|
+
# assumed.
|
261
|
+
#
|
262
|
+
# @example
|
263
|
+
# relative = Path('src/lib/module1/class.rb')
|
264
|
+
# .relative_from('src/lib/module2')
|
265
|
+
# #=> <Path '../module1/class.rb'>
|
266
|
+
#
|
267
|
+
# @return [Path] Relative path from argument to receiver.
|
268
|
+
# @see Pathname#relative_path_from
|
269
|
+
#
|
270
|
+
# rubocop:disable AbcSize
|
271
|
+
# rubocop:disable CyclomaticComplexity
|
272
|
+
# rubocop:disable MethodLength
|
273
|
+
# rubocop:disable PerceivedComplexity
|
274
|
+
# rubocop:disable LineLength
|
275
|
+
#
|
276
|
+
def relative_from(base)
|
277
|
+
base = Path(base).cleanpath
|
278
|
+
path = cleanpath
|
279
|
+
|
280
|
+
return Path '.' if base == path
|
281
|
+
|
282
|
+
if (base.relative? && path.absolute?) || (base.absolute? && path.relative?)
|
283
|
+
raise ArgumentError.new \
|
284
|
+
"Different prefix: #{base.inspect} and #{path.inspect}"
|
285
|
+
end
|
286
|
+
|
287
|
+
base = base.components(empty: true)
|
288
|
+
path = path.components(empty: true)
|
289
|
+
base.shift && path.shift while base.first == path.first && !(base.empty? || path.empty?)
|
290
|
+
|
291
|
+
Path(*((['..'] * base.size) + path))
|
292
|
+
end
|
293
|
+
alias relative_path_from relative_from
|
294
|
+
# rubocop:enable all
|
295
|
+
|
296
|
+
# Return cleaned path with all dot components removed.
|
297
|
+
#
|
298
|
+
# No file system will accessed and not symlinks will be resolved.
|
299
|
+
#
|
300
|
+
# @example
|
301
|
+
# Path('./file.txt').cleanpath
|
302
|
+
# #=> <Path file.txt>
|
303
|
+
#
|
304
|
+
# @example
|
305
|
+
# Path('path/to/another/../file/../../txt').cleanpath
|
306
|
+
# #=> <Path path/txt>
|
307
|
+
#
|
308
|
+
# @return [Path] Cleaned path.
|
309
|
+
#
|
310
|
+
def cleanpath
|
311
|
+
path = Pathname.new(self).cleanpath
|
312
|
+
if path == internal_path
|
313
|
+
self
|
314
|
+
elsif internal_path[-1] == Path.separator
|
315
|
+
Path path, ''
|
316
|
+
else
|
317
|
+
Path path
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
# @!group Path Predicates
|
5
|
+
|
6
|
+
# Check if path is an absolute path.
|
7
|
+
#
|
8
|
+
# An absolute path is a path with a leading slash.
|
9
|
+
#
|
10
|
+
# @return [Boolean] True if path is absolute.
|
11
|
+
# @see #relative?
|
12
|
+
#
|
13
|
+
def absolute?
|
14
|
+
internal_path[0] == '/'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if path is a relative path.
|
18
|
+
#
|
19
|
+
# A relative path does not start with a slash.
|
20
|
+
#
|
21
|
+
# @return [Boolean] True if path is relative.
|
22
|
+
# @see #absolute?
|
23
|
+
#
|
24
|
+
def relative?
|
25
|
+
!absolute?
|
26
|
+
end
|
27
|
+
|
28
|
+
# @overload mountpoint?([Path, String], ...)
|
29
|
+
# Join current and given paths and check if resulting
|
30
|
+
# path points to a mountpoint.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# Path('/').mountpoint?('tmp')
|
34
|
+
# #=> true
|
35
|
+
#
|
36
|
+
# @overload mountpoint?
|
37
|
+
# Check if current path is a mountpoint.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# Path('/tmp').mountpoint?
|
41
|
+
# #=> true
|
42
|
+
#
|
43
|
+
# @return [Boolean] True if path is a mountpoint, false otherwise.
|
44
|
+
# @see Pathname#mountpoint?
|
45
|
+
#
|
46
|
+
def mountpoint?(*args)
|
47
|
+
with_path(*args) do |path|
|
48
|
+
Backend.instance.mountpoint? path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check if file or directory is a dot file.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# Path("~/.gitconfig").dotfile?
|
56
|
+
# #=> true
|
57
|
+
#
|
58
|
+
# @return [Boolean] True if file is a dot file otherwise false.
|
59
|
+
#
|
60
|
+
def dotfile?
|
61
|
+
name[0] == '.'
|
62
|
+
end
|
63
|
+
end
|