thor 0.13.6 → 0.13.7

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.
@@ -1,8 +1,8 @@
1
1
  == 0.13, released 2010-02-03
2
2
 
3
- * Several bug fixes
3
+ * Added Thor::Shell::HTML
4
4
  * Decoupled Thor::Group and Thor, so it's easier to vendor
5
- * Added check_unknown_options! in case you want error messages to be raised in valid switches.
5
+ * Added check_unknown_options! in case you want error messages to be raised in valid switches
6
6
  * run(command) should return the results of command
7
7
 
8
8
  == 0.12, released 2010-01-02
@@ -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.
@@ -35,6 +34,20 @@ class Thor
35
34
  end
36
35
  end
37
36
 
37
+ # Defines the long description of the next task.
38
+ #
39
+ # ==== Parameters
40
+ # long description<String>
41
+ #
42
+ def long_desc(long_description, options={})
43
+ if options[:for]
44
+ task = find_and_refresh_task(options[:for])
45
+ task.long_description = long_description if long_description
46
+ else
47
+ @long_desc = long_description
48
+ end
49
+ end
50
+
38
51
  # Maps an input to a task. If you define:
39
52
  #
40
53
  # map "-T" => "list"
@@ -121,8 +134,17 @@ class Thor
121
134
  # script.invoke(:task, first_arg, second_arg, third_arg)
122
135
  #
123
136
  def start(original_args=ARGV, config={})
137
+ @@original_args = original_args
124
138
  super do |given_args|
125
- meth = normalize_task_name(given_args.shift)
139
+ meth = given_args.first.to_s
140
+
141
+ if !meth.empty? && (map[meth] || meth !~ /^\-/)
142
+ given_args.shift
143
+ else
144
+ meth = nil
145
+ end
146
+
147
+ meth = normalize_task_name(meth)
126
148
  task = all_tasks[meth]
127
149
 
128
150
  if task
@@ -153,7 +175,12 @@ class Thor
153
175
  shell.say " #{banner(task)}"
154
176
  shell.say
155
177
  class_options_help(shell, nil => task.options.map { |_, o| o })
156
- shell.say task.description
178
+ if task.long_description
179
+ shell.say "Description:"
180
+ shell.print_wrapped(task.long_description, :ident => 2)
181
+ else
182
+ shell.say task.description
183
+ end
157
184
  end
158
185
 
159
186
  # Prints help information for this class.
@@ -161,8 +188,8 @@ class Thor
161
188
  # ==== Parameters
162
189
  # shell<Thor::Shell>
163
190
  #
164
- def help(shell)
165
- list = printable_tasks
191
+ def help(shell, subcommand = false)
192
+ list = printable_tasks(true, subcommand)
166
193
  Thor::Util.thor_classes_in(self).each do |klass|
167
194
  list += klass.printable_tasks(false)
168
195
  end
@@ -175,17 +202,24 @@ class Thor
175
202
  end
176
203
 
177
204
  # Returns tasks ready to be printed.
178
- def printable_tasks(all=true)
205
+ def printable_tasks(all = true, subcommand = false)
179
206
  (all ? all_tasks : tasks).map do |_, task|
180
207
  item = []
181
- item << banner(task)
208
+ item << banner(task, false, subcommand)
182
209
  item << (task.description ? "# #{task.description.gsub(/\s+/m,' ')}" : "")
183
210
  item
184
211
  end
185
212
  end
186
213
 
187
- def handle_argument_error(task, error) #:nodoc:
188
- raise InvocationError, "#{task.name.inspect} was called incorrectly. Call as #{task.formatted_usage(self, banner_base == "thor").inspect}."
214
+ def subcommands
215
+ @@subcommands ||= {}
216
+ end
217
+
218
+ def subcommand(subcommand, subcommand_class)
219
+ subcommand = subcommand.to_s
220
+ subcommands[subcommand] = subcommand_class
221
+ subcommand_class.subcommand_help subcommand
222
+ define_method(subcommand) { |*_| subcommand_class.start(subcommand_args) }
189
223
  end
190
224
 
191
225
  protected
@@ -195,8 +229,8 @@ class Thor
195
229
  # the task that is going to be invoked and a boolean which indicates if
196
230
  # the namespace should be displayed as arguments.
197
231
  #
198
- def banner(task)
199
- "#{banner_base} #{task.formatted_usage(self, banner_base == "thor")}"
232
+ def banner(task, namespace = nil, subcommand = false)
233
+ "#{$0} #{task.formatted_usage(self, $thor_runner, subcommand)}"
200
234
  end
201
235
 
202
236
  def baseclass #:nodoc:
@@ -205,8 +239,8 @@ class Thor
205
239
 
206
240
  def create_task(meth) #:nodoc:
207
241
  if @usage && @desc
208
- tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options)
209
- @usage, @desc, @method_options = nil
242
+ tasks[meth.to_s] = Thor::Task.new(meth, @desc, @long_desc, @usage, method_options)
243
+ @usage, @desc, @long_desc, @method_options = nil
210
244
  true
211
245
  elsif self.all_tasks[meth.to_s] || meth.to_sym == :method_missing
212
246
  true
@@ -227,10 +261,21 @@ class Thor
227
261
  # If a map can't be found use the sent name or the default task.
228
262
  #
229
263
  def normalize_task_name(meth) #:nodoc:
230
- mapping = map[meth.to_s]
231
- meth = mapping || meth || default_task
264
+ meth = map[meth.to_s] || meth || default_task
232
265
  meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
233
266
  end
267
+
268
+ def subcommand_help(cmd)
269
+ desc "help [COMMAND]", "Describe subcommands or one specific subcommand"
270
+ class_eval <<-RUBY
271
+ def help(task = nil, subcommand = true); super; end
272
+ RUBY
273
+ end
274
+
275
+ end
276
+
277
+ def subcommand_args
278
+ @@original_args[1..-1]
234
279
  end
235
280
 
236
281
  include Thor::Base
@@ -238,7 +283,7 @@ class Thor
238
283
  map HELP_MAPPINGS => :help
239
284
 
240
285
  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)
286
+ def help(task = nil, subcommand = false)
287
+ task ? self.class.task_help(shell, task) : self.class.help(shell, subcommand)
243
288
  end
244
289
  end
@@ -190,12 +190,13 @@ class Thor
190
190
  #
191
191
  def apply(path, config={})
192
192
  verbose = config.fetch(:verbose, true)
193
- path = find_in_source_paths(path) unless path =~ /^http\:\/\//
193
+ is_uri = path =~ /^https?\:\/\//
194
+ path = find_in_source_paths(path) unless is_uri
194
195
 
195
196
  say_status :apply, path, verbose
196
197
  shell.padding += 1 if verbose
197
198
 
198
- if URI(path).is_a?(URI::HTTP)
199
+ if is_uri
199
200
  contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read }
200
201
  else
201
202
  contents = open(path) {|io| io.read }
@@ -20,7 +20,9 @@ class Thor
20
20
  #
21
21
  # create_file "config/apach.conf", "your apache config"
22
22
  #
23
- def create_file(destination, data=nil, config={}, &block)
23
+ def create_file(destination, *args, &block)
24
+ config = args.last.is_a?(Hash) ? args.pop : {}
25
+ data = args.first
24
26
  action CreateFile.new(self, destination, block || data.to_s, config)
25
27
  end
26
28
  alias :add_file :create_file
@@ -40,7 +40,9 @@ class Thor
40
40
  # directory "doc"
41
41
  # directory "doc", "docs", :recursive => false
42
42
  #
43
- def directory(source, destination=nil, config={}, &block)
43
+ def directory(source, *args, &block)
44
+ config = args.last.is_a?(Hash) ? args.pop : {}
45
+ destination = args.first || source
44
46
  action Directory.new(self, source, destination || source, config, &block)
45
47
  end
46
48
 
@@ -18,8 +18,9 @@ class Thor
18
18
  #
19
19
  # copy_file "doc/README"
20
20
  #
21
- def copy_file(source, destination=nil, config={}, &block)
22
- destination ||= source
21
+ def copy_file(source, *args, &block)
22
+ config = args.last.is_a?(Hash) ? args.pop : {}
23
+ destination = args.first || source
23
24
  source = File.expand_path(find_in_source_paths(source.to_s))
24
25
 
25
26
  create_file destination, nil, config do
@@ -46,7 +47,10 @@ class Thor
46
47
  # content.split("\n").first
47
48
  # end
48
49
  #
49
- def get(source, destination=nil, config={}, &block)
50
+ def get(source, *args, &block)
51
+ config = args.last.is_a?(Hash) ? args.pop : {}
52
+ destination = args.first
53
+
50
54
  source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ /^http\:\/\//
51
55
  render = open(source) {|input| input.binmode.read }
52
56
 
@@ -74,8 +78,10 @@ class Thor
74
78
  #
75
79
  # template "doc/README"
76
80
  #
77
- def template(source, destination=nil, config={}, &block)
78
- destination ||= source
81
+ def template(source, *args, &block)
82
+ config = args.last.is_a?(Hash) ? args.pop : {}
83
+ destination = args.first || source
84
+
79
85
  source = File.expand_path(find_in_source_paths(source.to_s))
80
86
  context = instance_eval('binding')
81
87
 
@@ -382,13 +382,17 @@ class Thor
382
382
  end
383
383
 
384
384
  def handle_no_task_error(task) #:nodoc:
385
- if self.banner_base == "thor"
385
+ if $thor_runner
386
386
  raise UndefinedTaskError, "Could not find task #{task.inspect} in #{namespace.inspect} namespace."
387
387
  else
388
388
  raise UndefinedTaskError, "Could not find task #{task.inspect}."
389
389
  end
390
390
  end
391
391
 
392
+ def handle_argument_error(task, error) #:nodoc:
393
+ raise InvocationError, "#{task.name.inspect} was called incorrectly. Call as #{self.banner(task).inspect}."
394
+ end
395
+
392
396
  protected
393
397
 
394
398
  # Prints the class options per group. If an option does not belong to
@@ -516,11 +520,6 @@ class Thor
516
520
  false
517
521
  end
518
522
 
519
- # Returns the base for banner.
520
- def banner_base
521
- @banner_base ||= $thor_runner ? "thor" : File.basename($0.split(" ").first)
522
- end
523
-
524
523
  # SIGNATURE: Sets the baseclass. This is where the superclass lookup
525
524
  # finishes.
526
525
  def baseclass #:nodoc:
@@ -65,7 +65,7 @@ class Thor
65
65
  else
66
66
  self[$1] == args.first
67
67
  end
68
- else
68
+ else
69
69
  self[method]
70
70
  end
71
71
  end
@@ -228,7 +228,7 @@ class Thor::Group
228
228
  # The banner for this class. You can customize it if you are invoking the
229
229
  # thor class by another ways which is not the Thor::Runner.
230
230
  def banner
231
- "#{banner_base} #{self_task.formatted_usage(self, false)}"
231
+ "#{$0} #{self_task.formatted_usage(self, false)}"
232
232
  end
233
233
 
234
234
  # Represents the whole class as a task.
@@ -241,7 +241,7 @@ class Thor::Group
241
241
  end
242
242
 
243
243
  def create_task(meth) #:nodoc:
244
- tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil)
244
+ tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil, nil)
245
245
  true
246
246
  end
247
247
  end
@@ -51,6 +51,11 @@ class Thor
51
51
 
52
52
  private
53
53
 
54
+ def no_or_skip?(arg)
55
+ arg =~ /^--(no|skip)-([-\w]+)$/
56
+ $2
57
+ end
58
+
54
59
  def last?
55
60
  @pile.empty?
56
61
  end
@@ -114,7 +119,7 @@ class Thor
114
119
  array
115
120
  end
116
121
 
117
- # Check if the peel is numeric ofrmat and return a Float or Integer.
122
+ # Check if the peek is numeric format and return a Float or Integer.
118
123
  # Otherwise raises an error.
119
124
  #
120
125
  def parse_numeric(name)
@@ -127,10 +132,16 @@ class Thor
127
132
  $&.index('.') ? shift.to_f : shift.to_i
128
133
  end
129
134
 
130
- # Parse string, i.e., just return the current value in the pile.
135
+ # Parse string:
136
+ # for --string-arg, just return the current value in the pile
137
+ # for --no-string-arg, nil
131
138
  #
132
139
  def parse_string(name)
133
- shift
140
+ if no_or_skip?(name)
141
+ nil
142
+ else
143
+ shift
144
+ end
134
145
  end
135
146
 
136
147
  # Raises an error if @non_assigned_required array is not empty.
@@ -124,11 +124,6 @@ class Thor
124
124
  end
125
125
  end
126
126
 
127
- def no_or_skip?(arg)
128
- arg =~ /^--(no|skip)-([-\w]+)$/
129
- $2
130
- end
131
-
132
127
  # Check if the given argument is actually a shortcut.
133
128
  #
134
129
  def normalize_switch(arg)
@@ -13,7 +13,7 @@ class Thor::Runner < Thor #:nodoc:
13
13
 
14
14
  # Override Thor#help so it can give information about any class and any method.
15
15
  #
16
- def help(meth=nil)
16
+ def help(meth = nil)
17
17
  if meth && !self.respond_to?(meth)
18
18
  initialize_thorfiles(meth)
19
19
  klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
@@ -39,7 +39,7 @@ class Thor::Runner < Thor #:nodoc:
39
39
  def install(name)
40
40
  initialize_thorfiles
41
41
 
42
- # If a directory name is provided as the argument, look for a 'main.thor'
42
+ # If a directory name is provided as the argument, look for a 'main.thor'
43
43
  # task in said directory.
44
44
  begin
45
45
  if File.directory?(File.expand_path(name))
@@ -139,7 +139,7 @@ class Thor::Runner < Thor #:nodoc:
139
139
  end
140
140
 
141
141
  desc "list [SEARCH]", "List the available thor tasks (--substring means .*SEARCH)"
142
- method_options :substring => :boolean, :group => :string, :all => :boolean
142
+ method_options :substring => :boolean, :group => :string, :all => :boolean, :debug => :boolean
143
143
  def list(search="")
144
144
  initialize_thorfiles
145
145
 
@@ -156,8 +156,8 @@ class Thor::Runner < Thor #:nodoc:
156
156
 
157
157
  private
158
158
 
159
- def self.banner(task)
160
- "thor " + task.formatted_usage(self, false)
159
+ def self.banner(task, all = false, subcommand = false)
160
+ "thor " + task.formatted_usage(self, all, subcommand)
161
161
  end
162
162
 
163
163
  def thor_root
@@ -199,7 +199,7 @@ class Thor::Runner < Thor #:nodoc:
199
199
  #
200
200
  def initialize_thorfiles(relevant_to=nil, skip_lookup=false)
201
201
  thorfiles(relevant_to, skip_lookup).each do |f|
202
- Thor::Util.load_thorfile(f) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f))
202
+ Thor::Util.load_thorfile(f, nil, options[:debug]) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f))
203
203
  end
204
204
  end
205
205
 
@@ -267,16 +267,11 @@ class Thor::Runner < Thor #:nodoc:
267
267
  raise Error, "No Thor tasks available" if klasses.empty?
268
268
  show_modules if with_modules && !thor_yaml.empty?
269
269
 
270
- # Remove subclasses
271
- klasses.dup.each do |klass|
272
- klasses -= Thor::Util.thor_classes_in(klass)
273
- end
274
-
275
270
  list = Hash.new { |h,k| h[k] = [] }
276
271
  groups = klasses.select { |k| k.ancestors.include?(Thor::Group) }
277
272
 
278
273
  # Get classes which inherit from Thor
279
- (klasses - groups).each { |k| list[k.namespace] += k.printable_tasks(false) }
274
+ (klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_tasks(false) }
280
275
 
281
276
  # Get classes which inherit from Thor::Base
282
277
  groups.map! { |k| k.printable_tasks(false).first }
@@ -1,5 +1,4 @@
1
1
  require 'rbconfig'
2
- require 'thor/shell/color'
3
2
 
4
3
  class Thor
5
4
  module Base
@@ -7,7 +6,9 @@ class Thor
7
6
  # it will use a colored log, otherwise it will use a basic one without color.
8
7
  #
9
8
  def self.shell
10
- @shell ||= if Config::CONFIG['host_os'] =~ /mswin|mingw/
9
+ @shell ||= if ENV['THOR_SHELL'] && ENV['THOR_SHELL'].size > 0
10
+ Thor::Shell.const_get(ENV['THOR_SHELL'])
11
+ elsif Config::CONFIG['host_os'] =~ /mswin|mingw/
11
12
  Thor::Shell::Basic
12
13
  else
13
14
  Thor::Shell::Color
@@ -24,6 +25,10 @@ class Thor
24
25
  module Shell
25
26
  SHELL_DELEGATED_METHODS = [:ask, :yes?, :no?, :say, :say_status, :print_table]
26
27
 
28
+ autoload :Basic, 'thor/shell/basic'
29
+ autoload :Color, 'thor/shell/color'
30
+ autoload :HTML, 'thor/shell/HTML'
31
+
27
32
  # Add shell to initialize config values.
28
33
  #
29
34
  # ==== Configuration
@@ -42,8 +42,8 @@ class Thor
42
42
  $stdout.puts(message)
43
43
  else
44
44
  $stdout.print(message)
45
- $stdout.flush
46
45
  end
46
+ $stdout.flush
47
47
  end
48
48
 
49
49
  # Say a status with the given color and appends the message. Since this
@@ -81,16 +81,20 @@ class Thor
81
81
  # Array[Array[String, String, ...]]
82
82
  #
83
83
  # ==== Options
84
- # ident<Integer>:: Ident the first column by ident value.
84
+ # ident<Integer>:: Indent the first column by ident value.
85
+ # colwidth<Integer>:: Force the first column to colwidth spaces wide.
85
86
  #
86
87
  def print_table(table, options={})
87
88
  return if table.empty?
88
89
 
89
- formats, ident = [], options[:ident].to_i
90
+ formats, ident, colwidth = [], options[:ident].to_i, options[:colwidth]
90
91
  options[:truncate] = terminal_width if options[:truncate] == true
91
92
 
92
- 0.upto(table.first.length - 2) do |i|
93
- maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size
93
+ formats << "%-#{colwidth + 2}s" if colwidth
94
+ start = colwidth ? 1 : 0
95
+
96
+ start.upto(table.first.length - 2) do |i|
97
+ maxima ||= table.max{|a,b| a[i].size <=> b[i].size }[i].size
94
98
  formats << "%-#{maxima + 2}s"
95
99
  end
96
100
 
@@ -105,7 +109,35 @@ class Thor
105
109
  end
106
110
 
107
111
  sentence = truncate(sentence, options[:truncate]) if options[:truncate]
108
- $stdout.puts sentence
112
+ $stdout.puts sentence
113
+ end
114
+ end
115
+
116
+ # Prints a long string, word-wrapping the text to the current width of the
117
+ # terminal display. Ideal for printing heredocs.
118
+ #
119
+ # ==== Parameters
120
+ # String
121
+ #
122
+ # ==== Options
123
+ # ident<Integer>:: Indent each line of the printed paragraph by ident value.
124
+ #
125
+ def print_wrapped(message, options={})
126
+ ident = options[:ident] || 0
127
+ width = terminal_width - ident
128
+ paras = message.split("\n\n")
129
+
130
+ paras.map! do |unwrapped|
131
+ unwrapped.strip.gsub(/\n/, " ").squeeze(" ").
132
+ gsub(/.{1,#{width}}(?:\s|\Z)/){($& + 5.chr).
133
+ gsub(/\n\005/,"\n").gsub(/\005/,"\n")}
134
+ end
135
+
136
+ paras.each do |para|
137
+ para.split("\n").each do |line|
138
+ $stdout.puts line.insert(0, " " * ident)
139
+ end
140
+ $stdout.puts unless para == paras.last
109
141
  end
110
142
  end
111
143
 
@@ -0,0 +1,121 @@
1
+ require 'thor/shell/basic'
2
+
3
+ class Thor
4
+ module Shell
5
+ # Inherit from Thor::Shell::Basic and add set_color behavior. Check
6
+ # Thor::Shell::Basic to see all available methods.
7
+ #
8
+ class HTML < Basic
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>"
16
+
17
+ # Set the terminal's foreground HTML color to black.
18
+ BLACK = '<span style="color: black;">'
19
+ # Set the terminal's foreground HTML color to red.
20
+ RED = '<span style="color: red;">'
21
+ # Set the terminal's foreground HTML color to green.
22
+ GREEN = '<span style="color: green;">'
23
+ # Set the terminal's foreground HTML color to yellow.
24
+ YELLOW = '<span style="color: yellow;">'
25
+ # Set the terminal's foreground HTML color to blue.
26
+ BLUE = '<span style="color: blue;">'
27
+ # Set the terminal's foreground HTML color to magenta.
28
+ MAGENTA = '<span style="color: magenta;">'
29
+ # Set the terminal's foreground HTML color to cyan.
30
+ CYAN = '<span style="color: cyan;">'
31
+ # Set the terminal's foreground HTML color to white.
32
+ WHITE = '<span style="color: white;">'
33
+
34
+ # Set the terminal's background HTML color to black.
35
+ ON_BLACK = '<span style="background-color: black">'
36
+ # Set the terminal's background HTML color to red.
37
+ ON_RED = '<span style="background-color: red">'
38
+ # Set the terminal's background HTML color to green.
39
+ ON_GREEN = '<span style="background-color: green">'
40
+ # Set the terminal's background HTML color to yellow.
41
+ ON_YELLOW = '<span style="background-color: yellow">'
42
+ # Set the terminal's background HTML color to blue.
43
+ ON_BLUE = '<span style="background-color: blue">'
44
+ # Set the terminal's background HTML color to magenta.
45
+ ON_MAGENTA = '<span style="background-color: magenta">'
46
+ # Set the terminal's background HTML color to cyan.
47
+ ON_CYAN = '<span style="background-color: cyan">'
48
+ # Set the terminal's background HTML color to white.
49
+ ON_WHITE = '<span style="background-color: white">'
50
+
51
+ # Set color by using a string or one of the defined constants. If a third
52
+ # option is set to true, it also adds bold to the string. This is based
53
+ # on Highline implementation and it automatically appends CLEAR to the end
54
+ # of the returned String.
55
+ #
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}"
60
+ end
61
+
62
+ # Ask something to the user and receives a response.
63
+ #
64
+ # ==== Example
65
+ # ask("What is your name?")
66
+ #
67
+ # TODO: Implement #ask for Thor::Shell::HTML
68
+ def ask(statement, color=nil)
69
+ raise NotImplementedError, "Implement #ask for Thor::Shell::HTML"
70
+ end
71
+
72
+ protected
73
+
74
+ # Overwrite show_diff to show diff with colors if Diff::LCS is
75
+ # available.
76
+ #
77
+ def show_diff(destination, content) #:nodoc:
78
+ if diff_lcs_loaded? && ENV['THOR_DIFF'].nil? && ENV['RAILS_DIFF'].nil?
79
+ actual = File.binread(destination).to_s.split("\n")
80
+ content = content.to_s.split("\n")
81
+
82
+ Diff::LCS.sdiff(actual, content).each do |diff|
83
+ output_diff_line(diff)
84
+ end
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def output_diff_line(diff) #:nodoc:
91
+ case diff.action
92
+ when '-'
93
+ say "- #{diff.old_element.chomp}", :red, true
94
+ when '+'
95
+ say "+ #{diff.new_element.chomp}", :green, true
96
+ when '!'
97
+ say "- #{diff.old_element.chomp}", :red, true
98
+ say "+ #{diff.new_element.chomp}", :green, true
99
+ else
100
+ say " #{diff.old_element.chomp}", nil, true
101
+ end
102
+ end
103
+
104
+ # Check if Diff::LCS is loaded. If it is, use it to create pretty output
105
+ # for diff.
106
+ #
107
+ def diff_lcs_loaded? #:nodoc:
108
+ return true if defined?(Diff::LCS)
109
+ return @diff_lcs_loaded unless @diff_lcs_loaded.nil?
110
+
111
+ @diff_lcs_loaded = begin
112
+ require 'diff/lcs'
113
+ true
114
+ rescue LoadError
115
+ false
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -1,11 +1,11 @@
1
1
  class Thor
2
- class Task < Struct.new(:name, :description, :usage, :options)
2
+ class Task < Struct.new(:name, :description, :long_description, :usage, :options)
3
3
  FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
4
4
 
5
5
  # A dynamic task that handles method missing scenarios.
6
6
  class Dynamic < Task
7
7
  def initialize(name, options=nil)
8
- super(name.to_s, "A dynamically-generated task", name.to_s, options)
8
+ super(name.to_s, "A dynamically-generated task", name.to_s, name.to_s, options)
9
9
  end
10
10
 
11
11
  def run(instance, args=[])
@@ -17,8 +17,8 @@ class Thor
17
17
  end
18
18
  end
19
19
 
20
- def initialize(name, description, usage, options=nil)
21
- super(name.to_s, description, usage, options || {})
20
+ def initialize(name, description, long_description, usage, options=nil)
21
+ super(name.to_s, description, long_description, usage, options || {})
22
22
  end
23
23
 
24
24
  def initialize_copy(other) #:nodoc:
@@ -39,18 +39,18 @@ class Thor
39
39
  instance.class.handle_no_task_error(name) : (raise e)
40
40
  end
41
41
 
42
- # Returns the formatted usage by injecting given required arguments
42
+ # Returns the formatted usage by injecting given required arguments
43
43
  # and required options into the given usage.
44
- def formatted_usage(klass, namespace=true)
44
+ def formatted_usage(klass, namespace = true, subcommand = false)
45
45
  namespace = klass.namespace unless namespace == false
46
46
 
47
- # Add namespace
48
- formatted = if namespace
49
- "#{namespace.gsub(/^(default|thor:runner:)/,'')}:"
50
- else
51
- ""
47
+ if namespace
48
+ formatted = "#{namespace.gsub(/^(default)/,'')}:"
49
+ formatted.sub!(/.$/, ' ') if subcommand
52
50
  end
53
51
 
52
+ formatted ||= ""
53
+
54
54
  # Add usage with required arguments
55
55
  formatted << if klass && !klass.arguments.empty?
56
56
  usage.to_s.gsub(/^#{name}/) do |match|
@@ -99,4 +99,4 @@ class Thor
99
99
  end
100
100
 
101
101
  end
102
- end
102
+ end
@@ -147,13 +147,18 @@ class Thor
147
147
  # Receives a path and load the thor file in the path. The file is evaluated
148
148
  # inside the sandbox to avoid namespacing conflicts.
149
149
  #
150
- def self.load_thorfile(path, content=nil)
150
+ def self.load_thorfile(path, content=nil, debug=false)
151
151
  content ||= File.binread(path)
152
152
 
153
153
  begin
154
154
  Thor::Sandbox.class_eval(content, path)
155
155
  rescue Exception => e
156
156
  $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
157
+ if debug
158
+ $stderr.puts *e.backtrace
159
+ else
160
+ $stderr.puts e.backtrace.first
161
+ end
157
162
  end
158
163
  end
159
164
 
@@ -1,3 +1,3 @@
1
1
  class Thor
2
- VERSION = "0.13.6".freeze
2
+ VERSION = "0.13.7".freeze
3
3
  end
@@ -192,10 +192,28 @@ describe Thor::Actions do
192
192
  @foo = "FOO"
193
193
  say_status :cool, :padding
194
194
  TEMPLATE
195
- @template.instance_eval "def read; self; end" # Make the string respond to read
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
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
@@ -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, Scripts::MyScript,
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
 
@@ -37,6 +37,7 @@ END
37
37
  end
38
38
 
39
39
  desc "example_default_task", "example!"
40
+ method_options :with => :string
40
41
  def example_default_task
41
42
  options.empty? ? "default task" : options
42
43
  end
@@ -52,6 +53,12 @@ END
52
53
  end
53
54
 
54
55
  desc "long_description", "a" * 80
56
+ long_desc <<-D
57
+ This is a really really really long description.
58
+ Here you go. So very long.
59
+
60
+ It even has two paragraphs.
61
+ D
55
62
  def long_description
56
63
  end
57
64
 
@@ -114,6 +121,17 @@ class MyChildScript < MyScript
114
121
  remove_task :boom, :undefine => true
115
122
  end
116
123
 
124
+ class Barn < Thor
125
+ desc "open [ITEM]", "open the barn door"
126
+ def open(item = nil)
127
+ if item == "shotgun"
128
+ puts "That's going to leave a mark."
129
+ else
130
+ puts "Open sesame!"
131
+ end
132
+ end
133
+ end
134
+
117
135
  module Scripts
118
136
  class MyScript < MyChildScript
119
137
  argument :accessor, :type => :string
@@ -136,6 +154,9 @@ module Scripts
136
154
  def task_conflict
137
155
  puts "task"
138
156
  end
157
+
158
+ desc "barn", "commands to manage the barn"
159
+ subcommand "barn", Barn
139
160
  end
140
161
 
141
162
  class ChildDefault < Thor
@@ -176,6 +176,11 @@ describe Thor::Options do
176
176
  parse("--no-foo")["foo"].must be_nil
177
177
  end
178
178
 
179
+ it "does not consume an argument for --no-switch format" do
180
+ create "--cheese" => :string
181
+ parse('burger', '--no-cheese', 'fries')["cheese"].must be_nil
182
+ end
183
+
179
184
  it "accepts a --switch format on non required types" do
180
185
  create "--foo" => :string
181
186
  parse("--foo")["foo"].must == "foo"
@@ -85,7 +85,7 @@ describe Thor::Runner do
85
85
  it "does not swallow Thor InvocationError" do
86
86
  ARGV.replace ["my_script:animal"]
87
87
  content = capture(:stderr) { Thor::Runner.start }
88
- 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".'
89
89
  end
90
90
  end
91
91
 
@@ -129,6 +129,15 @@ TABLE
129
129
  abc #123 firs...
130
130
  #0 empty
131
131
  xyz #786 last...
132
+ TABLE
133
+ end
134
+
135
+ it "honors the colwidth option" do
136
+ content = capture(:stdout){ shell.print_table(@table, :colwidth => 10)}
137
+ content.must == <<-TABLE
138
+ abc #123 first three
139
+ #0 empty
140
+ xyz #786 last three
132
141
  TABLE
133
142
  end
134
143
  end
@@ -0,0 +1,27 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Thor::Shell::HTML do
4
+ def shell
5
+ @shell ||= Thor::Shell::HTML.new
6
+ end
7
+
8
+ describe "#say" do
9
+ it "set the color if specified" do
10
+ $stdout.should_receive(:puts).with('<span style="color: green;">Wow! Now we have colors!</span>')
11
+ shell.say "Wow! Now we have colors!", :green
12
+ end
13
+
14
+ it "does not use a new line even with colors" do
15
+ $stdout.should_receive(:print).with('<span style="color: green;">Wow! Now we have colors! </span>')
16
+ shell.say "Wow! Now we have colors! ", :green
17
+ end
18
+ end
19
+
20
+ describe "#say_status" do
21
+ it "uses color to say status" do
22
+ $stdout.should_receive(:puts).with('<strong><span style="color: red;"> conflict</span></strong> README')
23
+ shell.say_status :conflict, "README", :red
24
+ end
25
+ end
26
+
27
+ end
@@ -21,6 +21,18 @@ describe Thor::Shell do
21
21
  it "returns the shell in use" do
22
22
  MyCounter.new([1,2]).shell.must be_kind_of(Thor::Base.shell)
23
23
  end
24
+
25
+ it "uses $THOR_SHELL" do
26
+ class Thor::Shell::TestShell < Thor::Shell::Basic; end
27
+
28
+ Thor::Base.shell.must == shell.class
29
+ ENV['THOR_SHELL'] = 'TestShell'
30
+ Thor::Base.shell = nil
31
+ Thor::Base.shell.must == Thor::Shell::TestShell
32
+ ENV['THOR_SHELL'] = ''
33
+ Thor::Base.shell = shell.class
34
+ Thor::Base.shell.must == shell.class
35
+ end
24
36
  end
25
37
 
26
38
  describe "with_padding" do
@@ -31,4 +43,5 @@ describe Thor::Shell do
31
43
  end
32
44
  end
33
45
  end
46
+
34
47
  end
@@ -19,6 +19,7 @@ load File.join(File.dirname(__FILE__), "fixtures", "script.thor")
19
19
  load File.join(File.dirname(__FILE__), "fixtures", "invoke.thor")
20
20
 
21
21
  # Set shell to basic
22
+ $0 = "thor"
22
23
  Thor::Base.shell = Thor::Shell::Basic
23
24
 
24
25
  Kernel.module_eval do
@@ -6,7 +6,7 @@ describe Thor::Task do
6
6
  options[key] = Thor::Option.parse(key, value)
7
7
  end
8
8
 
9
- @task ||= Thor::Task.new(:can_has, "I can has cheezburger", "can_has", options)
9
+ @task ||= Thor::Task.new(:can_has, "I can has cheezburger", "I can has cheezburger\nLots and lots of it", "can_has", options)
10
10
  end
11
11
 
12
12
  describe "#formatted_usage" do
@@ -46,7 +46,7 @@ describe Thor::Task do
46
46
 
47
47
  describe "#dup" do
48
48
  it "dup options hash" do
49
- task = Thor::Task.new("can_has", nil, nil, :foo => true, :bar => :required)
49
+ task = Thor::Task.new("can_has", nil, nil, nil, :foo => true, :bar => :required)
50
50
  task.dup.options.delete(:foo)
51
51
  task.options[:foo].must_not be_nil
52
52
  end
@@ -41,6 +41,10 @@ describe Thor do
41
41
  MyScript.start([]).must == "default task"
42
42
  end
43
43
 
44
+ it "invokes the default task if no command is specified even if switches are given" do
45
+ MyScript.start(["--with", "option"]).must == {"with"=>"option"}
46
+ end
47
+
44
48
  it "inherits the default task from parent" do
45
49
  MyChildScript.default_task.must == "example_default_task"
46
50
  end
@@ -112,7 +116,7 @@ describe Thor do
112
116
  end
113
117
 
114
118
  it "raises an error if a required param is not provided" do
115
- capture(:stderr) { MyScript.start(["animal"]) }.strip.must == '"animal" was called incorrectly. Call as "my_script:animal TYPE".'
119
+ capture(:stderr) { MyScript.start(["animal"]) }.strip.must == '"animal" was called incorrectly. Call as "thor my_script:animal TYPE".'
116
120
  end
117
121
 
118
122
  it "raises an error if the invoked task does not exist" do
@@ -137,6 +141,21 @@ describe Thor do
137
141
  end
138
142
  end
139
143
 
144
+ describe "#subcommand" do
145
+ it "maps a given subcommand to another Thor subclass" do
146
+ barn_help = capture(:stdout){ Scripts::MyDefaults.start(["barn"]) }
147
+ barn_help.must include("barn help [COMMAND] # Describe subcommands or one specific subcommand")
148
+ end
149
+
150
+ it "passes commands to subcommand classes" do
151
+ capture(:stdout){ Scripts::MyDefaults.start(["barn", "open"]) }.strip.must == "Open sesame!"
152
+ end
153
+
154
+ it "passes arguments to subcommand classes" do
155
+ capture(:stdout){ Scripts::MyDefaults.start(["barn", "open", "shotgun"]) }.strip.must == "That's going to leave a mark."
156
+ end
157
+ end
158
+
140
159
  describe "#help" do
141
160
  def shell
142
161
  @shell ||= Thor::Base.shell.new
@@ -184,7 +203,7 @@ describe Thor do
184
203
 
185
204
  describe "for a specific task" do
186
205
  it "provides full help info when talking about a specific task" do
187
- capture(:stdout) { MyScript.task_help(shell, "foo") }.must == <<END
206
+ capture(:stdout) { MyScript.task_help(shell, "foo") }.must == <<-END
188
207
  Usage:
189
208
  thor my_script:foo BAR
190
209
 
@@ -206,6 +225,24 @@ END
206
225
  it "normalizes names before claiming they don't exist" do
207
226
  capture(:stdout) { MyScript.task_help(shell, "name-with-dashes") }.must =~ /thor my_script:name-with-dashes/
208
227
  end
228
+
229
+ it "uses the long description if it exists" do
230
+ capture(:stdout) { MyScript.task_help(shell, "long_description") }.must == <<-HELP
231
+ Usage:
232
+ thor my_script:long_description
233
+
234
+ Description:
235
+ This is a really really really long description. Here you go. So very long.
236
+
237
+ It even has two paragraphs.
238
+ HELP
239
+ end
240
+
241
+ it "doesn't assign the long description to the next task without one" do
242
+ capture(:stdout) do
243
+ MyScript.task_help(shell, "name_with_dashes")
244
+ end.must_not =~ /so very long/i
245
+ end
209
246
  end
210
247
 
211
248
  describe "instance method" do
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thor
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 37
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
8
  - 13
8
- - 6
9
- version: 0.13.6
9
+ - 7
10
+ version: 0.13.7
10
11
  platform: ruby
11
12
  authors:
12
13
  - Yehuda Katz
@@ -15,7 +16,7 @@ autorequire:
15
16
  bindir: bin
16
17
  cert_chain: []
17
18
 
18
- date: 2010-04-30 00:00:00 +02:00
19
+ date: 2010-06-28 00:00:00 +02:00
19
20
  default_executable:
20
21
  dependencies: []
21
22
 
@@ -62,9 +63,47 @@ files:
62
63
  - lib/thor/shell.rb
63
64
  - lib/thor/shell/basic.rb
64
65
  - lib/thor/shell/color.rb
66
+ - lib/thor/shell/html.rb
65
67
  - lib/thor/task.rb
66
68
  - lib/thor/util.rb
67
69
  - lib/thor/version.rb
70
+ - spec/actions/create_file_spec.rb
71
+ - spec/actions/directory_spec.rb
72
+ - spec/actions/empty_directory_spec.rb
73
+ - spec/actions/file_manipulation_spec.rb
74
+ - spec/actions/inject_into_file_spec.rb
75
+ - spec/actions_spec.rb
76
+ - spec/base_spec.rb
77
+ - spec/core_ext/hash_with_indifferent_access_spec.rb
78
+ - spec/core_ext/ordered_hash_spec.rb
79
+ - spec/fixtures/application.rb
80
+ - spec/fixtures/bundle/execute.rb
81
+ - spec/fixtures/doc/config.rb
82
+ - spec/group_spec.rb
83
+ - spec/invocation_spec.rb
84
+ - spec/parser/argument_spec.rb
85
+ - spec/parser/arguments_spec.rb
86
+ - spec/parser/option_spec.rb
87
+ - spec/parser/options_spec.rb
88
+ - spec/rake_compat_spec.rb
89
+ - spec/runner_spec.rb
90
+ - spec/shell/basic_spec.rb
91
+ - spec/shell/color_spec.rb
92
+ - spec/shell/html_spec.rb
93
+ - spec/shell_spec.rb
94
+ - spec/spec_helper.rb
95
+ - spec/task_spec.rb
96
+ - spec/thor_spec.rb
97
+ - spec/util_spec.rb
98
+ - spec/fixtures/bundle/main.thor
99
+ - spec/fixtures/doc/%file_name%.rb.tt
100
+ - spec/fixtures/doc/README
101
+ - spec/fixtures/group.thor
102
+ - spec/fixtures/invoke.thor
103
+ - spec/fixtures/path with spaces
104
+ - spec/fixtures/script.thor
105
+ - spec/fixtures/task.thor
106
+ - spec/spec.opts
68
107
  has_rdoc: true
69
108
  homepage: http://yehudakatz.com
70
109
  licenses: []
@@ -75,23 +114,27 @@ rdoc_options:
75
114
  require_paths:
76
115
  - lib
77
116
  required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
78
118
  requirements:
79
119
  - - ">="
80
120
  - !ruby/object:Gem::Version
121
+ hash: 3
81
122
  segments:
82
123
  - 0
83
124
  version: "0"
84
125
  required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
85
127
  requirements:
86
128
  - - ">="
87
129
  - !ruby/object:Gem::Version
130
+ hash: 3
88
131
  segments:
89
132
  - 0
90
133
  version: "0"
91
134
  requirements: []
92
135
 
93
136
  rubyforge_project: textmate
94
- rubygems_version: 1.3.6
137
+ rubygems_version: 1.3.7
95
138
  signing_key:
96
139
  specification_version: 3
97
140
  summary: A scripting framework that replaces rake, sake and rubigen
@@ -118,6 +161,7 @@ test_files:
118
161
  - spec/runner_spec.rb
119
162
  - spec/shell/basic_spec.rb
120
163
  - spec/shell/color_spec.rb
164
+ - spec/shell/html_spec.rb
121
165
  - spec/shell_spec.rb
122
166
  - spec/spec_helper.rb
123
167
  - spec/task_spec.rb
@@ -128,6 +172,7 @@ test_files:
128
172
  - spec/fixtures/doc/README
129
173
  - spec/fixtures/group.thor
130
174
  - spec/fixtures/invoke.thor
175
+ - spec/fixtures/path with spaces
131
176
  - spec/fixtures/script.thor
132
177
  - spec/fixtures/task.thor
133
178
  - spec/spec.opts