thor 0.14.6 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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