thor 0.20.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +7 -9
- data/lib/thor/actions/create_file.rb +1 -1
- data/lib/thor/actions/create_link.rb +3 -2
- data/lib/thor/actions/directory.rb +7 -17
- data/lib/thor/actions/file_manipulation.rb +25 -14
- data/lib/thor/actions/inject_into_file.rb +20 -10
- data/lib/thor/actions.rb +34 -15
- data/lib/thor/base.rb +63 -43
- data/lib/thor/command.rb +21 -14
- data/lib/thor/core_ext/hash_with_indifferent_access.rb +6 -0
- data/lib/thor/error.rb +83 -0
- data/lib/thor/group.rb +3 -3
- data/lib/thor/invocation.rb +1 -0
- data/lib/thor/line_editor/basic.rb +1 -1
- data/lib/thor/line_editor/readline.rb +6 -6
- data/lib/thor/line_editor.rb +2 -2
- data/lib/thor/nested_context.rb +29 -0
- data/lib/thor/parser/arguments.rb +7 -3
- data/lib/thor/parser/option.rb +20 -7
- data/lib/thor/parser/options.rb +40 -6
- data/lib/thor/parser.rb +4 -4
- data/lib/thor/rake_compat.rb +1 -0
- data/lib/thor/runner.rb +5 -4
- data/lib/thor/shell/basic.rb +87 -12
- data/lib/thor/shell/color.rb +10 -2
- data/lib/thor/shell/html.rb +3 -3
- data/lib/thor/shell.rb +5 -5
- data/lib/thor/util.rb +18 -2
- data/lib/thor/version.rb +1 -1
- data/lib/thor.rb +16 -9
- data/thor.gemspec +10 -2
- metadata +20 -11
- data/CHANGELOG.md +0 -193
- data/lib/thor/core_ext/io_binary_read.rb +0 -12
- data/lib/thor/core_ext/ordered_hash.rb +0 -129
data/lib/thor/command.rb
CHANGED
@@ -49,24 +49,32 @@ class Thor
|
|
49
49
|
|
50
50
|
formatted ||= "".dup
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
usage.to_s.gsub(/^#{name}/) do |match|
|
55
|
-
match << " " << klass.arguments.map(&:usage).compact.join(" ")
|
56
|
-
end
|
57
|
-
else
|
58
|
-
usage.to_s
|
59
|
-
end
|
52
|
+
Array(usage).map do |specific_usage|
|
53
|
+
formatted_specific_usage = formatted
|
60
54
|
|
61
|
-
|
62
|
-
formatted << " #{required_options}"
|
55
|
+
formatted_specific_usage += required_arguments_for(klass, specific_usage)
|
63
56
|
|
64
|
-
|
65
|
-
|
57
|
+
# Add required options
|
58
|
+
formatted_specific_usage += " #{required_options}"
|
59
|
+
|
60
|
+
# Strip and go!
|
61
|
+
formatted_specific_usage.strip
|
62
|
+
end.join("\n")
|
66
63
|
end
|
67
64
|
|
68
65
|
protected
|
69
66
|
|
67
|
+
# Add usage with required arguments
|
68
|
+
def required_arguments_for(klass, usage)
|
69
|
+
if klass && !klass.arguments.empty?
|
70
|
+
usage.to_s.gsub(/^#{name}/) do |match|
|
71
|
+
match << " " << klass.arguments.map(&:usage).compact.join(" ")
|
72
|
+
end
|
73
|
+
else
|
74
|
+
usage.to_s
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
70
78
|
def not_debugging?(instance)
|
71
79
|
!(instance.class.respond_to?(:debugging) && instance.class.debugging)
|
72
80
|
end
|
@@ -97,8 +105,7 @@ class Thor
|
|
97
105
|
def handle_argument_error?(instance, error, caller)
|
98
106
|
not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin
|
99
107
|
saned = sans_backtrace(error.backtrace, caller)
|
100
|
-
|
101
|
-
saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
|
108
|
+
saned.empty? || saned.size == 1
|
102
109
|
end
|
103
110
|
end
|
104
111
|
|
data/lib/thor/error.rb
CHANGED
@@ -1,4 +1,19 @@
|
|
1
1
|
class Thor
|
2
|
+
Correctable = if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable) # rubocop:disable Naming/ConstantName
|
3
|
+
# In order to support versions of Ruby that don't have keyword
|
4
|
+
# arguments, we need our own spell checker class that doesn't take key
|
5
|
+
# words. Even though this code wouldn't be hit because of the check
|
6
|
+
# above, it's still necessary because the interpreter would otherwise be
|
7
|
+
# unable to parse the file.
|
8
|
+
class NoKwargSpellChecker < DidYouMean::SpellChecker # :nodoc:
|
9
|
+
def initialize(dictionary)
|
10
|
+
@dictionary = dictionary
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
DidYouMean::Correctable
|
15
|
+
end
|
16
|
+
|
2
17
|
# Thor::Error is raised when it's caused by wrong usage of thor classes. Those
|
3
18
|
# errors have their backtrace suppressed and are nicely shown to the user.
|
4
19
|
#
|
@@ -10,6 +25,35 @@ class Thor
|
|
10
25
|
|
11
26
|
# Raised when a command was not found.
|
12
27
|
class UndefinedCommandError < Error
|
28
|
+
class SpellChecker
|
29
|
+
attr_reader :error
|
30
|
+
|
31
|
+
def initialize(error)
|
32
|
+
@error = error
|
33
|
+
end
|
34
|
+
|
35
|
+
def corrections
|
36
|
+
@corrections ||= spell_checker.correct(error.command).map(&:inspect)
|
37
|
+
end
|
38
|
+
|
39
|
+
def spell_checker
|
40
|
+
NoKwargSpellChecker.new(error.all_commands)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :command, :all_commands
|
45
|
+
|
46
|
+
def initialize(command, all_commands, namespace)
|
47
|
+
@command = command
|
48
|
+
@all_commands = all_commands
|
49
|
+
|
50
|
+
message = "Could not find command #{command.inspect}"
|
51
|
+
message = namespace ? "#{message} in #{namespace.inspect} namespace." : "#{message}."
|
52
|
+
|
53
|
+
super(message)
|
54
|
+
end
|
55
|
+
|
56
|
+
prepend Correctable if Correctable
|
13
57
|
end
|
14
58
|
UndefinedTaskError = UndefinedCommandError
|
15
59
|
|
@@ -22,6 +66,33 @@ class Thor
|
|
22
66
|
end
|
23
67
|
|
24
68
|
class UnknownArgumentError < Error
|
69
|
+
class SpellChecker
|
70
|
+
attr_reader :error
|
71
|
+
|
72
|
+
def initialize(error)
|
73
|
+
@error = error
|
74
|
+
end
|
75
|
+
|
76
|
+
def corrections
|
77
|
+
@corrections ||=
|
78
|
+
error.unknown.flat_map { |unknown| spell_checker.correct(unknown) }.uniq.map(&:inspect)
|
79
|
+
end
|
80
|
+
|
81
|
+
def spell_checker
|
82
|
+
@spell_checker ||= NoKwargSpellChecker.new(error.switches)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :switches, :unknown
|
87
|
+
|
88
|
+
def initialize(switches, unknown)
|
89
|
+
@switches = switches
|
90
|
+
@unknown = unknown
|
91
|
+
|
92
|
+
super("Unknown switches #{unknown.map(&:inspect).join(', ')}")
|
93
|
+
end
|
94
|
+
|
95
|
+
prepend Correctable if Correctable
|
25
96
|
end
|
26
97
|
|
27
98
|
class RequiredArgumentMissingError < InvocationError
|
@@ -29,4 +100,16 @@ class Thor
|
|
29
100
|
|
30
101
|
class MalformattedArgumentError < InvocationError
|
31
102
|
end
|
103
|
+
|
104
|
+
if Correctable
|
105
|
+
if DidYouMean.respond_to?(:correct_error)
|
106
|
+
DidYouMean.correct_error(Thor::UndefinedCommandError, UndefinedCommandError::SpellChecker)
|
107
|
+
DidYouMean.correct_error(Thor::UnknownArgumentError, UnknownArgumentError::SpellChecker)
|
108
|
+
else
|
109
|
+
DidYouMean::SPELL_CHECKERS.merge!(
|
110
|
+
'Thor::UndefinedCommandError' => UndefinedCommandError::SpellChecker,
|
111
|
+
'Thor::UnknownArgumentError' => UnknownArgumentError::SpellChecker
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
32
115
|
end
|
data/lib/thor/group.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "base"
|
2
2
|
|
3
3
|
# Thor has a special class called Thor::Group. The main difference to Thor class
|
4
4
|
# is that it invokes all commands at once. It also include some methods that allows
|
@@ -61,7 +61,7 @@ class Thor::Group
|
|
61
61
|
invocations[name] = false
|
62
62
|
invocation_blocks[name] = block if block_given?
|
63
63
|
|
64
|
-
class_eval <<-METHOD, __FILE__, __LINE__
|
64
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
65
65
|
def _invoke_#{name.to_s.gsub(/\W/, '_')}
|
66
66
|
klass, command = self.class.prepare_for_invocation(nil, #{name.inspect})
|
67
67
|
|
@@ -120,7 +120,7 @@ class Thor::Group
|
|
120
120
|
invocations[name] = true
|
121
121
|
invocation_blocks[name] = block if block_given?
|
122
122
|
|
123
|
-
class_eval <<-METHOD, __FILE__, __LINE__
|
123
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
124
124
|
def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')}
|
125
125
|
return unless options[#{name.inspect}]
|
126
126
|
|
data/lib/thor/invocation.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
begin
|
2
|
-
require "readline"
|
3
|
-
rescue LoadError
|
4
|
-
end
|
5
|
-
|
6
1
|
class Thor
|
7
2
|
module LineEditor
|
8
3
|
class Readline < Basic
|
9
4
|
def self.available?
|
5
|
+
begin
|
6
|
+
require "readline"
|
7
|
+
rescue LoadError
|
8
|
+
end
|
9
|
+
|
10
10
|
Object.const_defined?(:Readline)
|
11
11
|
end
|
12
12
|
|
13
13
|
def readline
|
14
14
|
if echo?
|
15
15
|
::Readline.completion_append_character = nil
|
16
|
-
#
|
16
|
+
# rb-readline does not allow Readline.completion_proc= to receive nil.
|
17
17
|
if complete = completion_proc
|
18
18
|
::Readline.completion_proc = complete
|
19
19
|
end
|
data/lib/thor/line_editor.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
class Thor
|
2
|
+
class NestedContext
|
3
|
+
def initialize
|
4
|
+
@depth = 0
|
5
|
+
end
|
6
|
+
|
7
|
+
def enter
|
8
|
+
push
|
9
|
+
|
10
|
+
yield
|
11
|
+
ensure
|
12
|
+
pop
|
13
|
+
end
|
14
|
+
|
15
|
+
def entered?
|
16
|
+
@depth > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def push
|
22
|
+
@depth += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def pop
|
26
|
+
@depth -= 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -9,7 +9,7 @@ class Thor
|
|
9
9
|
arguments = []
|
10
10
|
|
11
11
|
args.each do |item|
|
12
|
-
break if item =~ /^-/
|
12
|
+
break if item.is_a?(String) && item =~ /^-/
|
13
13
|
arguments << item
|
14
14
|
end
|
15
15
|
|
@@ -30,7 +30,11 @@ class Thor
|
|
30
30
|
|
31
31
|
arguments.each do |argument|
|
32
32
|
if !argument.default.nil?
|
33
|
-
|
33
|
+
begin
|
34
|
+
@assigns[argument.human_name] = argument.default.dup
|
35
|
+
rescue TypeError # Compatibility shim for un-dup-able Fixnum in Ruby < 2.4
|
36
|
+
@assigns[argument.human_name] = argument.default
|
37
|
+
end
|
34
38
|
elsif argument.required?
|
35
39
|
@non_assigned_required << argument
|
36
40
|
end
|
@@ -82,7 +86,7 @@ class Thor
|
|
82
86
|
end
|
83
87
|
|
84
88
|
def current_is_value?
|
85
|
-
peek && peek.to_s !~
|
89
|
+
peek && peek.to_s !~ /^-{1,2}\S+/
|
86
90
|
end
|
87
91
|
|
88
92
|
# Runs through the argument array getting strings that contains ":" and
|
data/lib/thor/parser/option.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
class Thor
|
2
2
|
class Option < Argument #:nodoc:
|
3
|
-
attr_reader :aliases, :group, :lazy_default, :hide
|
3
|
+
attr_reader :aliases, :group, :lazy_default, :hide, :repeatable
|
4
4
|
|
5
5
|
VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
|
6
6
|
|
7
7
|
def initialize(name, options = {})
|
8
8
|
@check_default_type = options[:check_default_type]
|
9
9
|
options[:required] = false unless options.key?(:required)
|
10
|
+
@repeatable = options.fetch(:repeatable, false)
|
10
11
|
super
|
11
|
-
@lazy_default
|
12
|
-
@group
|
13
|
-
@aliases
|
14
|
-
@hide
|
12
|
+
@lazy_default = options[:lazy_default]
|
13
|
+
@group = options[:group].to_s.capitalize if options[:group]
|
14
|
+
@aliases = Array(options[:aliases])
|
15
|
+
@hide = options[:hide]
|
15
16
|
end
|
16
17
|
|
17
18
|
# This parse quick options given as method_options. It makes several
|
@@ -111,7 +112,7 @@ class Thor
|
|
111
112
|
|
112
113
|
def validate!
|
113
114
|
raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
|
114
|
-
validate_default_type!
|
115
|
+
validate_default_type!
|
115
116
|
end
|
116
117
|
|
117
118
|
def validate_default_type!
|
@@ -128,7 +129,19 @@ class Thor
|
|
128
129
|
@default.class.name.downcase.to_sym
|
129
130
|
end
|
130
131
|
|
131
|
-
|
132
|
+
expected_type = (@repeatable && @type != :hash) ? :array : @type
|
133
|
+
|
134
|
+
if default_type != expected_type
|
135
|
+
err = "Expected #{expected_type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})"
|
136
|
+
|
137
|
+
if @check_default_type
|
138
|
+
raise ArgumentError, err
|
139
|
+
elsif @check_default_type == nil
|
140
|
+
Thor.deprecation_warning "#{err}.\n" +
|
141
|
+
'This will be rejected in the future unless you explicitly pass the options `check_default_type: false`' +
|
142
|
+
' or call `allow_incompatible_default_type!` in your code'
|
143
|
+
end
|
144
|
+
end
|
132
145
|
end
|
133
146
|
|
134
147
|
def dasherized?
|
data/lib/thor/parser/options.rb
CHANGED
@@ -44,6 +44,8 @@ class Thor
|
|
44
44
|
@shorts = {}
|
45
45
|
@switches = {}
|
46
46
|
@extra = []
|
47
|
+
@stopped_parsing_after_extra_index = nil
|
48
|
+
@is_treated_as_value = false
|
47
49
|
|
48
50
|
options.each do |option|
|
49
51
|
@switches[option.switch_name] = option
|
@@ -66,14 +68,26 @@ class Thor
|
|
66
68
|
if result == OPTS_END
|
67
69
|
shift
|
68
70
|
@parsing_options = false
|
71
|
+
@stopped_parsing_after_extra_index ||= @extra.size
|
69
72
|
super
|
70
73
|
else
|
71
74
|
result
|
72
75
|
end
|
73
76
|
end
|
74
77
|
|
78
|
+
def shift
|
79
|
+
@is_treated_as_value = false
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def unshift(arg, is_value: false)
|
84
|
+
@is_treated_as_value = is_value
|
85
|
+
super(arg)
|
86
|
+
end
|
87
|
+
|
75
88
|
def parse(args) # rubocop:disable MethodLength
|
76
89
|
@pile = args.dup
|
90
|
+
@is_treated_as_value = false
|
77
91
|
@parsing_options = true
|
78
92
|
|
79
93
|
while peek
|
@@ -86,7 +100,10 @@ class Thor
|
|
86
100
|
when SHORT_SQ_RE
|
87
101
|
unshift($1.split("").map { |f| "-#{f}" })
|
88
102
|
next
|
89
|
-
when EQ_RE
|
103
|
+
when EQ_RE
|
104
|
+
unshift($2, is_value: true)
|
105
|
+
switch = $1
|
106
|
+
when SHORT_NUM
|
90
107
|
unshift($2)
|
91
108
|
switch = $1
|
92
109
|
when LONG_RE, SHORT_RE
|
@@ -95,10 +112,12 @@ class Thor
|
|
95
112
|
|
96
113
|
switch = normalize_switch(switch)
|
97
114
|
option = switch_option(switch)
|
98
|
-
|
115
|
+
result = parse_peek(switch, option)
|
116
|
+
assign_result!(option, result)
|
99
117
|
elsif @stop_on_unknown
|
100
118
|
@parsing_options = false
|
101
119
|
@extra << shifted
|
120
|
+
@stopped_parsing_after_extra_index ||= @extra.size
|
102
121
|
@extra << shift while peek
|
103
122
|
break
|
104
123
|
elsif match
|
@@ -120,18 +139,31 @@ class Thor
|
|
120
139
|
end
|
121
140
|
|
122
141
|
def check_unknown!
|
142
|
+
to_check = @stopped_parsing_after_extra_index ? @extra[0...@stopped_parsing_after_extra_index] : @extra
|
143
|
+
|
123
144
|
# an unknown option starts with - or -- and has no more --'s afterward.
|
124
|
-
unknown =
|
125
|
-
raise UnknownArgumentError
|
145
|
+
unknown = to_check.select { |str| str =~ /^--?(?:(?!--).)*$/ }
|
146
|
+
raise UnknownArgumentError.new(@switches.keys, unknown) unless unknown.empty?
|
126
147
|
end
|
127
148
|
|
128
149
|
protected
|
129
150
|
|
151
|
+
def assign_result!(option, result)
|
152
|
+
if option.repeatable && option.type == :hash
|
153
|
+
(@assigns[option.human_name] ||= {}).merge!(result)
|
154
|
+
elsif option.repeatable
|
155
|
+
(@assigns[option.human_name] ||= []) << result
|
156
|
+
else
|
157
|
+
@assigns[option.human_name] = result
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
130
161
|
# Check if the current value in peek is a registered switch.
|
131
162
|
#
|
132
163
|
# Two booleans are returned. The first is true if the current value
|
133
164
|
# starts with a hyphen; the second is true if it is a registered switch.
|
134
165
|
def current_is_switch?
|
166
|
+
return [false, false] if @is_treated_as_value
|
135
167
|
case peek
|
136
168
|
when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
|
137
169
|
[true, switch?($1)]
|
@@ -143,6 +175,7 @@ class Thor
|
|
143
175
|
end
|
144
176
|
|
145
177
|
def current_is_switch_formatted?
|
178
|
+
return false if @is_treated_as_value
|
146
179
|
case peek
|
147
180
|
when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE
|
148
181
|
true
|
@@ -152,11 +185,12 @@ class Thor
|
|
152
185
|
end
|
153
186
|
|
154
187
|
def current_is_value?
|
188
|
+
return true if @is_treated_as_value
|
155
189
|
peek && (!parsing_options? || super)
|
156
190
|
end
|
157
191
|
|
158
192
|
def switch?(arg)
|
159
|
-
switch_option(normalize_switch(arg))
|
193
|
+
!switch_option(normalize_switch(arg)).nil?
|
160
194
|
end
|
161
195
|
|
162
196
|
def switch_option(arg)
|
@@ -189,7 +223,7 @@ class Thor
|
|
189
223
|
shift
|
190
224
|
false
|
191
225
|
else
|
192
|
-
!no_or_skip?(switch)
|
226
|
+
@switches.key?(switch) || !no_or_skip?(switch)
|
193
227
|
end
|
194
228
|
else
|
195
229
|
@switches.key?(switch) || !no_or_skip?(switch)
|
data/lib/thor/parser.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
require_relative "parser/argument"
|
2
|
+
require_relative "parser/arguments"
|
3
|
+
require_relative "parser/option"
|
4
|
+
require_relative "parser/options"
|
data/lib/thor/rake_compat.rb
CHANGED
@@ -25,6 +25,7 @@ class Thor
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def self.included(base)
|
28
|
+
super(base)
|
28
29
|
# Hack. Make rakefile point to invoker, so rdoc task is generated properly.
|
29
30
|
rakefile = File.basename(caller[0].match(/(.*):\d+/)[1])
|
30
31
|
Rake.application.instance_variable_set(:@rakefile, rakefile)
|
data/lib/thor/runner.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require "thor/core_ext/io_binary_read"
|
1
|
+
require_relative "../thor"
|
2
|
+
require_relative "group"
|
4
3
|
|
5
4
|
require "yaml"
|
6
5
|
require "digest/md5"
|
7
6
|
require "pathname"
|
8
7
|
|
9
8
|
class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
|
9
|
+
autoload :OpenURI, "open-uri"
|
10
|
+
|
10
11
|
map "-T" => :list, "-i" => :install, "-u" => :update, "-v" => :version
|
11
12
|
|
12
13
|
def self.banner(command, all = false, subcommand = false)
|
@@ -111,7 +112,7 @@ class Thor::Runner < Thor #:nodoc: # rubocop:disable ClassLength
|
|
111
112
|
|
112
113
|
desc "version", "Show Thor version"
|
113
114
|
def version
|
114
|
-
|
115
|
+
require_relative "version"
|
115
116
|
say "Thor #{Thor::VERSION}"
|
116
117
|
end
|
117
118
|
|