thor 0.14.6 → 0.15.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.
Files changed (54) hide show
  1. data/.autotest +8 -0
  2. data/.document +5 -0
  3. data/.gemtest +0 -0
  4. data/.gitignore +44 -0
  5. data/.rspec +2 -0
  6. data/.travis.yml +9 -0
  7. data/CHANGELOG.rdoc +4 -4
  8. data/Gemfile +19 -0
  9. data/{LICENSE → LICENSE.md} +2 -2
  10. data/README.md +21 -300
  11. data/Thorfile +21 -15
  12. data/lib/thor.rb +56 -11
  13. data/lib/thor/actions.rb +7 -3
  14. data/lib/thor/actions/create_link.rb +1 -1
  15. data/lib/thor/actions/directory.rb +7 -3
  16. data/lib/thor/actions/empty_directory.rb +24 -5
  17. data/lib/thor/actions/file_manipulation.rb +40 -2
  18. data/lib/thor/base.rb +66 -28
  19. data/lib/thor/error.rb +6 -1
  20. data/lib/thor/group.rb +20 -8
  21. data/lib/thor/invocation.rb +4 -2
  22. data/lib/thor/parser/arguments.rb +6 -2
  23. data/lib/thor/parser/option.rb +3 -2
  24. data/lib/thor/parser/options.rb +13 -8
  25. data/lib/thor/rake_compat.rb +13 -8
  26. data/lib/thor/runner.rb +16 -4
  27. data/lib/thor/shell.rb +2 -2
  28. data/lib/thor/shell/basic.rb +86 -29
  29. data/lib/thor/shell/color.rb +40 -4
  30. data/lib/thor/shell/html.rb +28 -26
  31. data/lib/thor/task.rb +26 -8
  32. data/lib/thor/util.rb +26 -7
  33. data/lib/thor/version.rb +1 -1
  34. data/spec/actions/create_link_spec.rb +81 -0
  35. data/spec/actions/empty_directory_spec.rb +32 -0
  36. data/spec/actions/file_manipulation_spec.rb +61 -1
  37. data/spec/actions_spec.rb +4 -0
  38. data/spec/base_spec.rb +10 -5
  39. data/spec/exit_condition_spec.rb +19 -0
  40. data/spec/fixtures/script.thor +8 -2
  41. data/spec/group_spec.rb +39 -1
  42. data/spec/parser/arguments_spec.rb +1 -0
  43. data/spec/parser/options_spec.rb +12 -2
  44. data/spec/rake_compat_spec.rb +11 -7
  45. data/spec/register_spec.rb +43 -0
  46. data/spec/runner_spec.rb +34 -3
  47. data/spec/shell/basic_spec.rb +50 -3
  48. data/spec/shell/color_spec.rb +46 -6
  49. data/spec/shell/html_spec.rb +10 -5
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/task_spec.rb +26 -16
  52. data/spec/thor_spec.rb +56 -3
  53. data/thor.gemspec +26 -0
  54. metadata +174 -117
@@ -82,6 +82,10 @@ describe Thor::Actions do
82
82
  end
83
83
  end
84
84
 
85
+ it "creates proper relative paths for absolute file location" do
86
+ runner.relative_to_original_destination_root('/test/file').should == "/test/file"
87
+ end
88
+
85
89
  describe "#source_paths_for_search" do
86
90
  it "add source_root to source_paths_for_search" do
87
91
  MyCounter.source_paths_for_search.should include(File.expand_path("fixtures", File.dirname(__FILE__)))
@@ -227,10 +227,15 @@ describe Thor::Base do
227
227
  end
228
228
 
229
229
  describe "#start" do
230
- it "raises an error instead of rescueing if --debug is given" do
231
- lambda {
232
- MyScript.start ["what", "--debug"]
233
- }.should raise_error(Thor::UndefinedTaskError, 'Could not find task "what" in "my_script" namespace.')
230
+ it "raises an error instead of rescueing if THOR_DEBUG=1 is given" do
231
+ begin
232
+ ENV["THOR_DEBUG"] = 1
233
+ lambda {
234
+ MyScript.start ["what", "--debug"]
235
+ }.should raise_error(Thor::UndefinedTaskError, 'Could not find task "what" in "my_script" namespace.')
236
+ rescue
237
+ ENV["THOR_DEBUG"] = nil
238
+ end
234
239
  end
235
240
 
236
241
  it "does not steal args" do
@@ -247,7 +252,7 @@ describe Thor::Base do
247
252
 
248
253
  it "checks unknown options except specified" do
249
254
  capture(:stderr) {
250
- MyScript.start(["with_optional", "NAME", "--omg", "--invalid"]).should == ["NAME", {}]
255
+ MyScript.start(["with_optional", "NAME", "--omg", "--invalid"]).should == ["NAME", {}, ["--omg", "--invalid"]]
251
256
  }.strip.should be_empty
252
257
  end
253
258
  end
@@ -0,0 +1,19 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'thor/base'
3
+
4
+ describe "Exit conditions" do
5
+ it "should exit 0, not bubble up EPIPE, if EPIPE is raised" do
6
+ epiped = false
7
+
8
+ task = Class.new(Thor) do
9
+ desc "my_action", "testing EPIPE"
10
+ define_method :my_action do
11
+ epiped = true
12
+ raise Errno::EPIPE
13
+ end
14
+ end
15
+
16
+ lambda { task.start(["my_action"]) }.should raise_error(SystemExit)
17
+ epiped.should == true
18
+ end
19
+ end
@@ -10,6 +10,8 @@ class MyScript < Thor
10
10
 
11
11
  map "-T" => :animal, ["-f", "--foo"] => :foo
12
12
 
13
+ map "animal_prison" => "zoo"
14
+
13
15
  desc "zoo", "zoo around"
14
16
  def zoo
15
17
  true
@@ -26,11 +28,15 @@ class MyScript < Thor
26
28
  [type]
27
29
  end
28
30
 
31
+ map "hid" => "hidden"
32
+
29
33
  desc "hidden TYPE", "this is hidden", :hide => true
30
34
  def hidden(type)
31
35
  [type]
32
36
  end
33
37
 
38
+ map "fu" => "zoo"
39
+
34
40
  desc "foo BAR", <<END
35
41
  do some fooing
36
42
  This is more info!
@@ -77,8 +83,8 @@ END
77
83
  method_option :lazy_array, :type => :array, :lazy_default => %w[eat at joes]
78
84
  method_option :lazy_hash, :type => :hash, :lazy_default => {'swedish' => 'meatballs'}
79
85
  desc "with_optional NAME", "invoke with optional name"
80
- def with_optional(name=nil)
81
- [ name, options ]
86
+ def with_optional(name=nil, *args)
87
+ [ name, options, args ]
82
88
  end
83
89
 
84
90
  class AnotherScript < Thor
@@ -32,7 +32,7 @@ describe Thor::Group do
32
32
  end
33
33
 
34
34
  it "raises an error when a Thor group task expects arguments" do
35
- lambda { WhinyGenerator.start }.should raise_error(ArgumentError, /Are you sure it has arity equals to 0\?/)
35
+ lambda { WhinyGenerator.start }.should raise_error(ArgumentError, /thor wrong_arity takes 1 argument, but it should not/)
36
36
  end
37
37
 
38
38
  it "invokes help message if any of the shortcuts is given" do
@@ -175,4 +175,42 @@ describe Thor::Group do
175
175
  end
176
176
  end
177
177
  end
178
+
179
+ describe "edge-cases" do
180
+ it "can handle boolean options followed by arguments" do
181
+ klass = Class.new(Thor::Group) do
182
+ desc "say hi to name"
183
+ argument :name, :type => :string
184
+ class_option :loud, :type => :boolean
185
+
186
+ def hi
187
+ name.upcase! if options[:loud]
188
+ "Hi #{name}"
189
+ end
190
+ end
191
+
192
+ klass.start(["jose"]).should == ["Hi jose"]
193
+ klass.start(["jose", "--loud"]).should == ["Hi JOSE"]
194
+ klass.start(["--loud", "jose"]).should == ["Hi JOSE"]
195
+ end
196
+
197
+ it "provides extra args as `args`" do
198
+ klass = Class.new(Thor::Group) do
199
+ desc "say hi to name"
200
+ argument :name, :type => :string
201
+ class_option :loud, :type => :boolean
202
+
203
+ def hi
204
+ name.upcase! if options[:loud]
205
+ out = "Hi #{name}"
206
+ out << ": " << args.join(", ") unless args.empty?
207
+ out
208
+ end
209
+ end
210
+
211
+ klass.start(["jose"]).should == ["Hi jose"]
212
+ klass.start(["jose", "--loud"]).should == ["Hi JOSE"]
213
+ klass.start(["--loud", "jose"]).should == ["Hi JOSE"]
214
+ end
215
+ end
178
216
  end
@@ -26,6 +26,7 @@ describe Thor::Arguments do
26
26
  create :string => nil, :hash => nil
27
27
  parse("product", "title:string", "age:integer")["string"].should == "product"
28
28
  parse("product", "title:string", "age:integer")["hash"].should == { "title" => "string", "age" => "integer"}
29
+ parse("product", "url:http://www.amazon.com/gp/product/123")["hash"].should == { "url" => "http://www.amazon.com/gp/product/123" }
29
30
  end
30
31
 
31
32
  it "accepts arrays" do
@@ -121,13 +121,13 @@ describe Thor::Options do
121
121
  check_unknown!
122
122
  end
123
123
 
124
- it "excepts underscores in commandline args hash for boolean" do
124
+ it "accepts underscores in commandline args hash for boolean" do
125
125
  create :foo_bar => :boolean
126
126
  parse("--foo_bar")["foo_bar"].should == true
127
127
  parse("--no_foo_bar")["foo_bar"].should == false
128
128
  end
129
129
 
130
- it "excepts underscores in commandline args hash for strings" do
130
+ it "accepts underscores in commandline args hash for strings" do
131
131
  create :foo_bar => :string, :baz_foo => :string
132
132
  parse("--foo_bar", "baz")["foo_bar"].should == "baz"
133
133
  parse("--baz_foo", "foo bar")["baz_foo"].should == "foo bar"
@@ -229,6 +229,10 @@ describe Thor::Options do
229
229
  parse("--foo", "--bar")["foo"].should == true
230
230
  end
231
231
 
232
+ it "uses the default value if no switch is given" do
233
+ parse("")["foo"].should == false
234
+ end
235
+
232
236
  it "accepts --opt=value assignment" do
233
237
  parse("--foo=true")["foo"].should == true
234
238
  parse("--foo=false")["foo"].should == false
@@ -258,6 +262,12 @@ describe Thor::Options do
258
262
  parse("--no-foo-bar")["foo_bar"].should == false
259
263
  parse("--skip-foo-bar")["foo_bar"].should == false
260
264
  end
265
+
266
+ it "doesn't eat the next part of the param" do
267
+ create :foo => :boolean
268
+ parse("--foo", "bar").should == {"foo" => true}
269
+ @opt.remaining.should == ["bar"]
270
+ end
261
271
  end
262
272
 
263
273
  describe "with :hash type" do
@@ -2,20 +2,24 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
  require 'thor/rake_compat'
3
3
  require 'rake/tasklib'
4
4
 
5
+ $main = self
6
+
5
7
  class RakeTask < Rake::TaskLib
6
8
  def initialize
7
9
  define
8
10
  end
9
11
 
10
12
  def define
11
- desc "Say it's cool"
12
- task :cool do
13
- puts "COOL"
14
- end
13
+ $main.instance_eval do
14
+ desc "Say it's cool"
15
+ task :cool do
16
+ puts "COOL"
17
+ end
15
18
 
16
- namespace :hiper_mega do
17
- task :super do
18
- puts "HIPER MEGA SUPER"
19
+ namespace :hiper_mega do
20
+ task :super do
21
+ puts "HIPER MEGA SUPER"
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -40,6 +40,19 @@ class GroupPlugin < Thor::Group
40
40
  end
41
41
  end
42
42
 
43
+ class ClassOptionGroupPlugin < Thor::Group
44
+ class_option :who,
45
+ :type => :string,
46
+ :aliases => "-w",
47
+ :default => "zebra"
48
+ end
49
+
50
+ class CompatibleWith19Plugin < ClassOptionGroupPlugin
51
+ desc "animal"
52
+ def animal
53
+ p options[:who]
54
+ end
55
+ end
43
56
 
44
57
  BoringVendorProvidedCLI.register(
45
58
  ExcitingPluginCLI,
@@ -60,6 +73,12 @@ BoringVendorProvidedCLI.register(
60
73
  "Do a bunch of things in a row",
61
74
  "purple monkey dishwasher")
62
75
 
76
+ BoringVendorProvidedCLI.register(
77
+ CompatibleWith19Plugin,
78
+ 'zoo',
79
+ "zoo [-w animal]",
80
+ "Shows a provided animal or just zebra")
81
+
63
82
  describe ".register-ing a Thor subclass" do
64
83
  it "registers the plugin as a subcommand" do
65
84
  fireworks_output = capture(:stdout) { BoringVendorProvidedCLI.start(%w[exciting fireworks]) }
@@ -71,6 +90,18 @@ describe ".register-ing a Thor subclass" do
71
90
  help_output.should include('do exciting things')
72
91
  end
73
92
 
93
+ context "when $thor_runner is false" do
94
+ it "includes the plugin's subcommand name in subcommand's help" do
95
+ begin
96
+ $thor_runner = false
97
+ help_output = capture(:stdout) { BoringVendorProvidedCLI.start(%w[exciting]) }
98
+ help_output.should include('thor exciting_plugin_c_l_i fireworks')
99
+ ensure
100
+ $thor_runner = true
101
+ end
102
+ end
103
+ end
104
+
74
105
  context "when hidden" do
75
106
  it "omits the hidden plugin's usage from the help" do
76
107
  help_output = capture(:stdout) { BoringVendorProvidedCLI.start(%w[help]) }
@@ -90,3 +121,15 @@ describe ".register-ing a Thor::Group subclass" do
90
121
  group_output.should == "part one\npart two\n"
91
122
  end
92
123
  end
124
+
125
+ describe "1.8 and 1.9 syntax compatibility" do
126
+ it "is compatible with both 1.8 and 1.9 syntax w/o task options" do
127
+ group_output = capture(:stdout) { BoringVendorProvidedCLI.start(%w[zoo]) }
128
+ group_output.should match /zebra/
129
+ end
130
+
131
+ it "is compatible with both 1.8 and 1.9 syntax w/task options" do
132
+ group_output = capture(:stdout) { BoringVendorProvidedCLI.start(%w[zoo -w lion]) }
133
+ group_output.should match /lion/
134
+ end
135
+ end
@@ -2,6 +2,16 @@ require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
2
2
  require 'thor/runner'
3
3
 
4
4
  describe Thor::Runner do
5
+ def when_no_thorfiles_exist
6
+ old_dir = Dir.pwd
7
+ Dir.chdir '..'
8
+ delete = Thor::Base.subclasses.select {|e| e.namespace == 'default' }
9
+ delete.each {|e| Thor::Base.subclasses.delete e }
10
+ yield
11
+ Thor::Base.subclasses.concat delete
12
+ Dir.chdir old_dir
13
+ end
14
+
5
15
  describe "#help" do
6
16
  it "shows information about Thor::Runner itself" do
7
17
  capture(:stdout){ Thor::Runner.start(["help"]) }.should =~ /List the available thor tasks/
@@ -33,6 +43,14 @@ describe Thor::Runner do
33
43
  content = capture(:stderr){ Thor::Runner.start(["help", "unknown"]) }
34
44
  content.strip.should == 'Could not find task "unknown" in "default" namespace.'
35
45
  end
46
+
47
+ it "raises error if a class/task cannot be found for a setup without thorfiles" do
48
+ when_no_thorfiles_exist do
49
+ Thor::Runner.should_receive :exit
50
+ content = capture(:stderr){ Thor::Runner.start(["help", "unknown"]) }
51
+ content.strip.should == 'Could not find task "unknown".'
52
+ end
53
+ end
36
54
  end
37
55
 
38
56
  describe "#start" do
@@ -72,6 +90,15 @@ describe Thor::Runner do
72
90
  content.strip.should == 'Could not find task "unknown" in "default" namespace.'
73
91
  end
74
92
 
93
+ it "raises an error if class/task can't be found in a setup without thorfiles" do
94
+ when_no_thorfiles_exist do
95
+ ARGV.replace ["unknown"]
96
+ Thor::Runner.should_receive :exit
97
+ content = capture(:stderr){ Thor::Runner.start }
98
+ content.strip.should == 'Could not find task "unknown".'
99
+ end
100
+ end
101
+
75
102
  it "does not swallow NoMethodErrors that occur inside the called method" do
76
103
  ARGV.replace ["my_script:call_unexistent_method"]
77
104
  lambda { Thor::Runner.start }.should raise_error(NoMethodError)
@@ -79,13 +106,13 @@ describe Thor::Runner do
79
106
 
80
107
  it "does not swallow Thor::Group InvocationError" do
81
108
  ARGV.replace ["whiny_generator"]
82
- lambda { Thor::Runner.start }.should raise_error(ArgumentError, /Are you sure it has arity equals to 0\?/)
109
+ lambda { Thor::Runner.start }.should raise_error(ArgumentError, /thor wrong_arity takes 1 argument, but it should not/)
83
110
  end
84
111
 
85
112
  it "does not swallow Thor InvocationError" do
86
113
  ARGV.replace ["my_script:animal"]
87
114
  content = capture(:stderr) { Thor::Runner.start }
88
- content.strip.should == '"animal" was called incorrectly. Call as "thor my_script:animal TYPE".'
115
+ content.strip.should == 'thor animal requires at least 1 argument: "thor my_script:animal TYPE".'
89
116
  end
90
117
  end
91
118
 
@@ -197,7 +224,11 @@ describe Thor::Runner do
197
224
 
198
225
  it "updates existing thor files" do
199
226
  path = File.join(Thor::Util.thor_root, @original_yaml["random"][:filename])
200
- File.should_receive(:delete).with(path)
227
+ if File.directory? path
228
+ FileUtils.should_receive(:rm_rf).with(path)
229
+ else
230
+ File.should_receive(:delete).with(path)
231
+ end
201
232
  silence(:stdout) { Thor::Runner.start(["update", "random"]) }
202
233
  end
203
234
 
@@ -21,6 +21,19 @@ describe Thor::Shell::Basic do
21
21
  $stdin.should_receive(:gets).and_return('Sure')
22
22
  shell.ask("Should I overwrite it?").should == "Sure"
23
23
  end
24
+
25
+ it "prints a message to the user with the available options and determines the correctness of the answer" do
26
+ $stdout.should_receive(:print).with('What\'s your favorite Neopolitan flavor? ["strawberry", "chocolate", "vanilla"] ')
27
+ $stdin.should_receive(:gets).and_return('chocolate')
28
+ shell.ask("What's your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]).should == "chocolate"
29
+ end
30
+
31
+ it "prints a message to the user with the available options and reasks the question after an incorrect repsonse" do
32
+ $stdout.should_receive(:print).with('What\'s your favorite Neopolitan flavor? ["strawberry", "chocolate", "vanilla"] ').twice
33
+ $stdout.should_receive(:puts).with('Your response must be one of: ["strawberry", "chocolate", "vanilla"]. Please try again.')
34
+ $stdin.should_receive(:gets).and_return('moose tracks', 'chocolate')
35
+ shell.ask("What's your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"]).should == "chocolate"
36
+ end
24
37
  end
25
38
 
26
39
  describe "#yes?" do
@@ -122,8 +135,8 @@ xyz #786 last three
122
135
  TABLE
123
136
  end
124
137
 
125
- it "prints a table with identation" do
126
- content = capture(:stdout){ shell.print_table(@table, :ident => 2) }
138
+ it "prints a table with indentation" do
139
+ content = capture(:stdout){ shell.print_table(@table, :indent => 2) }
127
140
  content.should == <<-TABLE
128
141
  abc #123 first three
129
142
  #0 empty
@@ -133,7 +146,7 @@ TABLE
133
146
 
134
147
  it "uses maximum terminal width" do
135
148
  shell.should_receive(:terminal_width).and_return(20)
136
- content = capture(:stdout){ shell.print_table(@table, :ident => 2, :truncate => true) }
149
+ content = capture(:stdout){ shell.print_table(@table, :indent => 2, :truncate => true) }
137
150
  content.should == <<-TABLE
138
151
  abc #123 firs...
139
152
  #0 empty
@@ -147,6 +160,40 @@ TABLE
147
160
  abc #123 first three
148
161
  #0 empty
149
162
  xyz #786 last three
163
+ TABLE
164
+ end
165
+
166
+ it "prints tables with implicit columns" do
167
+ 2.times { @table.first.pop }
168
+ content = capture(:stdout){ shell.print_table(@table) }
169
+ content.should == <<-TABLE
170
+ abc
171
+ #0 empty
172
+ xyz #786 last three
173
+ TABLE
174
+ end
175
+
176
+ it "prints a table with small numbers, and right-aligns them" do
177
+ table = [
178
+ ["Name", "Number", "Color"],
179
+ ["Erik", 1, "green"]
180
+ ]
181
+ content = capture(:stdout){ shell.print_table(table) }
182
+ content.should == <<-TABLE
183
+ Name Number Color
184
+ Erik 1 green
185
+ TABLE
186
+ end
187
+
188
+ it "prints a table with big numbers" do
189
+ table = [
190
+ ["Name", "Number", "Color"],
191
+ ["Erik", 1234567890123, "green"]
192
+ ]
193
+ content = capture(:stdout){ shell.print_table(table) }
194
+ content.should == <<-TABLE
195
+ Name Number Color
196
+ Erik 1234567890123 green
150
197
  TABLE
151
198
  end
152
199
  end