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.
@@ -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