thor 0.14.6 → 0.19.4

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