thor 0.13.4 → 0.14.3

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 (45) hide show
  1. data/CHANGELOG.rdoc +5 -3
  2. data/{README.rdoc → README.md} +65 -55
  3. data/Thorfile +3 -2
  4. data/lib/thor/actions/create_file.rb +3 -1
  5. data/lib/thor/actions/directory.rb +4 -2
  6. data/lib/thor/actions/file_manipulation.rb +30 -7
  7. data/lib/thor/actions.rb +31 -9
  8. data/lib/thor/base.rb +38 -15
  9. data/lib/thor/core_ext/hash_with_indifferent_access.rb +1 -1
  10. data/lib/thor/group.rb +22 -20
  11. data/lib/thor/invocation.rb +45 -57
  12. data/lib/thor/parser/argument.rb +15 -15
  13. data/lib/thor/parser/arguments.rb +14 -3
  14. data/lib/thor/parser/option.rb +38 -46
  15. data/lib/thor/parser/options.rb +26 -22
  16. data/lib/thor/runner.rb +11 -16
  17. data/lib/thor/shell/basic.rb +48 -12
  18. data/lib/thor/shell/html.rb +121 -0
  19. data/lib/thor/shell.rb +7 -2
  20. data/lib/thor/task.rb +63 -51
  21. data/lib/thor/util.rb +15 -16
  22. data/lib/thor/version.rb +1 -1
  23. data/lib/thor.rb +118 -45
  24. data/spec/actions/directory_spec.rb +2 -2
  25. data/spec/actions/file_manipulation_spec.rb +7 -0
  26. data/spec/actions_spec.rb +21 -3
  27. data/spec/base_spec.rb +9 -3
  28. data/spec/fixtures/doc/block_helper.rb +3 -0
  29. data/spec/fixtures/doc/components/.empty_directory +0 -0
  30. data/spec/fixtures/group.thor +16 -4
  31. data/spec/fixtures/path with spaces +0 -0
  32. data/spec/fixtures/script.thor +48 -4
  33. data/spec/group_spec.rb +3 -3
  34. data/spec/invocation_spec.rb +1 -8
  35. data/spec/parser/option_spec.rb +2 -2
  36. data/spec/parser/options_spec.rb +27 -0
  37. data/spec/runner_spec.rb +16 -8
  38. data/spec/shell/basic_spec.rb +13 -4
  39. data/spec/shell/html_spec.rb +27 -0
  40. data/spec/shell_spec.rb +13 -0
  41. data/spec/spec_helper.rb +4 -3
  42. data/spec/task_spec.rb +7 -7
  43. data/spec/thor_spec.rb +103 -6
  44. data/spec/util_spec.rb +3 -7
  45. metadata +63 -7
@@ -35,15 +35,17 @@ class Thor
35
35
  # say("I know you knew that.")
36
36
  #
37
37
  def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/))
38
- message = message.to_s
39
- message = set_color(message, color) if color
38
+ message = message.to_s
39
+ message = set_color(message, color) if color
40
+
41
+ spaces = " " * padding
40
42
 
41
43
  if force_new_line
42
- $stdout.puts(message)
44
+ $stdout.puts(spaces + message)
43
45
  else
44
- $stdout.print(message)
45
- $stdout.flush
46
+ $stdout.print(spaces + message)
46
47
  end
48
+ $stdout.flush
47
49
  end
48
50
 
49
51
  # Say a status with the given color and appends the message. Since this
@@ -58,14 +60,16 @@ class Thor
58
60
 
59
61
  status = status.to_s.rjust(12)
60
62
  status = set_color status, color, true if color
61
- say "#{status}#{spaces}#{message}", nil, true
63
+
64
+ $stdout.puts "#{status}#{spaces}#{message}"
65
+ $stdout.flush
62
66
  end
63
67
 
64
68
  # Make a question the to user and returns true if the user replies "y" or
65
69
  # "yes".
66
70
  #
67
71
  def yes?(statement, color=nil)
68
- ask(statement, color) =~ is?(:yes)
72
+ !!(ask(statement, color) =~ is?(:yes))
69
73
  end
70
74
 
71
75
  # Make a question the to user and returns true if the user replies "n" or
@@ -81,16 +85,20 @@ class Thor
81
85
  # Array[Array[String, String, ...]]
82
86
  #
83
87
  # ==== Options
84
- # ident<Integer>:: Ident the first column by ident value.
88
+ # ident<Integer>:: Indent the first column by ident value.
89
+ # colwidth<Integer>:: Force the first column to colwidth spaces wide.
85
90
  #
86
91
  def print_table(table, options={})
87
92
  return if table.empty?
88
93
 
89
- formats, ident = [], options[:ident].to_i
94
+ formats, ident, colwidth = [], options[:ident].to_i, options[:colwidth]
90
95
  options[:truncate] = terminal_width if options[:truncate] == true
91
96
 
92
- 0.upto(table.first.length - 2) do |i|
93
- maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size
97
+ formats << "%-#{colwidth + 2}s" if colwidth
98
+ start = colwidth ? 1 : 0
99
+
100
+ start.upto(table.first.length - 2) do |i|
101
+ maxima ||= table.max{|a,b| a[i].size <=> b[i].size }[i].size
94
102
  formats << "%-#{maxima + 2}s"
95
103
  end
96
104
 
@@ -105,7 +113,35 @@ class Thor
105
113
  end
106
114
 
107
115
  sentence = truncate(sentence, options[:truncate]) if options[:truncate]
108
- $stdout.puts sentence
116
+ $stdout.puts sentence
117
+ end
118
+ end
119
+
120
+ # Prints a long string, word-wrapping the text to the current width of the
121
+ # terminal display. Ideal for printing heredocs.
122
+ #
123
+ # ==== Parameters
124
+ # String
125
+ #
126
+ # ==== Options
127
+ # ident<Integer>:: Indent each line of the printed paragraph by ident value.
128
+ #
129
+ def print_wrapped(message, options={})
130
+ ident = options[:ident] || 0
131
+ width = terminal_width - ident
132
+ paras = message.split("\n\n")
133
+
134
+ paras.map! do |unwrapped|
135
+ unwrapped.strip.gsub(/\n/, " ").squeeze(" ").
136
+ gsub(/.{1,#{width}}(?:\s|\Z)/){($& + 5.chr).
137
+ gsub(/\n\005/,"\n").gsub(/\005/,"\n")}
138
+ end
139
+
140
+ paras.each do |para|
141
+ para.split("\n").each do |line|
142
+ $stdout.puts line.insert(0, " " * ident)
143
+ end
144
+ $stdout.puts unless para == paras.last
109
145
  end
110
146
  end
111
147
 
@@ -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
data/lib/thor/shell.rb CHANGED
@@ -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 RbConfig::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
data/lib/thor/task.rb CHANGED
@@ -1,24 +1,9 @@
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
- # A dynamic task that handles method missing scenarios.
6
- class Dynamic < Task
7
- def initialize(name, options=nil)
8
- super(name.to_s, "A dynamically-generated task", name.to_s, options)
9
- end
10
-
11
- def run(instance, args=[])
12
- if (instance.methods & [name.to_s, name.to_sym]).empty?
13
- super
14
- else
15
- instance.class.handle_no_task_error(name)
16
- end
17
- end
18
- end
19
-
20
- def initialize(name, description, usage, options=nil)
21
- super(name.to_s, description, usage, options || {})
5
+ def initialize(name, description, long_description, usage, options=nil)
6
+ super(name.to_s, description, long_description, usage, options || {})
22
7
  end
23
8
 
24
9
  def initialize_copy(other) #:nodoc:
@@ -26,6 +11,10 @@ class Thor
26
11
  self.options = other.options.dup if other.options
27
12
  end
28
13
 
14
+ def hidden?
15
+ false
16
+ end
17
+
29
18
  # By default, a task invokes a method in the thor class. You can change this
30
19
  # implementation to create custom tasks.
31
20
  def run(instance, args=[])
@@ -39,18 +28,17 @@ class Thor
39
28
  instance.class.handle_no_task_error(name) : (raise e)
40
29
  end
41
30
 
42
- # Returns the formatted usage by injecting given required arguments
31
+ # Returns the formatted usage by injecting given required arguments
43
32
  # and required options into the given usage.
44
- def formatted_usage(klass, namespace=true)
45
- namespace = klass.namespace unless namespace == false
46
-
47
- # Add namespace
48
- formatted = if namespace
49
- "#{namespace.gsub(/^(default|thor:runner:)/,'')}:"
50
- else
51
- ""
33
+ def formatted_usage(klass, namespace = true, subcommand = false)
34
+ if namespace
35
+ namespace = klass.namespace
36
+ formatted = "#{namespace.gsub(/^(default)/,'')}:"
37
+ formatted.sub!(/.$/, ' ') if subcommand
52
38
  end
53
39
 
40
+ formatted ||= ""
41
+
54
42
  # Add usage with required arguments
55
43
  formatted << if klass && !klass.arguments.empty?
56
44
  usage.to_s.gsub(/^#{name}/) do |match|
@@ -67,36 +55,60 @@ class Thor
67
55
  formatted.strip
68
56
  end
69
57
 
70
- protected
58
+ protected
71
59
 
72
- def not_debugging?(instance)
73
- !(instance.class.respond_to?(:debugging) && instance.class.debugging)
74
- end
60
+ def not_debugging?(instance)
61
+ !(instance.class.respond_to?(:debugging) && instance.class.debugging)
62
+ end
75
63
 
76
- def required_options
77
- @required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ")
78
- end
64
+ def required_options
65
+ @required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ")
66
+ end
79
67
 
80
- # Given a target, checks if this class name is not a private/protected method.
81
- def public_method?(instance) #:nodoc:
82
- collection = instance.private_methods + instance.protected_methods
83
- (collection & [name.to_s, name.to_sym]).empty?
84
- end
68
+ # Given a target, checks if this class name is not a private/protected method.
69
+ def public_method?(instance) #:nodoc:
70
+ collection = instance.private_methods + instance.protected_methods
71
+ (collection & [name.to_s, name.to_sym]).empty?
72
+ end
85
73
 
86
- def sans_backtrace(backtrace, caller) #:nodoc:
87
- saned = backtrace.reject { |frame| frame =~ FILE_REGEXP }
88
- saned -= caller
89
- end
74
+ def sans_backtrace(backtrace, caller) #:nodoc:
75
+ saned = backtrace.reject { |frame| frame =~ FILE_REGEXP }
76
+ saned -= caller
77
+ end
90
78
 
91
- def handle_argument_error?(instance, error, caller)
92
- not_debugging?(instance) && error.message =~ /wrong number of arguments/ &&
93
- sans_backtrace(error.backtrace, caller).empty?
79
+ def handle_argument_error?(instance, error, caller)
80
+ not_debugging?(instance) && error.message =~ /wrong number of arguments/ && begin
81
+ saned = sans_backtrace(error.backtrace, caller)
82
+ # Ruby 1.9 always include the called method in the backtrace
83
+ saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
94
84
  end
85
+ end
95
86
 
96
- def handle_no_method_error?(instance, error, caller)
97
- not_debugging?(instance) &&
98
- error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
99
- end
87
+ def handle_no_method_error?(instance, error, caller)
88
+ not_debugging?(instance) &&
89
+ error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
90
+ end
91
+ end
92
+
93
+ # A task that is hidden in help messages but still invocable.
94
+ class HiddenTask < Task
95
+ def hidden?
96
+ true
97
+ end
98
+ end
99
+
100
+ # A dynamic task that handles method missing scenarios.
101
+ class DynamicTask < Task
102
+ def initialize(name, options=nil)
103
+ super(name.to_s, "A dynamically-generated task", name.to_s, name.to_s, options)
104
+ end
100
105
 
106
+ def run(instance, args=[])
107
+ if (instance.methods & [name.to_s, name.to_sym]).empty?
108
+ super
109
+ else
110
+ instance.class.handle_no_task_error(name)
111
+ end
112
+ end
101
113
  end
102
- end
114
+ end
data/lib/thor/util.rb CHANGED
@@ -128,38 +128,37 @@ class Thor
128
128
  # ==== Parameters
129
129
  # namespace<String>
130
130
  #
131
- def self.find_class_and_task_by_namespace(namespace)
132
- if namespace.include?(?:)
131
+ def self.find_class_and_task_by_namespace(namespace, fallback = true)
132
+ if namespace.include?(?:) # look for a namespaced task
133
133
  pieces = namespace.split(":")
134
134
  task = pieces.pop
135
135
  klass = Thor::Util.find_by_namespace(pieces.join(":"))
136
136
  end
137
-
138
- unless klass
137
+ unless klass # look for a Thor::Group with the right name
139
138
  klass, task = Thor::Util.find_by_namespace(namespace), nil
140
139
  end
141
-
142
- return klass, task
143
- end
144
-
145
- # The same as namespace_to_thor_class_and_task!, but raises an error if a klass
146
- # could not be found.
147
- def self.find_class_and_task_by_namespace!(namespace)
148
- klass, task = find_class_and_task_by_namespace(namespace)
149
- raise Error, "Could not find namespace or task #{namespace.inspect}." unless klass
140
+ if !klass && fallback # try a task in the default namespace
141
+ task = namespace
142
+ klass = Thor::Util.find_by_namespace('')
143
+ end
150
144
  return klass, task
151
145
  end
152
146
 
153
147
  # Receives a path and load the thor file in the path. The file is evaluated
154
148
  # inside the sandbox to avoid namespacing conflicts.
155
149
  #
156
- def self.load_thorfile(path, content=nil)
150
+ def self.load_thorfile(path, content=nil, debug=false)
157
151
  content ||= File.binread(path)
158
152
 
159
153
  begin
160
154
  Thor::Sandbox.class_eval(content, path)
161
155
  rescue Exception => e
162
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
163
162
  end
164
163
  end
165
164
 
@@ -217,8 +216,8 @@ class Thor
217
216
  #
218
217
  def self.ruby_command
219
218
  @ruby_command ||= begin
220
- ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
221
- ruby << Config::CONFIG['EXEEXT']
219
+ ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
220
+ ruby << RbConfig::CONFIG['EXEEXT']
222
221
 
223
222
  # escape string in case path to ruby executable contain spaces.
224
223
  ruby.sub!(/.*\s.*/m, '"\&"')
data/lib/thor/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Thor
2
- VERSION = "0.13.4".freeze
2
+ VERSION = "0.14.3".freeze
3
3
  end