rubypath 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,139 @@
1
+ class Path::Backend
2
+
3
+ #
4
+ class Sys
5
+
6
+ def initialize(root = nil)
7
+ @root = ::File.expand_path root if root
8
+ @umask = File.umask
9
+ end
10
+
11
+ def quit
12
+ File.umask @umask
13
+ end
14
+
15
+ def home(user)
16
+ ::File.expand_path "~#{user}"
17
+ end
18
+
19
+ def getwd
20
+ ::Dir.getwd
21
+ end
22
+
23
+ def user
24
+ require 'etc'
25
+
26
+ Etc.getlogin
27
+ end
28
+
29
+ def r(path)
30
+ return path unless @root
31
+ ::File.expand_path("#{@root}/#{::File.expand_path(path)}")
32
+ end
33
+
34
+ def ur(path)
35
+ return path unless @root
36
+
37
+ if path.slice(0, @root.length) == @root
38
+ path.slice(@root.length, path.length - @root.length)
39
+ else
40
+ path
41
+ end
42
+ end
43
+
44
+ def fs(path, obj, method, *args)
45
+ # puts "[FS] #{obj} #{method} #{args.inspect}"
46
+ obj.send method, *args
47
+ rescue Errno::ENOENT
48
+ raise Errno::ENOENT.new path
49
+ rescue Errno::EISDIR
50
+ raise Errno::EISDIR.new path
51
+ rescue Errno::ENOTDIR
52
+ raise Errno::ENOTDIR.new path
53
+ end
54
+
55
+ ## OPERATIONS
56
+
57
+ def expand_path(path, base)
58
+ ::File.expand_path path, base
59
+ end
60
+
61
+ def exists?(path)
62
+ fs path, ::File, :exists?, r(path)
63
+ end
64
+
65
+ def mkdir(path)
66
+ fs path, ::Dir, :mkdir, r(path)
67
+ end
68
+
69
+ def mkpath(path)
70
+ fs path, ::FileUtils, :mkdir_p, r(path)
71
+ end
72
+
73
+ def directory?(path)
74
+ fs path, ::File, :directory?, r(path)
75
+ end
76
+
77
+ def file?(path)
78
+ fs path, ::File, :file?, r(path)
79
+ end
80
+
81
+ def touch(path)
82
+ fs path, ::FileUtils, :touch, r(path)
83
+ end
84
+
85
+ def write(path, content, *args)
86
+ fs path, ::IO, :write, r(path), content, *args
87
+ end
88
+
89
+ def read(path, *args)
90
+ fs path, ::IO, :read, r(path), *args
91
+ end
92
+
93
+ def mtime(path)
94
+ fs path, ::File, :mtime, r(path)
95
+ end
96
+
97
+ def mtime=(path, time)
98
+ fs path, ::File, :utime, atime(path), time, r(path)
99
+ end
100
+
101
+ def atime(path)
102
+ fs path, ::File, :atime, r(path)
103
+ end
104
+
105
+ def atime=(path, time)
106
+ fs path, ::File, :utime, time, mtime(path), r(path)
107
+ end
108
+
109
+ def entries(path)
110
+ fs path, ::Dir, :entries, r(path)
111
+ end
112
+
113
+ def glob(pattern, flags = 0, &block)
114
+ if block_given?
115
+ fs pattern, ::Dir, :glob, r(pattern), flags do |path|
116
+ yield ur(path)
117
+ end
118
+ else
119
+ fs(pattern, ::Dir, :glob, r(pattern), flags).map{|path| ur path }
120
+ end
121
+ end
122
+
123
+ def get_umask
124
+ File.umask
125
+ end
126
+
127
+ def set_umask(mask)
128
+ File.umask mask
129
+ end
130
+
131
+ def mode(path)
132
+ fs(path, ::File, :stat, r(path)).mode & 0777
133
+ end
134
+
135
+ def chmod(path, mode)
136
+ fs path, ::File, :chmod, mode, r(path)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,87 @@
1
+ class Path
2
+ class Backend
3
+ class << self
4
+ def instance
5
+ @instance ||= new
6
+ end
7
+
8
+ def delegate(mth)
9
+ define_method mth do |*args|
10
+ backend.send mth, *args
11
+ end
12
+ end
13
+
14
+ def mock(*args, &block)
15
+ self.instance.mock(*args, &block)
16
+ end
17
+ end
18
+
19
+ attr_accessor :backend
20
+ def initialize
21
+ self.backend = Backend::Sys.new
22
+ end
23
+
24
+ def mock(opts = {}, &block)
25
+ if opts[:root]
26
+ # Use real file system scoped to given directory (chroot like)
27
+ if opts[:root] == :tmp
28
+ ::Dir.mktmpdir('rubypath') do |path|
29
+ use_backend Backend::Sys.new(path), &block
30
+ end
31
+ else
32
+ use_backend Backend::Sys.new(opts[:root]), &block
33
+ end
34
+ else
35
+ # Use mock FS
36
+ use_backend Backend::Mock.new, &block
37
+ end
38
+ end
39
+
40
+ def use_backend(be)
41
+ old_backend, self.backend = backend, be
42
+ yield
43
+ backend.quit if backend.respond_to? :quit
44
+ self.backend = old_backend
45
+ end
46
+
47
+ delegate :expand_path
48
+ delegate :getwd
49
+ delegate :exists?
50
+ delegate :mkdir
51
+ delegate :mkpath
52
+ delegate :directory?
53
+ delegate :file?
54
+ delegate :touch
55
+ delegate :write
56
+ delegate :read
57
+ delegate :mtime
58
+ delegate :mtime=
59
+ delegate :entries
60
+ delegate :glob
61
+ delegate :atime
62
+ delegate :atime=
63
+ delegate :get_umask
64
+ delegate :set_umask
65
+ delegate :mode
66
+ delegate :chmod
67
+ end
68
+
69
+ private
70
+
71
+ def invoke_backend(mth, *args)
72
+ args << self if args.empty?
73
+ self.class.send :invoke_backend, mth, *args
74
+ end
75
+
76
+ class << self
77
+
78
+ private
79
+
80
+ def invoke_backend(mth, *args)
81
+ Backend.instance.send mth, *args
82
+ end
83
+ end
84
+
85
+ require 'rubypath/backend/mock'
86
+ require 'rubypath/backend/sys'
87
+ end
@@ -0,0 +1,22 @@
1
+ class Path
2
+ # @!group Comparison
3
+
4
+ # Compare path to given object. If object is a string, Path or #{Path.like?}
5
+ # they will be compared using the string paths. Otherwise they are assumed
6
+ # as not equal.
7
+ #
8
+ # @param other [Object] Object to compare path with.
9
+ # @return [Boolean] True if object represents same path.
10
+ #
11
+ def eql?(other)
12
+ case other
13
+ when String
14
+ internal_path.eql? other
15
+ when Path
16
+ internal_path.eql? other.path
17
+ else
18
+ Path.new(other).eql?(self) if Path.like? other
19
+ end
20
+ end
21
+ alias_method :==, :eql?
22
+ end
@@ -0,0 +1,109 @@
1
+ class Path
2
+
3
+ class << self
4
+ # @!group Construction
5
+
6
+ # Create new {Path}.
7
+ #
8
+ # If single argument is a path object it will be returned and no new one
9
+ # will be created. If not arguments are given {Path::EMPTY} will be
10
+ # returned.
11
+ #
12
+ # @see #initialize
13
+ #
14
+ def new(*args)
15
+ args.flatten!
16
+ return Path::EMPTY if args.empty?
17
+ return args.first if args.size == 1 && args.first.is_a?(self)
18
+ super
19
+ end
20
+
21
+ # Check if given object is like a path.
22
+ #
23
+ # An object is like a path if
24
+ # 1. It is a {Path} object.
25
+ # 2. It is a string.
26
+ # 3. It responds to {#to_path} and {#to_path} returns a string.
27
+ # 4. It responds to {#path} and {#path} returns a string.
28
+ #
29
+ # If no rule matches it is not considered to be like a path.
30
+ #
31
+ # @return [Boolean] True if object is path like, false otherwise.
32
+ #
33
+ def like?(obj)
34
+ return true if obj.is_a?(self)
35
+ return true if obj.is_a?(String)
36
+ return true if obj.respond_to?(:to_path) && obj.to_path.is_a?(String)
37
+ return true if obj.respond_to?(:path) && obj.path.is_a?(String)
38
+ false
39
+ end
40
+
41
+ # Convert given object to path string using {::Path.like?} rules.
42
+ #
43
+ # @note Should not be used directly.
44
+ #
45
+ # @return [String]
46
+ # @raise [ArgumentError] If given object is not {::Path.like?}.
47
+ # @see ::Path.like?
48
+ #
49
+ def like_path(obj)
50
+ case obj
51
+ when String
52
+ return obj
53
+ else
54
+ [:to_path, :path, :to_str, :to_s].each do |mth|
55
+ if obj.respond_to?(mth) && obj.send(mth).is_a?(String)
56
+ return obj.send(mth)
57
+ end
58
+ end
59
+ end
60
+
61
+ raise ArgumentError.new \
62
+ "Argument #{obj.inspect} cannot be converted to path string."
63
+ end
64
+
65
+ # Return system file path separator.
66
+ #
67
+ # @return [String] File separator.
68
+ # @see ::File::SEPARATOR
69
+ #
70
+ def separator
71
+ ::File::SEPARATOR
72
+ end
73
+
74
+ # Allow class object to be used as a bock.
75
+ #
76
+ # @example
77
+ # %w(path/to/fileA path/to/fileB).map(&Path)
78
+ #
79
+ def to_proc
80
+ proc {|*args| Path.new(*args) }
81
+ end
82
+ end
83
+
84
+ # @!group Construction
85
+
86
+ # Initialize new {Path} object.
87
+ #
88
+ # Given arguments will be converted to String using `#to_path`, `#path` or
89
+ # `#to_s` in this order if they return a String object.
90
+ #
91
+ # @overload initialize([[String, #to_path, #path, #to_s], ...]
92
+ #
93
+ def initialize(*args)
94
+ parts = args.flatten
95
+ @path = if parts.size > 1
96
+ ::File.join(*parts.map{|p| Path.like_path p })
97
+ elsif parts.size == 1
98
+ Path.like_path(parts.first).dup
99
+ else
100
+ ''
101
+ end
102
+ end
103
+
104
+ # Empty path.
105
+ #
106
+ # @return [Path] Empty path.
107
+ #
108
+ EMPTY = Path.new('')
109
+ end
@@ -0,0 +1,76 @@
1
+ class Path
2
+ class << self
3
+
4
+ # Returns the current working directory.
5
+ #
6
+ # @return [Path] Current working directory.
7
+ # @see ::Dir.getwd
8
+ #
9
+ def getwd
10
+ new Backend.instance.getwd
11
+ end
12
+
13
+ def glob(pattern, flags = ::File::FNM_EXTGLOB)
14
+ if block_given?
15
+ Backend.instance.glob(pattern, flags) {|path| yield Path path }
16
+ else
17
+ Backend.instance.glob(pattern, flags).map(&Path)
18
+ end
19
+ end
20
+ end
21
+
22
+ # Create directory.
23
+ #
24
+ # Given arguments will be joined with current path before directory is
25
+ # created.
26
+ #
27
+ # @raise [Errno::ENOENT] If parent directory could not created.
28
+ # @return [Path] Path to created directory.
29
+ # @see #mkpath
30
+ #
31
+ def mkdir(*args)
32
+ with_path(*args) do |path|
33
+ Backend.instance.mkdir path
34
+ Path path
35
+ end
36
+ end
37
+
38
+ # Create directory and all missing parent directories.
39
+ #
40
+ # Given arguments will be joined with current path before directories
41
+ # are created.
42
+ #
43
+ # @return [Path] Path to created directory.
44
+ # @see #mkdir
45
+ # @see ::FileUtils.mkdir_p
46
+ #
47
+ def mkpath(*args)
48
+ with_path(*args) do |path|
49
+ Backend.instance.mkpath path
50
+ Path path
51
+ end
52
+ end
53
+ alias_method :mkdir_p, :mkpath
54
+
55
+ # Return list of entries in directory. That includes special directories
56
+ # (`.`, `..`).
57
+ #
58
+ # Given arguments will be joined before children are listed for directory.
59
+ #
60
+ # @return [Array<Path>] Entries in directory.
61
+ #
62
+ def entries(*args)
63
+ invoke_backend(:entries, internal_path).map(&Path)
64
+ end
65
+
66
+ #
67
+ def glob(pattern, flags = ::File::FNM_EXTGLOB)
68
+ Path.glob ::File.join(escaped_glob_path, pattern), flags
69
+ end
70
+
71
+ private
72
+
73
+ def escaped_glob_path
74
+ internal_path.gsub(/[\[\]\*\?\{\}]/, '\\\\\0')
75
+ end
76
+ end
@@ -0,0 +1,157 @@
1
+ class Path
2
+ # @!group File Extensions
3
+
4
+ # Return list of all file extensions.
5
+ #
6
+ # @example
7
+ # Path.new('/path/to/template.de.html.erb').extensions
8
+ # #=> ['de', 'html', 'erb']
9
+ #
10
+ # @return [Array<String>] List of file extensions.
11
+ #
12
+ def extensions
13
+ if dotfile?
14
+ name.split('.')[2..-1]
15
+ else
16
+ name.split('.')[1..-1]
17
+ end
18
+ end
19
+ alias_method :exts, :extensions
20
+
21
+ # Return last file extension.
22
+ #
23
+ # @example
24
+ # Path.new('/path/to/template.de.html.erb').extension
25
+ # #=> 'erb'
26
+ #
27
+ # @return [String] Last file extensions.
28
+ #
29
+ def extension
30
+ extensions.last
31
+ end
32
+ alias_method :ext, :extension
33
+
34
+ # Return last file extension include dot character.
35
+ #
36
+ # @return [String] Ext name.
37
+ # @see ::File.extname
38
+ #
39
+ def extname
40
+ ::File.extname name
41
+ end
42
+
43
+ # Return the file name without any extensions.
44
+ #
45
+ # @example
46
+ # Path("template.de.html.slim").pure_name
47
+ # #=> "template"
48
+ #
49
+ # @example
50
+ # Path("~/.gitconfig").pure_name
51
+ # #=> ".gitconfig"
52
+ #
53
+ # @return [String] File name without extensions.
54
+ #
55
+ def pure_name
56
+ if dotfile?
57
+ name.split('.', 3)[0..1].join('.')
58
+ else
59
+ name.split('.', 2)[0]
60
+ end
61
+ end
62
+
63
+ # Replace file extensions with given new ones or by a given
64
+ # translation map.
65
+ #
66
+ # @overload replace_extensions(exts)
67
+ # Replace all extensions with given new ones. Number of given extensions
68
+ # does not need to match number of existing extensions.
69
+ #
70
+ # @example
71
+ # Path('file.de.txt').replace_extensions(%w(en html))
72
+ # #=> <Path "file.en.html">
73
+ #
74
+ # @example
75
+ # Path('file.de.mobile.html.haml').replace_extensions(%w(int txt))
76
+ # #=> <Path "file.int.txt">
77
+ #
78
+ # @param exts [Array<String>] New extensions.
79
+ #
80
+ # @overload replace_extensions(ext, [ext, [..]])
81
+ # Replace all extensions with given new ones. Number of given extensions
82
+ # does not need to match number of existing extensions.
83
+ #
84
+ # @example
85
+ # Path('file.de.txt').replace_extensions('en', 'html')
86
+ # #=> <Path "file.en.html">
87
+ #
88
+ # @example
89
+ # Path('file.de.mobile.html.haml').replace_extensions('en', 'html')
90
+ # #=> <Path "file.en.html">
91
+ #
92
+ # @example
93
+ # Path('file.de.txt').replace_extensions('html')
94
+ # #=> <Path "file.html">
95
+ #
96
+ # @param ext [String] New extensions.
97
+ #
98
+ # @overload replace_extensions(map)
99
+ # Replace all matching extensions.
100
+ #
101
+ # @example
102
+ # Path('file.de.html.haml').replace_extensions('de' => 'en', 'haml' => 'slim')
103
+ # #=> <Path "file.en.html.slim">
104
+ #
105
+ # @param map [Hash<String, String>] Translation map as hash.
106
+ #
107
+ # @return [Path] Path to new filename.
108
+ #
109
+ def replace_extensions(*args)
110
+ args.flatten!
111
+ extensions = self.extensions
112
+
113
+ if (replace = (args.last.is_a?(Hash) ? args.pop : nil))
114
+ if args.empty?
115
+ extensions.map! do |ext|
116
+ replace[ext] ? replace[ext].to_s : ext
117
+ end
118
+ else
119
+ raise ArgumentError.new 'Cannot replace extensions with array ' \
120
+ 'and hash at the same time.'
121
+ end
122
+ else
123
+ extensions = args.map(&:to_s)
124
+ end
125
+
126
+ if extensions == self.extensions
127
+ self
128
+ else
129
+ if only_filename?
130
+ Path "#{pure_name}.#{extensions.join('.')}"
131
+ else
132
+ dirname.join "#{pure_name}.#{extensions.join('.')}"
133
+ end
134
+ end
135
+ end
136
+
137
+ # Replace last extension with one or multiple new extensions.
138
+ #
139
+ # @example
140
+ # Path('file.de.txt').replace_extension('html')
141
+ # #=> <Path "file.de.html">
142
+ #
143
+ # @example
144
+ # Path('file.de.txt').replace_extension('html', 'erb')
145
+ # #=> <Path "file.de.html.erb">
146
+ #
147
+ # @return [Path] Path to new filename.
148
+ #
149
+ def replace_extension(*args)
150
+ extensions = self.extensions
151
+ extensions.pop
152
+ extensions += args.flatten
153
+
154
+ replace_extensions extensions
155
+ end
156
+
157
+ end