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
@@ -50,10 +50,46 @@ class Thor
50
50
  # on Highline implementation and it automatically appends CLEAR to the end
51
51
  # of the returned String.
52
52
  #
53
- def set_color(string, color, bold=false)
54
- color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol)
55
- bold = bold ? BOLD : ""
56
- "#{bold}#{color}#{string}#{CLEAR}"
53
+ # Pass foreground, background and bold options to this method as
54
+ # symbols.
55
+ #
56
+ # Example:
57
+ #
58
+ # set_color "Hi!", :red, :on_white, :bold
59
+ #
60
+ # The available colors are:
61
+ #
62
+ # :bold
63
+ # :black
64
+ # :red
65
+ # :green
66
+ # :yellow
67
+ # :blue
68
+ # :magenta
69
+ # :cyan
70
+ # :white
71
+ # :on_black
72
+ # :on_red
73
+ # :on_green
74
+ # :on_yellow
75
+ # :on_blue
76
+ # :on_magenta
77
+ # :on_cyan
78
+ # :on_white
79
+ def set_color(string, *colors)
80
+ if colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) }
81
+ ansi_colors = colors.map { |color| lookup_color(color) }
82
+ "#{ansi_colors.join}#{string}#{CLEAR}"
83
+ else
84
+ # The old API was `set_color(color, bold=boolean)`. We
85
+ # continue to support the old API because you should never
86
+ # break old APIs unnecessarily :P
87
+ foreground, bold = colors
88
+ foreground = self.class.const_get(foreground.to_s.upcase) if foreground.is_a?(Symbol)
89
+
90
+ bold = bold ? BOLD : ""
91
+ "#{bold}#{foreground}#{string}#{CLEAR}"
92
+ end
57
93
  end
58
94
 
59
95
  protected
@@ -7,56 +7,58 @@ class Thor
7
7
  #
8
8
  class HTML < Basic
9
9
  # The start of an HTML bold sequence.
10
- BOLD = "<strong>"
11
- # The end of an HTML bold sequence.
12
- END_BOLD = "</strong>"
13
-
14
- # Embed in a String to clear previous color selection.
15
- CLEAR = "</span>"
10
+ BOLD = "font-weight: bold"
16
11
 
17
12
  # Set the terminal's foreground HTML color to black.
18
- BLACK = '<span style="color: black;">'
13
+ BLACK = 'color: black'
19
14
  # Set the terminal's foreground HTML color to red.
20
- RED = '<span style="color: red;">'
15
+ RED = 'color: red'
21
16
  # Set the terminal's foreground HTML color to green.
22
- GREEN = '<span style="color: green;">'
17
+ GREEN = 'color: green'
23
18
  # Set the terminal's foreground HTML color to yellow.
24
- YELLOW = '<span style="color: yellow;">'
19
+ YELLOW = 'color: yellow'
25
20
  # Set the terminal's foreground HTML color to blue.
26
- BLUE = '<span style="color: blue;">'
21
+ BLUE = 'color: blue'
27
22
  # Set the terminal's foreground HTML color to magenta.
28
- MAGENTA = '<span style="color: magenta;">'
23
+ MAGENTA = 'color: magenta'
29
24
  # Set the terminal's foreground HTML color to cyan.
30
- CYAN = '<span style="color: cyan;">'
25
+ CYAN = 'color: cyan'
31
26
  # Set the terminal's foreground HTML color to white.
32
- WHITE = '<span style="color: white;">'
27
+ WHITE = 'color: white'
33
28
 
34
29
  # Set the terminal's background HTML color to black.
35
- ON_BLACK = '<span style="background-color: black">'
30
+ ON_BLACK = 'background-color: black'
36
31
  # Set the terminal's background HTML color to red.
37
- ON_RED = '<span style="background-color: red">'
32
+ ON_RED = 'background-color: red'
38
33
  # Set the terminal's background HTML color to green.
39
- ON_GREEN = '<span style="background-color: green">'
34
+ ON_GREEN = 'background-color: green'
40
35
  # Set the terminal's background HTML color to yellow.
41
- ON_YELLOW = '<span style="background-color: yellow">'
36
+ ON_YELLOW = 'background-color: yellow'
42
37
  # Set the terminal's background HTML color to blue.
43
- ON_BLUE = '<span style="background-color: blue">'
38
+ ON_BLUE = 'background-color: blue'
44
39
  # Set the terminal's background HTML color to magenta.
45
- ON_MAGENTA = '<span style="background-color: magenta">'
40
+ ON_MAGENTA = 'background-color: magenta'
46
41
  # Set the terminal's background HTML color to cyan.
47
- ON_CYAN = '<span style="background-color: cyan">'
42
+ ON_CYAN = 'background-color: cyan'
48
43
  # Set the terminal's background HTML color to white.
49
- ON_WHITE = '<span style="background-color: white">'
44
+ ON_WHITE = 'background-color: white'
50
45
 
51
46
  # Set color by using a string or one of the defined constants. If a third
52
47
  # option is set to true, it also adds bold to the string. This is based
53
48
  # on Highline implementation and it automatically appends CLEAR to the end
54
49
  # of the returned String.
55
50
  #
56
- def set_color(string, color, bold=false)
57
- color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol)
58
- bold, end_bold = bold ? [BOLD, END_BOLD] : ['', '']
59
- "#{bold}#{color}#{string}#{CLEAR}#{end_bold}"
51
+ def set_color(string, *colors)
52
+ if colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) }
53
+ html_colors = colors.map { |color| lookup_color(color) }
54
+ "<span style=\"#{html_colors.join("; ")};\">#{string}</span>"
55
+ else
56
+ color, bold = colors
57
+ html_color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol)
58
+ styles = [html_color]
59
+ styles << BOLD if bold
60
+ "<span style=\"#{styles.join("; ")};\">#{string}</span>"
61
+ end
60
62
  end
61
63
 
62
64
  # Ask something to the user and receives a response.
@@ -18,11 +18,21 @@ class Thor
18
18
  # By default, a task invokes a method in the thor class. You can change this
19
19
  # implementation to create custom tasks.
20
20
  def run(instance, args=[])
21
- public_method?(instance) ?
22
- instance.send(name, *args) : instance.class.handle_no_task_error(name)
21
+ arity = nil
22
+
23
+ if private_method?(instance)
24
+ instance.class.handle_no_task_error(name)
25
+ elsif public_method?(instance)
26
+ arity = instance.method(name).arity
27
+ instance.send(name, *args)
28
+ elsif local_method?(instance, :method_missing)
29
+ instance.send(:method_missing, name.to_sym, *args)
30
+ else
31
+ instance.class.handle_no_task_error(name)
32
+ end
23
33
  rescue ArgumentError => e
24
34
  handle_argument_error?(instance, e, caller) ?
25
- instance.class.handle_argument_error(self, e) : (raise e)
35
+ instance.class.handle_argument_error(self, e, arity) : (raise e)
26
36
  rescue NoMethodError => e
27
37
  handle_no_method_error?(instance, e, caller) ?
28
38
  instance.class.handle_no_task_error(name) : (raise e)
@@ -34,8 +44,8 @@ class Thor
34
44
  if namespace
35
45
  namespace = klass.namespace
36
46
  formatted = "#{namespace.gsub(/^(default)/,'')}:"
37
- formatted.sub!(/.$/, ' ') if subcommand
38
47
  end
48
+ formatted = "#{klass.namespace.split(':').last} " if subcommand
39
49
 
40
50
  formatted ||= ""
41
51
 
@@ -65,14 +75,22 @@ class Thor
65
75
  @required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ")
66
76
  end
67
77
 
68
- # Given a target, checks if this class name is not a private/protected method.
78
+ # Given a target, checks if this class name is a public method.
69
79
  def public_method?(instance) #:nodoc:
70
- collection = instance.private_methods + instance.protected_methods
71
- (collection & [name.to_s, name.to_sym]).empty?
80
+ !(instance.public_methods & [name.to_s, name.to_sym]).empty?
81
+ end
82
+
83
+ def private_method?(instance)
84
+ !(instance.private_methods & [name.to_s, name.to_sym]).empty?
85
+ end
86
+
87
+ def local_method?(instance, name)
88
+ methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false)
89
+ !(methods & [name.to_s, name.to_sym]).empty?
72
90
  end
73
91
 
74
92
  def sans_backtrace(backtrace, caller) #:nodoc:
75
- saned = backtrace.reject { |frame| frame =~ FILE_REGEXP }
93
+ saned = backtrace.reject { |frame| frame =~ FILE_REGEXP || (frame =~ /\.java:/ && RUBY_PLATFORM =~ /java/) }
76
94
  saned -= caller
77
95
  end
78
96
 
@@ -8,11 +8,11 @@ class Thor
8
8
  #
9
9
  # 1) Methods to convert thor namespaces to constants and vice-versa.
10
10
  #
11
- # Thor::Utils.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz"
11
+ # Thor::Util.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz"
12
12
  #
13
13
  # 2) Loading thor files and sandboxing:
14
14
  #
15
- # Thor::Utils.load_thorfile("~/.thor/foo")
15
+ # Thor::Util.load_thorfile("~/.thor/foo")
16
16
  #
17
17
  module Util
18
18
 
@@ -153,11 +153,11 @@ class Thor
153
153
  begin
154
154
  Thor::Sandbox.class_eval(content, path)
155
155
  rescue Exception => e
156
- $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
156
+ $stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}")
157
157
  if debug
158
- $stderr.puts *e.backtrace
158
+ $stderr.puts(*e.backtrace)
159
159
  else
160
- $stderr.puts e.backtrace.first
160
+ $stderr.puts(e.backtrace.first)
161
161
  end
162
162
  end
163
163
  end
@@ -184,7 +184,7 @@ class Thor
184
184
  end
185
185
  end
186
186
 
187
- # Returns the root where thor files are located, dependending on the OS.
187
+ # Returns the root where thor files are located, depending on the OS.
188
188
  #
189
189
  def self.thor_root
190
190
  File.join(user_home, ".thor").gsub(/\\/, '/')
@@ -216,9 +216,28 @@ class Thor
216
216
  #
217
217
  def self.ruby_command
218
218
  @ruby_command ||= begin
219
- ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
219
+ ruby_name = RbConfig::CONFIG['ruby_install_name']
220
+ ruby = File.join(RbConfig::CONFIG['bindir'], ruby_name)
220
221
  ruby << RbConfig::CONFIG['EXEEXT']
221
222
 
223
+ # avoid using different name than ruby (on platforms supporting links)
224
+ if ruby_name != 'ruby' && File.respond_to?(:readlink)
225
+ begin
226
+ alternate_ruby = File.join(RbConfig::CONFIG['bindir'], 'ruby')
227
+ alternate_ruby << RbConfig::CONFIG['EXEEXT']
228
+
229
+ # ruby is a symlink
230
+ if File.symlink? alternate_ruby
231
+ linked_ruby = File.readlink alternate_ruby
232
+
233
+ # symlink points to 'ruby_install_name'
234
+ ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby
235
+ end
236
+ rescue NotImplementedError
237
+ # just ignore on windows
238
+ end
239
+ end
240
+
222
241
  # escape string in case path to ruby executable contain spaces.
223
242
  ruby.sub!(/.*\s.*/m, '"\&"')
224
243
  ruby
@@ -1,3 +1,3 @@
1
1
  class Thor
2
- VERSION = "0.14.6".freeze
2
+ VERSION = "0.15.0"
3
3
  end
@@ -0,0 +1,81 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'thor/actions'
3
+ require 'tempfile'
4
+
5
+ describe Thor::Actions::CreateLink do
6
+ before(:each) do
7
+ @hardlink_to = File.join(Dir.tmpdir, 'linkdest.rb')
8
+ ::FileUtils.rm_rf(destination_root)
9
+ ::FileUtils.rm_rf(@hardlink_to)
10
+ end
11
+
12
+ def create_link(destination=nil, config={}, options={})
13
+ @base = MyCounter.new([1,2], options, { :destination_root => destination_root })
14
+ @base.stub!(:file_name).and_return('rdoc')
15
+
16
+ @tempfile = Tempfile.new("config.rb")
17
+
18
+ @action = Thor::Actions::CreateLink.new(@base, destination, @tempfile.path,
19
+ { :verbose => !@silence }.merge(config))
20
+ end
21
+
22
+ def invoke!
23
+ capture(:stdout){ @action.invoke! }
24
+ end
25
+
26
+ def silence!
27
+ @silence = true
28
+ end
29
+
30
+ describe "#invoke!" do
31
+ it "creates a symbolic link for :symbolic => true" do
32
+ create_link("doc/config.rb", :symbolic => true)
33
+ invoke!
34
+ destination_path = File.join(destination_root, "doc/config.rb")
35
+ File.exists?(destination_path).should be_true
36
+ File.symlink?(destination_path).should be_true
37
+ end
38
+
39
+ it "creates a hard link for :symbolic => false" do
40
+ create_link(@hardlink_to, :symbolic => false)
41
+ invoke!
42
+ destination_path = @hardlink_to
43
+ File.exists?(destination_path).should be_true
44
+ File.symlink?(destination_path).should be_false
45
+ end
46
+
47
+ it "creates a symbolic link by default" do
48
+ create_link("doc/config.rb")
49
+ invoke!
50
+ destination_path = File.join(destination_root, "doc/config.rb")
51
+ File.exists?(destination_path).should be_true
52
+ File.symlink?(destination_path).should be_true
53
+ end
54
+
55
+ it "does not create a link if pretending" do
56
+ create_link("doc/config.rb", {}, :pretend => true)
57
+ invoke!
58
+ File.exists?(File.join(destination_root, "doc/config.rb")).should be_false
59
+ end
60
+
61
+ it "shows created status to the user" do
62
+ create_link("doc/config.rb")
63
+ invoke!.should == " create doc/config.rb\n"
64
+ end
65
+
66
+ it "does not show any information if log status is false" do
67
+ silence!
68
+ create_link("doc/config.rb")
69
+ invoke!.should be_empty
70
+ end
71
+ end
72
+
73
+ describe "#identical?" do
74
+ it "returns true if the destination link exists and is identical" do
75
+ create_link("doc/config.rb")
76
+ @action.identical?.should be_false
77
+ invoke!
78
+ @action.identical?.should be_true
79
+ end
80
+ end
81
+ end
@@ -95,4 +95,36 @@ describe Thor::Actions::EmptyDirectory do
95
95
  @action.exists?.should be_true
96
96
  end
97
97
  end
98
+
99
+ context "protected methods" do
100
+ describe "#convert_encoded_instructions" do
101
+ before :each do
102
+ empty_directory("test_dir")
103
+ @action.base.stub!(:file_name).and_return("expected")
104
+ end
105
+
106
+ it "accepts and executes a 'legal' %\w+% encoded instruction" do
107
+ @action.send(:convert_encoded_instructions, "%file_name%.txt").should == "expected.txt"
108
+ end
109
+
110
+ it "ignores an 'illegal' %\w+% encoded instruction" do
111
+ @action.send(:convert_encoded_instructions, "%some_name%.txt").should == "%some_name%.txt"
112
+ end
113
+
114
+ it "ignores incorrectly encoded instruction" do
115
+ @action.send(:convert_encoded_instructions, "%some.name%.txt").should == "%some.name%.txt"
116
+ end
117
+
118
+ it "raises an error if the instruction refers to a private method" do
119
+ module PrivExt
120
+ private
121
+ def private_file_name
122
+ "something_hidden"
123
+ end
124
+ end
125
+ @action.base.extend(PrivExt)
126
+ expect { @action.send(:convert_encoded_instructions, "%private_file_name%.txt") }.to raise_error Thor::PrivateMethodEncodedError
127
+ end
128
+ end
129
+ end
98
130
  end
@@ -173,7 +173,7 @@ describe Thor::Actions do
173
173
  it "converts enconded instructions" do
174
174
  runner.should_receive(:file_name).and_return("rdoc")
175
175
  action :template, "doc/%file_name%.rb.tt"
176
- file = File.join(destination_root, "doc/rdoc.rb.tt")
176
+ file = File.join(destination_root, "doc/rdoc.rb")
177
177
  File.exists?(file).should be_true
178
178
  end
179
179
 
@@ -187,6 +187,13 @@ describe Thor::Actions do
187
187
  end
188
188
  File.read(File.join(destination_root, "doc/config.rb")).should =~ /^OMG/
189
189
  end
190
+
191
+ it "guesses the destination name when given only a source" do
192
+ action :template, "doc/config.yaml.tt"
193
+
194
+ file = File.join(destination_root, "doc/config.yaml")
195
+ File.exists?(file).should be_true
196
+ end
190
197
  end
191
198
 
192
199
  describe "when changing existent files" do
@@ -307,4 +314,57 @@ describe Thor::Actions do
307
314
  end
308
315
  end
309
316
  end
317
+
318
+ describe "when adjusting comments" do
319
+ before(:each) do
320
+ ::FileUtils.cp_r(source_root, destination_root)
321
+ end
322
+
323
+ def file
324
+ File.join(destination_root, "doc", "COMMENTER")
325
+ end
326
+
327
+ unmodified_comments_file = /__start__\n # greenblue\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/
328
+
329
+ describe "#uncomment_lines" do
330
+ it "uncomments all matching lines in the file" do
331
+ action :uncomment_lines, "doc/COMMENTER", "green"
332
+ File.binread(file).should =~ /__start__\n greenblue\n# yellowblue\n#yellowred\n greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/
333
+
334
+ action :uncomment_lines, "doc/COMMENTER", "red"
335
+ File.binread(file).should =~ /__start__\n greenblue\n# yellowblue\nyellowred\n greenred\norange\n purple\n ind#igo\n # ind#igo\n__end__/
336
+ end
337
+
338
+ it "correctly uncomments lines with hashes in them" do
339
+ action :uncomment_lines, "doc/COMMENTER", "ind#igo"
340
+ File.binread(file).should =~ /__start__\n # greenblue\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n ind#igo\n ind#igo\n__end__/
341
+ end
342
+
343
+ it "does not modify already uncommented lines in the file" do
344
+ action :uncomment_lines, "doc/COMMENTER", "orange"
345
+ action :uncomment_lines, "doc/COMMENTER", "purple"
346
+ File.binread(file).should =~ unmodified_comments_file
347
+ end
348
+ end
349
+
350
+ describe "#comment_lines" do
351
+ it "comments lines which are not commented" do
352
+ action :comment_lines, "doc/COMMENTER", "orange"
353
+ File.binread(file).should =~ /__start__\n # greenblue\n# yellowblue\n#yellowred\n #greenred\n# orange\n purple\n ind#igo\n # ind#igo\n__end__/
354
+
355
+ action :comment_lines, "doc/COMMENTER", "purple"
356
+ File.binread(file).should =~ /__start__\n # greenblue\n# yellowblue\n#yellowred\n #greenred\n# orange\n # purple\n ind#igo\n # ind#igo\n__end__/
357
+ end
358
+
359
+ it "correctly comments lines with hashes in them" do
360
+ action :comment_lines, "doc/COMMENTER", "ind#igo"
361
+ File.binread(file).should =~ /__start__\n # greenblue\n# yellowblue\n#yellowred\n #greenred\norange\n purple\n # ind#igo\n # ind#igo\n__end__/
362
+ end
363
+
364
+ it "does not modify already commented lines" do
365
+ action :comment_lines, "doc/COMMENTER", "green"
366
+ File.binread(file).should =~ unmodified_comments_file
367
+ end
368
+ end
369
+ end
310
370
  end