thor 1.2.2 → 1.5.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 +4 -4
- data/CONTRIBUTING.md +17 -0
- data/README.md +2 -3
- data/lib/thor/actions/create_file.rb +2 -1
- data/lib/thor/actions/directory.rb +1 -1
- data/lib/thor/actions/empty_directory.rb +1 -1
- data/lib/thor/actions/file_manipulation.rb +51 -19
- data/lib/thor/actions/inject_into_file.rb +49 -5
- data/lib/thor/actions.rb +14 -15
- data/lib/thor/base.rb +138 -10
- data/lib/thor/command.rb +13 -4
- data/lib/thor/core_ext/hash_with_indifferent_access.rb +4 -0
- data/lib/thor/error.rb +18 -23
- data/lib/thor/group.rb +11 -0
- data/lib/thor/invocation.rb +1 -1
- data/lib/thor/nested_context.rb +2 -2
- data/lib/thor/parser/argument.rb +17 -1
- data/lib/thor/parser/arguments.rb +32 -16
- data/lib/thor/parser/option.rb +21 -6
- data/lib/thor/parser/options.rb +44 -5
- data/lib/thor/runner.rb +12 -12
- data/lib/thor/shell/basic.rb +47 -167
- data/lib/thor/shell/color.rb +6 -46
- data/lib/thor/shell/column_printer.rb +29 -0
- data/lib/thor/shell/html.rb +4 -46
- data/lib/thor/shell/lcs_diff.rb +49 -0
- data/lib/thor/shell/table_printer.rb +118 -0
- data/lib/thor/shell/terminal.rb +42 -0
- data/lib/thor/shell/wrapped_printer.rb +38 -0
- data/lib/thor/shell.rb +1 -1
- data/lib/thor/util.rb +4 -3
- data/lib/thor/version.rb +1 -1
- data/lib/thor.rb +199 -8
- data/thor.gemspec +16 -12
- metadata +13 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83a74d97baca896e3f78dfdcb081c118c8599ecbcf79085cbff299f393ccdd7c
|
|
4
|
+
data.tar.gz: c96fd32b0d35ea099f176a8febae1eb8814bf81548c442eb3a43d05d1c58f407
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e81da702b50b15939c310e1f24b1410bfed9d29364aeaa1972e97c1faf34102853531ee55897358a43914b49351d7a1d213d888b302a24c3d1f779b26bf4d310
|
|
7
|
+
data.tar.gz: bf2139f49455edc3a076a4e2eefd7db7a2b8be46038638f1fc7203f378cffb49317044d0ad51af172b74c3fce163c5a8b8d85c5c99ee8227137475c0632b614a
|
data/CONTRIBUTING.md
CHANGED
|
@@ -13,3 +13,20 @@ Here are some reasons why a pull request may not be merged:
|
|
|
13
13
|
If you would like to help in this process, you can start by evaluating open pull requests against the criteria above. For example, if a pull request does not include specs for new functionality, you can add a comment like: “If you would like this feature to be added to Thor, please add specs to ensure that it does not break in the future.” This will help move a pull request closer to being merged.
|
|
14
14
|
|
|
15
15
|
Include this emoji in the top of your ticket to signal to us that you read this file: 🌈
|
|
16
|
+
|
|
17
|
+
Specs
|
|
18
|
+
-----
|
|
19
|
+
|
|
20
|
+
Ensure that all specs and code linting checks pass before submitting a pull request.
|
|
21
|
+
|
|
22
|
+
To execute the specs locally, run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle exec rspec
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Linting checks are done with RuboCop. To run the linter, use:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle exec rubocop
|
|
32
|
+
```
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ users.
|
|
|
15
15
|
|
|
16
16
|
Please note: Thor, by design, is a system tool created to allow seamless file and url
|
|
17
17
|
access, which should not receive application user input. It relies on [open-uri][open-uri],
|
|
18
|
-
which combined with application user input would provide a command injection attack
|
|
18
|
+
which, combined with application user input, would provide a command injection attack
|
|
19
19
|
vector.
|
|
20
20
|
|
|
21
21
|
[rake]: https://github.com/ruby/rake
|
|
@@ -27,10 +27,9 @@ Installation
|
|
|
27
27
|
|
|
28
28
|
Usage and documentation
|
|
29
29
|
-----------------------
|
|
30
|
-
Please see the [wiki][] for basic usage and other documentation on using Thor.
|
|
30
|
+
Please see the [wiki][] for basic usage and other documentation on using Thor.
|
|
31
31
|
|
|
32
32
|
[wiki]: https://github.com/rails/thor/wiki
|
|
33
|
-
[homepage]: http://whatisthor.com/
|
|
34
33
|
|
|
35
34
|
Contributing
|
|
36
35
|
------------
|
|
@@ -43,7 +43,8 @@ class Thor
|
|
|
43
43
|
# Boolean:: true if it is identical, false otherwise.
|
|
44
44
|
#
|
|
45
45
|
def identical?
|
|
46
|
-
|
|
46
|
+
# binread uses ASCII-8BIT, so to avoid false negatives, the string must use the same
|
|
47
|
+
exists? && File.binread(destination) == String.new(render).force_encoding("ASCII-8BIT")
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Holds the content to be added to the file.
|
|
@@ -58,7 +58,7 @@ class Thor
|
|
|
58
58
|
def initialize(base, source, destination = nil, config = {}, &block)
|
|
59
59
|
@source = File.expand_path(Dir[Util.escape_globs(base.find_in_source_paths(source.to_s))].first)
|
|
60
60
|
@block = block
|
|
61
|
-
super(base, destination, {:
|
|
61
|
+
super(base, destination, {recursive: true}.merge(config))
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def invoke!
|
|
@@ -10,7 +10,6 @@ class Thor
|
|
|
10
10
|
# destination<String>:: the relative path to the destination root.
|
|
11
11
|
# config<Hash>:: give :verbose => false to not log the status, and
|
|
12
12
|
# :mode => :preserve, to preserve the file mode from the source.
|
|
13
|
-
|
|
14
13
|
#
|
|
15
14
|
# ==== Examples
|
|
16
15
|
#
|
|
@@ -66,12 +65,15 @@ class Thor
|
|
|
66
65
|
# ==== Parameters
|
|
67
66
|
# source<String>:: the address of the given content.
|
|
68
67
|
# destination<String>:: the relative path to the destination root.
|
|
69
|
-
# config<Hash>:: give :verbose => false to not log the status
|
|
68
|
+
# config<Hash>:: give :verbose => false to not log the status, and
|
|
69
|
+
# :http_headers => <Hash> to add headers to an http request.
|
|
70
70
|
#
|
|
71
71
|
# ==== Examples
|
|
72
72
|
#
|
|
73
73
|
# get "http://gist.github.com/103208", "doc/README"
|
|
74
74
|
#
|
|
75
|
+
# get "http://gist.github.com/103208", "doc/README", :http_headers => {"Content-Type" => "application/json"}
|
|
76
|
+
#
|
|
75
77
|
# get "http://gist.github.com/103208" do |content|
|
|
76
78
|
# content.split("\n").first
|
|
77
79
|
# end
|
|
@@ -82,7 +84,7 @@ class Thor
|
|
|
82
84
|
|
|
83
85
|
render = if source =~ %r{^https?\://}
|
|
84
86
|
require "open-uri"
|
|
85
|
-
URI.send(:open, source) { |input| input.binmode.read }
|
|
87
|
+
URI.send(:open, source, config.fetch(:http_headers, {})) { |input| input.binmode.read }
|
|
86
88
|
else
|
|
87
89
|
source = File.expand_path(find_in_source_paths(source.to_s))
|
|
88
90
|
File.open(source) { |input| input.binmode.read }
|
|
@@ -120,12 +122,7 @@ class Thor
|
|
|
120
122
|
context = config.delete(:context) || instance_eval("binding")
|
|
121
123
|
|
|
122
124
|
create_file destination, nil, config do
|
|
123
|
-
|
|
124
|
-
capturable_erb = if match && match[1] >= "2.2.0" # Ruby 2.6+
|
|
125
|
-
CapturableERB.new(::File.binread(source), :trim_mode => "-", :eoutvar => "@output_buffer")
|
|
126
|
-
else
|
|
127
|
-
CapturableERB.new(::File.binread(source), nil, "-", "@output_buffer")
|
|
128
|
-
end
|
|
125
|
+
capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer")
|
|
129
126
|
content = capturable_erb.tap do |erb|
|
|
130
127
|
erb.filename = source
|
|
131
128
|
end.result(context)
|
|
@@ -245,6 +242,35 @@ class Thor
|
|
|
245
242
|
insert_into_file(path, *(args << config), &block)
|
|
246
243
|
end
|
|
247
244
|
|
|
245
|
+
# Run a regular expression replacement on a file, raising an error if the
|
|
246
|
+
# contents of the file are not changed.
|
|
247
|
+
#
|
|
248
|
+
# ==== Parameters
|
|
249
|
+
# path<String>:: path of the file to be changed
|
|
250
|
+
# flag<Regexp|String>:: the regexp or string to be replaced
|
|
251
|
+
# replacement<String>:: the replacement, can be also given as a block
|
|
252
|
+
# config<Hash>:: give :verbose => false to not log the status, and
|
|
253
|
+
# :force => true, to force the replacement regardless of runner behavior.
|
|
254
|
+
#
|
|
255
|
+
# ==== Example
|
|
256
|
+
#
|
|
257
|
+
# gsub_file! 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'
|
|
258
|
+
#
|
|
259
|
+
# gsub_file! 'README', /rake/, :green do |match|
|
|
260
|
+
# match << " no more. Use thor!"
|
|
261
|
+
# end
|
|
262
|
+
#
|
|
263
|
+
def gsub_file!(path, flag, *args, &block)
|
|
264
|
+
config = args.last.is_a?(Hash) ? args.pop : {}
|
|
265
|
+
|
|
266
|
+
return unless behavior == :invoke || config.fetch(:force, false)
|
|
267
|
+
|
|
268
|
+
path = File.expand_path(path, destination_root)
|
|
269
|
+
say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
|
|
270
|
+
|
|
271
|
+
actually_gsub_file(path, flag, args, true, &block) unless options[:pretend]
|
|
272
|
+
end
|
|
273
|
+
|
|
248
274
|
# Run a regular expression replacement on a file.
|
|
249
275
|
#
|
|
250
276
|
# ==== Parameters
|
|
@@ -270,16 +296,11 @@ class Thor
|
|
|
270
296
|
path = File.expand_path(path, destination_root)
|
|
271
297
|
say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
|
|
272
298
|
|
|
273
|
-
unless options[:pretend]
|
|
274
|
-
content = File.binread(path)
|
|
275
|
-
content.gsub!(flag, *args, &block)
|
|
276
|
-
File.open(path, "wb") { |file| file.write(content) }
|
|
277
|
-
end
|
|
299
|
+
actually_gsub_file(path, flag, args, false, &block) unless options[:pretend]
|
|
278
300
|
end
|
|
279
301
|
|
|
280
|
-
# Uncomment all lines matching a given regex.
|
|
281
|
-
#
|
|
282
|
-
# between the comment hash and the beginning of the line.
|
|
302
|
+
# Uncomment all lines matching a given regex. Preserves indentation before
|
|
303
|
+
# the comment hash and removes the hash and any immediate following space.
|
|
283
304
|
#
|
|
284
305
|
# ==== Parameters
|
|
285
306
|
# path<String>:: path of the file to be changed
|
|
@@ -293,7 +314,7 @@ class Thor
|
|
|
293
314
|
def uncomment_lines(path, flag, *args)
|
|
294
315
|
flag = flag.respond_to?(:source) ? flag.source : flag
|
|
295
316
|
|
|
296
|
-
gsub_file(path, /^(\s*)#[[:blank:]]
|
|
317
|
+
gsub_file(path, /^(\s*)#[[:blank:]]?(.*#{flag})/, '\1\2', *args)
|
|
297
318
|
end
|
|
298
319
|
|
|
299
320
|
# Comment all lines matching a given regex. It will leave the space
|
|
@@ -352,7 +373,7 @@ class Thor
|
|
|
352
373
|
end
|
|
353
374
|
|
|
354
375
|
def with_output_buffer(buf = "".dup) #:nodoc:
|
|
355
|
-
raise ArgumentError, "Buffer
|
|
376
|
+
raise ArgumentError, "Buffer cannot be a frozen object" if buf.frozen?
|
|
356
377
|
old_buffer = output_buffer
|
|
357
378
|
self.output_buffer = buf
|
|
358
379
|
yield
|
|
@@ -361,6 +382,17 @@ class Thor
|
|
|
361
382
|
self.output_buffer = old_buffer
|
|
362
383
|
end
|
|
363
384
|
|
|
385
|
+
def actually_gsub_file(path, flag, args, error_on_no_change, &block)
|
|
386
|
+
content = File.binread(path)
|
|
387
|
+
success = content.gsub!(flag, *args, &block)
|
|
388
|
+
|
|
389
|
+
if success.nil? && error_on_no_change
|
|
390
|
+
raise Thor::Error, "The content of #{path} did not change"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
File.open(path, "wb") { |file| file.write(content) }
|
|
394
|
+
end
|
|
395
|
+
|
|
364
396
|
# Thor::Actions#capture depends on what kind of buffer is used in ERB.
|
|
365
397
|
# Thus CapturableERB fixes ERB to use String buffer.
|
|
366
398
|
class CapturableERB < ERB
|
|
@@ -2,6 +2,38 @@ require_relative "empty_directory"
|
|
|
2
2
|
|
|
3
3
|
class Thor
|
|
4
4
|
module Actions
|
|
5
|
+
WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"}
|
|
6
|
+
|
|
7
|
+
# Injects the given content into a file, raising an error if the contents of
|
|
8
|
+
# the file are not changed. Different from gsub_file, this method is reversible.
|
|
9
|
+
#
|
|
10
|
+
# ==== Parameters
|
|
11
|
+
# destination<String>:: Relative path to the destination root
|
|
12
|
+
# data<String>:: Data to add to the file. Can be given as a block.
|
|
13
|
+
# config<Hash>:: give :verbose => false to not log the status and the flag
|
|
14
|
+
# for injection (:after or :before) or :force => true for
|
|
15
|
+
# insert two or more times the same content.
|
|
16
|
+
#
|
|
17
|
+
# ==== Examples
|
|
18
|
+
#
|
|
19
|
+
# insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n"
|
|
20
|
+
#
|
|
21
|
+
# insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
|
|
22
|
+
# gems = ask "Which gems would you like to add?"
|
|
23
|
+
# gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
def insert_into_file!(destination, *args, &block)
|
|
27
|
+
data = block_given? ? block : args.shift
|
|
28
|
+
|
|
29
|
+
config = args.shift || {}
|
|
30
|
+
config[:after] = /\z/ unless config.key?(:before) || config.key?(:after)
|
|
31
|
+
config = config.merge({error_on_no_change: true})
|
|
32
|
+
|
|
33
|
+
action InjectIntoFile.new(self, destination, data, config)
|
|
34
|
+
end
|
|
35
|
+
alias_method :inject_into_file!, :insert_into_file!
|
|
36
|
+
|
|
5
37
|
# Injects the given content into a file. Different from gsub_file, this
|
|
6
38
|
# method is reversible.
|
|
7
39
|
#
|
|
@@ -21,8 +53,6 @@ class Thor
|
|
|
21
53
|
# gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
|
|
22
54
|
# end
|
|
23
55
|
#
|
|
24
|
-
WARNINGS = { unchanged_no_flag: 'File unchanged! Either the supplied flag value not found or the content has already been inserted!' }
|
|
25
|
-
|
|
26
56
|
def insert_into_file(destination, *args, &block)
|
|
27
57
|
data = block_given? ? block : args.shift
|
|
28
58
|
|
|
@@ -37,7 +67,7 @@ class Thor
|
|
|
37
67
|
attr_reader :replacement, :flag, :behavior
|
|
38
68
|
|
|
39
69
|
def initialize(base, destination, data, config)
|
|
40
|
-
super(base, destination, {:
|
|
70
|
+
super(base, destination, {verbose: true}.merge(config))
|
|
41
71
|
|
|
42
72
|
@behavior, @flag = if @config.key?(:after)
|
|
43
73
|
[:after, @config.delete(:after)]
|
|
@@ -47,6 +77,7 @@ class Thor
|
|
|
47
77
|
|
|
48
78
|
@replacement = data.is_a?(Proc) ? data.call : data
|
|
49
79
|
@flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp)
|
|
80
|
+
@error_on_no_change = @config.fetch(:error_on_no_change, false)
|
|
50
81
|
end
|
|
51
82
|
|
|
52
83
|
def invoke!
|
|
@@ -59,6 +90,10 @@ class Thor
|
|
|
59
90
|
if exists?
|
|
60
91
|
if replace!(/#{flag}/, content, config[:force])
|
|
61
92
|
say_status(:invoke)
|
|
93
|
+
elsif @error_on_no_change
|
|
94
|
+
raise Thor::Error, "The content of #{destination} did not change"
|
|
95
|
+
elsif replacement_present?
|
|
96
|
+
say_status(:unchanged, color: :blue)
|
|
62
97
|
else
|
|
63
98
|
say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
|
|
64
99
|
end
|
|
@@ -96,6 +131,8 @@ class Thor
|
|
|
96
131
|
end
|
|
97
132
|
elsif warning
|
|
98
133
|
warning
|
|
134
|
+
elsif behavior == :unchanged
|
|
135
|
+
:unchanged
|
|
99
136
|
else
|
|
100
137
|
:subtract
|
|
101
138
|
end
|
|
@@ -103,11 +140,18 @@ class Thor
|
|
|
103
140
|
super(status, (color || config[:verbose]))
|
|
104
141
|
end
|
|
105
142
|
|
|
143
|
+
def content
|
|
144
|
+
@content ||= File.read(destination)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def replacement_present?
|
|
148
|
+
content.include?(replacement)
|
|
149
|
+
end
|
|
150
|
+
|
|
106
151
|
# Adds the content to the file.
|
|
107
152
|
#
|
|
108
153
|
def replace!(regexp, string, force)
|
|
109
|
-
|
|
110
|
-
if force || !content.include?(replacement)
|
|
154
|
+
if force || !replacement_present?
|
|
111
155
|
success = content.gsub!(regexp, string)
|
|
112
156
|
|
|
113
157
|
File.open(destination, "wb") { |file| file.write(content) } unless pretend?
|
data/lib/thor/actions.rb
CHANGED
|
@@ -46,17 +46,17 @@ class Thor
|
|
|
46
46
|
# Add runtime options that help actions execution.
|
|
47
47
|
#
|
|
48
48
|
def add_runtime_options!
|
|
49
|
-
class_option :force, :
|
|
50
|
-
:
|
|
49
|
+
class_option :force, type: :boolean, aliases: "-f", group: :runtime,
|
|
50
|
+
desc: "Overwrite files that already exist"
|
|
51
51
|
|
|
52
|
-
class_option :pretend, :
|
|
53
|
-
:
|
|
52
|
+
class_option :pretend, type: :boolean, aliases: "-p", group: :runtime,
|
|
53
|
+
desc: "Run but do not make any changes"
|
|
54
54
|
|
|
55
|
-
class_option :quiet, :
|
|
56
|
-
:
|
|
55
|
+
class_option :quiet, type: :boolean, aliases: "-q", group: :runtime,
|
|
56
|
+
desc: "Suppress status output"
|
|
57
57
|
|
|
58
|
-
class_option :skip, :
|
|
59
|
-
:
|
|
58
|
+
class_option :skip, type: :boolean, aliases: "-s", group: :runtime,
|
|
59
|
+
desc: "Skip files that already exist"
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -113,9 +113,9 @@ class Thor
|
|
|
113
113
|
#
|
|
114
114
|
def relative_to_original_destination_root(path, remove_dot = true)
|
|
115
115
|
root = @destination_stack[0]
|
|
116
|
-
if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil,
|
|
116
|
+
if path.start_with?(root) && [File::SEPARATOR, File::ALT_SEPARATOR, nil, ""].include?(path[root.size..root.size])
|
|
117
117
|
path = path.dup
|
|
118
|
-
path[0...root.size] =
|
|
118
|
+
path[0...root.size] = "."
|
|
119
119
|
remove_dot ? (path[2..-1] || "") : path
|
|
120
120
|
else
|
|
121
121
|
path
|
|
@@ -223,8 +223,7 @@ class Thor
|
|
|
223
223
|
|
|
224
224
|
contents = if is_uri
|
|
225
225
|
require "open-uri"
|
|
226
|
-
|
|
227
|
-
URI.send(:open, path, "Accept" => "application/x-thor-template", &:read)
|
|
226
|
+
URI.open(path, "Accept" => "application/x-thor-template", &:read)
|
|
228
227
|
else
|
|
229
228
|
File.open(path, &:read)
|
|
230
229
|
end
|
|
@@ -285,7 +284,7 @@ class Thor
|
|
|
285
284
|
#
|
|
286
285
|
def run_ruby_script(command, config = {})
|
|
287
286
|
return unless behavior == :invoke
|
|
288
|
-
run command, config.merge(:
|
|
287
|
+
run command, config.merge(with: Thor::Util.ruby_command)
|
|
289
288
|
end
|
|
290
289
|
|
|
291
290
|
# Run a thor command. A hash of options can be given and it's converted to
|
|
@@ -316,7 +315,7 @@ class Thor
|
|
|
316
315
|
args.push Thor::Options.to_switches(config)
|
|
317
316
|
command = args.join(" ").strip
|
|
318
317
|
|
|
319
|
-
run command, :
|
|
318
|
+
run command, with: :thor, verbose: verbose, pretend: pretend, capture: capture
|
|
320
319
|
end
|
|
321
320
|
|
|
322
321
|
protected
|
|
@@ -324,7 +323,7 @@ class Thor
|
|
|
324
323
|
# Allow current root to be shared between invocations.
|
|
325
324
|
#
|
|
326
325
|
def _shared_configuration #:nodoc:
|
|
327
|
-
super.merge!(:
|
|
326
|
+
super.merge!(destination_root: destination_root)
|
|
328
327
|
end
|
|
329
328
|
|
|
330
329
|
def _cleanup_options_and_set(options, key) #:nodoc:
|
data/lib/thor/base.rb
CHANGED
|
@@ -13,8 +13,9 @@ class Thor
|
|
|
13
13
|
autoload :RakeCompat, File.expand_path("rake_compat", __dir__)
|
|
14
14
|
autoload :Group, File.expand_path("group", __dir__)
|
|
15
15
|
|
|
16
|
-
# Shortcuts for help.
|
|
16
|
+
# Shortcuts for help and tree commands.
|
|
17
17
|
HELP_MAPPINGS = %w(-h -? --help -D)
|
|
18
|
+
TREE_MAPPINGS = %w(-t --tree)
|
|
18
19
|
|
|
19
20
|
# Thor methods that should not be overwritten by the user.
|
|
20
21
|
THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root
|
|
@@ -24,9 +25,9 @@ class Thor
|
|
|
24
25
|
|
|
25
26
|
class << self
|
|
26
27
|
def deprecation_warning(message) #:nodoc:
|
|
27
|
-
unless ENV[
|
|
28
|
+
unless ENV["THOR_SILENCE_DEPRECATION"]
|
|
28
29
|
warn "Deprecation warning: #{message}\n" +
|
|
29
|
-
|
|
30
|
+
"You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION."
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -60,6 +61,7 @@ class Thor
|
|
|
60
61
|
|
|
61
62
|
command_options = config.delete(:command_options) # hook for start
|
|
62
63
|
parse_options = parse_options.merge(command_options) if command_options
|
|
64
|
+
|
|
63
65
|
if local_options.is_a?(Array)
|
|
64
66
|
array_options = local_options
|
|
65
67
|
hash_options = {}
|
|
@@ -73,9 +75,24 @@ class Thor
|
|
|
73
75
|
# Let Thor::Options parse the options first, so it can remove
|
|
74
76
|
# declared options from the array. This will leave us with
|
|
75
77
|
# a list of arguments that weren't declared.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
current_command = config[:current_command]
|
|
79
|
+
stop_on_unknown = self.class.stop_on_unknown_option? current_command
|
|
80
|
+
|
|
81
|
+
# Give a relation of options.
|
|
82
|
+
# After parsing, Thor::Options check whether right relations are kept
|
|
83
|
+
relations = if current_command.nil?
|
|
84
|
+
{exclusive_option_names: [], at_least_one_option_names: []}
|
|
85
|
+
else
|
|
86
|
+
current_command.options_relation
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
self.class.class_exclusive_option_names.map { |n| relations[:exclusive_option_names] << n }
|
|
90
|
+
self.class.class_at_least_one_option_names.map { |n| relations[:at_least_one_option_names] << n }
|
|
91
|
+
|
|
92
|
+
disable_required_check = self.class.disable_required_check? current_command
|
|
93
|
+
|
|
94
|
+
opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)
|
|
95
|
+
|
|
79
96
|
self.options = opts.parse(array_options)
|
|
80
97
|
self.options = config[:class_options].merge(options) if config[:class_options]
|
|
81
98
|
|
|
@@ -310,9 +327,92 @@ class Thor
|
|
|
310
327
|
# :hide:: -- If you want to hide this option from the help.
|
|
311
328
|
#
|
|
312
329
|
def class_option(name, options = {})
|
|
330
|
+
unless [ Symbol, String ].any? { |klass| name.is_a?(klass) }
|
|
331
|
+
raise ArgumentError, "Expected a Symbol or String, got #{name.inspect}"
|
|
332
|
+
end
|
|
313
333
|
build_option(name, options, class_options)
|
|
314
334
|
end
|
|
315
335
|
|
|
336
|
+
# Adds and declares option group for exclusive options in the
|
|
337
|
+
# block and arguments. You can declare options as the outside of the block.
|
|
338
|
+
#
|
|
339
|
+
# ==== Parameters
|
|
340
|
+
# Array[Thor::Option.name]
|
|
341
|
+
#
|
|
342
|
+
# ==== Examples
|
|
343
|
+
#
|
|
344
|
+
# class_exclusive do
|
|
345
|
+
# class_option :one
|
|
346
|
+
# class_option :two
|
|
347
|
+
# end
|
|
348
|
+
#
|
|
349
|
+
# Or
|
|
350
|
+
#
|
|
351
|
+
# class_option :one
|
|
352
|
+
# class_option :two
|
|
353
|
+
# class_exclusive :one, :two
|
|
354
|
+
#
|
|
355
|
+
# If you give "--one" and "--two" at the same time ExclusiveArgumentsError
|
|
356
|
+
# will be raised.
|
|
357
|
+
#
|
|
358
|
+
def class_exclusive(*args, &block)
|
|
359
|
+
register_options_relation_for(:class_options,
|
|
360
|
+
:class_exclusive_option_names, *args, &block)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Adds and declares option group for required at least one of options in the
|
|
364
|
+
# block and arguments. You can declare options as the outside of the block.
|
|
365
|
+
#
|
|
366
|
+
# ==== Examples
|
|
367
|
+
#
|
|
368
|
+
# class_at_least_one do
|
|
369
|
+
# class_option :one
|
|
370
|
+
# class_option :two
|
|
371
|
+
# end
|
|
372
|
+
#
|
|
373
|
+
# Or
|
|
374
|
+
#
|
|
375
|
+
# class_option :one
|
|
376
|
+
# class_option :two
|
|
377
|
+
# class_at_least_one :one, :two
|
|
378
|
+
#
|
|
379
|
+
# If you do not give "--one" and "--two" AtLeastOneRequiredArgumentError
|
|
380
|
+
# will be raised.
|
|
381
|
+
#
|
|
382
|
+
# You can use class_at_least_one and class_exclusive at the same time.
|
|
383
|
+
#
|
|
384
|
+
# class_exclusive do
|
|
385
|
+
# class_at_least_one do
|
|
386
|
+
# class_option :one
|
|
387
|
+
# class_option :two
|
|
388
|
+
# end
|
|
389
|
+
# end
|
|
390
|
+
#
|
|
391
|
+
# Then it is required either only one of "--one" or "--two".
|
|
392
|
+
#
|
|
393
|
+
def class_at_least_one(*args, &block)
|
|
394
|
+
register_options_relation_for(:class_options,
|
|
395
|
+
:class_at_least_one_option_names, *args, &block)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Returns this class exclusive options array set, looking up in the ancestors chain.
|
|
399
|
+
#
|
|
400
|
+
# ==== Returns
|
|
401
|
+
# Array[Array[Thor::Option.name]]
|
|
402
|
+
#
|
|
403
|
+
def class_exclusive_option_names
|
|
404
|
+
@class_exclusive_option_names ||= from_superclass(:class_exclusive_option_names, [])
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Returns this class at least one of required options array set, looking up in the ancestors chain.
|
|
408
|
+
#
|
|
409
|
+
# ==== Returns
|
|
410
|
+
# Array[Array[Thor::Option.name]]
|
|
411
|
+
#
|
|
412
|
+
def class_at_least_one_option_names
|
|
413
|
+
@class_at_least_one_option_names ||= from_superclass(:class_at_least_one_option_names, [])
|
|
414
|
+
end
|
|
415
|
+
|
|
316
416
|
# Removes a previous defined argument. If :undefine is given, undefine
|
|
317
417
|
# accessors as well.
|
|
318
418
|
#
|
|
@@ -565,12 +665,12 @@ class Thor
|
|
|
565
665
|
item.push(option.description ? "# #{option.description}" : "")
|
|
566
666
|
|
|
567
667
|
list << item
|
|
568
|
-
list << ["", "# Default: #{option.
|
|
569
|
-
list << ["", "# Possible values: #{option.
|
|
668
|
+
list << ["", "# Default: #{option.print_default}"] if option.show_default?
|
|
669
|
+
list << ["", "# Possible values: #{option.enum_to_s}"] if option.enum
|
|
570
670
|
end
|
|
571
671
|
|
|
572
672
|
shell.say(group_name ? "#{group_name} options:" : "Options:")
|
|
573
|
-
shell.print_table(list, :
|
|
673
|
+
shell.print_table(list, indent: 2)
|
|
574
674
|
shell.say ""
|
|
575
675
|
end
|
|
576
676
|
|
|
@@ -587,7 +687,7 @@ class Thor
|
|
|
587
687
|
# options<Hash>:: Described in both class_option and method_option.
|
|
588
688
|
# scope<Hash>:: Options hash that is being built up
|
|
589
689
|
def build_option(name, options, scope) #:nodoc:
|
|
590
|
-
scope[name] = Thor::Option.new(name, {:
|
|
690
|
+
scope[name] = Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options))
|
|
591
691
|
end
|
|
592
692
|
|
|
593
693
|
# Receives a hash of options, parse them and add to the scope. This is a
|
|
@@ -693,6 +793,34 @@ class Thor
|
|
|
693
793
|
def dispatch(command, given_args, given_opts, config) #:nodoc:
|
|
694
794
|
raise NotImplementedError
|
|
695
795
|
end
|
|
796
|
+
|
|
797
|
+
# Register a relation of options for target(method_option/class_option)
|
|
798
|
+
# by args and block.
|
|
799
|
+
def register_options_relation_for(target, relation, *args, &block) # :nodoc:
|
|
800
|
+
opt = args.pop if args.last.is_a? Hash
|
|
801
|
+
opt ||= {}
|
|
802
|
+
names = args.map{ |arg| arg.to_s }
|
|
803
|
+
names += built_option_names(target, opt, &block) if block_given?
|
|
804
|
+
command_scope_member(relation, opt) << names
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Get target(method_options or class_options) options
|
|
808
|
+
# of before and after by block evaluation.
|
|
809
|
+
def built_option_names(target, opt = {}, &block) # :nodoc:
|
|
810
|
+
before = command_scope_member(target, opt).map{ |k,v| v.name }
|
|
811
|
+
instance_eval(&block)
|
|
812
|
+
after = command_scope_member(target, opt).map{ |k,v| v.name }
|
|
813
|
+
after - before
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Get command scope member by name.
|
|
817
|
+
def command_scope_member(name, options = {}) # :nodoc:
|
|
818
|
+
if options[:for]
|
|
819
|
+
find_and_refresh_command(options[:for]).send(name)
|
|
820
|
+
else
|
|
821
|
+
send(name)
|
|
822
|
+
end
|
|
823
|
+
end
|
|
696
824
|
end
|
|
697
825
|
end
|
|
698
826
|
end
|
data/lib/thor/command.rb
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
class Thor
|
|
2
|
-
class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name)
|
|
2
|
+
class Command < Struct.new(:name, :description, :long_description, :wrap_long_description, :usage, :options, :options_relation, :ancestor_name)
|
|
3
3
|
FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
|
|
4
4
|
|
|
5
|
-
def initialize(name, description, long_description, usage, options = nil)
|
|
6
|
-
super(name.to_s, description, long_description, usage, options || {})
|
|
5
|
+
def initialize(name, description, long_description, wrap_long_description, usage, options = nil, options_relation = nil)
|
|
6
|
+
super(name.to_s, description, long_description, wrap_long_description, usage, options || {}, options_relation || {})
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def initialize_copy(other) #:nodoc:
|
|
10
10
|
super(other)
|
|
11
11
|
self.options = other.options.dup if other.options
|
|
12
|
+
self.options_relation = other.options_relation.dup if other.options_relation
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def hidden?
|
|
@@ -62,6 +63,14 @@ class Thor
|
|
|
62
63
|
end.join("\n")
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
def method_exclusive_option_names #:nodoc:
|
|
67
|
+
self.options_relation[:exclusive_option_names] || []
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def method_at_least_one_option_names #:nodoc:
|
|
71
|
+
self.options_relation[:at_least_one_option_names] || []
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
protected
|
|
66
75
|
|
|
67
76
|
# Add usage with required arguments
|
|
@@ -127,7 +136,7 @@ class Thor
|
|
|
127
136
|
# A dynamic command that handles method missing scenarios.
|
|
128
137
|
class DynamicCommand < Command
|
|
129
138
|
def initialize(name, options = nil)
|
|
130
|
-
super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options)
|
|
139
|
+
super(name.to_s, "A dynamically-generated command", name.to_s, nil, name.to_s, options)
|
|
131
140
|
end
|
|
132
141
|
|
|
133
142
|
def run(instance, args = [])
|