rubypath 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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