thor 0.13.6 → 0.13.7

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