thor 0.9.9 → 0.11.5

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 (65) hide show
  1. data/CHANGELOG.rdoc +29 -4
  2. data/README.rdoc +234 -0
  3. data/Thorfile +57 -0
  4. data/VERSION +1 -0
  5. data/bin/rake2thor +4 -0
  6. data/bin/thor +1 -1
  7. data/lib/thor.rb +216 -119
  8. data/lib/thor/actions.rb +272 -0
  9. data/lib/thor/actions/create_file.rb +102 -0
  10. data/lib/thor/actions/directory.rb +87 -0
  11. data/lib/thor/actions/empty_directory.rb +133 -0
  12. data/lib/thor/actions/file_manipulation.rb +195 -0
  13. data/lib/thor/actions/inject_into_file.rb +78 -0
  14. data/lib/thor/base.rb +510 -0
  15. data/lib/thor/core_ext/hash_with_indifferent_access.rb +75 -0
  16. data/lib/thor/core_ext/ordered_hash.rb +100 -0
  17. data/lib/thor/error.rb +25 -1
  18. data/lib/thor/group.rb +263 -0
  19. data/lib/thor/invocation.rb +178 -0
  20. data/lib/thor/parser.rb +4 -0
  21. data/lib/thor/parser/argument.rb +67 -0
  22. data/lib/thor/parser/arguments.rb +145 -0
  23. data/lib/thor/parser/option.rb +132 -0
  24. data/lib/thor/parser/options.rb +142 -0
  25. data/lib/thor/rake_compat.rb +67 -0
  26. data/lib/thor/runner.rb +232 -242
  27. data/lib/thor/shell.rb +72 -0
  28. data/lib/thor/shell/basic.rb +220 -0
  29. data/lib/thor/shell/color.rb +108 -0
  30. data/lib/thor/task.rb +97 -60
  31. data/lib/thor/util.rb +230 -55
  32. data/spec/actions/create_file_spec.rb +170 -0
  33. data/spec/actions/directory_spec.rb +118 -0
  34. data/spec/actions/empty_directory_spec.rb +91 -0
  35. data/spec/actions/file_manipulation_spec.rb +242 -0
  36. data/spec/actions/inject_into_file_spec.rb +80 -0
  37. data/spec/actions_spec.rb +291 -0
  38. data/spec/base_spec.rb +236 -0
  39. data/spec/core_ext/hash_with_indifferent_access_spec.rb +43 -0
  40. data/spec/core_ext/ordered_hash_spec.rb +115 -0
  41. data/spec/fixtures/bundle/execute.rb +6 -0
  42. data/spec/fixtures/doc/config.rb +1 -0
  43. data/spec/group_spec.rb +177 -0
  44. data/spec/invocation_spec.rb +107 -0
  45. data/spec/parser/argument_spec.rb +47 -0
  46. data/spec/parser/arguments_spec.rb +64 -0
  47. data/spec/parser/option_spec.rb +212 -0
  48. data/spec/parser/options_spec.rb +255 -0
  49. data/spec/rake_compat_spec.rb +64 -0
  50. data/spec/runner_spec.rb +204 -0
  51. data/spec/shell/basic_spec.rb +206 -0
  52. data/spec/shell/color_spec.rb +41 -0
  53. data/spec/shell_spec.rb +25 -0
  54. data/spec/spec_helper.rb +52 -0
  55. data/spec/task_spec.rb +82 -0
  56. data/spec/thor_spec.rb +234 -0
  57. data/spec/util_spec.rb +196 -0
  58. metadata +69 -25
  59. data/README.markdown +0 -76
  60. data/Rakefile +0 -6
  61. data/lib/thor/options.rb +0 -242
  62. data/lib/thor/ordered_hash.rb +0 -64
  63. data/lib/thor/task_hash.rb +0 -22
  64. data/lib/thor/tasks.rb +0 -77
  65. data/lib/thor/tasks/package.rb +0 -18
@@ -0,0 +1,142 @@
1
+ class Thor
2
+ # This is a modified version of Daniel Berger's Getopt::Long class, licensed
3
+ # under Ruby's license.
4
+ #
5
+ class Options < Arguments #:nodoc:
6
+ LONG_RE = /^(--\w+[-\w+]*)$/
7
+ SHORT_RE = /^(-[a-z])$/i
8
+ EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i
9
+ SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
10
+ SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
11
+
12
+ # Receives a hash and makes it switches.
13
+ #
14
+ def self.to_switches(options)
15
+ options.map do |key, value|
16
+ case value
17
+ when true
18
+ "--#{key}"
19
+ when Array
20
+ "--#{key} #{value.map{ |v| v.inspect }.join(' ')}"
21
+ when Hash
22
+ "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}"
23
+ when nil, false
24
+ ""
25
+ else
26
+ "--#{key} #{value.inspect}"
27
+ end
28
+ end.join(" ")
29
+ end
30
+
31
+ # Takes a hash of Thor::Option objects.
32
+ #
33
+ def initialize(options={})
34
+ options = options.values
35
+ super(options)
36
+ @shorts, @switches = {}, {}
37
+
38
+ options.each do |option|
39
+ @switches[option.switch_name] = option
40
+
41
+ option.aliases.each do |short|
42
+ @shorts[short.to_s] ||= option.switch_name
43
+ end
44
+ end
45
+ end
46
+
47
+ def parse(args)
48
+ @pile = args.dup
49
+
50
+ while peek
51
+ if current_is_switch?
52
+ case shift
53
+ when SHORT_SQ_RE
54
+ unshift($1.split('').map { |f| "-#{f}" })
55
+ next
56
+ when EQ_RE, SHORT_NUM
57
+ unshift($2)
58
+ switch = $1
59
+ when LONG_RE, SHORT_RE
60
+ switch = $1
61
+ end
62
+
63
+ switch = normalize_switch(switch)
64
+ next unless option = switch_option(switch)
65
+
66
+ @assigns[option.human_name] = parse_peek(switch, option)
67
+ else
68
+ shift
69
+ end
70
+ end
71
+
72
+ check_requirement!
73
+ @assigns
74
+ end
75
+
76
+ protected
77
+
78
+ # Returns true if the current value in peek is a registered switch.
79
+ #
80
+ def current_is_switch?
81
+ case peek
82
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
83
+ switch?($1)
84
+ when SHORT_SQ_RE
85
+ $1.split('').any? { |f| switch?("-#{f}") }
86
+ end
87
+ end
88
+
89
+ def switch?(arg)
90
+ switch_option(arg) || @shorts.key?(arg)
91
+ end
92
+
93
+ def switch_option(arg)
94
+ if match = no_or_skip?(arg)
95
+ @switches[arg] || @switches["--#{match}"]
96
+ else
97
+ @switches[arg]
98
+ end
99
+ end
100
+
101
+ def no_or_skip?(arg)
102
+ arg =~ /^--(no|skip)-([-\w]+)$/
103
+ $2
104
+ end
105
+
106
+ # Check if the given argument is actually a shortcut.
107
+ #
108
+ def normalize_switch(arg)
109
+ @shorts.key?(arg) ? @shorts[arg] : arg
110
+ end
111
+
112
+ # Parse boolean values which can be given as --foo=true, --foo or --no-foo.
113
+ #
114
+ def parse_boolean(switch)
115
+ if current_is_value?
116
+ ["true", "TRUE", "t", "T", true].include?(shift)
117
+ else
118
+ @switches.key?(switch) || !no_or_skip?(switch)
119
+ end
120
+ end
121
+
122
+ # Parse the value at the peek analyzing if it requires an input or not.
123
+ #
124
+ def parse_peek(switch, option)
125
+ unless current_is_value?
126
+ if option.boolean?
127
+ # No problem for boolean types
128
+ elsif no_or_skip?(switch)
129
+ return nil # User set value to nil
130
+ elsif option.string? && !option.required?
131
+ return option.human_name # Return the option name
132
+ else
133
+ raise MalformattedArgumentError, "no value provided for option '#{switch}'"
134
+ end
135
+ end
136
+
137
+ @non_assigned_required.delete(option)
138
+ send(:"parse_#{option.type}", switch)
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,67 @@
1
+ require 'rake'
2
+
3
+ class Thor
4
+ # Adds a compatibility layer to your Thor classes which allows you to use
5
+ # rake package tasks. For example, to use rspec rake tasks, one can do:
6
+ #
7
+ # require 'thor/rake_compat'
8
+ #
9
+ # class Default < Thor
10
+ # include Thor::RakeCompat
11
+ #
12
+ # Spec::Rake::SpecTask.new(:spec) do |t|
13
+ # t.spec_opts = ['--options', "spec/spec.opts"]
14
+ # t.spec_files = FileList['spec/**/*_spec.rb']
15
+ # end
16
+ # end
17
+ #
18
+ module RakeCompat
19
+ def self.rake_classes
20
+ @rake_classes ||= []
21
+ end
22
+
23
+ def self.included(base)
24
+ # Hack. Make rakefile point to invoker, so rdoc task is generated properly.
25
+ Rake.application.instance_variable_set(:@rakefile, caller[0].match(/(.*):\d+/)[1])
26
+ self.rake_classes << base
27
+ end
28
+ end
29
+ end
30
+
31
+ class Object #:nodoc:
32
+ alias :rake_task :task
33
+ alias :rake_namespace :namespace
34
+
35
+ def task(*args, &block)
36
+ task = rake_task(*args, &block)
37
+
38
+ if klass = Thor::RakeCompat.rake_classes.last
39
+ non_namespaced_name = task.name.split(':').last
40
+
41
+ description = non_namespaced_name
42
+ description << task.arg_names.map{ |n| n.to_s.upcase }.join(' ')
43
+ description.strip!
44
+
45
+ klass.desc description, task.comment || non_namespaced_name
46
+ klass.class_eval <<-METHOD
47
+ def #{non_namespaced_name}(#{task.arg_names.join(', ')})
48
+ Rake::Task[#{task.name.to_sym.inspect}].invoke(#{task.arg_names.join(', ')})
49
+ end
50
+ METHOD
51
+ end
52
+
53
+ task
54
+ end
55
+
56
+ def namespace(name, &block)
57
+ if klass = Thor::RakeCompat.rake_classes.last
58
+ const_name = Thor::Util.camel_case(name.to_s).to_sym
59
+ klass.const_set(const_name, Class.new(Thor))
60
+ new_klass = klass.const_get(const_name)
61
+ Thor::RakeCompat.rake_classes << new_klass
62
+ end
63
+
64
+ rake_namespace(name, &block)
65
+ Thor::RakeCompat.rake_classes.pop
66
+ end
67
+ end
data/lib/thor/runner.rb CHANGED
@@ -1,305 +1,295 @@
1
- require 'thor'
2
- require "thor/util"
3
- require "open-uri"
4
- require "fileutils"
5
- require "yaml"
6
- require "digest/md5"
7
- require "readline"
8
-
9
- class Thor::Runner < Thor
10
-
11
- def self.globs_for(path)
12
- ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
13
- end
1
+ require 'fileutils'
2
+ require 'open-uri'
3
+ require 'yaml'
4
+ require 'digest/md5'
5
+ require 'pathname'
14
6
 
7
+ class Thor::Runner < Thor #:nodoc:
15
8
  map "-T" => :list, "-i" => :install, "-u" => :update
16
-
17
- desc "install NAME", "install a Thor file into your system tasks, optionally named for future updates"
18
- method_options :as => :optional, :relative => :boolean
9
+
10
+ # Override Thor#help so it can give information about any class and any method.
11
+ #
12
+ def help(meth=nil)
13
+ if meth && !self.respond_to?(meth)
14
+ initialize_thorfiles(meth)
15
+ klass, task = Thor::Util.namespace_to_thor_class_and_task(meth)
16
+ # Send mapping -h because it works with Thor::Group too
17
+ klass.start(["-h", task].compact, :shell => self.shell)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ # If a task is not found on Thor::Runner, method missing is invoked and
24
+ # Thor::Runner is then responsable for finding the task in all classes.
25
+ #
26
+ def method_missing(meth, *args)
27
+ meth = meth.to_s
28
+ initialize_thorfiles(meth)
29
+ klass, task = Thor::Util.namespace_to_thor_class_and_task(meth)
30
+ args.unshift(task) if task
31
+ klass.start(args, :shell => shell)
32
+ end
33
+
34
+ desc "install NAME", "Install an optionally named Thor file into your system tasks"
35
+ method_options :as => :string, :relative => :boolean
19
36
  def install(name)
20
37
  initialize_thorfiles
21
38
 
22
- base = name
23
- package = :file
24
-
39
+ # If a directory name is provided as the argument, look for a 'main.thor'
40
+ # task in said directory.
25
41
  begin
26
42
  if File.directory?(File.expand_path(name))
27
43
  base, package = File.join(name, "main.thor"), :directory
28
- contents = open(base).read
44
+ contents = open(base).read
29
45
  else
30
- contents = open(name).read
46
+ base, package = name, :file
47
+ contents = open(name).read
31
48
  end
32
49
  rescue OpenURI::HTTPError
33
- raise Error, "Error opening URI `#{name}'"
50
+ raise Error, "Error opening URI '#{name}'"
34
51
  rescue Errno::ENOENT
35
- raise Error, "Error opening file `#{name}'"
52
+ raise Error, "Error opening file '#{name}'"
36
53
  end
37
-
38
- is_uri = File.exist?(name) ? false : true
39
-
40
- puts "Your Thorfile contains: "
41
- puts contents
42
- print "Do you wish to continue [y/N]? "
43
- response = Readline.readline
44
-
45
- return false unless response =~ /^\s*y/i
46
-
47
- constants = Thor::Util.constants_in_contents(contents, base)
48
-
54
+
55
+ say "Your Thorfile contains:"
56
+ say contents
57
+
58
+ return false if no?("Do you wish to continue [y/N]?")
59
+
49
60
  as = options["as"] || begin
50
61
  first_line = contents.split("\n")[0]
51
62
  (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
52
63
  end
53
-
54
- if !as
55
- print "Please specify a name for #{name} in the system repository [#{name}]: "
56
- as = Readline.readline
57
- as = name if as.empty?
64
+
65
+ unless as
66
+ basename = File.basename(name)
67
+ as = ask("Please specify a name for #{name} in the system repository [#{basename}]:")
68
+ as = basename if as.empty?
69
+ end
70
+
71
+ location = if options[:relative] || name =~ /^http:\/\//
72
+ name
73
+ else
74
+ File.expand_path(name)
58
75
  end
59
-
60
- FileUtils.mkdir_p thor_root
61
-
62
- yaml_file = File.join(thor_root, "thor.yml")
63
- FileUtils.touch(yaml_file)
64
- yaml = thor_yaml
65
-
66
- location = (options[:relative] || is_uri) ? name : File.expand_path(name)
67
- yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => location, :constants => constants}
68
-
69
- save_yaml(yaml)
70
-
71
- puts "Storing thor file in your system repository"
72
-
73
- destination = File.join(thor_root, yaml[as][:filename])
74
-
76
+
77
+ thor_yaml[as] = {
78
+ :filename => Digest::MD5.hexdigest(name + as),
79
+ :location => location,
80
+ :namespaces => Thor::Util.namespaces_in_content(contents, base)
81
+ }
82
+
83
+ save_yaml(thor_yaml)
84
+ say "Storing thor file in your system repository"
85
+ destination = File.join(thor_root, thor_yaml[as][:filename])
86
+
75
87
  if package == :file
76
- File.open(destination, "w") {|f| f.puts contents }
88
+ File.open(destination, "w") { |f| f.puts contents }
77
89
  else
78
90
  FileUtils.cp_r(name, destination)
79
91
  end
80
-
81
- yaml[as][:filename] # Indicate sucess
92
+
93
+ thor_yaml[as][:filename] # Indicate success
82
94
  end
83
-
84
- desc "uninstall NAME", "uninstall a named Thor module"
95
+
96
+ desc "uninstall NAME", "Uninstall a named Thor module"
85
97
  def uninstall(name)
86
- yaml = thor_yaml
87
- raise Error, "Can't find module `#{name}'" unless yaml[name]
88
-
89
- puts "Uninstalling #{name}."
90
-
91
- file = File.join(thor_root, "#{yaml[name][:filename]}")
92
- FileUtils.rm_rf(file)
93
- yaml.delete(name)
94
- save_yaml(yaml)
95
-
98
+ raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
99
+ say "Uninstalling #{name}."
100
+ FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}"))
101
+
102
+ thor_yaml.delete(name)
103
+ save_yaml(thor_yaml)
104
+
96
105
  puts "Done."
97
106
  end
98
-
99
- desc "update NAME", "update a Thor file from its original location"
107
+
108
+ desc "update NAME", "Update a Thor file from its original location"
100
109
  def update(name)
101
- yaml = thor_yaml
102
- raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
110
+ raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
103
111
 
104
- puts "Updating `#{name}' from #{yaml[name][:location]}"
105
- old_filename = yaml[name][:filename]
112
+ say "Updating '#{name}' from #{thor_yaml[name][:location]}"
113
+
114
+ old_filename = thor_yaml[name][:filename]
106
115
  self.options = self.options.merge("as" => name)
107
- filename = install(yaml[name][:location])
116
+ filename = install(thor_yaml[name][:location])
117
+
108
118
  unless filename == old_filename
109
119
  File.delete(File.join(thor_root, old_filename))
110
120
  end
111
121
  end
112
-
113
- desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
122
+
123
+ desc "installed", "List the installed Thor modules and tasks"
114
124
  method_options :internal => :boolean
115
125
  def installed
116
- thor_root_glob.each do |f|
117
- next if f =~ /thor\.yml$/
118
- load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))
119
- end
126
+ initialize_thorfiles(nil, true)
127
+
128
+ klasses = Thor::Base.subclasses
129
+ klasses -= [Thor, Thor::Runner] unless options["internal"]
120
130
 
121
- klasses = Thor.subclasses
122
- klasses -= [Thor, Thor::Runner] unless options['internal']
123
131
  display_klasses(true, klasses)
124
132
  end
125
-
126
- desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
127
- method_options :substring => :boolean,
128
- :group => :optional,
129
- :all => :boolean
130
- def list(search = "")
133
+
134
+ desc "list [SEARCH]", "List the available thor tasks (--substring means .*SEARCH)"
135
+ method_options :substring => :boolean, :group => :string, :all => :boolean
136
+ def list(search="")
131
137
  initialize_thorfiles
138
+
132
139
  search = ".*#{search}" if options["substring"]
133
140
  search = /^#{search}.*/i
134
- group = options[:group] || 'standard'
141
+ group = options[:group] || "standard"
135
142
 
136
- classes = Thor.subclasses.select do |k|
137
- (options[:all] || k.group_name == group) &&
138
- Thor::Util.constant_to_thor_path(k.name) =~ search
143
+ klasses = Thor::Base.subclasses.select do |k|
144
+ (options[:all] || k.group == group) && k.namespace =~ search
139
145
  end
140
- display_klasses(false, classes)
141
- end
142
146
 
143
- # Override Thor#help so we can give info about not-yet-loaded tasks
144
- def help(task = nil)
145
- initialize_thorfiles(task) if task && task.include?(?:)
146
- super
147
+ display_klasses(false, klasses)
147
148
  end
148
-
149
- def method_missing(meth, *args)
150
- meth = meth.to_s
151
- super(meth.to_sym, *args) unless meth.include? ?:
152
149
 
153
- initialize_thorfiles(meth)
154
- task = Thor[meth]
155
- task.parse task.klass.new, ARGV[1..-1]
156
- end
157
-
158
- def self.thor_root
159
- File.join(ENV["HOME"] || ENV["APPDATA"], ".thor")
160
- end
150
+ private
161
151
 
162
- def self.thor_root_glob
163
- # On Windows thor_root will be something like this:
164
- #
165
- # C:\Documents and Settings\james\.thor
166
- #
167
- # If we don't #gsub the \ character, Dir.glob will fail.
168
- files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
169
- files.map! do |file|
170
- File.directory?(file) ? File.join(file, "main.thor") : file
152
+ def thor_root
153
+ Thor::Util.thor_root
171
154
  end
172
- end
173
-
174
- private
175
- def thor_root
176
- self.class.thor_root
177
- end
178
155
 
179
- def thor_root_glob
180
- self.class.thor_root_glob
181
- end
182
-
183
- def thor_yaml
184
- yaml_file = File.join(thor_root, "thor.yml")
185
- yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
186
- yaml || {}
187
- end
188
-
189
- def save_yaml(yaml)
190
- yaml_file = File.join(thor_root, "thor.yml")
191
- File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
192
- end
193
-
194
- def display_klasses(with_modules = false, klasses = Thor.subclasses)
195
- klasses -= [Thor, Thor::Runner] unless with_modules
196
- raise Error, "No Thor tasks available" if klasses.empty?
197
-
198
- if with_modules && !(yaml = thor_yaml).empty?
199
- max_name = yaml.max {|(xk,xv),(yk,yv)| xk.to_s.size <=> yk.to_s.size }.first.size
200
- modules_label = "Modules"
201
- namespaces_label = "Namespaces"
202
- column_width = [max_name + 4, modules_label.size + 1].max
203
-
204
- print "%-#{column_width}s" % modules_label
205
- puts namespaces_label
206
- print "%-#{column_width}s" % ("-" * modules_label.size)
207
- puts "-" * namespaces_label.size
208
-
209
- yaml.each do |name, info|
210
- print "%-#{column_width}s" % name
211
- puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
156
+ def thor_yaml
157
+ @thor_yaml ||= begin
158
+ yaml_file = File.join(thor_root, "thor.yml")
159
+ yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
160
+ yaml || {}
212
161
  end
213
-
214
- puts
215
162
  end
216
-
217
- # Calculate the largest base class name
218
- max_base = klasses.max do |x,y|
219
- Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
220
- end.name.size
221
-
222
- # Calculate the size of the largest option description
223
- max_left_item = klasses.max do |x,y|
224
- (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
225
- end
226
-
227
- max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
228
-
229
- unless klasses.empty?
230
- puts # add some spacing
231
- klasses.each { |k| display_tasks(k, max_base, max_left); }
232
- else
233
- puts "\033[1;34mNo Thor tasks available\033[0m"
163
+
164
+ # Save the yaml file. If none exists in thor root, creates one.
165
+ #
166
+ def save_yaml(yaml)
167
+ yaml_file = File.join(thor_root, "thor.yml")
168
+
169
+ unless File.exists?(yaml_file)
170
+ FileUtils.mkdir_p(thor_root)
171
+ yaml_file = File.join(thor_root, "thor.yml")
172
+ FileUtils.touch(yaml_file)
173
+ end
174
+
175
+ File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml }
234
176
  end
235
- end
236
-
237
- def display_tasks(klass, max_base, max_left)
238
- if klass.tasks.values.length > 1
239
-
240
- base = Thor::Util.constant_to_thor_path(klass.name)
241
-
242
- if base.to_a.empty?
243
- base = 'default'
244
- puts "\033[1;35m#{base}\033[0m"
245
- else
246
- puts "\033[1;34m#{base}\033[0m"
177
+
178
+ # Load the thorfiles. If relevant_to is supplied, looks for specific files
179
+ # in the thor_root instead of loading them all.
180
+ #
181
+ # By default, it also traverses the current path until find Thor files, as
182
+ # described in thorfiles. This look up can be skipped by suppliying
183
+ # skip_lookup true.
184
+ #
185
+ def initialize_thorfiles(relevant_to=nil, skip_lookup=false)
186
+ thorfiles(relevant_to, skip_lookup).each do |f|
187
+ Thor::Util.load_thorfile(f) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f))
247
188
  end
248
- puts "-" * base.length
249
-
250
- klass.tasks.each true do |name, task|
251
- format_string = "%-#{max_left + max_base + 5}s"
252
- print format_string % task.formatted_usage(true)
253
- puts task.description
189
+ end
190
+
191
+ # Finds Thorfiles by traversing from your current directory down to the root
192
+ # directory of your system. If at any time we find a Thor file, we stop.
193
+ #
194
+ # We also ensure that system-wide Thorfiles are loaded first, so local
195
+ # Thorfiles can override them.
196
+ #
197
+ # ==== Example
198
+ #
199
+ # If we start at /Users/wycats/dev/thor ...
200
+ #
201
+ # 1. /Users/wycats/dev/thor
202
+ # 2. /Users/wycats/dev
203
+ # 3. /Users/wycats <-- we find a Thorfile here, so we stop
204
+ #
205
+ # Suppose we start at c:\Documents and Settings\james\dev\thor ...
206
+ #
207
+ # 1. c:\Documents and Settings\james\dev\thor
208
+ # 2. c:\Documents and Settings\james\dev
209
+ # 3. c:\Documents and Settings\james
210
+ # 4. c:\Documents and Settings
211
+ # 5. c:\ <-- no Thorfiles found!
212
+ #
213
+ def thorfiles(relevant_to=nil, skip_lookup=false)
214
+ # Deal with deprecated thor when :namespaces: is available as constants
215
+ save_yaml(thor_yaml) if Thor::Util.convert_constants_to_namespaces(thor_yaml)
216
+
217
+ thorfiles = []
218
+
219
+ unless skip_lookup
220
+ Pathname.pwd.ascend do |path|
221
+ thorfiles = Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten
222
+ break unless thorfiles.empty?
223
+ end
254
224
  end
255
-
256
- unless klass.opts.empty?
257
- puts "\nglobal options: #{Options.new(klass.opts)}"
225
+
226
+ files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Thor::Util.thor_root_glob)
227
+ files += thorfiles
228
+ files -= ["#{thor_root}/thor.yml"]
229
+
230
+ files.map! do |file|
231
+ File.directory?(file) ? File.join(file, "main.thor") : file
258
232
  end
259
-
260
- puts # add some spacing
261
233
  end
262
- end
263
234
 
264
- def initialize_thorfiles(relevant_to = nil)
265
- thorfiles(relevant_to).each {|f| load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
266
- end
267
-
268
- def load_thorfile(path)
269
- txt = File.read(path)
270
- begin
271
- Thor::Tasks.class_eval txt, path
272
- rescue Object => e
273
- $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
274
- end
275
- end
276
-
277
- def thorfiles(relevant_to = nil)
278
- path = Dir.pwd
279
- thorfiles = []
280
-
281
- # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
282
- while thorfiles.empty?
283
- thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
284
- path = File.dirname(path)
285
- break if path == "/"
235
+ # Load thorfiles relevant to the given method. If you provide "foo:bar" it
236
+ # will load all thor files in the thor.yaml that has "foo" e "foo:bar"
237
+ # namespaces registered.
238
+ #
239
+ def thorfiles_relevant_to(meth)
240
+ lookup = [ meth, meth.split(":")[0...-1].join(":") ]
241
+
242
+ files = thor_yaml.select do |k, v|
243
+ v[:namespaces] && !(v[:namespaces] & lookup).empty?
244
+ end
245
+
246
+ files.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
286
247
  end
287
248
 
288
- # We want to load system-wide Thorfiles first
289
- # so the local Thorfiles will override them.
290
- files = (relevant_to ? thorfiles_relevant_to(relevant_to) :
291
- thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
292
-
293
- files.map! do |file|
294
- File.directory?(file) ? File.join(file, "main.thor") : file
249
+ # Display information about the given klasses. If with_module is given,
250
+ # it shows a table with information extracted from the yaml file.
251
+ #
252
+ def display_klasses(with_modules=false, klasses=Thor.subclasses)
253
+ klasses -= [Thor, Thor::Runner] unless with_modules
254
+ raise Error, "No Thor tasks available" if klasses.empty?
255
+
256
+ if with_modules && !thor_yaml.empty?
257
+ info = []
258
+ labels = ["Modules", "Namespaces"]
259
+
260
+ info << labels
261
+ info << [ "-" * labels[0].size, "-" * labels[1].size ]
262
+
263
+ thor_yaml.each do |name, hash|
264
+ info << [ name, hash[:namespaces].join(", ") ]
265
+ end
266
+
267
+ print_table info
268
+ say ""
269
+ end
270
+
271
+ unless klasses.empty?
272
+ klasses.dup.each do |klass|
273
+ klasses -= Thor::Util.thor_classes_in(klass)
274
+ end
275
+
276
+ klasses.each { |k| display_tasks(k) }
277
+ else
278
+ say "\033[1;34mNo Thor tasks available\033[0m"
279
+ end
295
280
  end
296
- end
297
281
 
298
- def thorfiles_relevant_to(meth)
299
- klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
300
- thor_yaml.select do |k, v|
301
- v[:constants] && v[:constants].include?(klass_str)
302
- end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
303
- end
282
+ # Display tasks from the given Thor class.
283
+ #
284
+ def display_tasks(klass)
285
+ unless klass.tasks.empty?
286
+ base = klass.namespace
304
287
 
288
+ color = base == "default" ? :magenta : :blue
289
+ say shell.set_color(base, color, true)
290
+ say "-" * base.length
291
+
292
+ klass.help(shell, :short => true, :ident => 0, :namespace => true)
293
+ end
294
+ end
305
295
  end