rubypath 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,164 @@
1
+ class Path
2
+ # @!group File Operations
3
+
4
+ # Return base name without path.
5
+ #
6
+ # @return [String] Base name.
7
+ #
8
+ def name
9
+ ::File.basename internal_path
10
+ end
11
+ alias_method :basename, :name
12
+
13
+ # Create new file at pointed location or update modification time if file
14
+ # exists.
15
+ #
16
+ # @example
17
+ # Path('/path/to/file.txt').touch
18
+ # #=> <Path:"/path/to/file.txt">
19
+ #
20
+ # @return [Path] Path to touched file.
21
+ #
22
+ def touch(*args)
23
+ with_path(*args) do |path|
24
+ invoke_backend :touch, path
25
+ Path path
26
+ end
27
+ end
28
+
29
+ # Create a file at pointed location and all missing parent directories.
30
+ #
31
+ # Given arguments will be joined with current path before directories and
32
+ # file is created.
33
+ #
34
+ # If file already exists nothing will be done.
35
+ #
36
+ # @example
37
+ # Path('/path/to/file.txt').mkfile
38
+ # #=> <Path:"/path/to/file.txt">
39
+ #
40
+ # @example
41
+ # Path('/').mkfile('path', 'to', 'file.txt')
42
+ # #=> <Path:"/path/to/file.txt">
43
+ #
44
+ # @return [Path] Path to created or existent file.
45
+ #
46
+ def mkfile(*args)
47
+ with_path(*args) do |path|
48
+ if !path.exists? && path.parent && !path.parent.exists?
49
+ path.parent.mkpath
50
+ end
51
+
52
+ if path.exists?
53
+ raise Errno::ENOENT.new path.to_s unless path.file?
54
+ else
55
+ path.touch
56
+ end
57
+ end
58
+ end
59
+
60
+ # Search for a file in current directory or parent directories.
61
+ #
62
+ # Given search pattern can either be a regular expression or a shell glob
63
+ # expression.
64
+ #
65
+ # @example
66
+ # Path.cwd.lookup('project.{yml,yaml}')
67
+ # #=> <Path:"/path/config.yml">
68
+ #
69
+ # @example
70
+ # Path.cwd.lookup(/config(_\d+).ya?ml/)
71
+ # #=> <Path:"/path/config_354.yaml">
72
+ #
73
+ # @example
74
+ # Path('~').lookup('*config', ::File::FNM_DOTMATCH)
75
+ # #=> <Path:"/gome/user/.gitconfig">
76
+ #
77
+ # @param pattern [String|RegExp] Expression file name must match.
78
+ # @param flags [Integer] Additional flags. See {::File.fnmatch}.
79
+ # Defaults to `File::FNM_EXTGLOB`.
80
+ # @return [Path] Path to found file or nil.
81
+ #
82
+ def lookup(pattern, flags = ::File::FNM_EXTGLOB)
83
+ expand.ascend do |path|
84
+ case pattern
85
+ when String
86
+ path.entries.each do |c|
87
+ return path.join(c) if ::File.fnmatch?(pattern, c.name, flags)
88
+ end
89
+ when Regexp
90
+ path.entries.each do |c|
91
+ return path.join(c) if pattern =~ c.name
92
+ end
93
+ end
94
+ end
95
+
96
+ nil
97
+ end
98
+
99
+ # Return file modification time.
100
+ #
101
+ # @return [Time] Time of last modification.
102
+ #
103
+ def mtime
104
+ invoke_backend :mtime
105
+ end
106
+
107
+ # Set last modification time.
108
+ #
109
+ # @param [Time] Time of last modification.
110
+ #
111
+ def mtime=(time)
112
+ invoke_backend :mtime=, internal_path, time
113
+ end
114
+
115
+ # Return file access time.
116
+ #
117
+ # @return [Time] Time of last access.
118
+ #
119
+ def atime
120
+ invoke_backend :atime
121
+ end
122
+
123
+ # Set last access time.
124
+ #
125
+ # @param [Time] Time of last access.
126
+ #
127
+ def atime=(time)
128
+ invoke_backend :atime=, internal_path, time
129
+ end
130
+
131
+ def mode
132
+ invoke_backend :mode
133
+ end
134
+
135
+ def chmod(mode)
136
+ invoke_backend :chmod, internal_path, mode
137
+ end
138
+
139
+ class << self
140
+
141
+ # Read or set process umask.
142
+ #
143
+ # @overload umask
144
+ # Read process umask.
145
+ #
146
+ # @return [Integer] Process umask.
147
+ #
148
+ # @overload umask(mask)
149
+ # Set process umask.
150
+ #
151
+ # @param mask [Integer] New process umask.
152
+ #
153
+ # @see File.umask
154
+ #
155
+ def umask(mask = nil)
156
+ if mask
157
+ invoke_backend :set_umask, mask
158
+ else
159
+ invoke_backend :get_umask
160
+ end
161
+ end
162
+ alias_method :umask=, :umask
163
+ end
164
+ end
@@ -0,0 +1,32 @@
1
+ class Path
2
+ # @!group File Predicates
3
+
4
+ # Check if path points to file.
5
+ #
6
+ # @return [Boolean] True if path is a file.
7
+ # @see ::File.file?
8
+ #
9
+ def file?
10
+ invoke_backend :file?
11
+ end
12
+
13
+ # Check if path points to an existing location.
14
+ #
15
+ # @return [Boolean] True if path exists.
16
+ # @see ::File.exists?
17
+ #
18
+ def exists?
19
+ invoke_backend :exists?
20
+ end
21
+ alias_method :exist?, :exists?
22
+ alias_method :existent?, :exists?
23
+
24
+ # Check if path points to a directory.
25
+ #
26
+ # @return [Boolean] True if path is a directory.
27
+ # @see ::File.directory?
28
+ #
29
+ def directory?
30
+ invoke_backend :directory?
31
+ end
32
+ end
@@ -0,0 +1,59 @@
1
+ class Path
2
+ # @!group Identity
3
+
4
+ # Return path as string. String will be duped before it gets returned and
5
+ # cannot be used to modify the path object.
6
+ #
7
+ # @return [String] Path as string.
8
+ def path
9
+ internal_path.dup
10
+ end
11
+ alias_method :to_path, :path
12
+ alias_method :to_str, :path
13
+ alias_method :to_s, :path
14
+
15
+ # Return a useful object string representation.
16
+ #
17
+ # @return [String] Useful object representation
18
+ def inspect
19
+ "<#{self.class.name}:#{internal_path}>"
20
+ end
21
+
22
+
23
+
24
+ protected
25
+
26
+ # Return internal path object without duping.
27
+ #
28
+ # Must not be modified to not change internal state.
29
+ #
30
+ # @return [String] Internal path.
31
+ # @see #path
32
+ #
33
+ def internal_path
34
+ @path
35
+ end
36
+
37
+ # If arguments are provided the current path will be joined with given
38
+ # arguments to the result will be yielded. If no arguments are given the
39
+ # current path will be yielded.
40
+ #
41
+ # Internal helper method.
42
+ #
43
+ # @example
44
+ # def handle_both(*args)
45
+ # with_path(*args) do |path|
46
+ # # do something
47
+ # end
48
+ # end
49
+ #
50
+ # Returns whatever the block returns.
51
+ #
52
+ def with_path(*args)
53
+ if args.any?
54
+ yield join(*args)
55
+ else
56
+ yield self
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,82 @@
1
+ class Path
2
+ # @!group IO Operations
3
+
4
+ # Write given content to file.
5
+ #
6
+ # @overload write(content, [..])
7
+ # Write given content to file. An existing file will be truncated otherwise
8
+ # a file will be created.
9
+ #
10
+ # Additional arguments will be passed to {::IO.write}.
11
+ #
12
+ # @example
13
+ # Path('/path/to/file.txt').write('CONTENT')
14
+ # #=> 7
15
+ #
16
+ # @param content [String] Content to write to file.
17
+ #
18
+ # @overload write(content, offset, [..])
19
+ # Write content at specific position in file. Content will be replaced
20
+ # starting at given offset.
21
+ #
22
+ # Additional arguments will be passed to {::IO.write}.
23
+ #
24
+ # @example
25
+ # path.write('CONTENT', 4)
26
+ # #=> 7
27
+ # path.read
28
+ # #=> "1234CONTENT2345678"
29
+ #
30
+ # @param content [String] Content to write to file.
31
+ # @param offset [Integer] Offset where to start writing. If nil file will
32
+ # be truncated.
33
+ #
34
+ # @see IO.write
35
+ # @return [Path] Self.
36
+ #
37
+ def write(content, *args)
38
+ invoke_backend :write, self, content, *args
39
+ self
40
+ end
41
+
42
+ # Read file content from disk.
43
+ #
44
+ # @overload read([..])
45
+ # Read all content from file.
46
+ #
47
+ # Additional arguments will be passed to {::IO.read}.
48
+ #
49
+ # @example
50
+ # Path('file.txt').read
51
+ # #=> "CONTENT"
52
+ #
53
+ # @overload read(length, [..])
54
+ # Read given amount of bytes from file.
55
+ #
56
+ # Additional arguments will be passed to {::IO.read}.
57
+ #
58
+ # @example
59
+ # Path('file.txt').read(4)
60
+ # #=> "CONT"
61
+ #
62
+ # @param length [Integer] Number of bytes to read.
63
+ #
64
+ # @overload read(length, offset, [..])
65
+ # Read given amount of bytes from file starting at given offset.
66
+ #
67
+ # Additional arguments will be passed to {::IO.read}.
68
+ #
69
+ # @example
70
+ # Path('file.txt').read(4, 2)
71
+ # #=> "NTEN"
72
+ #
73
+ # @param length [Integer] Number of bytes to read.
74
+ # @param offset [Integer] Where to start reading.
75
+ #
76
+ # @see IO.read
77
+ # @return [String] Read content.
78
+ #
79
+ def read(*args)
80
+ invoke_backend :read, self, *args
81
+ end
82
+ end
@@ -0,0 +1,42 @@
1
+ class Path
2
+ class << self
3
+ # @!group Mocking / Virtual File System
4
+
5
+ # Configure current path backend. Can be used to configure specified
6
+ # test scenario. If no virtual or scoped path backend is set the default
7
+ # one will be used.
8
+ #
9
+ # Do not forget to use mock file system in your specs:
10
+ # See more {Backend.mock}.
11
+ #
12
+ # around do |example|
13
+ # Path::Backend.mock &example
14
+ # end
15
+ #
16
+ # *Note*: Not all operations are supported.
17
+ #
18
+ # @example
19
+ # Path.mock do |root|
20
+ # root.mkpath '/a/b/c/d/e'
21
+ # root.touch '/a/b/test.txt'
22
+ # root.join('/a/c/lorem.yaml').write YAML.dump({'lorem' => 'ipsum'})
23
+ # #...
24
+ # end
25
+ #
26
+ # @example Configure backend (only with virtual file system)
27
+ # Path.mock do |root, backend|
28
+ # backend.current_user = 'test'
29
+ # backend.homes = {'test' => '/path/to/test/home'}
30
+ # #...
31
+ # end
32
+ #
33
+ # @yield |root, backend| Yield file system root path and current backend.
34
+ # @yieldparam root [Path] Root path of current packend.
35
+ # @yieldparam backend [Backend] Current backend.
36
+ #
37
+ def mock(opts = {})
38
+ yield Path('/'), Backend.instance.backend if block_given?
39
+ nil
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,253 @@
1
+ class Path
2
+ # @!group Path Operations
3
+
4
+ # Join path with given arguments.
5
+ #
6
+ # @overload initialize([[Path, String, #to_path, #path, #to_s], ...]
7
+ # Join all given arguments to build a new path.
8
+ #
9
+ # @example
10
+ # Path('/').join('test', %w(a b), 5, Pathname.new('file'))
11
+ # # => <Path:"/test/a/b/5/file">
12
+ #
13
+ # @return [Path]
14
+ #
15
+ def join(*args)
16
+ parts = args.flatten
17
+ case parts.size
18
+ when 0
19
+ self
20
+ when 1
21
+ join = Path parts.shift
22
+ join.absolute? ? join : Path(::File.join(path, join.path))
23
+ else
24
+ join(parts.shift).join(*parts)
25
+ end
26
+ end
27
+
28
+ # Iterate over all path components.
29
+ #
30
+ # @overload each_component
31
+ # Return a enumerator to iterate over all path components.
32
+ #
33
+ # @example Iterate over path components using a enumerator
34
+ # enum = Path('/path/to/file.txt').each_component
35
+ # enum.each{|fn| puts fn}
36
+ # # => "path"
37
+ # # => "to"
38
+ # # => "file.txt"
39
+ #
40
+ # @example Map each path component and create a new path
41
+ # path = Path('/path/to/file.txt')
42
+ # Path path.each_component.map{|fn| fn.length}
43
+ # # => <Path:"/4/2/8">
44
+ #
45
+ # @return [Enumerator] Return a enumerator for all path components.
46
+ #
47
+ # @overload each_component(&block)
48
+ # Yield given block for each path components.
49
+ #
50
+ # @example Print each file name
51
+ # Path('/path/to/file.txt').each_component{|fn| puts fn}
52
+ # # => "path"
53
+ # # => "to"
54
+ # # => "file.txt"
55
+ #
56
+ # @param block [Proc] Block to invoke with each path component.
57
+ # If no block is given an enumerator will returned.
58
+ # @return [self] Self.
59
+ #
60
+ def each_component(opts = {}, &block)
61
+ rv = if opts[:empty]
62
+ # split eats leading slashes
63
+ ary = path.split(Path.separator)
64
+ # so add an empty string if path ends with slash
65
+ ary << '' if path[-1] == Path.separator
66
+ ary.each(&block)
67
+ else
68
+ Pathname(path).each_filename(&block)
69
+ end
70
+ block ? self : rv
71
+ end
72
+
73
+ # Return an array with all path components.
74
+ #
75
+ # @example
76
+ # Path('path/to/file').components
77
+ # # => ["path", "to", "file"]
78
+ #
79
+ # @example
80
+ # Path('/path/to/file').components
81
+ # # => ["path", "to", "file"]
82
+ #
83
+ # @return [Array<String>] File names.
84
+ #
85
+ def components
86
+ each_component.to_a
87
+ end
88
+
89
+ # Converts a pathname to an absolute pathname. Given arguments will be
90
+ # joined to current path before expanding path. Relative paths are referenced
91
+ # from the current working directory of the process unless the `:base` option
92
+ # is set, which will be used as the starting point.
93
+ #
94
+ # The given pathname may start with a "~", which expands to the process
95
+ # owner's home directory (the environment variable HOME must be set
96
+ # correctly). "~user" expands to the named user's home directory.
97
+ #
98
+ # @example
99
+ # Path('path/to/../tmp').expand
100
+ # #=> <Path:"path/tmp">
101
+ #
102
+ # @example
103
+ # Path('~/tmp').expand
104
+ # #=> <Path:"/home/user/tmp">
105
+ #
106
+ # @example
107
+ # Path('~oma/tmp').expand
108
+ # #=> <Path:"/home/oma/tmp">
109
+ #
110
+ # @example
111
+ # Path('~/tmp').expand('../file.txt')
112
+ # #=> <Path:"/home/user/file.txt">
113
+ #
114
+ # @return [Path] Expanded path.
115
+ # @see ::File#expand_path
116
+ #
117
+ def expand(*args)
118
+ opts = args.last.is_a?(Hash) ? args.pop : {}
119
+
120
+ with_path(*args) do |path|
121
+ base = Path.like_path(opts[:base] || Backend.instance.getwd)
122
+ expanded_path = Backend.instance.expand_path(path, base)
123
+ if expanded_path != internal_path
124
+ Path expanded_path
125
+ else
126
+ self
127
+ end
128
+ end
129
+ end
130
+
131
+ alias_method :expand_path, :expand
132
+ alias_method :absolute, :expand
133
+ alias_method :absolute_path, :expand
134
+
135
+ # Check if path consists of only a filename.
136
+ #
137
+ # @example
138
+ # Path('file.txt').only_filename?
139
+ # #=> true
140
+ #
141
+ # @return [Boolean] True if path consists of only a filename.
142
+ #
143
+ def only_filename?
144
+ internal_path.index(Path.separator).nil?
145
+ end
146
+
147
+ # Return path to parent directory. If path is already an absolute or relative
148
+ # root nil will be returned.
149
+ #
150
+ # @example Get parent directory:
151
+ # Path.new('/path/to/file').dir.path
152
+ # #=> '/path/to'
153
+ #
154
+ # @example Try to get parent of absolute root:
155
+ # Path.new('/').dir
156
+ # #=> nil
157
+ #
158
+ # @example Try to get parent of relative root:
159
+ # Path.new('.').dir
160
+ # #=> nil
161
+ #
162
+ # @return [Path] Parent path or nil if path already points to an absolute
163
+ # or relative root.
164
+ #
165
+ def dirname
166
+ return nil if %w(. /).include? internal_path
167
+
168
+ dir = ::File.dirname internal_path
169
+ dir.empty? ? nil : self.class.new(dir)
170
+ end
171
+
172
+ alias_method :parent, :dirname
173
+
174
+ # Yield given block for path and each ancestor.
175
+ #
176
+ # @example
177
+ # Path('/path/to/file.txt').ascend{|path| p path}
178
+ # #<Path:/path/to/file.txt>
179
+ # #<Path:/path/to>
180
+ # #<Path:/path>
181
+ # #<Path:/>
182
+ # #=> <Path:/path/to/file.txt>
183
+ #
184
+ # @example
185
+ # Path('path/to/file.txt').ascend{|path| p path}
186
+ # #<Path:path/to/file.txt>
187
+ # #<Path:path/to>
188
+ # #<Path:path>
189
+ # #<Path:.>
190
+ # #=> <Path:path/to/file.txt>
191
+ #
192
+ # @yield |path| Yield path and ancestors.
193
+ # @yieldparam path [Path] Path or ancestor.
194
+ # @return [Path] Self.
195
+ #
196
+ def ascend
197
+ return to_enum(:ascend) unless block_given?
198
+
199
+ path = self
200
+ loop do
201
+ yield path
202
+ break unless (path = path.parent)
203
+ end
204
+
205
+ self
206
+ end
207
+
208
+ alias_method :each_ancestors, :ascend
209
+
210
+ # Return an array of all ancestors.
211
+ #
212
+ # @example
213
+ # Path('/path/to/file').ancestors
214
+ # # => [<Path:/path/to/file.txt>, <Path:/path/to>, <Path:/path>, <Path:/>]
215
+ #
216
+ # @return [Array<Path>] All ancestors.
217
+ #
218
+ def ancestors
219
+ each_ancestors.to_a
220
+ end
221
+
222
+ # Return given path as a relative path by just striping leading slashes.
223
+ #
224
+ # @example
225
+ # Path.new('/path/to/file').as_relative
226
+ # #=> <Path 'path/to/file'>
227
+ #
228
+ # @return [Path] Path transformed to relative path.
229
+ #
230
+ def as_relative
231
+ if (rel_path = internal_path.gsub(/^\/+/, '')) != internal_path
232
+ Path rel_path
233
+ else
234
+ self
235
+ end
236
+ end
237
+
238
+ # Return given path as a absolute path by just prepending a leading slash.
239
+ #
240
+ # @example
241
+ # Path.new('path/to/file').as_absolute
242
+ # #=> <Path '/path/to/file'>
243
+ #
244
+ # @return [Path] Path transformed to absolute path.
245
+ #
246
+ def as_absolute
247
+ if internal_path[0] != '/'
248
+ Path "/#{internal_path}"
249
+ else
250
+ self
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,61 @@
1
+ class Path
2
+ # @!group Path Predicates
3
+
4
+ # Check if path is an absolute path.
5
+ #
6
+ # An absolute path is a path with a leading slash.
7
+ #
8
+ # @return [Boolean] True if path is absolute.
9
+ # @see #relative?
10
+ #
11
+ def absolute?
12
+ internal_path[0] == '/'
13
+ end
14
+
15
+ # Check if path is a relative path.
16
+ #
17
+ # A relative path does not start with a slash.
18
+ #
19
+ # @return [Boolean] True if path is relative.
20
+ # @see #absolute?
21
+ #
22
+ def relative?
23
+ !absolute?
24
+ end
25
+
26
+ # @overload mountpoint?([Path, String], ...)
27
+ # Join current and given paths and check if resulting
28
+ # path points to a mountpoint.
29
+ #
30
+ # @example
31
+ # Path('/').mountpoint?('tmp')
32
+ # #=> true
33
+ #
34
+ # @overload mountpoint?
35
+ # Check if current path is a mountpoint.
36
+ #
37
+ # @example
38
+ # Path('/tmp').mountpoint?
39
+ # #=> true
40
+ #
41
+ # @return [Boolean] True if path is a mountpoint, false otherwise.
42
+ # @see Pathname#mountpoint?
43
+ #
44
+ def mountpoint?(*args)
45
+ with_path(*args) do |path|
46
+ Backend.instance.mountpoint? path
47
+ end
48
+ end
49
+
50
+ # Check if file or directory is a dot file.
51
+ #
52
+ # @example
53
+ # Path("~/.gitconfig").dotfile?
54
+ # #=> true
55
+ #
56
+ # @return [Boolean] True if file is a dot file otherwise false.
57
+ #
58
+ def dotfile?
59
+ name[0] == '.'
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ class Path
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ PATCH = 0
6
+ STAGE = nil
7
+ STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.').freeze
8
+
9
+ def self.to_s; STRING end
10
+ end
11
+ end