dry-cli 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class CLI
5
+ # Program name
6
+ #
7
+ # @since 0.1.0
8
+ # @api private
9
+ module ProgramName
10
+ # @since 0.1.0
11
+ # @api private
12
+ SEPARATOR = ' '
13
+
14
+ # @since 0.1.0
15
+ # @api private
16
+ def self.call(names = [], program_name: $PROGRAM_NAME)
17
+ [File.basename(program_name), names].flatten.join(SEPARATOR)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli/command_registry'
4
+
5
+ module Dry
6
+ class CLI
7
+ # Registry mixin
8
+ #
9
+ # @since 0.1.0
10
+ module Registry
11
+ # @since 0.1.0
12
+ # @api private
13
+ def self.extended(base)
14
+ base.class_eval do
15
+ @commands = CommandRegistry.new
16
+ end
17
+ end
18
+
19
+ # Register a command
20
+ #
21
+ # @param name [String] the command name
22
+ # @param command [NilClass,Dry::CLI::Command] the optional command
23
+ # @param aliases [Array<String>] an optional list of aliases
24
+ # @param options [Hash] a set of options
25
+ #
26
+ # @since 0.1.0
27
+ #
28
+ # @example Register a command
29
+ # require "dry/cli"
30
+ #
31
+ # module Foo
32
+ # module Commands
33
+ # extend Dry::CLI::Registry
34
+ #
35
+ # class Hello < Dry::CLI::Command
36
+ # end
37
+ #
38
+ # register "hi", Hello
39
+ # end
40
+ # end
41
+ #
42
+ # @example Register a command with aliases
43
+ # require "dry/cli"
44
+ #
45
+ # module Foo
46
+ # module Commands
47
+ # extend Dry::CLI::Registry
48
+ #
49
+ # class Hello < Dry::CLI::Command
50
+ # end
51
+ #
52
+ # register "hello", Hello, aliases: ["hi", "ciao"]
53
+ # end
54
+ # end
55
+ #
56
+ # @example Register a group of commands
57
+ # require "dry/cli"
58
+ #
59
+ # module Foo
60
+ # module Commands
61
+ # extend Dry::CLI::Registry
62
+ #
63
+ # module Generate
64
+ # class App < Dry::CLI::Command
65
+ # end
66
+ #
67
+ # class Action < Dry::CLI::Command
68
+ # end
69
+ # end
70
+ #
71
+ # register "generate", aliases: ["g"] do |prefix|
72
+ # prefix.register "app", Generate::App
73
+ # prefix.register "action", Generate::Action
74
+ # end
75
+ # end
76
+ # end
77
+ def register(name, command = nil, aliases: [], **options)
78
+ if block_given?
79
+ yield Prefix.new(@commands, name, aliases)
80
+ else
81
+ @commands.set(name, command, aliases, **options)
82
+ end
83
+ end
84
+
85
+ # Register a before callback.
86
+ #
87
+ # @param command_name [String] the name used for command registration
88
+ # @param callback [Class, #call] the callback object. If a class is given,
89
+ # it MUST respond to `#call`.
90
+ # @param blk [Proc] the callback espressed as a block
91
+ #
92
+ # @raise [Dry::CLI::UnknownCommandError] if the command isn't registered
93
+ # @raise [Dry::CLI::InvalidCallbackError] if the given callback doesn't
94
+ # implement the required interface
95
+ #
96
+ # @since 0.2.0
97
+ #
98
+ # @example
99
+ # require "dry/cli"
100
+ #
101
+ # module Foo
102
+ # module Commands
103
+ # extend Dry::CLI::Registry
104
+ #
105
+ # class Hello < Dry::CLI::Command
106
+ # def call(*)
107
+ # puts "hello"
108
+ # end
109
+ # end
110
+ #
111
+ # register "hello", Hello
112
+ # before "hello", -> { puts "I'm about to say.." }
113
+ # end
114
+ # end
115
+ #
116
+ # @example Register an object as callback
117
+ # require "dry/cli"
118
+ #
119
+ # module Callbacks
120
+ # class Hello
121
+ # def call(*)
122
+ # puts "world"
123
+ # end
124
+ # end
125
+ # end
126
+ #
127
+ # module Foo
128
+ # module Commands
129
+ # extend Dry::CLI::Registry
130
+ #
131
+ # class Hello < Dry::CLI::Command
132
+ # def call(*)
133
+ # puts "I'm about to say.."
134
+ # end
135
+ # end
136
+ #
137
+ # register "hello", Hello
138
+ # before "hello", Callbacks::Hello.new
139
+ # end
140
+ # end
141
+ #
142
+ # @example Register a class as callback
143
+ # require "dry/cli"
144
+ #
145
+ # module Callbacks
146
+ # class Hello
147
+ # def call(*)
148
+ # puts "world"
149
+ # end
150
+ # end
151
+ # end
152
+ #
153
+ # module Foo
154
+ # module Commands
155
+ # extend Dry::CLI::Registry
156
+ #
157
+ # class Hello < Dry::CLI::Command
158
+ # def call(*)
159
+ # puts "I'm about to say.."
160
+ # end
161
+ # end
162
+ #
163
+ # register "hello", Hello
164
+ # before "hello", Callbacks::Hello
165
+ # end
166
+ # end
167
+ def before(command_name, callback = nil, &blk)
168
+ command(command_name).before_callbacks.append(&_callback(callback, blk))
169
+ end
170
+
171
+ # Register an after callback.
172
+ #
173
+ # @param command_name [String] the name used for command registration
174
+ # @param callback [Class, #call] the callback object. If a class is given,
175
+ # it MUST respond to `#call`.
176
+ # @param blk [Proc] the callback espressed as a block
177
+ #
178
+ # @raise [Dry::CLI::UnknownCommandError] if the command isn't registered
179
+ # @raise [Dry::CLI::InvalidCallbackError] if the given callback doesn't
180
+ # implement the required interface
181
+ #
182
+ # @since 0.2.0
183
+ #
184
+ # @example
185
+ # require "dry/cli"
186
+ #
187
+ # module Foo
188
+ # module Commands
189
+ # extend Dry::CLI::Registry
190
+ #
191
+ # class Hello < Dry::CLI::Command
192
+ # def call(*)
193
+ # puts "hello"
194
+ # end
195
+ # end
196
+ #
197
+ # register "hello", Hello
198
+ # after "hello", -> { puts "world" }
199
+ # end
200
+ # end
201
+ #
202
+ # @example Register an object as callback
203
+ # require "dry/cli"
204
+ #
205
+ # module Callbacks
206
+ # class World
207
+ # def call(*)
208
+ # puts "world"
209
+ # end
210
+ # end
211
+ # end
212
+ #
213
+ # module Foo
214
+ # module Commands
215
+ # extend Dry::CLI::Registry
216
+ #
217
+ # class Hello < Dry::CLI::Command
218
+ # def call(*)
219
+ # puts "hello"
220
+ # end
221
+ # end
222
+ #
223
+ # register "hello", Hello
224
+ # after "hello", Callbacks::World.new
225
+ # end
226
+ # end
227
+ #
228
+ # @example Register a class as callback
229
+ # require "dry/cli"
230
+ #
231
+ # module Callbacks
232
+ # class World
233
+ # def call(*)
234
+ # puts "world"
235
+ # end
236
+ # end
237
+ # end
238
+ #
239
+ # module Foo
240
+ # module Commands
241
+ # extend Dry::CLI::Registry
242
+ #
243
+ # class Hello < Dry::CLI::Command
244
+ # def call(*)
245
+ # puts "hello"
246
+ # end
247
+ # end
248
+ #
249
+ # register "hello", Hello
250
+ # after "hello", Callbacks::World
251
+ # end
252
+ # end
253
+ def after(command_name, callback = nil, &blk)
254
+ command(command_name).after_callbacks.append(&_callback(callback, blk))
255
+ end
256
+
257
+ # @since 0.1.0
258
+ # @api private
259
+ def get(arguments)
260
+ @commands.get(arguments)
261
+ end
262
+
263
+ private
264
+
265
+ COMMAND_NAME_SEPARATOR = ' '
266
+
267
+ # @since 0.2.0
268
+ # @api private
269
+ def command(command_name)
270
+ get(command_name.split(COMMAND_NAME_SEPARATOR)).tap do |result|
271
+ raise UnknownCommandError, command_name unless result.found?
272
+ end
273
+ end
274
+
275
+ # @since 0.2.0
276
+ # @api private
277
+ #
278
+ def _callback(callback, blk)
279
+ return blk if blk.respond_to?(:to_proc)
280
+
281
+ case callback
282
+ when ->(c) { c.respond_to?(:call) }
283
+ callback.method(:call)
284
+ when Class
285
+ begin
286
+ _callback(callback.new, blk)
287
+ rescue ArgumentError
288
+ raise InvalidCallbackError, callback
289
+ end
290
+ else
291
+ raise InvalidCallbackError, callback
292
+ end
293
+ end
294
+
295
+ # Command name prefix
296
+ #
297
+ # @since 0.1.0
298
+ class Prefix
299
+ # @since 0.1.0
300
+ # @api private
301
+ def initialize(registry, prefix, aliases)
302
+ @registry = registry
303
+ @prefix = prefix
304
+
305
+ registry.set(prefix, nil, aliases)
306
+ end
307
+
308
+ # @since 0.1.0
309
+ #
310
+ # @see Dry::CLI::Registry#register
311
+ def register(name, command, aliases: [], **options)
312
+ command_name = "#{prefix} #{name}"
313
+ registry.set(command_name, command, aliases, **options)
314
+ end
315
+
316
+ private
317
+
318
+ # @since 0.1.0
319
+ # @api private
320
+ attr_reader :registry
321
+
322
+ # @since 0.1.0
323
+ # @api private
324
+ attr_reader :prefix
325
+ end
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli/program_name'
4
+
5
+ module Dry
6
+ class CLI
7
+ # Command(s) usage
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
11
+ module Usage
12
+ # @since 0.1.0
13
+ # @api private
14
+ SUBCOMMAND_BANNER = ' [SUBCOMMAND]'
15
+
16
+ # @since 0.1.0
17
+ # @api private
18
+ def self.call(result, out)
19
+ out.puts 'Commands:'
20
+ max_length, commands = commands_and_arguments(result)
21
+
22
+ commands.each do |banner, node|
23
+ usage = description(node.command) if node.leaf?
24
+ out.puts "#{justify(banner, max_length, usage)}#{usage}"
25
+ end
26
+ end
27
+
28
+ # @since 0.1.0
29
+ # @api private
30
+ def self.commands_and_arguments(result)
31
+ max_length = 0
32
+ ret = commands(result).each_with_object({}) do |(name, node), memo|
33
+ args = if node.leaf?
34
+ arguments(node.command)
35
+ else
36
+ SUBCOMMAND_BANNER
37
+ end
38
+
39
+ partial = " #{command_name(result, name)}#{args}"
40
+ max_length = partial.bytesize if max_length < partial.bytesize
41
+ memo[partial] = node
42
+ end
43
+
44
+ [max_length, ret]
45
+ end
46
+
47
+ # @since 0.1.0
48
+ # @api private
49
+ def self.arguments(command)
50
+ return unless CLI.command?(command)
51
+
52
+ required_arguments = command.required_arguments
53
+ optional_arguments = command.optional_arguments
54
+
55
+ required = required_arguments.map { |arg| arg.name.upcase }.join(' ') if required_arguments.any? # rubocop:disable Metrics/LineLength
56
+ optional = optional_arguments.map { |arg| "[#{arg.name.upcase}]" }.join(' ') if optional_arguments.any? # rubocop:disable Metrics/LineLength
57
+ result = [required, optional].compact
58
+
59
+ " #{result.join(' ')}" unless result.empty?
60
+ end
61
+
62
+ # @since 0.1.0
63
+ # @api private
64
+ def self.description(command)
65
+ return unless CLI.command?(command)
66
+
67
+ " # #{command.description}" unless command.description.nil?
68
+ end
69
+
70
+ # @since 0.1.0
71
+ # @api private
72
+ def self.justify(string, padding, usage)
73
+ return string.chomp(' ') if usage.nil?
74
+
75
+ string.ljust(padding + padding / 2)
76
+ end
77
+
78
+ # @since 0.1.0
79
+ # @api private
80
+ def self.commands(result)
81
+ result.children.sort_by { |name, _| name }
82
+ end
83
+
84
+ # @since 0.1.0
85
+ # @api private
86
+ def self.command_name(result, name)
87
+ ProgramName.call([result.names, name])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,443 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'fileutils'
5
+
6
+ module Dry
7
+ class CLI
8
+ module Utils
9
+ # Files utilities
10
+ #
11
+ # @since 0.3.1
12
+ module Files # rubocop:disable Metrics/ModuleLength
13
+ # Creates an empty file for the given path.
14
+ # All the intermediate directories are created.
15
+ # If the path already exists, it doesn't change the contents
16
+ #
17
+ # @param path [String,Pathname] the path to file
18
+ #
19
+ # @since 0.3.1
20
+ def self.touch(path)
21
+ mkdir_p(path)
22
+ FileUtils.touch(path)
23
+ end
24
+
25
+ # Creates a new file or rewrites the contents
26
+ # of an existing file for the given path and content
27
+ # All the intermediate directories are created.
28
+ #
29
+ # @param path [String,Pathname] the path to file
30
+ # @param content [String, Array<String>] the content to write
31
+ #
32
+ # @since 0.3.1
33
+ def self.write(path, *content)
34
+ mkdir_p(path)
35
+ open(path, ::File::CREAT | ::File::WRONLY | ::File::TRUNC, *content) # rubocop:disable LineLength, Security/Open - this isn't a call to `::Kernel.open`, but to `self.open`
36
+ end
37
+
38
+ # Copies source into destination.
39
+ # All the intermediate directories are created.
40
+ # If the destination already exists, it overrides the contents.
41
+ #
42
+ # @param source [String,Pathname] the path to the source file
43
+ # @param destination [String,Pathname] the path to the destination file
44
+ #
45
+ # @since 0.3.1
46
+ def self.cp(source, destination)
47
+ mkdir_p(destination)
48
+ FileUtils.cp(source, destination)
49
+ end
50
+
51
+ # Creates a directory for the given path.
52
+ # It assumes that all the tokens in `path` are meant to be a directory.
53
+ # All the intermediate directories are created.
54
+ #
55
+ # @param path [String,Pathname] the path to directory
56
+ #
57
+ # @since 0.3.1
58
+ #
59
+ # @see .mkdir_p
60
+ #
61
+ # @example
62
+ # require "dry/cli/utils/files"
63
+ #
64
+ # Dry::CLI::Utils::Files.mkdir("path/to/directory")
65
+ # # => creates the `path/to/directory` directory
66
+ #
67
+ # # WRONG this isn't probably what you want, check `.mkdir_p`
68
+ # Dry::CLI::Utils::Files.mkdir("path/to/file.rb")
69
+ # # => creates the `path/to/file.rb` directory
70
+ def self.mkdir(path)
71
+ FileUtils.mkdir_p(path)
72
+ end
73
+
74
+ # Creates a directory for the given path.
75
+ # It assumes that all the tokens, but the last, in `path` are meant to be
76
+ # a directory, whereas the last is meant to be a file.
77
+ # All the intermediate directories are created.
78
+ #
79
+ # @param path [String,Pathname] the path to directory
80
+ #
81
+ # @since 0.3.1
82
+ #
83
+ # @see .mkdir
84
+ #
85
+ # @example
86
+ # require "dry/cli/utils/files"
87
+ #
88
+ # Dry::CLI::Utils::Files.mkdir_p("path/to/file.rb")
89
+ # # => creates the `path/to` directory, but NOT `file.rb`
90
+ #
91
+ # # WRONG it doesn't create the last directory, check `.mkdir`
92
+ # Dry::CLI::Utils::Files.mkdir_p("path/to/directory")
93
+ # # => creates the `path/to` directory
94
+ def self.mkdir_p(path)
95
+ Pathname.new(path).dirname.mkpath
96
+ end
97
+
98
+ # Deletes given path (file).
99
+ #
100
+ # @param path [String,Pathname] the path to file
101
+ #
102
+ # @raise [Errno::ENOENT] if the path doesn't exist
103
+ #
104
+ # @since 0.3.1
105
+ def self.delete(path)
106
+ FileUtils.rm(path)
107
+ end
108
+
109
+ # Deletes given path (directory).
110
+ #
111
+ # @param path [String,Pathname] the path to file
112
+ #
113
+ # @raise [Errno::ENOENT] if the path doesn't exist
114
+ #
115
+ # @since 0.3.1
116
+ def self.delete_directory(path)
117
+ FileUtils.remove_entry_secure(path)
118
+ end
119
+
120
+ # Adds a new line at the top of the file
121
+ #
122
+ # @param path [String,Pathname] the path to file
123
+ # @param line [String] the line to add
124
+ #
125
+ # @raise [Errno::ENOENT] if the path doesn't exist
126
+ #
127
+ # @see .append
128
+ #
129
+ # @since 0.3.1
130
+ def self.unshift(path, line)
131
+ content = ::File.readlines(path)
132
+ content.unshift("#{line}\n")
133
+
134
+ write(path, content)
135
+ end
136
+
137
+ # Adds a new line at the bottom of the file
138
+ #
139
+ # @param path [String,Pathname] the path to file
140
+ # @param contents [String] the contents to add
141
+ #
142
+ # @raise [Errno::ENOENT] if the path doesn't exist
143
+ #
144
+ # @see .unshift
145
+ #
146
+ # @since 0.3.1
147
+ def self.append(path, contents)
148
+ mkdir_p(path)
149
+
150
+ content = ::File.readlines(path)
151
+ content << "\n" unless content.last.end_with?("\n")
152
+ content << "#{contents}\n"
153
+
154
+ write(path, content)
155
+ end
156
+
157
+ # Replace first line in `path` that contains `target` with `replacement`.
158
+ #
159
+ # @param path [String,Pathname] the path to file
160
+ # @param target [String,Regexp] the target to replace
161
+ # @param replacement [String] the replacement
162
+ #
163
+ # @raise [Errno::ENOENT] if the path doesn't exist
164
+ # @raise [ArgumentError] if `target` cannot be found in `path`
165
+ #
166
+ # @see .replace_last_line
167
+ #
168
+ # @since 0.3.1
169
+ def self.replace_first_line(path, target, replacement)
170
+ content = ::File.readlines(path)
171
+ content[index(content, path, target)] = "#{replacement}\n"
172
+
173
+ write(path, content)
174
+ end
175
+
176
+ # Replace last line in `path` that contains `target` with `replacement`.
177
+ #
178
+ # @param path [String,Pathname] the path to file
179
+ # @param target [String,Regexp] the target to replace
180
+ # @param replacement [String] the replacement
181
+ #
182
+ # @raise [Errno::ENOENT] if the path doesn't exist
183
+ # @raise [ArgumentError] if `target` cannot be found in `path`
184
+ #
185
+ # @see .replace_first_line
186
+ #
187
+ # @since 0.3.1
188
+ def self.replace_last_line(path, target, replacement)
189
+ content = ::File.readlines(path)
190
+ content[-index(content.reverse, path, target) - 1] = "#{replacement}\n"
191
+
192
+ write(path, content)
193
+ end
194
+
195
+ # Inject `contents` in `path` before `target`.
196
+ #
197
+ # @param path [String,Pathname] the path to file
198
+ # @param target [String,Regexp] the target to replace
199
+ # @param contents [String] the contents to inject
200
+ #
201
+ # @raise [Errno::ENOENT] if the path doesn't exist
202
+ # @raise [ArgumentError] if `target` cannot be found in `path`
203
+ #
204
+ # @see .inject_line_after
205
+ # @see .inject_line_before_last
206
+ # @see .inject_line_after_last
207
+ #
208
+ # @since 0.3.1
209
+ def self.inject_line_before(path, target, contents)
210
+ _inject_line_before(path, target, contents, method(:index))
211
+ end
212
+
213
+ # Inject `contents` in `path` after last `target`.
214
+ #
215
+ # @param path [String,Pathname] the path to file
216
+ # @param target [String,Regexp] the target to replace
217
+ # @param contents [String] the contents to inject
218
+ #
219
+ # @raise [Errno::ENOENT] if the path doesn't exist
220
+ # @raise [ArgumentError] if `target` cannot be found in `path`
221
+ #
222
+ # @see .inject_line_before
223
+ # @see .inject_line_after
224
+ # @see .inject_line_after_last
225
+ #
226
+ # @since 1.3.0
227
+ def self.inject_line_before_last(path, target, contents)
228
+ _inject_line_before(path, target, contents, method(:rindex))
229
+ end
230
+
231
+ # Inject `contents` in `path` after `target`.
232
+ #
233
+ # @param path [String,Pathname] the path to file
234
+ # @param target [String,Regexp] the target to replace
235
+ # @param contents [String] the contents to inject
236
+ #
237
+ # @raise [Errno::ENOENT] if the path doesn't exist
238
+ # @raise [ArgumentError] if `target` cannot be found in `path`
239
+ #
240
+ # @see .inject_line_before
241
+ # @see .inject_line_before_last
242
+ # @see .inject_line_after_last
243
+ #
244
+ # @since 0.3.1
245
+ def self.inject_line_after(path, target, contents)
246
+ _inject_line_after(path, target, contents, method(:index))
247
+ end
248
+
249
+ # Inject `contents` in `path` after last `target`.
250
+ #
251
+ # @param path [String,Pathname] the path to file
252
+ # @param target [String,Regexp] the target to replace
253
+ # @param contents [String] the contents to inject
254
+ #
255
+ # @raise [Errno::ENOENT] if the path doesn't exist
256
+ # @raise [ArgumentError] if `target` cannot be found in `path`
257
+ #
258
+ # @see .inject_line_before
259
+ # @see .inject_line_after
260
+ # @see .inject_line_before_last
261
+ # @see .inject_line_after_last
262
+ #
263
+ # @since 1.3.0
264
+ def self.inject_line_after_last(path, target, contents)
265
+ _inject_line_after(path, target, contents, method(:rindex))
266
+ end
267
+
268
+ # Removes line from `path`, matching `target`.
269
+ #
270
+ # @param path [String,Pathname] the path to file
271
+ # @param target [String,Regexp] the target to remove
272
+ #
273
+ # @raise [Errno::ENOENT] if the path doesn't exist
274
+ # @raise [ArgumentError] if `target` cannot be found in `path`
275
+ #
276
+ # @since 0.3.1
277
+ def self.remove_line(path, target)
278
+ content = ::File.readlines(path)
279
+ i = index(content, path, target)
280
+
281
+ content.delete_at(i)
282
+ write(path, content)
283
+ end
284
+
285
+ # Removes `target` block from `path`
286
+ #
287
+ # @param path [String,Pathname] the path to file
288
+ # @param target [String] the target block to remove
289
+ #
290
+ # @raise [Errno::ENOENT] if the path doesn't exist
291
+ # @raise [ArgumentError] if `target` cannot be found in `path`
292
+ #
293
+ # @since 0.3.1
294
+ #
295
+ # @example
296
+ # require "dry/cli/utils/files"
297
+ #
298
+ # puts File.read("app.rb")
299
+ #
300
+ # # class App
301
+ # # configure do
302
+ # # root __dir__
303
+ # # end
304
+ # # end
305
+ #
306
+ # Dry::CLI::Utils::Files.remove_block("app.rb", "configure")
307
+ #
308
+ # puts File.read("app.rb")
309
+ #
310
+ # # class App
311
+ # # end
312
+ def self.remove_block(path, target)
313
+ content = ::File.readlines(path)
314
+ starting = index(content, path, target)
315
+ line = content[starting]
316
+ size = line[/\A[[:space:]]*/].bytesize
317
+ closing = (' ' * size) + (target.match?(/{/) ? '}' : 'end')
318
+ ending = starting + index(content[starting..-1], path, closing)
319
+
320
+ content.slice!(starting..ending)
321
+ write(path, content)
322
+
323
+ remove_block(path, target) if match?(content, target)
324
+ end
325
+
326
+ # Checks if `path` exist
327
+ #
328
+ # @param path [String,Pathname] the path to file
329
+ #
330
+ # @return [TrueClass,FalseClass] the result of the check
331
+ #
332
+ # @since 0.3.1
333
+ #
334
+ # @example
335
+ # require "dry/cli/utils/files"
336
+ #
337
+ # Dry::CLI::Utils::Files.exist?(__FILE__) # => true
338
+ # Dry::CLI::Utils::Files.exist?(__dir__) # => true
339
+ #
340
+ # Dry::CLI::Utils::Files.exist?("missing_file") # => false
341
+ def self.exist?(path)
342
+ File.exist?(path)
343
+ end
344
+
345
+ # Checks if `path` is a directory
346
+ #
347
+ # @param path [String,Pathname] the path to directory
348
+ #
349
+ # @return [TrueClass,FalseClass] the result of the check
350
+ #
351
+ # @since 0.3.1
352
+ #
353
+ # @example
354
+ # require "dry/cli/utils/files"
355
+ #
356
+ # Dry::CLI::Utils::Files.directory?(__dir__) # => true
357
+ # Dry::CLI::Utils::Files.directory?(__FILE__) # => false
358
+ #
359
+ # Dry::CLI::Utils::Files.directory?("missing_directory") # => false
360
+ def self.directory?(path)
361
+ File.directory?(path)
362
+ end
363
+
364
+ # private
365
+
366
+ # @since 0.3.1
367
+ # @api private
368
+ def self.match?(content, target)
369
+ !line_number(content, target).nil?
370
+ end
371
+
372
+ private_class_method :match?
373
+
374
+ # @since 0.3.1
375
+ # @api private
376
+ def self.open(path, mode, *content)
377
+ ::File.open(path, mode) do |file|
378
+ file.write(Array(content).flatten.join)
379
+ end
380
+ end
381
+
382
+ private_class_method :open
383
+
384
+ # @since 0.3.1
385
+ # @api private
386
+ def self.index(content, path, target)
387
+ line_number(content, target) ||
388
+ raise(ArgumentError, "Cannot find `#{target}' inside `#{path}'.")
389
+ end
390
+
391
+ private_class_method :index
392
+
393
+ # @since 1.3.0
394
+ # @api private
395
+ def self.rindex(content, path, target)
396
+ line_number(content, target, finder: content.method(:rindex)) ||
397
+ raise(ArgumentError, "Cannot find `#{target}' inside `#{path}'.")
398
+ end
399
+
400
+ private_class_method :rindex
401
+
402
+ # @since 1.3.0
403
+ # @api private
404
+ def self._inject_line_before(path, target, contents, finder)
405
+ content = ::File.readlines(path)
406
+ i = finder.call(content, path, target)
407
+
408
+ content.insert(i, "#{contents}\n")
409
+ write(path, content)
410
+ end
411
+
412
+ private_class_method :_inject_line_before
413
+
414
+ # @since 1.3.0
415
+ # @api private
416
+ def self._inject_line_after(path, target, contents, finder)
417
+ content = ::File.readlines(path)
418
+ i = finder.call(content, path, target)
419
+
420
+ content.insert(i + 1, "#{contents}\n")
421
+ write(path, content)
422
+ end
423
+
424
+ private_class_method :_inject_line_after
425
+
426
+ # @since 0.3.1
427
+ # @api private
428
+ def self.line_number(content, target, finder: content.method(:index))
429
+ finder.call do |l|
430
+ case target
431
+ when ::String
432
+ l.include?(target)
433
+ when Regexp
434
+ l =~ target
435
+ end
436
+ end
437
+ end
438
+
439
+ private_class_method :line_number
440
+ end
441
+ end
442
+ end
443
+ end