rubypath 1.0.0 → 1.0.1
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.
- 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
|