thor 0.16.0 → 1.2.1

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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +15 -0
  3. data/README.md +23 -6
  4. data/bin/thor +1 -1
  5. data/lib/thor/actions/create_file.rb +34 -35
  6. data/lib/thor/actions/create_link.rb +9 -5
  7. data/lib/thor/actions/directory.rb +33 -23
  8. data/lib/thor/actions/empty_directory.rb +75 -85
  9. data/lib/thor/actions/file_manipulation.rb +103 -36
  10. data/lib/thor/actions/inject_into_file.rb +46 -36
  11. data/lib/thor/actions.rb +90 -68
  12. data/lib/thor/base.rb +302 -244
  13. data/lib/thor/command.rb +142 -0
  14. data/lib/thor/core_ext/hash_with_indifferent_access.rb +52 -24
  15. data/lib/thor/error.rb +90 -10
  16. data/lib/thor/group.rb +70 -74
  17. data/lib/thor/invocation.rb +63 -55
  18. data/lib/thor/line_editor/basic.rb +37 -0
  19. data/lib/thor/line_editor/readline.rb +88 -0
  20. data/lib/thor/line_editor.rb +17 -0
  21. data/lib/thor/nested_context.rb +29 -0
  22. data/lib/thor/parser/argument.rb +24 -28
  23. data/lib/thor/parser/arguments.rb +110 -102
  24. data/lib/thor/parser/option.rb +53 -15
  25. data/lib/thor/parser/options.rb +174 -97
  26. data/lib/thor/parser.rb +4 -4
  27. data/lib/thor/rake_compat.rb +12 -11
  28. data/lib/thor/runner.rb +159 -155
  29. data/lib/thor/shell/basic.rb +216 -93
  30. data/lib/thor/shell/color.rb +53 -40
  31. data/lib/thor/shell/html.rb +61 -58
  32. data/lib/thor/shell.rb +29 -36
  33. data/lib/thor/util.rb +231 -213
  34. data/lib/thor/version.rb +1 -1
  35. data/lib/thor.rb +303 -166
  36. data/thor.gemspec +27 -24
  37. metadata +36 -226
  38. data/.gitignore +0 -44
  39. data/.rspec +0 -2
  40. data/.travis.yml +0 -7
  41. data/CHANGELOG.rdoc +0 -134
  42. data/Gemfile +0 -15
  43. data/Thorfile +0 -30
  44. data/bin/rake2thor +0 -86
  45. data/lib/thor/core_ext/dir_escape.rb +0 -0
  46. data/lib/thor/core_ext/file_binary_read.rb +0 -9
  47. data/lib/thor/core_ext/ordered_hash.rb +0 -100
  48. data/lib/thor/task.rb +0 -132
  49. data/spec/actions/create_file_spec.rb +0 -170
  50. data/spec/actions/create_link_spec.rb +0 -81
  51. data/spec/actions/directory_spec.rb +0 -149
  52. data/spec/actions/empty_directory_spec.rb +0 -130
  53. data/spec/actions/file_manipulation_spec.rb +0 -370
  54. data/spec/actions/inject_into_file_spec.rb +0 -135
  55. data/spec/actions_spec.rb +0 -331
  56. data/spec/base_spec.rb +0 -279
  57. data/spec/core_ext/hash_with_indifferent_access_spec.rb +0 -43
  58. data/spec/core_ext/ordered_hash_spec.rb +0 -115
  59. data/spec/exit_condition_spec.rb +0 -19
  60. data/spec/fixtures/application.rb +0 -2
  61. data/spec/fixtures/app{1}/README +0 -3
  62. data/spec/fixtures/bundle/execute.rb +0 -6
  63. data/spec/fixtures/bundle/main.thor +0 -1
  64. data/spec/fixtures/doc/%file_name%.rb.tt +0 -1
  65. data/spec/fixtures/doc/COMMENTER +0 -10
  66. data/spec/fixtures/doc/README +0 -3
  67. data/spec/fixtures/doc/block_helper.rb +0 -3
  68. data/spec/fixtures/doc/components/.empty_directory +0 -0
  69. data/spec/fixtures/doc/config.rb +0 -1
  70. data/spec/fixtures/doc/config.yaml.tt +0 -1
  71. data/spec/fixtures/enum.thor +0 -10
  72. data/spec/fixtures/group.thor +0 -114
  73. data/spec/fixtures/invoke.thor +0 -112
  74. data/spec/fixtures/path with spaces +0 -0
  75. data/spec/fixtures/script.thor +0 -190
  76. data/spec/fixtures/task.thor +0 -10
  77. data/spec/group_spec.rb +0 -216
  78. data/spec/invocation_spec.rb +0 -100
  79. data/spec/parser/argument_spec.rb +0 -53
  80. data/spec/parser/arguments_spec.rb +0 -66
  81. data/spec/parser/option_spec.rb +0 -202
  82. data/spec/parser/options_spec.rb +0 -330
  83. data/spec/rake_compat_spec.rb +0 -72
  84. data/spec/register_spec.rb +0 -135
  85. data/spec/runner_spec.rb +0 -241
  86. data/spec/shell/basic_spec.rb +0 -300
  87. data/spec/shell/color_spec.rb +0 -81
  88. data/spec/shell/html_spec.rb +0 -32
  89. data/spec/shell_spec.rb +0 -47
  90. data/spec/spec_helper.rb +0 -59
  91. data/spec/task_spec.rb +0 -80
  92. data/spec/thor_spec.rb +0 -418
  93. data/spec/util_spec.rb +0 -196
@@ -1,6 +1,6 @@
1
1
  class Thor
2
- class Arguments #:nodoc:
3
- NUMERIC = /(\d*\.\d+|\d+)/
2
+ class Arguments #:nodoc: # rubocop:disable ClassLength
3
+ NUMERIC = /[-+]?(\d*\.\d+|\d+)/
4
4
 
5
5
  # Receives an array of args and returns two arrays, one with arguments
6
6
  # and one with switches.
@@ -9,11 +9,11 @@ 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
 
16
- return arguments, args[Range.new(arguments.size, -1)]
16
+ [arguments, args[Range.new(arguments.size, -1)]]
17
17
  end
18
18
 
19
19
  def self.parse(*args)
@@ -23,13 +23,18 @@ class Thor
23
23
 
24
24
  # Takes an array of Thor::Argument objects.
25
25
  #
26
- def initialize(arguments=[])
27
- @assigns, @non_assigned_required = {}, []
26
+ def initialize(arguments = [])
27
+ @assigns = {}
28
+ @non_assigned_required = []
28
29
  @switches = arguments
29
30
 
30
31
  arguments.each do |argument|
31
- if argument.default != nil
32
- @assigns[argument.human_name] = argument.default
32
+ if !argument.default.nil?
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
33
38
  elsif argument.required?
34
39
  @non_assigned_required << argument
35
40
  end
@@ -53,119 +58,122 @@ class Thor
53
58
  @pile
54
59
  end
55
60
 
56
- private
57
-
58
- def no_or_skip?(arg)
59
- arg =~ /^--(no|skip)-([-\w]+)$/
60
- $2
61
- end
61
+ private
62
62
 
63
- def last?
64
- @pile.empty?
65
- end
63
+ def no_or_skip?(arg)
64
+ arg =~ /^--(no|skip)-([-\w]+)$/
65
+ $2
66
+ end
66
67
 
67
- def peek
68
- @pile.first
69
- end
68
+ def last?
69
+ @pile.empty?
70
+ end
70
71
 
71
- def shift
72
- @pile.shift
73
- end
72
+ def peek
73
+ @pile.first
74
+ end
74
75
 
75
- def unshift(arg)
76
- unless arg.kind_of?(Array)
77
- @pile.unshift(arg)
78
- else
79
- @pile = arg + @pile
80
- end
81
- end
76
+ def shift
77
+ @pile.shift
78
+ end
82
79
 
83
- def current_is_value?
84
- peek && peek.to_s !~ /^-/
80
+ def unshift(arg)
81
+ if arg.is_a?(Array)
82
+ @pile = arg + @pile
83
+ else
84
+ @pile.unshift(arg)
85
85
  end
86
+ end
86
87
 
87
- # Runs through the argument array getting strings that contains ":" and
88
- # mark it as a hash:
89
- #
90
- # [ "name:string", "age:integer" ]
91
- #
92
- # Becomes:
93
- #
94
- # { "name" => "string", "age" => "integer" }
95
- #
96
- def parse_hash(name)
97
- return shift if peek.is_a?(Hash)
98
- hash = {}
99
-
100
- while current_is_value? && peek.include?(?:)
101
- key, value = shift.split(':',2)
102
- hash[key] = value
103
- end
104
- hash
105
- end
88
+ def current_is_value?
89
+ peek && peek.to_s !~ /^-{1,2}\S+/
90
+ end
106
91
 
107
- # Runs through the argument array getting all strings until no string is
108
- # found or a switch is found.
109
- #
110
- # ["a", "b", "c"]
111
- #
112
- # And returns it as an array:
113
- #
114
- # ["a", "b", "c"]
115
- #
116
- def parse_array(name)
117
- return shift if peek.is_a?(Array)
118
- array = []
119
-
120
- while current_is_value?
121
- array << shift
122
- end
123
- array
92
+ # Runs through the argument array getting strings that contains ":" and
93
+ # mark it as a hash:
94
+ #
95
+ # [ "name:string", "age:integer" ]
96
+ #
97
+ # Becomes:
98
+ #
99
+ # { "name" => "string", "age" => "integer" }
100
+ #
101
+ def parse_hash(name)
102
+ return shift if peek.is_a?(Hash)
103
+ hash = {}
104
+
105
+ while current_is_value? && peek.include?(":")
106
+ key, value = shift.split(":", 2)
107
+ raise MalformattedArgumentError, "You can't specify '#{key}' more than once in option '#{name}'; got #{key}:#{hash[key]} and #{key}:#{value}" if hash.include? key
108
+ hash[key] = value
124
109
  end
110
+ hash
111
+ end
125
112
 
126
- # Check if the peek is numeric format and return a Float or Integer.
127
- # Otherwise raises an error.
128
- #
129
- def parse_numeric(name)
130
- return shift if peek.is_a?(Numeric)
113
+ # Runs through the argument array getting all strings until no string is
114
+ # found or a switch is found.
115
+ #
116
+ # ["a", "b", "c"]
117
+ #
118
+ # And returns it as an array:
119
+ #
120
+ # ["a", "b", "c"]
121
+ #
122
+ def parse_array(name)
123
+ return shift if peek.is_a?(Array)
124
+ array = []
125
+ array << shift while current_is_value?
126
+ array
127
+ end
131
128
 
132
- unless peek =~ NUMERIC && $& == peek
133
- raise MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}"
134
- end
129
+ # Check if the peek is numeric format and return a Float or Integer.
130
+ # Check if the peek is included in enum if enum is provided.
131
+ # Otherwise raises an error.
132
+ #
133
+ def parse_numeric(name)
134
+ return shift if peek.is_a?(Numeric)
135
135
 
136
- $&.index('.') ? shift.to_f : shift.to_i
136
+ unless peek =~ NUMERIC && $& == peek
137
+ raise MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}"
137
138
  end
138
139
 
139
- # Parse string:
140
- # for --string-arg, just return the current value in the pile
141
- # for --no-string-arg, nil
142
- #
143
- def parse_string(name)
144
- if no_or_skip?(name)
145
- nil
146
- else
147
- value = shift
148
- if @switches.is_a?(Hash) && switch = @switches[name]
149
- if switch.enum && !switch.enum.include?(value)
150
- raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
151
- end
152
- end
153
- value
140
+ value = $&.index(".") ? shift.to_f : shift.to_i
141
+ if @switches.is_a?(Hash) && switch = @switches[name]
142
+ if switch.enum && !switch.enum.include?(value)
143
+ raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
154
144
  end
155
145
  end
146
+ value
147
+ end
156
148
 
157
- # Raises an error if @non_assigned_required array is not empty.
158
- #
159
- def check_requirement!
160
- unless @non_assigned_required.empty?
161
- names = @non_assigned_required.map do |o|
162
- o.respond_to?(:switch_name) ? o.switch_name : o.human_name
163
- end.join("', '")
164
-
165
- class_name = self.class.name.split('::').last.downcase
166
- raise RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'"
149
+ # Parse string:
150
+ # for --string-arg, just return the current value in the pile
151
+ # for --no-string-arg, nil
152
+ # Check if the peek is included in enum if enum is provided. Otherwise raises an error.
153
+ #
154
+ def parse_string(name)
155
+ if no_or_skip?(name)
156
+ nil
157
+ else
158
+ value = shift
159
+ if @switches.is_a?(Hash) && switch = @switches[name]
160
+ if switch.enum && !switch.enum.include?(value)
161
+ raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
162
+ end
167
163
  end
164
+ value
168
165
  end
166
+ end
169
167
 
168
+ # Raises an error if @non_assigned_required array is not empty.
169
+ #
170
+ def check_requirement!
171
+ return if @non_assigned_required.empty?
172
+ names = @non_assigned_required.map do |o|
173
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
174
+ end.join("', '")
175
+ class_name = self.class.name.split("::").last.downcase
176
+ raise RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'"
177
+ end
170
178
  end
171
179
  end
@@ -1,16 +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
- def initialize(name, options={})
7
+ def initialize(name, options = {})
8
+ @check_default_type = options[:check_default_type]
8
9
  options[:required] = false unless options.key?(:required)
10
+ @repeatable = options.fetch(:repeatable, false)
9
11
  super
10
- @lazy_default = options[:lazy_default]
11
- @group = options[:group].to_s.capitalize if options[:group]
12
- @aliases = Array(options[:aliases])
13
- @hide = options[: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]
14
16
  end
15
17
 
16
18
  # This parse quick options given as method_options. It makes several
@@ -44,7 +46,8 @@ class Thor
44
46
  if key.is_a?(Array)
45
47
  name, *aliases = key
46
48
  else
47
- name, aliases = key, []
49
+ name = key
50
+ aliases = []
48
51
  end
49
52
 
50
53
  name = name.to_s
@@ -55,7 +58,7 @@ class Thor
55
58
  default = nil
56
59
  if VALID_TYPES.include?(value)
57
60
  value
58
- elsif required = (value == :required)
61
+ elsif required = (value == :required) # rubocop:disable AssignmentInCondition
59
62
  :string
60
63
  end
61
64
  when TrueClass, FalseClass
@@ -65,7 +68,8 @@ class Thor
65
68
  when Hash, Array, String
66
69
  value.class.name.downcase.to_sym
67
70
  end
68
- self.new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
71
+
72
+ new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
69
73
  end
70
74
 
71
75
  def switch_name
@@ -76,14 +80,18 @@ class Thor
76
80
  @human_name ||= dasherized? ? undasherize(name) : name
77
81
  end
78
82
 
79
- def usage(padding=0)
83
+ def usage(padding = 0)
80
84
  sample = if banner && !banner.to_s.empty?
81
- "#{switch_name}=#{banner}"
85
+ "#{switch_name}=#{banner}".dup
82
86
  else
83
87
  switch_name
84
88
  end
85
89
 
86
- sample = "[#{sample}]" unless required?
90
+ sample = "[#{sample}]".dup unless required?
91
+
92
+ if boolean?
93
+ sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-")
94
+ end
87
95
 
88
96
  if aliases.empty?
89
97
  (" " * padding) << sample
@@ -104,18 +112,48 @@ class Thor
104
112
 
105
113
  def validate!
106
114
  raise ArgumentError, "An option cannot be boolean and required." if boolean? && required?
115
+ validate_default_type!
116
+ end
117
+
118
+ def validate_default_type!
119
+ default_type = case @default
120
+ when nil
121
+ return
122
+ when TrueClass, FalseClass
123
+ required? ? :string : :boolean
124
+ when Numeric
125
+ :numeric
126
+ when Symbol
127
+ :string
128
+ when Hash, Array, String
129
+ @default.class.name.downcase.to_sym
130
+ end
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
107
145
  end
108
146
 
109
147
  def dasherized?
110
- name.index('-') == 0
148
+ name.index("-") == 0
111
149
  end
112
150
 
113
151
  def undasherize(str)
114
- str.sub(/^-{1,2}/, '')
152
+ str.sub(/^-{1,2}/, "")
115
153
  end
116
154
 
117
155
  def dasherize(str)
118
- (str.length > 1 ? "--" : "-") + str.gsub('_', '-')
156
+ (str.length > 1 ? "--" : "-") + str.tr("_", "-")
119
157
  end
120
158
  end
121
159
  end