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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/LICENSE.txt +165 -0
- data/lib/rubypath.rb +29 -0
- data/lib/rubypath/backend.rb +96 -0
- data/lib/rubypath/backend/mock.rb +362 -0
- data/lib/rubypath/backend/sys.rb +163 -0
- data/lib/rubypath/comparison.rb +21 -0
- data/lib/rubypath/construction.rb +115 -0
- data/lib/rubypath/dir_operations.rb +161 -0
- data/lib/rubypath/extensions.rb +162 -0
- data/lib/rubypath/file_operations.rb +193 -0
- data/lib/rubypath/file_predicates.rb +34 -0
- data/lib/rubypath/identity.rb +59 -0
- data/lib/rubypath/io_operations.rb +84 -0
- data/lib/rubypath/mock.rb +44 -0
- data/lib/rubypath/path_operations.rb +320 -0
- data/lib/rubypath/path_predicates.rb +63 -0
- data/lib/rubypath/version.rb +15 -0
- data/rubypath.gemspec +33 -0
- metadata +22 -3
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
class Backend
|
5
|
+
# rubocop:disable ClassLength
|
6
|
+
class Sys
|
7
|
+
def initialize(root = nil)
|
8
|
+
@root = ::File.expand_path root if root
|
9
|
+
@umask = File.umask
|
10
|
+
end
|
11
|
+
|
12
|
+
def quit
|
13
|
+
File.umask @umask
|
14
|
+
end
|
15
|
+
|
16
|
+
def home(user)
|
17
|
+
::File.expand_path "~#{user}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def getwd
|
21
|
+
::Dir.getwd
|
22
|
+
end
|
23
|
+
|
24
|
+
def user
|
25
|
+
require 'etc'
|
26
|
+
|
27
|
+
Etc.getlogin
|
28
|
+
end
|
29
|
+
|
30
|
+
def r(path)
|
31
|
+
return path unless @root
|
32
|
+
::File.expand_path("#{@root}/#{::File.expand_path(path)}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def ur(path)
|
36
|
+
return path unless @root
|
37
|
+
|
38
|
+
if path.slice(0, @root.length) == @root
|
39
|
+
path.slice(@root.length, path.length - @root.length)
|
40
|
+
else
|
41
|
+
path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def fs(path, obj, method, *args)
|
46
|
+
# puts "[FS] #{obj} #{method} #{args.inspect}"
|
47
|
+
obj.send method, *args
|
48
|
+
rescue Errno::ENOENT
|
49
|
+
raise Errno::ENOENT.new path
|
50
|
+
rescue Errno::EISDIR
|
51
|
+
raise Errno::EISDIR.new path
|
52
|
+
rescue Errno::ENOTDIR
|
53
|
+
raise Errno::ENOTDIR.new path
|
54
|
+
rescue Errno::EACCES
|
55
|
+
raise Errno::EACCES.new path
|
56
|
+
end
|
57
|
+
|
58
|
+
## OPERATIONS
|
59
|
+
|
60
|
+
def expand_path(path, base)
|
61
|
+
::File.expand_path path, base
|
62
|
+
end
|
63
|
+
|
64
|
+
def exists?(path)
|
65
|
+
fs path, ::File, :exists?, r(path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def mkdir(path)
|
69
|
+
fs path, ::Dir, :mkdir, r(path)
|
70
|
+
end
|
71
|
+
|
72
|
+
def mkpath(path)
|
73
|
+
fs path, ::FileUtils, :mkdir_p, r(path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def directory?(path)
|
77
|
+
fs path, ::File, :directory?, r(path)
|
78
|
+
end
|
79
|
+
|
80
|
+
def file?(path)
|
81
|
+
fs path, ::File, :file?, r(path)
|
82
|
+
end
|
83
|
+
|
84
|
+
def touch(path)
|
85
|
+
fs path, ::FileUtils, :touch, r(path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def write(path, content, *args)
|
89
|
+
fs path, ::IO, :write, r(path), content, *args
|
90
|
+
end
|
91
|
+
|
92
|
+
def read(path, *args)
|
93
|
+
fs path, ::IO, :read, r(path), *args
|
94
|
+
end
|
95
|
+
|
96
|
+
def mtime(path)
|
97
|
+
fs path, ::File, :mtime, r(path)
|
98
|
+
end
|
99
|
+
|
100
|
+
def mtime=(path, time)
|
101
|
+
fs path, ::File, :utime, atime(path), time, r(path)
|
102
|
+
end
|
103
|
+
|
104
|
+
def atime(path)
|
105
|
+
fs path, ::File, :atime, r(path)
|
106
|
+
end
|
107
|
+
|
108
|
+
def atime=(path, time)
|
109
|
+
fs path, ::File, :utime, time, mtime(path), r(path)
|
110
|
+
end
|
111
|
+
|
112
|
+
def entries(path)
|
113
|
+
fs path, ::Dir, :entries, r(path)
|
114
|
+
end
|
115
|
+
|
116
|
+
def glob(pattern, flags = 0)
|
117
|
+
if block_given?
|
118
|
+
fs pattern, ::Dir, :glob, r(pattern), flags do |path|
|
119
|
+
yield ur(path)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
fs(pattern, ::Dir, :glob, r(pattern), flags).map {|path| ur path }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def umask
|
127
|
+
File.umask
|
128
|
+
end
|
129
|
+
|
130
|
+
def umask=(mask)
|
131
|
+
File.umask mask
|
132
|
+
end
|
133
|
+
|
134
|
+
def mode(path)
|
135
|
+
fs(path, ::File, :stat, r(path)).mode & 0o777
|
136
|
+
end
|
137
|
+
|
138
|
+
def chmod(path, mode)
|
139
|
+
fs path, ::File, :chmod, mode, r(path)
|
140
|
+
end
|
141
|
+
|
142
|
+
def unlink(path)
|
143
|
+
fs path, ::File, :unlink, r(path)
|
144
|
+
end
|
145
|
+
|
146
|
+
def rmtree(path)
|
147
|
+
fs path, ::FileUtils, :rm_r, r(path), force: true
|
148
|
+
end
|
149
|
+
|
150
|
+
def rmtree!(path)
|
151
|
+
fs path, ::FileUtils, :rm_r, r(path)
|
152
|
+
end
|
153
|
+
|
154
|
+
def safe_rmtree(path)
|
155
|
+
fs path, ::FileUtils, :rm_r, r(path), force: true, secure: true
|
156
|
+
end
|
157
|
+
|
158
|
+
def safe_rmtree!(path)
|
159
|
+
fs path, ::FileUtils, :rm_r, r(path), secure: true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
# @!group Comparison
|
5
|
+
|
6
|
+
# Compare path to given object. If object is a string, Path or #{Path.like?}
|
7
|
+
# they will be compared using the string paths. Otherwise they are assumed
|
8
|
+
# as not equal.
|
9
|
+
#
|
10
|
+
# @param other [Object] Object to compare path with.
|
11
|
+
# @return [Boolean] True if object represents same path.
|
12
|
+
#
|
13
|
+
def eql?(other)
|
14
|
+
if other.is_a?(Path)
|
15
|
+
cleanpath.internal_path == other.cleanpath.internal_path
|
16
|
+
elsif Path.like?(other)
|
17
|
+
Path.new(other).eql?(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
alias == eql?
|
21
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
class << self
|
5
|
+
# @!group Construction
|
6
|
+
|
7
|
+
# Create new {Path}.
|
8
|
+
#
|
9
|
+
# If single argument is a path object it will be returned and no new one
|
10
|
+
# will be created. If not arguments are given {Path::EMPTY} will be
|
11
|
+
# returned.
|
12
|
+
#
|
13
|
+
# @see #initialize
|
14
|
+
#
|
15
|
+
def new(*args)
|
16
|
+
args.flatten!
|
17
|
+
return Path::EMPTY if args.empty?
|
18
|
+
return args.first if args.size == 1 && args.first.is_a?(self)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if given object is like a path.
|
23
|
+
#
|
24
|
+
# An object is like a path if
|
25
|
+
# 1. It is a {Path} object.
|
26
|
+
# 2. It is a string.
|
27
|
+
# 3. It responds to {#to_path} and {#to_path} returns a string.
|
28
|
+
# 4. It responds to {#path} and {#path} returns a string.
|
29
|
+
#
|
30
|
+
# If no rule matches it is not considered to be like a path.
|
31
|
+
#
|
32
|
+
# @return [Boolean] True if object is path like, false otherwise.
|
33
|
+
#
|
34
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
35
|
+
#
|
36
|
+
def like?(obj)
|
37
|
+
return true if obj.is_a?(self)
|
38
|
+
return true if obj.is_a?(String)
|
39
|
+
return true if obj.respond_to?(:to_path) && obj.to_path.is_a?(String)
|
40
|
+
return true if obj.respond_to?(:path) && obj.path.is_a?(String)
|
41
|
+
false
|
42
|
+
end
|
43
|
+
# rubocop:enable all
|
44
|
+
|
45
|
+
# Convert given object to path string using {::Path.like?} rules.
|
46
|
+
#
|
47
|
+
# @note Should not be used directly.
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
# @raise [ArgumentError] If given object is not {::Path.like?}.
|
51
|
+
# @see ::Path.like?
|
52
|
+
#
|
53
|
+
# rubocop:disable Metrics/MethodLength
|
54
|
+
def like_path(obj)
|
55
|
+
case obj
|
56
|
+
when String
|
57
|
+
return obj
|
58
|
+
else
|
59
|
+
%i[to_path path to_str to_s].each do |mth|
|
60
|
+
if obj.respond_to?(mth) && obj.send(mth).is_a?(String)
|
61
|
+
return obj.send(mth)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
raise ArgumentError.new \
|
67
|
+
"Argument #{obj.inspect} cannot be converted to path string."
|
68
|
+
end
|
69
|
+
# rubocop:enable all
|
70
|
+
|
71
|
+
# Return system file path separator.
|
72
|
+
#
|
73
|
+
# @return [String] File separator.
|
74
|
+
# @see ::File::SEPARATOR
|
75
|
+
#
|
76
|
+
def separator
|
77
|
+
::File::SEPARATOR
|
78
|
+
end
|
79
|
+
|
80
|
+
# Allow class object to be used as a bock.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# %w(path/to/fileA path/to/fileB).map(&Path)
|
84
|
+
#
|
85
|
+
def to_proc
|
86
|
+
proc {|*args| Path.new(*args) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @!group Construction
|
91
|
+
|
92
|
+
# Initialize new {Path} object.
|
93
|
+
#
|
94
|
+
# Given arguments will be converted to String using `#to_path`, `#path` or
|
95
|
+
# `#to_s` in this order if they return a String object.
|
96
|
+
#
|
97
|
+
# @overload initialize([[String, #to_path, #path, #to_s], ...]
|
98
|
+
#
|
99
|
+
def initialize(*args)
|
100
|
+
parts = args.flatten
|
101
|
+
@path = if parts.size > 1
|
102
|
+
::File.join(*parts.map {|p| Path.like_path p })
|
103
|
+
elsif parts.size == 1
|
104
|
+
Path.like_path(parts.first).dup
|
105
|
+
else
|
106
|
+
''
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Empty path.
|
111
|
+
#
|
112
|
+
# @return [Path] Empty path.
|
113
|
+
#
|
114
|
+
EMPTY = Path.new('')
|
115
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Path
|
4
|
+
class << self
|
5
|
+
# Returns the current working directory.
|
6
|
+
#
|
7
|
+
# @return [Path] Current working directory.
|
8
|
+
# @see ::Dir.getwd
|
9
|
+
#
|
10
|
+
def getwd
|
11
|
+
new Backend.instance.getwd
|
12
|
+
end
|
13
|
+
|
14
|
+
def glob(pattern, flags = nil)
|
15
|
+
flags = default_glob_flags(flags)
|
16
|
+
|
17
|
+
if block_given?
|
18
|
+
Backend.instance.glob(pattern, flags) {|path| yield Path path }
|
19
|
+
else
|
20
|
+
Backend.instance.glob(pattern, flags).map(&Path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
#
|
26
|
+
def default_glob_flags(flags)
|
27
|
+
if flags.nil? && defined?(::File::FNM_EXTGLOB)
|
28
|
+
::File::FNM_EXTGLOB
|
29
|
+
else
|
30
|
+
flags.to_i
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @!group Directory Operations
|
36
|
+
|
37
|
+
# Create directory.
|
38
|
+
#
|
39
|
+
# Given arguments will be joined with current path before directory is
|
40
|
+
# created.
|
41
|
+
#
|
42
|
+
# @raise [Errno::ENOENT] If parent directory could not created.
|
43
|
+
# @return [Path] Path to created directory.
|
44
|
+
# @see #mkpath
|
45
|
+
#
|
46
|
+
def mkdir(*args)
|
47
|
+
with_path(*args) do |path|
|
48
|
+
Backend.instance.mkdir path
|
49
|
+
Path path
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create directory and all missing parent directories.
|
54
|
+
#
|
55
|
+
# Given arguments will be joined with current path before directories
|
56
|
+
# are created.
|
57
|
+
#
|
58
|
+
# @return [Path] Path to created directory.
|
59
|
+
# @see #mkdir
|
60
|
+
# @see ::FileUtils.mkdir_p
|
61
|
+
#
|
62
|
+
def mkpath(*args)
|
63
|
+
with_path(*args) do |path|
|
64
|
+
Backend.instance.mkpath path
|
65
|
+
Path path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias mkdir_p mkpath
|
69
|
+
|
70
|
+
# Return list of entries in directory. That includes special directories
|
71
|
+
# (`.`, `..`).
|
72
|
+
#
|
73
|
+
# Given arguments will be joined before children are listed for directory.
|
74
|
+
#
|
75
|
+
# @return [Array<Path>] Entries in directory.
|
76
|
+
#
|
77
|
+
def entries(*_args)
|
78
|
+
invoke_backend(:entries, internal_path).map(&Path)
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
def glob(pattern, flags = nil, &block)
|
83
|
+
Path.glob(::File.join(escaped_glob_path, pattern), flags, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Removes file or directory. If it's a directory it will be removed
|
87
|
+
# recursively.
|
88
|
+
#
|
89
|
+
# WARNING: This method causes local vulnerability if one of parent
|
90
|
+
# directories or removing directory tree are world writable (including
|
91
|
+
# `/tmp`, whose permission is 1777), and the current process has strong
|
92
|
+
# privilege such as Unix super user (root), and the system has symbolic link.
|
93
|
+
# For secure removing see {#safe_rmtree}.
|
94
|
+
#
|
95
|
+
# @return [Path] Path to removed file or directory.
|
96
|
+
#
|
97
|
+
def rmtree(*args)
|
98
|
+
with_path(*args) do |path|
|
99
|
+
invoke_backend :rmtree, internal_path
|
100
|
+
Path path
|
101
|
+
end
|
102
|
+
end
|
103
|
+
alias rm_rf rmtree
|
104
|
+
|
105
|
+
# Removes file or directory. If it's a directory it will be removed
|
106
|
+
# recursively.
|
107
|
+
#
|
108
|
+
# This method uses #{FileUtils#remove_entry_secure} to avoid TOCTTOU
|
109
|
+
# (time-of-check-to-time-of-use) local security vulnerability of {#rmtree}.
|
110
|
+
# {#rmtree} causes security hole when:
|
111
|
+
#
|
112
|
+
# * Parent directory is world writable (including `/tmp`).
|
113
|
+
# * Removing directory tree includes world writable directory.
|
114
|
+
# * The system has symbolic link.
|
115
|
+
#
|
116
|
+
# @return [Path] Path to removed file or directory.
|
117
|
+
#
|
118
|
+
def safe_rmtree(*args)
|
119
|
+
with_path(*args) do |path|
|
120
|
+
invoke_backend :safe_rmtree, internal_path
|
121
|
+
Path path
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Removes file or directory. If it's a directory it will be removed
|
126
|
+
# recursively.
|
127
|
+
#
|
128
|
+
# This method behaves exactly like {#rmtree} but will raise exceptions
|
129
|
+
# e.g. when file does not exist.
|
130
|
+
#
|
131
|
+
# @return [Path] Path to removed file or directory.
|
132
|
+
#
|
133
|
+
def rmtree!(*args)
|
134
|
+
with_path(*args) do |path|
|
135
|
+
invoke_backend :rmtree!, internal_path
|
136
|
+
Path path
|
137
|
+
end
|
138
|
+
end
|
139
|
+
alias rm_r rmtree!
|
140
|
+
|
141
|
+
# Removes file or directory. If it's a directory it will be removed
|
142
|
+
# recursively.
|
143
|
+
#
|
144
|
+
# This method behaves exactly like {#safe_rmtree} but will raise exceptions
|
145
|
+
# e.g. when file does not exist.
|
146
|
+
#
|
147
|
+
# @return [Path] Path to removed file or directory.
|
148
|
+
#
|
149
|
+
def safe_rmtree!(*args)
|
150
|
+
with_path(*args) do |path|
|
151
|
+
invoke_backend :safe_rmtree!, internal_path
|
152
|
+
Path path
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def escaped_glob_path
|
159
|
+
internal_path.gsub(/[\[\]\*\?\{\}]/, '\\\\\0')
|
160
|
+
end
|
161
|
+
end
|