dry-files 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/dry-files.gemspec +34 -0
- data/lib/dry-files.rb +3 -0
- data/lib/dry/files.rb +860 -0
- data/lib/dry/files/adapter.rb +21 -0
- data/lib/dry/files/error.rb +120 -0
- data/lib/dry/files/file_system.rb +377 -0
- data/lib/dry/files/memory_file_system.rb +429 -0
- data/lib/dry/files/memory_file_system/node.rb +246 -0
- data/lib/dry/files/path.rb +112 -0
- data/lib/dry/files/version.rb +7 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4d22584a76b85c83b8d1d50e0b805335347f205f7ac7da1ea31dd538a5b459fd
|
4
|
+
data.tar.gz: 4054a4f0bf0daa95cc3e23937d5acb9726606cf7974159146cd21787a186549b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d7881004a31c1106fb0a42b11015169fe1deac588aa1609ab3dd13c28c945f83cea7b722fb43250de665d862ec27f8f76071f88eac3b1567b2ea95a201cb8a4d
|
7
|
+
data.tar.gz: 3d2c8d0f875b00e889d0020a5023443b686529548bf1b28ca5007085bd88d4581affc6ad1a2d5319f861dfc768cef8e486f45959204478f05670121a239db124
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
<!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
|
2
|
+
|
3
|
+
## 0.1.0 2021-05-04
|
4
|
+
|
5
|
+
Initial release
|
6
|
+
|
7
|
+
### Added
|
8
|
+
|
9
|
+
- Introduced `Dry::Files`
|
10
|
+
- Introduced `Dry::Files#initialize` which accepts an optional `memory: true/false` argument to use the in-memory adapter (@jodosha)
|
11
|
+
- Introduced `Dry::Files#read` to read the file all at once (@jodosha)
|
12
|
+
- Introduced `Dry::Files#touch` to touch a file and create all the intermediate directories, if needed (@jodosha)
|
13
|
+
- Introduced `Dry::Files#write` to write/replace a file and create all the intermediate directories, if needed (@jodosha)
|
14
|
+
- Introduced `Dry::Files#join` to join the given path tokens (@jodosha)
|
15
|
+
- Introduced `Dry::Files#expand_path` to make the relative path absolute, starting from the current directory of from a custom one (@jodosha)
|
16
|
+
- Introduced `Dry::Files#pwd` to return the current directory (@jodosha)
|
17
|
+
- Introduced `Dry::Files#chdir` to temporary change the current directory (@jodosha)
|
18
|
+
- Introduced `Dry::Files#mkdir` to create intermediate directories for the given directory name (@jodosha)
|
19
|
+
- Introduced `Dry::Files#mkdir_p` to create intermediate directories for the given file name (@jodosha)
|
20
|
+
- Introduced `Dry::Files#cp` to copy source file into destination and create intermediate destination directories, if needed (@jodosha)
|
21
|
+
- Introduced `Dry::Files#delete` to delete a file (@jodosha)
|
22
|
+
- Introduced `Dry::Files#delete_directory` to delete a directory (@jodosha)
|
23
|
+
- Introduced `Dry::Files#exist?` to check if a path exists (@jodosha)
|
24
|
+
- Introduced `Dry::Files#directory?` to check if a path is a directory (@jodosha)
|
25
|
+
- Introduced `Dry::Files#executable?` to check if a path is an executable (@jodosha)
|
26
|
+
- Introduced `Dry::Files#unshift` to add a new line at the top of the file (@jodosha)
|
27
|
+
- Introduced `Dry::Files#append` to add a new line at the bottom of the file (@jodosha)
|
28
|
+
- Introduced `Dry::Files#replace_first_line` to replace first line that match target (@jodosha)
|
29
|
+
- Introduced `Dry::Files#replace_last_line` to replace last line that match target (@jodosha)
|
30
|
+
- Introduced `Dry::Files#inject_line_before` to inject content before the first match of target (@jodosha)
|
31
|
+
- Introduced `Dry::Files#inject_line_before_last` to inject content before the last match of target (@jodosha)
|
32
|
+
- Introduced `Dry::Files#inject_line_after` to inject content after the first match of target (@jodosha)
|
33
|
+
- Introduced `Dry::Files#inject_line_after_last` to inject content after the last match of target (@jodosha)
|
34
|
+
- Introduced `Dry::Files#inject_line_at_block_top` to inject content as the first line of the matching code block (@jodosha)
|
35
|
+
- Introduced `Dry::Files#inject_line_at_block_bottom` to inject content as the last line of the matching code block (@jodosha)
|
36
|
+
- Introduced `Dry::Files#remove_line` to remove the first matching line (@jodosha)
|
37
|
+
- Introduced `Dry::Files#remove_block` remove the first matching block (@jodosha)
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015-2021 dry-rb team
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
[gem]: https://rubygems.org/gems/dry-files
|
2
|
+
[actions]: https://github.com/dry-rb/dry-files/actions
|
3
|
+
[codacy]: https://www.codacy.com/gh/dry-rb/dry-files
|
4
|
+
[chat]: https://dry-rb.zulipchat.com
|
5
|
+
[inchpages]: http://inch-ci.org/github/dry-rb/dry-files
|
6
|
+
|
7
|
+
# dry-files [][chat]
|
8
|
+
|
9
|
+
[][gem]
|
10
|
+
[][actions]
|
11
|
+
[][codacy]
|
12
|
+
[][codacy]
|
13
|
+
[][inchpages]
|
14
|
+
|
15
|
+
## Links
|
16
|
+
|
17
|
+
* [User documentation](http://dry-rb.org/gems/dry-files)
|
18
|
+
* [API documentation](http://rubydoc.info/gems/dry-files)
|
19
|
+
|
20
|
+
## Supported Ruby versions
|
21
|
+
|
22
|
+
This library officially supports the following Ruby versions:
|
23
|
+
|
24
|
+
* MRI >= `2.5`
|
25
|
+
* jruby >= `9.2`
|
26
|
+
|
27
|
+
## License
|
28
|
+
|
29
|
+
See `LICENSE` file.
|
data/dry-files.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# this file is managed by dry-rb/devtools project
|
3
|
+
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'dry/files/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'dry-files'
|
10
|
+
spec.authors = ["Luca Guidi"]
|
11
|
+
spec.email = ["me@lucaguidi.com"]
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.version = Dry::Files::VERSION.dup
|
14
|
+
|
15
|
+
spec.summary = "file utilities"
|
16
|
+
spec.description = spec.summary
|
17
|
+
spec.homepage = 'https://dry-rb.org/gems/dry-files'
|
18
|
+
spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-files.gemspec", "lib/**/*"]
|
19
|
+
spec.bindir = 'bin'
|
20
|
+
spec.executables = []
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
24
|
+
spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-files/blob/master/CHANGELOG.md'
|
25
|
+
spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-files'
|
26
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-files/issues'
|
27
|
+
|
28
|
+
spec.required_ruby_version = ">= 2.5.0"
|
29
|
+
|
30
|
+
# to update dependencies edit project.yml
|
31
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.10"
|
33
|
+
spec.add_development_dependency "rubocop", "~> 1.12"
|
34
|
+
end
|
data/lib/dry-files.rb
ADDED
data/lib/dry/files.rb
ADDED
@@ -0,0 +1,860 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# dry-rb is a collection of next-generation Ruby libraries
|
4
|
+
#
|
5
|
+
# @api public
|
6
|
+
# @since 0.1.0
|
7
|
+
module Dry
|
8
|
+
# File manipulations
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
# @api public
|
12
|
+
class Files
|
13
|
+
require_relative "files/version"
|
14
|
+
require_relative "files/error"
|
15
|
+
require_relative "files/adapter"
|
16
|
+
|
17
|
+
# Creates a new instance
|
18
|
+
#
|
19
|
+
# Memory file system is experimental
|
20
|
+
#
|
21
|
+
# @param memory [TrueClass,FalseClass] use in-memory, ephemeral file system
|
22
|
+
# @param adapter [Dry::FileSystem]
|
23
|
+
#
|
24
|
+
# @return [Dry::Files] a new files instance
|
25
|
+
#
|
26
|
+
# @since 0.1.0
|
27
|
+
# @api public
|
28
|
+
def initialize(memory: false, adapter: Adapter.call(memory: memory))
|
29
|
+
@adapter = adapter
|
30
|
+
end
|
31
|
+
|
32
|
+
# Read file content
|
33
|
+
#
|
34
|
+
# @param path [String,Pathname] the path to file
|
35
|
+
#
|
36
|
+
# @return [String] the file contents
|
37
|
+
#
|
38
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
39
|
+
#
|
40
|
+
# @since 0.1.0
|
41
|
+
# @api public
|
42
|
+
#
|
43
|
+
# TODO: allow buffered read
|
44
|
+
def read(path)
|
45
|
+
adapter.read(path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Creates an empty file for the given path.
|
49
|
+
# All the intermediate directories are created.
|
50
|
+
# If the path already exists, it doesn't change the contents
|
51
|
+
#
|
52
|
+
# @param path [String,Pathname] the path to file
|
53
|
+
#
|
54
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
55
|
+
#
|
56
|
+
# @since 0.1.0
|
57
|
+
# @api public
|
58
|
+
def touch(path)
|
59
|
+
adapter.touch(path)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a new file or rewrites the contents
|
63
|
+
# of an existing file for the given path and content
|
64
|
+
# All the intermediate directories are created.
|
65
|
+
#
|
66
|
+
# @param path [String,Pathname] the path to file
|
67
|
+
# @param content [String, Array<String>] the content to write
|
68
|
+
#
|
69
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
70
|
+
#
|
71
|
+
# @since 0.1.0
|
72
|
+
# @api public
|
73
|
+
def write(path, *content)
|
74
|
+
adapter.write(path, *content)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a new string formed by joining the strings using Operating
|
78
|
+
# System path separator
|
79
|
+
#
|
80
|
+
# @param path [Array<String,Pathname>] path tokens
|
81
|
+
#
|
82
|
+
# @return [String] the joined path
|
83
|
+
#
|
84
|
+
# @since 0.1.0
|
85
|
+
# @api public
|
86
|
+
def join(*path)
|
87
|
+
adapter.join(*path)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Converts a path to an absolute path.
|
91
|
+
#
|
92
|
+
# Relative paths are referenced from the current working directory of
|
93
|
+
# the process unless `dir` is given.
|
94
|
+
#
|
95
|
+
# @param path [String,Pathname] the path to the file
|
96
|
+
# @param dir [String,Pathname] the base directory
|
97
|
+
#
|
98
|
+
# @return [String] the expanded path
|
99
|
+
#
|
100
|
+
# @since 0.1.0
|
101
|
+
def expand_path(path, dir = pwd)
|
102
|
+
adapter.expand_path(path, dir)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the name of the current working directory.
|
106
|
+
#
|
107
|
+
# @return [String] the current working directory.
|
108
|
+
#
|
109
|
+
# @since 0.1.0
|
110
|
+
def pwd
|
111
|
+
adapter.pwd
|
112
|
+
end
|
113
|
+
|
114
|
+
# Temporary changes the current working directory of the process to the
|
115
|
+
# given path and yield the given block.
|
116
|
+
#
|
117
|
+
# @param path [String,Pathname] the target directory
|
118
|
+
# @param blk [Proc] the code to execute with the target directory
|
119
|
+
#
|
120
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
121
|
+
#
|
122
|
+
# @since 0.1.0
|
123
|
+
def chdir(path, &blk)
|
124
|
+
adapter.chdir(path, &blk)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Creates a directory for the given path.
|
128
|
+
# It assumes that all the tokens in `path` are meant to be a directory.
|
129
|
+
# All the intermediate directories are created.
|
130
|
+
#
|
131
|
+
# @param path [String,Pathname] the path to directory
|
132
|
+
#
|
133
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
134
|
+
#
|
135
|
+
# @since 0.1.0
|
136
|
+
# @api public
|
137
|
+
#
|
138
|
+
# @see #mkdir_p
|
139
|
+
#
|
140
|
+
# @example
|
141
|
+
# require "dry/files"
|
142
|
+
#
|
143
|
+
# Dry::Files.new.mkdir("path/to/directory")
|
144
|
+
# # => creates the `path/to/directory` directory
|
145
|
+
#
|
146
|
+
# # WRONG this isn't probably what you want, check `.mkdir_p`
|
147
|
+
# Dry::Files.new.mkdir("path/to/file.rb")
|
148
|
+
# # => creates the `path/to/file.rb` directory
|
149
|
+
def mkdir(path)
|
150
|
+
adapter.mkdir(path)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Creates a directory for the given path.
|
154
|
+
# It assumes that all the tokens, but the last, in `path` are meant to be
|
155
|
+
# a directory, whereas the last is meant to be a file.
|
156
|
+
# All the intermediate directories are created.
|
157
|
+
#
|
158
|
+
# @param path [String,Pathname] the path to directory
|
159
|
+
#
|
160
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
161
|
+
#
|
162
|
+
# @since 0.1.0
|
163
|
+
# @api public
|
164
|
+
#
|
165
|
+
# @see #mkdir
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# require "dry/files"
|
169
|
+
#
|
170
|
+
# Dry::Files.new.mkdir_p("path/to/file.rb")
|
171
|
+
# # => creates the `path/to` directory, but NOT `file.rb`
|
172
|
+
#
|
173
|
+
# # WRONG it doesn't create the last directory, check `.mkdir`
|
174
|
+
# Dry::Files.new.mkdir_p("path/to/directory")
|
175
|
+
# # => creates the `path/to` directory
|
176
|
+
def mkdir_p(path)
|
177
|
+
adapter.mkdir_p(path)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Copies source into destination.
|
181
|
+
# All the intermediate directories are created.
|
182
|
+
# If the destination already exists, it overrides the contents.
|
183
|
+
#
|
184
|
+
# @param source [String,Pathname] the path to the source file
|
185
|
+
# @param destination [String,Pathname] the path to the destination file
|
186
|
+
#
|
187
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
188
|
+
#
|
189
|
+
# @since 0.1.0
|
190
|
+
# @api public
|
191
|
+
def cp(source, destination)
|
192
|
+
adapter.cp(source, destination)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Deletes given path (file).
|
196
|
+
#
|
197
|
+
# @param path [String,Pathname] the path to file
|
198
|
+
#
|
199
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
200
|
+
#
|
201
|
+
# @since 0.1.0
|
202
|
+
# @api public
|
203
|
+
def delete(path)
|
204
|
+
adapter.rm(path)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Deletes given path (directory).
|
208
|
+
#
|
209
|
+
# @param path [String,Pathname] the path to file
|
210
|
+
#
|
211
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
212
|
+
#
|
213
|
+
# @since 0.1.0
|
214
|
+
# @api public
|
215
|
+
def delete_directory(path)
|
216
|
+
adapter.rm_rf(path)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Checks if `path` exist
|
220
|
+
#
|
221
|
+
# @param path [String,Pathname] the path to file
|
222
|
+
#
|
223
|
+
# @return [TrueClass,FalseClass] the result of the check
|
224
|
+
#
|
225
|
+
# @since 0.1.0
|
226
|
+
# @api public
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
# require "dry/files"
|
230
|
+
#
|
231
|
+
# Dry::Files.new.exist?(__FILE__) # => true
|
232
|
+
# Dry::Files.new.exist?(__dir__) # => true
|
233
|
+
#
|
234
|
+
# Dry::Files.new.exist?("missing_file") # => false
|
235
|
+
def exist?(path)
|
236
|
+
adapter.exist?(path)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Checks if `path` is a directory
|
240
|
+
#
|
241
|
+
# @param path [String,Pathname] the path to directory
|
242
|
+
#
|
243
|
+
# @return [TrueClass,FalseClass] the result of the check
|
244
|
+
#
|
245
|
+
# @since 0.1.0
|
246
|
+
# @api public
|
247
|
+
#
|
248
|
+
# @example
|
249
|
+
# require "dry/files"
|
250
|
+
#
|
251
|
+
# Dry::Files.new.directory?(__dir__) # => true
|
252
|
+
# Dry::Files.new.directory?(__FILE__) # => false
|
253
|
+
#
|
254
|
+
# Dry::Files.new.directory?("missing_directory") # => false
|
255
|
+
def directory?(path)
|
256
|
+
adapter.directory?(path)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Checks if `path` is an executable
|
260
|
+
#
|
261
|
+
# @param path [String,Pathname] the path to file
|
262
|
+
#
|
263
|
+
# @return [TrueClass,FalseClass] the result of the check
|
264
|
+
#
|
265
|
+
# @since 0.1.0
|
266
|
+
# @api public
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# require "dry/files"
|
270
|
+
#
|
271
|
+
# Dry::Files.new.executable?("/path/to/ruby") # => true
|
272
|
+
# Dry::Files.new.executable?(__FILE__) # => false
|
273
|
+
#
|
274
|
+
# Dry::Files.new.directory?("missing_file") # => false
|
275
|
+
def executable?(path)
|
276
|
+
adapter.executable?(path)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Adds a new line at the top of the file
|
280
|
+
#
|
281
|
+
# @param path [String,Pathname] the path to file
|
282
|
+
# @param line [String] the line to add
|
283
|
+
#
|
284
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
285
|
+
#
|
286
|
+
# @see #append
|
287
|
+
#
|
288
|
+
# @since 0.1.0
|
289
|
+
# @api public
|
290
|
+
def unshift(path, line)
|
291
|
+
content = adapter.readlines(path)
|
292
|
+
content.unshift(newline(line))
|
293
|
+
|
294
|
+
write(path, content)
|
295
|
+
end
|
296
|
+
|
297
|
+
# Adds a new line at the bottom of the file
|
298
|
+
#
|
299
|
+
# @param path [String,Pathname] the path to file
|
300
|
+
# @param contents [String] the contents to add
|
301
|
+
#
|
302
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
303
|
+
#
|
304
|
+
# @see #unshift
|
305
|
+
#
|
306
|
+
# @since 0.1.0
|
307
|
+
# @api public
|
308
|
+
def append(path, contents)
|
309
|
+
mkdir_p(path)
|
310
|
+
|
311
|
+
content = adapter.readlines(path)
|
312
|
+
content << newline unless newline?(content.last)
|
313
|
+
content << newline(contents)
|
314
|
+
|
315
|
+
write(path, content)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Replace first line in `path` that contains `target` with `replacement`.
|
319
|
+
#
|
320
|
+
# @param path [String,Pathname] the path to file
|
321
|
+
# @param target [String,Regexp] the target to replace
|
322
|
+
# @param replacement [String] the replacement
|
323
|
+
#
|
324
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
325
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
326
|
+
#
|
327
|
+
# @see #replace_last_line
|
328
|
+
#
|
329
|
+
# @since 0.1.0
|
330
|
+
# @api public
|
331
|
+
def replace_first_line(path, target, replacement)
|
332
|
+
content = adapter.readlines(path)
|
333
|
+
content[index(content, path, target)] = newline(replacement)
|
334
|
+
|
335
|
+
write(path, content)
|
336
|
+
end
|
337
|
+
|
338
|
+
# Replace last line in `path` that contains `target` with `replacement`.
|
339
|
+
#
|
340
|
+
# @param path [String,Pathname] the path to file
|
341
|
+
# @param target [String,Regexp] the target to replace
|
342
|
+
# @param replacement [String] the replacement
|
343
|
+
#
|
344
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
345
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
346
|
+
#
|
347
|
+
# @see #replace_first_line
|
348
|
+
#
|
349
|
+
# @since 0.1.0
|
350
|
+
# @api public
|
351
|
+
def replace_last_line(path, target, replacement)
|
352
|
+
content = adapter.readlines(path)
|
353
|
+
content[-index(content.reverse, path, target) - CONTENT_OFFSET] = newline(replacement)
|
354
|
+
|
355
|
+
write(path, content)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Inject `contents` in `path` before `target`.
|
359
|
+
#
|
360
|
+
# @param path [String,Pathname] the path to file
|
361
|
+
# @param target [String,Regexp] the target to replace
|
362
|
+
# @param contents [String] the contents to inject
|
363
|
+
#
|
364
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
365
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
366
|
+
#
|
367
|
+
# @see #inject_line_after
|
368
|
+
# @see #inject_line_before_last
|
369
|
+
# @see #inject_line_after_last
|
370
|
+
#
|
371
|
+
# @since 0.1.0
|
372
|
+
# @api public
|
373
|
+
def inject_line_before(path, target, contents)
|
374
|
+
_inject_line_before(path, target, contents, method(:index))
|
375
|
+
end
|
376
|
+
|
377
|
+
# Inject `contents` in `path` after last `target`.
|
378
|
+
#
|
379
|
+
# @param path [String,Pathname] the path to file
|
380
|
+
# @param target [String,Regexp] the target to replace
|
381
|
+
# @param contents [String] the contents to inject
|
382
|
+
#
|
383
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
384
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
385
|
+
#
|
386
|
+
# @see #inject_line_before
|
387
|
+
# @see #inject_line_after
|
388
|
+
# @see #inject_line_after_last
|
389
|
+
#
|
390
|
+
# @since 0.1.0
|
391
|
+
# @api public
|
392
|
+
def inject_line_before_last(path, target, contents)
|
393
|
+
_inject_line_before(path, target, contents, method(:rindex))
|
394
|
+
end
|
395
|
+
|
396
|
+
# Inject `contents` in `path` after `target`.
|
397
|
+
#
|
398
|
+
# @param path [String,Pathname] the path to file
|
399
|
+
# @param target [String,Regexp] the target to replace
|
400
|
+
# @param contents [String] the contents to inject
|
401
|
+
#
|
402
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
403
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
404
|
+
#
|
405
|
+
# @see #inject_line_before
|
406
|
+
# @see #inject_line_before_last
|
407
|
+
# @see #inject_line_after_last
|
408
|
+
#
|
409
|
+
# @since 0.1.0
|
410
|
+
# @api public
|
411
|
+
def inject_line_after(path, target, contents)
|
412
|
+
_inject_line_after(path, target, contents, method(:index))
|
413
|
+
end
|
414
|
+
|
415
|
+
# Inject `contents` in `path` after last `target`.
|
416
|
+
#
|
417
|
+
# @param path [String,Pathname] the path to file
|
418
|
+
# @param target [String,Regexp] the target to replace
|
419
|
+
# @param contents [String] the contents to inject
|
420
|
+
#
|
421
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
422
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
423
|
+
#
|
424
|
+
# @see #inject_line_before
|
425
|
+
# @see #inject_line_after
|
426
|
+
# @see #inject_line_before_last
|
427
|
+
#
|
428
|
+
# @since 0.1.0
|
429
|
+
# @api public
|
430
|
+
def inject_line_after_last(path, target, contents)
|
431
|
+
_inject_line_after(path, target, contents, method(:rindex))
|
432
|
+
end
|
433
|
+
|
434
|
+
# Inject `contents` in `path` within the first Ruby block that matches `target`.
|
435
|
+
# The given `contents` will appear at the TOP of the Ruby block.
|
436
|
+
#
|
437
|
+
# @param path [String,Pathname] the path to file
|
438
|
+
# @param target [String,Regexp] the target matcher for Ruby block
|
439
|
+
# @param contents [String,Array<String>] the contents to inject
|
440
|
+
#
|
441
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
442
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
443
|
+
#
|
444
|
+
# @since 0.1.0
|
445
|
+
# @api public
|
446
|
+
#
|
447
|
+
# @example Inject a single line
|
448
|
+
# require "dry/files"
|
449
|
+
#
|
450
|
+
# files = Dry::Files.new
|
451
|
+
# path = "config/application.rb"
|
452
|
+
#
|
453
|
+
# File.read(path)
|
454
|
+
# # # frozen_string_literal: true
|
455
|
+
# #
|
456
|
+
# # class Application
|
457
|
+
# # configure do
|
458
|
+
# # root __dir__
|
459
|
+
# # end
|
460
|
+
# # end
|
461
|
+
#
|
462
|
+
# # inject a single line
|
463
|
+
# files.inject_line_at_block_top(path, /configure/, %(load_path.unshift("lib")))
|
464
|
+
#
|
465
|
+
# File.read(path)
|
466
|
+
# # # frozen_string_literal: true
|
467
|
+
# #
|
468
|
+
# # class Application
|
469
|
+
# # configure do
|
470
|
+
# # load_path.unshift("lib")
|
471
|
+
# # root __dir__
|
472
|
+
# # end
|
473
|
+
# # end
|
474
|
+
#
|
475
|
+
# @example Inject multiple lines
|
476
|
+
# require "dry/files"
|
477
|
+
#
|
478
|
+
# files = Dry::Files.new
|
479
|
+
# path = "config/application.rb"
|
480
|
+
#
|
481
|
+
# File.read(path)
|
482
|
+
# # # frozen_string_literal: true
|
483
|
+
# #
|
484
|
+
# # class Application
|
485
|
+
# # configure do
|
486
|
+
# # root __dir__
|
487
|
+
# # end
|
488
|
+
# # end
|
489
|
+
#
|
490
|
+
# # inject multiple lines
|
491
|
+
# files.inject_line_at_block_top(path,
|
492
|
+
# /configure/,
|
493
|
+
# [%(load_path.unshift("lib")), "settings.load!"])
|
494
|
+
#
|
495
|
+
# File.read(path)
|
496
|
+
# # # frozen_string_literal: true
|
497
|
+
# #
|
498
|
+
# # class Application
|
499
|
+
# # configure do
|
500
|
+
# # load_path.unshift("lib")
|
501
|
+
# # settings.load!
|
502
|
+
# # root __dir__
|
503
|
+
# # end
|
504
|
+
# # end
|
505
|
+
#
|
506
|
+
# @example Inject a block
|
507
|
+
# require "dry/files"
|
508
|
+
#
|
509
|
+
# files = Dry::Files.new
|
510
|
+
# path = "config/application.rb"
|
511
|
+
#
|
512
|
+
# File.read(path)
|
513
|
+
# # # frozen_string_literal: true
|
514
|
+
# #
|
515
|
+
# # class Application
|
516
|
+
# # configure do
|
517
|
+
# # root __dir__
|
518
|
+
# # end
|
519
|
+
# # end
|
520
|
+
#
|
521
|
+
# # inject a block
|
522
|
+
# block = <<~BLOCK
|
523
|
+
# settings do
|
524
|
+
# load!
|
525
|
+
# end
|
526
|
+
# BLOCK
|
527
|
+
# files.inject_line_at_block_top(path, /configure/, block)
|
528
|
+
#
|
529
|
+
# File.read(path)
|
530
|
+
# # # frozen_string_literal: true
|
531
|
+
# #
|
532
|
+
# # class Application
|
533
|
+
# # configure do
|
534
|
+
# # settings do
|
535
|
+
# # load!
|
536
|
+
# # end
|
537
|
+
# # root __dir__
|
538
|
+
# # end
|
539
|
+
# # end
|
540
|
+
def inject_line_at_block_top(path, target, *contents)
|
541
|
+
content = adapter.readlines(path)
|
542
|
+
starting = index(content, path, target)
|
543
|
+
offset = SPACE * (content[starting][SPACE_MATCHER].bytesize + INDENTATION)
|
544
|
+
|
545
|
+
contents = Array(contents).flatten
|
546
|
+
contents = _offset_block_lines(contents, offset)
|
547
|
+
|
548
|
+
content.insert(starting + CONTENT_OFFSET, contents)
|
549
|
+
write(path, content)
|
550
|
+
end
|
551
|
+
|
552
|
+
# Inject `contents` in `path` within the first Ruby block that matches `target`.
|
553
|
+
# The given `contents` will appear at the BOTTOM of the Ruby block.
|
554
|
+
#
|
555
|
+
# @param path [String,Pathname] the path to file
|
556
|
+
# @param target [String,Regexp] the target matcher for Ruby block
|
557
|
+
# @param contents [String,Array<String>] the contents to inject
|
558
|
+
#
|
559
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
560
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
561
|
+
#
|
562
|
+
# @since 0.1.0
|
563
|
+
# @api public
|
564
|
+
#
|
565
|
+
# @example Inject a single line
|
566
|
+
# require "dry/files"
|
567
|
+
#
|
568
|
+
# files = Dry::Files.new
|
569
|
+
# path = "config/application.rb"
|
570
|
+
#
|
571
|
+
# File.read(path)
|
572
|
+
# # # frozen_string_literal: true
|
573
|
+
# #
|
574
|
+
# # class Application
|
575
|
+
# # configure do
|
576
|
+
# # root __dir__
|
577
|
+
# # end
|
578
|
+
# # end
|
579
|
+
#
|
580
|
+
# # inject a single line
|
581
|
+
# files.inject_line_at_block_bottom(path, /configure/, %(load_path.unshift("lib")))
|
582
|
+
#
|
583
|
+
# File.read(path)
|
584
|
+
# # # frozen_string_literal: true
|
585
|
+
# #
|
586
|
+
# # class Application
|
587
|
+
# # configure do
|
588
|
+
# # root __dir__
|
589
|
+
# # load_path.unshift("lib")
|
590
|
+
# # end
|
591
|
+
# # end
|
592
|
+
#
|
593
|
+
# @example Inject multiple lines
|
594
|
+
# require "dry/files"
|
595
|
+
#
|
596
|
+
# files = Dry::Files.new
|
597
|
+
# path = "config/application.rb"
|
598
|
+
#
|
599
|
+
# File.read(path)
|
600
|
+
# # # frozen_string_literal: true
|
601
|
+
# #
|
602
|
+
# # class Application
|
603
|
+
# # configure do
|
604
|
+
# # root __dir__
|
605
|
+
# # end
|
606
|
+
# # end
|
607
|
+
#
|
608
|
+
# # inject multiple lines
|
609
|
+
# files.inject_line_at_block_bottom(path,
|
610
|
+
# /configure/,
|
611
|
+
# [%(load_path.unshift("lib")), "settings.load!"])
|
612
|
+
#
|
613
|
+
# File.read(path)
|
614
|
+
# # # frozen_string_literal: true
|
615
|
+
# #
|
616
|
+
# # class Application
|
617
|
+
# # configure do
|
618
|
+
# # root __dir__
|
619
|
+
# # load_path.unshift("lib")
|
620
|
+
# # settings.load!
|
621
|
+
# # end
|
622
|
+
# # end
|
623
|
+
#
|
624
|
+
# @example Inject a block
|
625
|
+
# require "dry/files"
|
626
|
+
#
|
627
|
+
# files = Dry::Files.new
|
628
|
+
# path = "config/application.rb"
|
629
|
+
#
|
630
|
+
# File.read(path)
|
631
|
+
# # # frozen_string_literal: true
|
632
|
+
# #
|
633
|
+
# # class Application
|
634
|
+
# # configure do
|
635
|
+
# # root __dir__
|
636
|
+
# # end
|
637
|
+
# # end
|
638
|
+
#
|
639
|
+
# # inject a block
|
640
|
+
# block = <<~BLOCK
|
641
|
+
# settings do
|
642
|
+
# load!
|
643
|
+
# end
|
644
|
+
# BLOCK
|
645
|
+
# files.inject_line_at_block_bottom(path, /configure/, block)
|
646
|
+
#
|
647
|
+
# File.read(path)
|
648
|
+
# # # frozen_string_literal: true
|
649
|
+
# #
|
650
|
+
# # class Application
|
651
|
+
# # configure do
|
652
|
+
# # root __dir__
|
653
|
+
# # settings do
|
654
|
+
# # load!
|
655
|
+
# # end
|
656
|
+
# # end
|
657
|
+
# # end
|
658
|
+
def inject_line_at_block_bottom(path, target, *contents)
|
659
|
+
content = adapter.readlines(path)
|
660
|
+
starting = index(content, path, target)
|
661
|
+
line = content[starting]
|
662
|
+
size = line[SPACE_MATCHER].bytesize
|
663
|
+
closing = (SPACE * size) +
|
664
|
+
(target.match?(INLINE_OPEN_BLOCK_MATCHER) ? INLINE_CLOSE_BLOCK : CLOSE_BLOCK)
|
665
|
+
ending = starting + index(content[starting..-CONTENT_OFFSET], path, closing)
|
666
|
+
offset = SPACE * (content[ending][SPACE_MATCHER].bytesize + INDENTATION)
|
667
|
+
|
668
|
+
contents = Array(contents).flatten
|
669
|
+
contents = _offset_block_lines(contents, offset)
|
670
|
+
|
671
|
+
content.insert(ending, contents)
|
672
|
+
write(path, content)
|
673
|
+
end
|
674
|
+
|
675
|
+
# Removes line from `path`, matching `target`.
|
676
|
+
#
|
677
|
+
# @param path [String,Pathname] the path to file
|
678
|
+
# @param target [String,Regexp] the target to remove
|
679
|
+
#
|
680
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
681
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
682
|
+
#
|
683
|
+
# @since 0.1.0
|
684
|
+
# @api public
|
685
|
+
def remove_line(path, target)
|
686
|
+
content = adapter.readlines(path)
|
687
|
+
i = index(content, path, target)
|
688
|
+
|
689
|
+
content.delete_at(i)
|
690
|
+
write(path, content)
|
691
|
+
end
|
692
|
+
|
693
|
+
# Removes `target` block from `path`
|
694
|
+
#
|
695
|
+
# @param path [String,Pathname] the path to file
|
696
|
+
# @param target [String] the target block to remove
|
697
|
+
#
|
698
|
+
# @raise [Dry::Files::IOError] in case of I/O error
|
699
|
+
# @raise [Dry::Files::MissingTargetError] if `target` cannot be found in `path`
|
700
|
+
#
|
701
|
+
# @since 0.1.0
|
702
|
+
# @api public
|
703
|
+
#
|
704
|
+
# @example
|
705
|
+
# require "dry/files"
|
706
|
+
#
|
707
|
+
# puts File.read("app.rb")
|
708
|
+
#
|
709
|
+
# # class App
|
710
|
+
# # configure do
|
711
|
+
# # root __dir__
|
712
|
+
# # end
|
713
|
+
# # end
|
714
|
+
#
|
715
|
+
# Dry::Files.new.remove_block("app.rb", "configure")
|
716
|
+
#
|
717
|
+
# puts File.read("app.rb")
|
718
|
+
#
|
719
|
+
# # class App
|
720
|
+
# # end
|
721
|
+
def remove_block(path, target)
|
722
|
+
content = adapter.readlines(path)
|
723
|
+
starting = index(content, path, target)
|
724
|
+
line = content[starting]
|
725
|
+
size = line[SPACE_MATCHER].bytesize
|
726
|
+
closing = (SPACE * size) +
|
727
|
+
(target.match?(INLINE_OPEN_BLOCK_MATCHER) ? INLINE_CLOSE_BLOCK : CLOSE_BLOCK)
|
728
|
+
ending = starting + index(content[starting..-CONTENT_OFFSET], path, closing)
|
729
|
+
|
730
|
+
content.slice!(starting..ending)
|
731
|
+
write(path, content)
|
732
|
+
|
733
|
+
remove_block(path, target) if match?(content, target)
|
734
|
+
end
|
735
|
+
|
736
|
+
private
|
737
|
+
|
738
|
+
# @since 0.1.0
|
739
|
+
# @api private
|
740
|
+
NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
|
741
|
+
private_constant :NEW_LINE
|
742
|
+
|
743
|
+
# @since 0.1.0
|
744
|
+
# @api private
|
745
|
+
CONTENT_OFFSET = 1
|
746
|
+
private_constant :CONTENT_OFFSET
|
747
|
+
|
748
|
+
# @since 0.1.0
|
749
|
+
# @api private
|
750
|
+
SPACE = " "
|
751
|
+
private_constant :SPACE
|
752
|
+
|
753
|
+
# @since 0.1.0
|
754
|
+
# @api private
|
755
|
+
INDENTATION = 2
|
756
|
+
private_constant :INDENTATION
|
757
|
+
|
758
|
+
# @since 0.1.0
|
759
|
+
# @api private
|
760
|
+
SPACE_MATCHER = /\A[[:space:]]*/.freeze
|
761
|
+
private_constant :SPACE_MATCHER
|
762
|
+
|
763
|
+
# @since 0.1.0
|
764
|
+
# @api private
|
765
|
+
INLINE_OPEN_BLOCK_MATCHER = "{"
|
766
|
+
private_constant :INLINE_OPEN_BLOCK_MATCHER
|
767
|
+
|
768
|
+
# @since 0.1.0
|
769
|
+
# @api private
|
770
|
+
INLINE_CLOSE_BLOCK = "}"
|
771
|
+
private_constant :INLINE_CLOSE_BLOCK
|
772
|
+
|
773
|
+
# @since 0.1.0
|
774
|
+
# @api private
|
775
|
+
CLOSE_BLOCK = "end"
|
776
|
+
private_constant :CLOSE_BLOCK
|
777
|
+
|
778
|
+
# @since 0.1.0
|
779
|
+
# @api private
|
780
|
+
attr_reader :adapter
|
781
|
+
|
782
|
+
# @since 0.1.0
|
783
|
+
# @api private
|
784
|
+
def newline(line = nil)
|
785
|
+
"#{line}#{NEW_LINE}"
|
786
|
+
end
|
787
|
+
|
788
|
+
# @since 0.1.0
|
789
|
+
# @api private
|
790
|
+
def newline?(content)
|
791
|
+
content.end_with?(NEW_LINE)
|
792
|
+
end
|
793
|
+
|
794
|
+
# @since 0.1.0
|
795
|
+
# @api private
|
796
|
+
def match?(content, target)
|
797
|
+
!line_number(content, target).nil?
|
798
|
+
end
|
799
|
+
|
800
|
+
# @since 0.1.0
|
801
|
+
# @api private
|
802
|
+
def index(content, path, target)
|
803
|
+
line_number(content, target) or
|
804
|
+
raise MissingTargetError.new(target, path)
|
805
|
+
end
|
806
|
+
|
807
|
+
# @since 0.1.0
|
808
|
+
# @api private
|
809
|
+
def rindex(content, path, target)
|
810
|
+
line_number(content, target, finder: content.method(:rindex)) or
|
811
|
+
raise MissingTargetError.new(target, path)
|
812
|
+
end
|
813
|
+
|
814
|
+
# @since 0.1.0
|
815
|
+
# @api private
|
816
|
+
def _inject_line_before(path, target, contents, finder)
|
817
|
+
content = adapter.readlines(path)
|
818
|
+
i = finder.call(content, path, target)
|
819
|
+
|
820
|
+
content.insert(i, newline(contents))
|
821
|
+
write(path, content)
|
822
|
+
end
|
823
|
+
|
824
|
+
# @since 0.1.0
|
825
|
+
# @api private
|
826
|
+
def _inject_line_after(path, target, contents, finder)
|
827
|
+
content = adapter.readlines(path)
|
828
|
+
i = finder.call(content, path, target)
|
829
|
+
|
830
|
+
content.insert(i + CONTENT_OFFSET, newline(contents))
|
831
|
+
write(path, content)
|
832
|
+
end
|
833
|
+
|
834
|
+
# @since 0.1.0
|
835
|
+
# @api private
|
836
|
+
def _offset_block_lines(contents, offset)
|
837
|
+
contents.map do |line|
|
838
|
+
if line.match?(NEW_LINE)
|
839
|
+
line = line.split(NEW_LINE)
|
840
|
+
_offset_block_lines(line, offset)
|
841
|
+
else
|
842
|
+
offset + line + NEW_LINE
|
843
|
+
end
|
844
|
+
end.join
|
845
|
+
end
|
846
|
+
|
847
|
+
# @since 0.1.0
|
848
|
+
# @api private
|
849
|
+
def line_number(content, target, finder: content.method(:index))
|
850
|
+
finder.call do |l|
|
851
|
+
case target
|
852
|
+
when ::String
|
853
|
+
l.include?(target)
|
854
|
+
when Regexp
|
855
|
+
l =~ target
|
856
|
+
end
|
857
|
+
end
|
858
|
+
end
|
859
|
+
end
|
860
|
+
end
|