rubypath 0.1.0

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,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