thor 0.13.4 → 0.14.3
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.
- data/CHANGELOG.rdoc +5 -3
- data/{README.rdoc → README.md} +65 -55
- data/Thorfile +3 -2
- data/lib/thor/actions/create_file.rb +3 -1
- data/lib/thor/actions/directory.rb +4 -2
- data/lib/thor/actions/file_manipulation.rb +30 -7
- data/lib/thor/actions.rb +31 -9
- data/lib/thor/base.rb +38 -15
- data/lib/thor/core_ext/hash_with_indifferent_access.rb +1 -1
- data/lib/thor/group.rb +22 -20
- data/lib/thor/invocation.rb +45 -57
- data/lib/thor/parser/argument.rb +15 -15
- data/lib/thor/parser/arguments.rb +14 -3
- data/lib/thor/parser/option.rb +38 -46
- data/lib/thor/parser/options.rb +26 -22
- data/lib/thor/runner.rb +11 -16
- data/lib/thor/shell/basic.rb +48 -12
- data/lib/thor/shell/html.rb +121 -0
- data/lib/thor/shell.rb +7 -2
- data/lib/thor/task.rb +63 -51
- data/lib/thor/util.rb +15 -16
- data/lib/thor/version.rb +1 -1
- data/lib/thor.rb +118 -45
- data/spec/actions/directory_spec.rb +2 -2
- data/spec/actions/file_manipulation_spec.rb +7 -0
- data/spec/actions_spec.rb +21 -3
- data/spec/base_spec.rb +9 -3
- data/spec/fixtures/doc/block_helper.rb +3 -0
- data/spec/fixtures/doc/components/.empty_directory +0 -0
- data/spec/fixtures/group.thor +16 -4
- data/spec/fixtures/path with spaces +0 -0
- data/spec/fixtures/script.thor +48 -4
- data/spec/group_spec.rb +3 -3
- data/spec/invocation_spec.rb +1 -8
- data/spec/parser/option_spec.rb +2 -2
- data/spec/parser/options_spec.rb +27 -0
- data/spec/runner_spec.rb +16 -8
- data/spec/shell/basic_spec.rb +13 -4
- data/spec/shell/html_spec.rb +27 -0
- data/spec/shell_spec.rb +13 -0
- data/spec/spec_helper.rb +4 -3
- data/spec/task_spec.rb +7 -7
- data/spec/thor_spec.rb +103 -6
- data/spec/util_spec.rb +3 -7
- metadata +63 -7
data/lib/thor.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require 'thor/base'
|
|
2
2
|
|
|
3
|
-
# TODO: Update thor to allow for git-style CLI (git bisect run)
|
|
4
3
|
class Thor
|
|
5
4
|
class << self
|
|
6
5
|
# Sets the default task when thor is executed without an explicit task to be called.
|
|
@@ -24,6 +23,7 @@ class Thor
|
|
|
24
23
|
# ==== Parameters
|
|
25
24
|
# usage<String>
|
|
26
25
|
# description<String>
|
|
26
|
+
# options<String>
|
|
27
27
|
#
|
|
28
28
|
def desc(usage, description, options={})
|
|
29
29
|
if options[:for]
|
|
@@ -31,7 +31,21 @@ class Thor
|
|
|
31
31
|
task.usage = usage if usage
|
|
32
32
|
task.description = description if description
|
|
33
33
|
else
|
|
34
|
-
@usage, @desc = usage, description
|
|
34
|
+
@usage, @desc, @hide = usage, description, options[:hide] || false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Defines the long description of the next task.
|
|
39
|
+
#
|
|
40
|
+
# ==== Parameters
|
|
41
|
+
# long description<String>
|
|
42
|
+
#
|
|
43
|
+
def long_desc(long_description, options={})
|
|
44
|
+
if options[:for]
|
|
45
|
+
task = find_and_refresh_task(options[:for])
|
|
46
|
+
task.long_description = long_description if long_description
|
|
47
|
+
else
|
|
48
|
+
@long_desc = long_description
|
|
35
49
|
end
|
|
36
50
|
end
|
|
37
51
|
|
|
@@ -112,32 +126,6 @@ class Thor
|
|
|
112
126
|
build_option(name, options, scope)
|
|
113
127
|
end
|
|
114
128
|
|
|
115
|
-
# Parses the task and options from the given args, instantiate the class
|
|
116
|
-
# and invoke the task. This method is used when the arguments must be parsed
|
|
117
|
-
# from an array. If you are inside Ruby and want to use a Thor class, you
|
|
118
|
-
# can simply initialize it:
|
|
119
|
-
#
|
|
120
|
-
# script = MyScript.new(args, options, config)
|
|
121
|
-
# script.invoke(:task, first_arg, second_arg, third_arg)
|
|
122
|
-
#
|
|
123
|
-
def start(original_args=ARGV, config={})
|
|
124
|
-
super do |given_args|
|
|
125
|
-
meth = normalize_task_name(given_args.shift)
|
|
126
|
-
task = all_tasks[meth]
|
|
127
|
-
|
|
128
|
-
if task
|
|
129
|
-
args, opts = Thor::Options.split(given_args)
|
|
130
|
-
config.merge!(:task_options => task.options)
|
|
131
|
-
else
|
|
132
|
-
args, opts = given_args, {}
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
task ||= Thor::Task::Dynamic.new(meth)
|
|
136
|
-
trailing = args[Range.new(arguments.size, -1)]
|
|
137
|
-
new(args, opts, config).invoke(task, trailing || [])
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
129
|
# Prints help information for the given task.
|
|
142
130
|
#
|
|
143
131
|
# ==== Parameters
|
|
@@ -153,7 +141,12 @@ class Thor
|
|
|
153
141
|
shell.say " #{banner(task)}"
|
|
154
142
|
shell.say
|
|
155
143
|
class_options_help(shell, nil => task.options.map { |_, o| o })
|
|
156
|
-
|
|
144
|
+
if task.long_description
|
|
145
|
+
shell.say "Description:"
|
|
146
|
+
shell.print_wrapped(task.long_description, :ident => 2)
|
|
147
|
+
else
|
|
148
|
+
shell.say task.description
|
|
149
|
+
end
|
|
157
150
|
end
|
|
158
151
|
|
|
159
152
|
# Prints help information for this class.
|
|
@@ -161,8 +154,8 @@ class Thor
|
|
|
161
154
|
# ==== Parameters
|
|
162
155
|
# shell<Thor::Shell>
|
|
163
156
|
#
|
|
164
|
-
def help(shell)
|
|
165
|
-
list = printable_tasks
|
|
157
|
+
def help(shell, subcommand = false)
|
|
158
|
+
list = printable_tasks(true, subcommand)
|
|
166
159
|
Thor::Util.thor_classes_in(self).each do |klass|
|
|
167
160
|
list += klass.printable_tasks(false)
|
|
168
161
|
end
|
|
@@ -175,28 +168,91 @@ class Thor
|
|
|
175
168
|
end
|
|
176
169
|
|
|
177
170
|
# Returns tasks ready to be printed.
|
|
178
|
-
def printable_tasks(all=true)
|
|
171
|
+
def printable_tasks(all = true, subcommand = false)
|
|
179
172
|
(all ? all_tasks : tasks).map do |_, task|
|
|
173
|
+
next if task.hidden?
|
|
180
174
|
item = []
|
|
181
|
-
item << banner(task)
|
|
175
|
+
item << banner(task, false, subcommand)
|
|
182
176
|
item << (task.description ? "# #{task.description.gsub(/\s+/m,' ')}" : "")
|
|
183
177
|
item
|
|
178
|
+
end.compact
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def subcommands
|
|
182
|
+
@subcommands ||= from_superclass(:subcommands, [])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def subcommand(subcommand, subcommand_class)
|
|
186
|
+
self.subcommands << subcommand.to_s
|
|
187
|
+
subcommand_class.subcommand_help subcommand
|
|
188
|
+
define_method(subcommand) { |*args| invoke subcommand_class, args }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Extend check unknown options to accept a hash of conditions.
|
|
192
|
+
#
|
|
193
|
+
# === Parameters
|
|
194
|
+
# options<Hash>: A hash containing :only and/or :except keys
|
|
195
|
+
def check_unknown_options!(options={})
|
|
196
|
+
@check_unknown_options ||= Hash.new
|
|
197
|
+
options.each do |key, value|
|
|
198
|
+
if value
|
|
199
|
+
@check_unknown_options[key] = Array(value)
|
|
200
|
+
else
|
|
201
|
+
@check_unknown_options.delete(key)
|
|
202
|
+
end
|
|
184
203
|
end
|
|
204
|
+
@check_unknown_options
|
|
185
205
|
end
|
|
186
206
|
|
|
187
|
-
|
|
188
|
-
|
|
207
|
+
# Overwrite check_unknown_options? to take subcommands and options into account.
|
|
208
|
+
def check_unknown_options?(config) #:nodoc:
|
|
209
|
+
options = check_unknown_options
|
|
210
|
+
return false unless options
|
|
211
|
+
|
|
212
|
+
task = config[:current_task]
|
|
213
|
+
return true unless task
|
|
214
|
+
|
|
215
|
+
name = task.name
|
|
216
|
+
|
|
217
|
+
if subcommands.include?(name)
|
|
218
|
+
false
|
|
219
|
+
elsif options[:except]
|
|
220
|
+
!options[:except].include?(name.to_sym)
|
|
221
|
+
elsif options[:only]
|
|
222
|
+
options[:only].include?(name.to_sym)
|
|
223
|
+
else
|
|
224
|
+
true
|
|
225
|
+
end
|
|
189
226
|
end
|
|
190
227
|
|
|
191
228
|
protected
|
|
192
229
|
|
|
230
|
+
# The method responsible for dispatching given the args.
|
|
231
|
+
def dispatch(meth, given_args, given_opts, config) #:nodoc:
|
|
232
|
+
meth ||= retrieve_task_name(given_args)
|
|
233
|
+
task = all_tasks[normalize_task_name(meth)]
|
|
234
|
+
|
|
235
|
+
if task
|
|
236
|
+
args, opts = Thor::Options.split(given_args)
|
|
237
|
+
else
|
|
238
|
+
args, opts = given_args, nil
|
|
239
|
+
task = Thor::DynamicTask.new(meth)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
opts = given_opts || opts || []
|
|
243
|
+
config.merge!(:current_task => task, :task_options => task.options)
|
|
244
|
+
|
|
245
|
+
trailing = args[Range.new(arguments.size, -1)]
|
|
246
|
+
new(args, opts, config).invoke_task(task, trailing || [])
|
|
247
|
+
end
|
|
248
|
+
|
|
193
249
|
# The banner for this class. You can customize it if you are invoking the
|
|
194
250
|
# thor class by another ways which is not the Thor::Runner. It receives
|
|
195
251
|
# the task that is going to be invoked and a boolean which indicates if
|
|
196
252
|
# the namespace should be displayed as arguments.
|
|
197
253
|
#
|
|
198
|
-
def banner(task)
|
|
199
|
-
"#{
|
|
254
|
+
def banner(task, namespace = nil, subcommand = false)
|
|
255
|
+
"#{basename} #{task.formatted_usage(self, $thor_runner, subcommand)}"
|
|
200
256
|
end
|
|
201
257
|
|
|
202
258
|
def baseclass #:nodoc:
|
|
@@ -205,10 +261,11 @@ class Thor
|
|
|
205
261
|
|
|
206
262
|
def create_task(meth) #:nodoc:
|
|
207
263
|
if @usage && @desc
|
|
208
|
-
|
|
209
|
-
|
|
264
|
+
base_class = @hide ? Thor::HiddenTask : Thor::Task
|
|
265
|
+
tasks[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
|
|
266
|
+
@usage, @desc, @long_desc, @method_options, @hide = nil
|
|
210
267
|
true
|
|
211
|
-
elsif self.all_tasks[meth
|
|
268
|
+
elsif self.all_tasks[meth] || meth == "method_missing"
|
|
212
269
|
true
|
|
213
270
|
else
|
|
214
271
|
puts "[WARNING] Attempted to create task #{meth.inspect} without usage or description. " <<
|
|
@@ -223,14 +280,30 @@ class Thor
|
|
|
223
280
|
@method_options = nil
|
|
224
281
|
end
|
|
225
282
|
|
|
283
|
+
# Retrieve the task name from given args.
|
|
284
|
+
def retrieve_task_name(args) #:nodoc:
|
|
285
|
+
meth = args.first.to_s unless args.empty?
|
|
286
|
+
|
|
287
|
+
if meth && (map[meth] || meth !~ /^\-/)
|
|
288
|
+
args.shift
|
|
289
|
+
else
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
226
294
|
# Receives a task name (can be nil), and try to get a map from it.
|
|
227
295
|
# If a map can't be found use the sent name or the default task.
|
|
228
|
-
#
|
|
229
296
|
def normalize_task_name(meth) #:nodoc:
|
|
230
|
-
|
|
231
|
-
meth = mapping || meth || default_task
|
|
297
|
+
meth = map[meth.to_s] || meth || default_task
|
|
232
298
|
meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
|
|
233
299
|
end
|
|
300
|
+
|
|
301
|
+
def subcommand_help(cmd)
|
|
302
|
+
desc "help [COMMAND]", "Describe subcommands or one specific subcommand"
|
|
303
|
+
class_eval <<-RUBY
|
|
304
|
+
def help(task = nil, subcommand = true); super; end
|
|
305
|
+
RUBY
|
|
306
|
+
end
|
|
234
307
|
end
|
|
235
308
|
|
|
236
309
|
include Thor::Base
|
|
@@ -238,7 +311,7 @@ class Thor
|
|
|
238
311
|
map HELP_MAPPINGS => :help
|
|
239
312
|
|
|
240
313
|
desc "help [TASK]", "Describe available tasks or one specific task"
|
|
241
|
-
def help(task=nil)
|
|
242
|
-
task ? self.class.task_help(shell, task) : self.class.help(shell)
|
|
314
|
+
def help(task = nil, subcommand = false)
|
|
315
|
+
task ? self.class.task_help(shell, task) : self.class.help(shell, subcommand)
|
|
243
316
|
end
|
|
244
317
|
end
|
|
@@ -37,7 +37,7 @@ describe Thor::Actions::Directory do
|
|
|
37
37
|
it "raises an error if the source does not exist" do
|
|
38
38
|
lambda {
|
|
39
39
|
invoke! "unknown"
|
|
40
|
-
}.must raise_error(Thor::Error, /Could not find "unknown" in source paths/)
|
|
40
|
+
}.must raise_error(Thor::Error, /Could not find "unknown" in any of your source paths/)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
it "copies the whole directory recursively to the default destination" do
|
|
@@ -84,7 +84,7 @@ describe Thor::Actions::Directory do
|
|
|
84
84
|
File.directory?(file).must be_true
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
it "does not copy .
|
|
87
|
+
it "does not copy .empty_directory files" do
|
|
88
88
|
invoke! "doc", "docs"
|
|
89
89
|
file = File.join(destination_root, "docs", "components", ".empty_directory")
|
|
90
90
|
File.exists?(file).must be_false
|
|
@@ -117,6 +117,13 @@ describe Thor::Actions do
|
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
describe "#template" do
|
|
120
|
+
it "allows using block helpers in the template" do
|
|
121
|
+
action :template, "doc/block_helper.rb"
|
|
122
|
+
|
|
123
|
+
file = File.join(destination_root, "doc/block_helper.rb")
|
|
124
|
+
File.read(file).must == "Hello world!"
|
|
125
|
+
end
|
|
126
|
+
|
|
120
127
|
it "evaluates the template given as source" do
|
|
121
128
|
runner.instance_variable_set("@klass", "Config")
|
|
122
129
|
action :template, "doc/config.rb"
|
data/spec/actions_spec.rb
CHANGED
|
@@ -107,7 +107,7 @@ describe Thor::Actions do
|
|
|
107
107
|
it "raises an error if source path is empty" do
|
|
108
108
|
lambda {
|
|
109
109
|
A.new.find_in_source_paths("foo")
|
|
110
|
-
}.must raise_error(Thor::Error, /
|
|
110
|
+
}.must raise_error(Thor::Error, /Currently you have no source paths/)
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
it "finds a template inside the source path" do
|
|
@@ -192,10 +192,28 @@ describe Thor::Actions do
|
|
|
192
192
|
@foo = "FOO"
|
|
193
193
|
say_status :cool, :padding
|
|
194
194
|
TEMPLATE
|
|
195
|
-
@template.
|
|
195
|
+
@template.stub(:read).and_return(@template)
|
|
196
196
|
|
|
197
|
+
@file = '/'
|
|
198
|
+
runner.stub(:open).and_return(@template)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "accepts a URL as the path" do
|
|
197
202
|
@file = "http://gist.github.com/103208.txt"
|
|
198
|
-
runner.should_receive(:open).and_return(@template)
|
|
203
|
+
runner.should_receive(:open).with(@file, "Accept" => "application/x-thor-template").and_return(@template)
|
|
204
|
+
action(:apply, @file)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "accepts a secure URL as the path" do
|
|
208
|
+
@file = "https://gist.github.com/103208.txt"
|
|
209
|
+
runner.should_receive(:open).with(@file, "Accept" => "application/x-thor-template").and_return(@template)
|
|
210
|
+
action(:apply, @file)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "accepts a local file path with spaces" do
|
|
214
|
+
@file = File.expand_path("fixtures/path with spaces", File.dirname(__FILE__))
|
|
215
|
+
runner.should_receive(:open).with(@file).and_return(@template)
|
|
216
|
+
action(:apply, @file)
|
|
199
217
|
end
|
|
200
218
|
|
|
201
219
|
it "opens a file and executes its content in the instance binding" do
|
data/spec/base_spec.rb
CHANGED
|
@@ -74,7 +74,7 @@ describe Thor::Base do
|
|
|
74
74
|
MyCounter.start(["1", "2", "--third", "3"])[2].must == 3
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
it "does not create an
|
|
77
|
+
it "does not create an accessor for it" do
|
|
78
78
|
BrokenCounter.start(["1", "2", "--third", "3"])[3].must be_false
|
|
79
79
|
end
|
|
80
80
|
end
|
|
@@ -176,8 +176,8 @@ describe Thor::Base do
|
|
|
176
176
|
it "returns tracked subclasses, grouped by the files they come from" do
|
|
177
177
|
thorfile = File.join(File.dirname(__FILE__), "fixtures", "script.thor")
|
|
178
178
|
Thor::Base.subclass_files[File.expand_path(thorfile)].must == [
|
|
179
|
-
MyScript, MyScript::AnotherScript, MyChildScript,
|
|
180
|
-
Scripts::MyDefaults, Scripts::ChildDefault
|
|
179
|
+
MyScript, MyScript::AnotherScript, MyChildScript, Barn,
|
|
180
|
+
Scripts::MyScript, Scripts::MyDefaults, Scripts::ChildDefault
|
|
181
181
|
]
|
|
182
182
|
end
|
|
183
183
|
|
|
@@ -244,6 +244,12 @@ describe Thor::Base do
|
|
|
244
244
|
MyScript.start(["foo", "bar", "--force", "true", "--unknown", "baz"])
|
|
245
245
|
}.strip.must == "Unknown switches '--unknown'"
|
|
246
246
|
end
|
|
247
|
+
|
|
248
|
+
it "checks unknown options except specified" do
|
|
249
|
+
capture(:stderr) {
|
|
250
|
+
MyScript.start(["with_optional", "NAME", "--omg", "--invalid"]).must == ["NAME", {}]
|
|
251
|
+
}.strip.must be_empty
|
|
252
|
+
end
|
|
247
253
|
end
|
|
248
254
|
|
|
249
255
|
describe "attr_*" do
|
|
File without changes
|
data/spec/fixtures/group.thor
CHANGED
|
@@ -6,9 +6,7 @@ class MyCounter < Thor::Group
|
|
|
6
6
|
from_superclass(:get_from_super, 13)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
File.expand_path(File.dirname(__FILE__))
|
|
11
|
-
end
|
|
9
|
+
source_root File.expand_path(File.dirname(__FILE__))
|
|
12
10
|
source_paths << File.expand_path("broken", File.dirname(__FILE__))
|
|
13
11
|
|
|
14
12
|
argument :first, :type => :numeric
|
|
@@ -20,7 +18,7 @@ class MyCounter < Thor::Group
|
|
|
20
18
|
|
|
21
19
|
desc <<-FOO
|
|
22
20
|
Description:
|
|
23
|
-
This generator
|
|
21
|
+
This generator runs three tasks: one, two and three.
|
|
24
22
|
FOO
|
|
25
23
|
|
|
26
24
|
def one
|
|
@@ -39,6 +37,13 @@ FOO
|
|
|
39
37
|
super
|
|
40
38
|
base.source_paths.unshift(File.expand_path(File.join(File.dirname(__FILE__), "doc")))
|
|
41
39
|
end
|
|
40
|
+
|
|
41
|
+
no_tasks do
|
|
42
|
+
def world(&block)
|
|
43
|
+
result = capture(&block)
|
|
44
|
+
concat(result.strip + " world!")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
class ClearCounter < MyCounter
|
|
@@ -81,3 +86,10 @@ class WhinyGenerator < Thor::Group
|
|
|
81
86
|
def wrong_arity(required)
|
|
82
87
|
end
|
|
83
88
|
end
|
|
89
|
+
|
|
90
|
+
class TaskConflict < Thor::Group
|
|
91
|
+
desc "A group with the same name as a default task"
|
|
92
|
+
def group
|
|
93
|
+
puts "group"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
File without changes
|
data/spec/fixtures/script.thor
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
class MyScript < Thor
|
|
2
|
-
check_unknown_options!
|
|
2
|
+
check_unknown_options! :except => :with_optional
|
|
3
3
|
|
|
4
4
|
attr_accessor :some_attribute
|
|
5
5
|
attr_writer :another_attribute
|
|
@@ -26,6 +26,11 @@ class MyScript < Thor
|
|
|
26
26
|
[type]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
desc "hidden TYPE", "this is hidden", :hide => true
|
|
30
|
+
def hidden(type)
|
|
31
|
+
[type]
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
desc "foo BAR", <<END
|
|
30
35
|
do some fooing
|
|
31
36
|
This is more info!
|
|
@@ -37,6 +42,7 @@ END
|
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
desc "example_default_task", "example!"
|
|
45
|
+
method_options :with => :string
|
|
40
46
|
def example_default_task
|
|
41
47
|
options.empty? ? "default task" : options
|
|
42
48
|
end
|
|
@@ -52,6 +58,12 @@ END
|
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
desc "long_description", "a" * 80
|
|
61
|
+
long_desc <<-D
|
|
62
|
+
This is a really really really long description.
|
|
63
|
+
Here you go. So very long.
|
|
64
|
+
|
|
65
|
+
It even has two paragraphs.
|
|
66
|
+
D
|
|
55
67
|
def long_description
|
|
56
68
|
end
|
|
57
69
|
|
|
@@ -60,6 +72,10 @@ END
|
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
method_options :all => :boolean
|
|
75
|
+
method_option :lazy, :lazy_default => "yes"
|
|
76
|
+
method_option :lazy_numeric, :type => :numeric, :lazy_default => 42
|
|
77
|
+
method_option :lazy_array, :type => :array, :lazy_default => %w[eat at joes]
|
|
78
|
+
method_option :lazy_hash, :type => :hash, :lazy_default => {'swedish' => 'meatballs'}
|
|
63
79
|
desc "with_optional NAME", "invoke with optional name"
|
|
64
80
|
def with_optional(name=nil)
|
|
65
81
|
[ name, options ]
|
|
@@ -114,6 +130,24 @@ class MyChildScript < MyScript
|
|
|
114
130
|
remove_task :boom, :undefine => true
|
|
115
131
|
end
|
|
116
132
|
|
|
133
|
+
class Barn < Thor
|
|
134
|
+
desc "open [ITEM]", "open the barn door"
|
|
135
|
+
def open(item = nil)
|
|
136
|
+
if item == "shotgun"
|
|
137
|
+
puts "That's going to leave a mark."
|
|
138
|
+
else
|
|
139
|
+
puts "Open sesame!"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
desc "paint [COLOR]", "paint the barn"
|
|
144
|
+
method_option :coats, :type => :numeric, :default => 2, :desc => 'how many coats of paint'
|
|
145
|
+
def paint(color='red')
|
|
146
|
+
puts "#{options[:coats]} coats of #{color} paint"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
end
|
|
150
|
+
|
|
117
151
|
module Scripts
|
|
118
152
|
class MyScript < MyChildScript
|
|
119
153
|
argument :accessor, :type => :string
|
|
@@ -126,11 +160,21 @@ module Scripts
|
|
|
126
160
|
end
|
|
127
161
|
|
|
128
162
|
class MyDefaults < Thor
|
|
163
|
+
check_unknown_options!
|
|
164
|
+
|
|
129
165
|
namespace :default
|
|
130
|
-
desc "
|
|
131
|
-
def
|
|
132
|
-
puts "
|
|
166
|
+
desc "cow", "prints 'moo'"
|
|
167
|
+
def cow
|
|
168
|
+
puts "moo"
|
|
133
169
|
end
|
|
170
|
+
|
|
171
|
+
desc "task_conflict", "only gets called when prepended with a colon"
|
|
172
|
+
def task_conflict
|
|
173
|
+
puts "task"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
desc "barn", "commands to manage the barn"
|
|
177
|
+
subcommand "barn", Barn
|
|
134
178
|
end
|
|
135
179
|
|
|
136
180
|
class ChildDefault < Thor
|
data/spec/group_spec.rb
CHANGED
|
@@ -36,11 +36,11 @@ describe Thor::Group do
|
|
|
36
36
|
|
|
37
37
|
describe "#desc" do
|
|
38
38
|
it "sets the description for a given class" do
|
|
39
|
-
MyCounter.desc.must == "Description:\n This generator
|
|
39
|
+
MyCounter.desc.must == "Description:\n This generator runs three tasks: one, two and three.\n"
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
it "can be inherited" do
|
|
43
|
-
BrokenCounter.desc.must == "Description:\n This generator
|
|
43
|
+
BrokenCounter.desc.must == "Description:\n This generator runs three tasks: one, two and three.\n"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
it "can be nil" do
|
|
@@ -59,7 +59,7 @@ describe Thor::Group do
|
|
|
59
59
|
|
|
60
60
|
it "shows description" do
|
|
61
61
|
@content.must =~ /Description:/
|
|
62
|
-
@content.must =~ /This generator
|
|
62
|
+
@content.must =~ /This generator runs three tasks: one, two and three./
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
it "shows options information" do
|
data/spec/invocation_spec.rb
CHANGED
|
@@ -37,13 +37,6 @@ describe Thor::Invocation do
|
|
|
37
37
|
base.invoke(B, :one, ["Jose"]).must == "Valim, Jose"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
it "accepts a Thor instance as argument" do
|
|
41
|
-
invoked = B.new([], :last_name => "Valim")
|
|
42
|
-
base = A.new
|
|
43
|
-
base.invoke(invoked, :one, ["Jose"]).must == "Valim, Jose"
|
|
44
|
-
base.invoke(invoked, :one, ["Jose"]).must be_nil
|
|
45
|
-
end
|
|
46
|
-
|
|
47
40
|
it "allows customized options to be given" do
|
|
48
41
|
base = A.new([], :last_name => "Wrong")
|
|
49
42
|
base.invoke(B, :one, ["Jose"], :last_name => "Valim").must == "Valim, Jose"
|
|
@@ -91,7 +84,7 @@ describe Thor::Invocation do
|
|
|
91
84
|
|
|
92
85
|
it "raises Thor::UndefinedTaskError if the task can't be found even if all tasks where already executed" do
|
|
93
86
|
base = C.new
|
|
94
|
-
silence(:stdout){ base.
|
|
87
|
+
silence(:stdout){ base.invoke_all }
|
|
95
88
|
|
|
96
89
|
lambda do
|
|
97
90
|
base.invoke("foo:bar")
|
data/spec/parser/option_spec.rb
CHANGED
|
@@ -6,8 +6,8 @@ describe Thor::Option do
|
|
|
6
6
|
Thor::Option.parse(key, value)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
def option(name,
|
|
10
|
-
@option ||= Thor::Option.new(name,
|
|
9
|
+
def option(name, *args)
|
|
10
|
+
@option ||= Thor::Option.new(name, *args)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
describe "#parse" do
|
data/spec/parser/options_spec.rb
CHANGED
|
@@ -47,6 +47,11 @@ describe Thor::Options do
|
|
|
47
47
|
it "accepts hashes" do
|
|
48
48
|
Thor::Options.to_switches(:count => {:a => :b}).must == "--count a:b"
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
it "accepts underscored options" do
|
|
52
|
+
Thor::Options.to_switches(:under_score_option => "foo bar").must == '--under_score_option "foo bar"'
|
|
53
|
+
end
|
|
54
|
+
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
describe "#parse" do
|
|
@@ -115,6 +120,18 @@ describe Thor::Options do
|
|
|
115
120
|
parse("--asdf---asdf", "baz", "--foo", "--asdf---dsf--asdf").must == {"foo" => "--asdf---dsf--asdf"}
|
|
116
121
|
check_unknown!
|
|
117
122
|
end
|
|
123
|
+
|
|
124
|
+
it "excepts underscores in commandline args hash for boolean" do
|
|
125
|
+
create :foo_bar => :boolean
|
|
126
|
+
parse("--foo_bar")["foo_bar"].must == true
|
|
127
|
+
parse("--no_foo_bar")["foo_bar"].must == false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "excepts underscores in commandline args hash for strings" do
|
|
131
|
+
create :foo_bar => :string, :baz_foo => :string
|
|
132
|
+
parse("--foo_bar", "baz")["foo_bar"].must == "baz"
|
|
133
|
+
parse("--baz_foo", "foo bar")["baz_foo"].must == "foo bar"
|
|
134
|
+
end
|
|
118
135
|
|
|
119
136
|
describe "with no input" do
|
|
120
137
|
it "and no switches returns an empty hash" do
|
|
@@ -170,12 +187,22 @@ describe Thor::Options do
|
|
|
170
187
|
parse("--foo=12")["foo"].must == "12"
|
|
171
188
|
parse("--foo=bar=baz")["foo"].must == "bar=baz"
|
|
172
189
|
end
|
|
190
|
+
|
|
191
|
+
it "must accept underscores switch=value assignment" do
|
|
192
|
+
create :foo_bar => :required
|
|
193
|
+
parse("--foo_bar=http://example.com/under_score/")["foo_bar"].must == "http://example.com/under_score/"
|
|
194
|
+
end
|
|
173
195
|
|
|
174
196
|
it "accepts a --no-switch format" do
|
|
175
197
|
create "--foo" => "bar"
|
|
176
198
|
parse("--no-foo")["foo"].must be_nil
|
|
177
199
|
end
|
|
178
200
|
|
|
201
|
+
it "does not consume an argument for --no-switch format" do
|
|
202
|
+
create "--cheese" => :string
|
|
203
|
+
parse('burger', '--no-cheese', 'fries')["cheese"].must be_nil
|
|
204
|
+
end
|
|
205
|
+
|
|
179
206
|
it "accepts a --switch format on non required types" do
|
|
180
207
|
create "--foo" => :string
|
|
181
208
|
parse("--foo")["foo"].must == "foo"
|
data/spec/runner_spec.rb
CHANGED
|
@@ -30,9 +30,8 @@ describe Thor::Runner do
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
it "raises error if a class/task cannot be found" do
|
|
33
|
-
Thor::Runner.should_receive(:exit).with(1)
|
|
34
33
|
content = capture(:stderr){ Thor::Runner.start(["help", "unknown"]) }
|
|
35
|
-
content.strip.must == 'Could not find
|
|
34
|
+
content.strip.must == 'Could not find task "unknown" in "default" namespace.'
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
@@ -68,10 +67,9 @@ describe Thor::Runner do
|
|
|
68
67
|
end
|
|
69
68
|
|
|
70
69
|
it "raises an error if class/task can't be found" do
|
|
71
|
-
Thor::Runner.should_receive(:exit).with(1)
|
|
72
70
|
ARGV.replace ["unknown"]
|
|
73
71
|
content = capture(:stderr){ Thor::Runner.start }
|
|
74
|
-
content.strip.must == 'Could not find
|
|
72
|
+
content.strip.must == 'Could not find task "unknown" in "default" namespace.'
|
|
75
73
|
end
|
|
76
74
|
|
|
77
75
|
it "does not swallow NoMethodErrors that occur inside the called method" do
|
|
@@ -87,7 +85,7 @@ describe Thor::Runner do
|
|
|
87
85
|
it "does not swallow Thor InvocationError" do
|
|
88
86
|
ARGV.replace ["my_script:animal"]
|
|
89
87
|
content = capture(:stderr) { Thor::Runner.start }
|
|
90
|
-
content.strip.must == '"animal" was called incorrectly. Call as "my_script:animal TYPE".'
|
|
88
|
+
content.strip.must == '"animal" was called incorrectly. Call as "thor my_script:animal TYPE".'
|
|
91
89
|
end
|
|
92
90
|
end
|
|
93
91
|
|
|
@@ -145,12 +143,22 @@ describe Thor::Runner do
|
|
|
145
143
|
|
|
146
144
|
it "presents tasks in the default namespace with an empty namespace" do
|
|
147
145
|
ARGV.replace ["list"]
|
|
148
|
-
capture(:stdout) { Thor::Runner.start }.must =~ /^thor :
|
|
146
|
+
capture(:stdout) { Thor::Runner.start }.must =~ /^thor :cow\s+# prints 'moo'/m
|
|
149
147
|
end
|
|
150
148
|
|
|
151
149
|
it "runs tasks with an empty namespace from the default namespace" do
|
|
152
|
-
ARGV.replace [":
|
|
153
|
-
capture(:stdout) { Thor::Runner.start }.must == "
|
|
150
|
+
ARGV.replace [":task_conflict"]
|
|
151
|
+
capture(:stdout) { Thor::Runner.start }.must == "task\n"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "runs groups even when there is a task with the same name" do
|
|
155
|
+
ARGV.replace ["task_conflict"]
|
|
156
|
+
capture(:stdout) { Thor::Runner.start }.must == "group\n"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "runs tasks with no colon in the default namespace" do
|
|
160
|
+
ARGV.replace ["cow"]
|
|
161
|
+
capture(:stdout) { Thor::Runner.start }.must == "moo\n"
|
|
154
162
|
end
|
|
155
163
|
end
|
|
156
164
|
|