thor 0.9.9 → 0.11.5

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