dry-cli 0.4.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/.codeclimate.yml +12 -0
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +77 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +70 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.md +31 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docsite/source/index.html.md +588 -0
- data/dry-cli.gemspec +36 -0
- data/lib/dry/cli.rb +129 -0
- data/lib/dry/cli/banner.rb +127 -0
- data/lib/dry/cli/command.rb +367 -0
- data/lib/dry/cli/command_registry.rb +211 -0
- data/lib/dry/cli/errors.rb +39 -0
- data/lib/dry/cli/option.rb +132 -0
- data/lib/dry/cli/parser.rb +140 -0
- data/lib/dry/cli/program_name.rb +21 -0
- data/lib/dry/cli/registry.rb +328 -0
- data/lib/dry/cli/usage.rb +91 -0
- data/lib/dry/cli/utils/files.rb +443 -0
- data/lib/dry/cli/version.rb +8 -0
- data/script/ci +51 -0
- metadata +169 -0
@@ -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
|