wycats-thor 0.9.8 → 0.10.26

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,145 @@
1
+ class Thor
2
+ class Arguments
3
+ NUMERIC = /(\d*\.\d+|\d+)/
4
+
5
+ # Receives an array of args and returns two arrays, one with arguments
6
+ # and one with switches.
7
+ #
8
+ def self.split(args)
9
+ arguments = []
10
+
11
+ args.each do |item|
12
+ break if item =~ /^-/
13
+ arguments << item
14
+ end
15
+
16
+ return arguments, args[Range.new(arguments.size, -1)]
17
+ end
18
+
19
+ def self.parse(base, args)
20
+ new(base).parse(args)
21
+ end
22
+
23
+ # Takes an array of Thor::Argument objects.
24
+ #
25
+ def initialize(arguments=[])
26
+ @assigns, @non_assigned_required = {}, []
27
+ @switches = arguments
28
+
29
+ arguments.each do |argument|
30
+ if argument.default
31
+ @assigns[argument.human_name] = argument.default
32
+ elsif argument.required?
33
+ @non_assigned_required << argument
34
+ end
35
+ end
36
+ end
37
+
38
+ def parse(args)
39
+ @pile = args.dup
40
+
41
+ @switches.each do |argument|
42
+ break unless peek
43
+ @non_assigned_required.delete(argument)
44
+ @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name)
45
+ end
46
+
47
+ check_requirement!
48
+ @assigns
49
+ end
50
+
51
+ private
52
+
53
+ def peek
54
+ @pile.first
55
+ end
56
+
57
+ def shift
58
+ @pile.shift
59
+ end
60
+
61
+ def unshift(arg)
62
+ unless arg.kind_of?(Array)
63
+ @pile.unshift(arg)
64
+ else
65
+ @pile = arg + @pile
66
+ end
67
+ end
68
+
69
+ def current_is_value?
70
+ peek && peek.to_s !~ /^-/
71
+ end
72
+
73
+ # Runs through the argument array getting strings that contains ":" and
74
+ # mark it as a hash:
75
+ #
76
+ # [ "name:string", "age:integer" ]
77
+ #
78
+ # Becomes:
79
+ #
80
+ # { "name" => "string", "age" => "integer" }
81
+ #
82
+ def parse_hash(name)
83
+ return shift if peek.is_a?(Hash)
84
+ hash = {}
85
+
86
+ while current_is_value? && peek.include?(?:)
87
+ key, value = shift.split(':')
88
+ hash[key] = value
89
+ end
90
+ hash
91
+ end
92
+
93
+ # Runs through the argument array getting all strings until no string is
94
+ # found or a switch is found.
95
+ #
96
+ # ["a", "b", "c"]
97
+ #
98
+ # And returns it as an array:
99
+ #
100
+ # ["a", "b", "c"]
101
+ #
102
+ def parse_array(name)
103
+ return shift if peek.is_a?(Array)
104
+ array = []
105
+
106
+ while current_is_value?
107
+ array << shift
108
+ end
109
+ array
110
+ end
111
+
112
+ # Check if the peel is numeric ofrmat and return a Float or Integer.
113
+ # Otherwise raises an error.
114
+ #
115
+ def parse_numeric(name)
116
+ return shift if peek.is_a?(Numeric)
117
+
118
+ unless peek =~ NUMERIC && $& == peek
119
+ raise MalformattedArgumentError, "expected numeric value for '#{name}'; got #{peek.inspect}"
120
+ end
121
+
122
+ $&.index('.') ? shift.to_f : shift.to_i
123
+ end
124
+
125
+ # Parse string, i.e., just return the current value in the pile.
126
+ #
127
+ def parse_string(name)
128
+ shift
129
+ end
130
+
131
+ # Raises an error if @non_assigned_required array is not empty.
132
+ #
133
+ def check_requirement!
134
+ unless @non_assigned_required.empty?
135
+ names = @non_assigned_required.map do |o|
136
+ o.respond_to?(:switch_name) ? o.switch_name : o.human_name
137
+ end.join("', '")
138
+
139
+ class_name = self.class.name.split('::').last.downcase
140
+ raise RequiredArgumentMissingError, "no value provided for required #{class_name} '#{names}'"
141
+ end
142
+ end
143
+
144
+ end
145
+ end
@@ -0,0 +1,125 @@
1
+ class Thor
2
+ class Option < Argument
3
+ attr_reader :aliases, :group
4
+
5
+ VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
6
+
7
+ def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, group=nil, aliases=nil)
8
+ super(name, description, required, type, default, banner)
9
+ @aliases = [*aliases].compact
10
+ @group = group.to_s.capitalize if group
11
+ end
12
+
13
+ # This parse quick options given as method_options. It makes several
14
+ # assumptions, but you can be more specific using the option method.
15
+ #
16
+ # parse :foo => "bar"
17
+ # #=> Option foo with default value bar
18
+ #
19
+ # parse [:foo, :baz] => "bar"
20
+ # #=> Option foo with default value bar and alias :baz
21
+ #
22
+ # parse :foo => :required
23
+ # #=> Required option foo without default value
24
+ #
25
+ # parse :foo => 2
26
+ # #=> Option foo with default value 2 and type numeric
27
+ #
28
+ # parse :foo => :numeric
29
+ # #=> Option foo without default value and type numeric
30
+ #
31
+ # parse :foo => true
32
+ # #=> Option foo with default value true and type boolean
33
+ #
34
+ # The valid types are :boolean, :numeric, :hash, :array and :string. If none
35
+ # is given a default type is assumed. This default type accepts arguments as
36
+ # string (--foo=value) or booleans (just --foo).
37
+ #
38
+ # By default all options are optional, unless :required is given.
39
+ #
40
+ def self.parse(key, value)
41
+ if key.is_a?(Array)
42
+ name, *aliases = key
43
+ else
44
+ name, aliases = key, []
45
+ end
46
+
47
+ name = name.to_s
48
+ default = value
49
+
50
+ type = case value
51
+ when Symbol
52
+ default = nil
53
+
54
+ if VALID_TYPES.include?(value)
55
+ value
56
+ elsif required = (value == :required)
57
+ :string
58
+ elsif value == :optional
59
+ # TODO Remove this warning in the future.
60
+ warn "Optional type is deprecated. Choose :boolean or :string instead. Assumed to be :boolean."
61
+ :boolean
62
+ end
63
+ when TrueClass, FalseClass
64
+ :boolean
65
+ when Numeric
66
+ :numeric
67
+ when Hash, Array, String
68
+ value.class.name.downcase.to_sym
69
+ end
70
+
71
+ self.new(name.to_s, nil, required, type, default, nil, nil, aliases)
72
+ end
73
+
74
+ def switch_name
75
+ @switch_name ||= dasherized? ? name : dasherize(name)
76
+ end
77
+
78
+ def human_name
79
+ @human_name ||= dasherized? ? undasherize(name) : name
80
+ end
81
+
82
+ def usage(padding=0)
83
+ sample = if banner && !banner.to_s.empty?
84
+ "#{switch_name}=#{banner}"
85
+ else
86
+ switch_name
87
+ end
88
+
89
+ sample = "[#{sample}]" unless required?
90
+
91
+ if aliases.empty?
92
+ (" " * padding) << sample
93
+ else
94
+ "#{aliases.join(', ')}, #{sample}"
95
+ end
96
+ end
97
+
98
+ def input_required?
99
+ type != :boolean
100
+ end
101
+
102
+ protected
103
+
104
+ def validate!
105
+ raise ArgumentError, "An option cannot be boolean and required." if type == :boolean && required?
106
+ end
107
+
108
+ def valid_type?(type)
109
+ VALID_TYPES.include?(type.to_sym)
110
+ end
111
+
112
+ def dasherized?
113
+ name.index('-') == 0
114
+ end
115
+
116
+ def undasherize(str)
117
+ str.sub(/^-{1,2}/, '')
118
+ end
119
+
120
+ def dasherize(str)
121
+ (str.length > 1 ? "--" : "-") + str.gsub('_', '-')
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,135 @@
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
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
+ if option.input_required?
126
+ return nil if no_or_skip?(switch)
127
+ raise MalformattedArgumentError, "no value provided for option '#{switch}'" unless current_is_value?
128
+ end
129
+
130
+ @non_assigned_required.delete(option)
131
+ send(:"parse_#{option.type}", switch)
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,4 @@
1
+ require 'thor/parser/argument'
2
+ require 'thor/parser/arguments'
3
+ require 'thor/parser/option'
4
+ require 'thor/parser/options'
data/lib/thor/runner.rb CHANGED
@@ -1,286 +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"
1
+ require 'fileutils'
2
+ require 'open-uri'
3
+ require 'yaml'
4
+ require 'digest/md5'
5
+ require 'pathname'
8
6
 
9
7
  class Thor::Runner < Thor
10
-
11
- def self.globs_for(path)
12
- ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
8
+ map "-T" => :list, "-i" => :install, "-u" => :update
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(meth)
16
+ klass.start(["-h", task].compact, :shell => self.shell) # send mapping -h because it works with Thor::Group too
17
+ else
18
+ super
19
+ end
13
20
  end
14
21
 
15
- 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
22
+ # If a task is not found on Thor::Runner, method missing is invoked and
23
+ # Thor::Runner is then responsable for finding the task in all classes.
24
+ #
25
+ def method_missing(meth, *args)
26
+ meth = meth.to_s
27
+ initialize_thorfiles(meth)
28
+ klass, task = Thor::Util.namespace_to_thor_class(meth)
29
+ args.unshift(task) if task
30
+ klass.start(args, :shell => shell)
31
+ end
32
+
33
+ desc "install NAME", "Install a Thor file into your system tasks, optionally named for future updates"
34
+ method_options :as => :string, :relative => :boolean
19
35
  def install(name)
20
36
  initialize_thorfiles
37
+
38
+ # If a directory name is provided as the argument, look for a 'main.thor'
39
+ # task in said directory.
21
40
  begin
22
- contents = open(name).read
41
+ if File.directory?(File.expand_path(name))
42
+ base, package = File.join(name, "main.thor"), :directory
43
+ contents = open(base).read
44
+ else
45
+ base, package = name, :file
46
+ contents = open(name).read
47
+ end
23
48
  rescue OpenURI::HTTPError
24
- raise Error, "Error opening URI `#{name}'"
49
+ raise Error, "Error opening URI '#{name}'"
25
50
  rescue Errno::ENOENT
26
- raise Error, "Error opening file `#{name}'"
51
+ raise Error, "Error opening file '#{name}'"
27
52
  end
28
-
29
- is_uri = File.exist?(name) ? false : true
30
-
31
- puts "Your Thorfile contains: "
32
- puts contents
33
- print "Do you wish to continue [y/N]? "
34
- response = Readline.readline
35
-
36
- return false unless response =~ /^\s*y/i
37
-
38
- constants = Thor::Util.constants_in_contents(contents)
39
-
40
- # name = name =~ /\.thor$/ || is_uri ? name : "#{name}.thor"
41
-
53
+
54
+ say "Your Thorfile contains:"
55
+ say contents
56
+
57
+ return false if no?("Do you wish to continue [y/N]?")
58
+
42
59
  as = options["as"] || begin
43
60
  first_line = contents.split("\n")[0]
44
61
  (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil
45
62
  end
46
-
47
- if !as
48
- print "Please specify a name for #{name} in the system repository [#{name}]: "
49
- as = Readline.readline
50
- as = name if as.empty?
63
+
64
+ unless as
65
+ basename = File.basename(name)
66
+ as = ask("Please specify a name for #{name} in the system repository [#{basename}]:")
67
+ as = basename if as.empty?
68
+ end
69
+
70
+ location = if options[:relative] || name =~ /^http:\/\//
71
+ name
72
+ else
73
+ File.expand_path(name)
51
74
  end
52
-
53
- FileUtils.mkdir_p thor_root
54
-
55
- yaml_file = File.join(thor_root, "thor.yml")
56
- FileUtils.touch(yaml_file)
57
- yaml = thor_yaml
58
-
59
- location = (options[:relative] || is_uri) ? name : File.expand_path(name)
60
- yaml[as] = {:filename => Digest::MD5.hexdigest(name + as), :location => location, :constants => constants}
61
-
62
- save_yaml(yaml)
63
-
64
- puts "Storing thor file in your system repository"
65
-
66
- File.open(File.join(thor_root, yaml[as][:filename]), "w") do |file|
67
- file.puts contents
75
+
76
+ thor_yaml[as] = {
77
+ :filename => Digest::MD5.hexdigest(name + as),
78
+ :location => location,
79
+ :namespaces => Thor::Util.namespaces_in_contents(contents, base)
80
+ }
81
+
82
+ save_yaml(thor_yaml)
83
+ say "Storing thor file in your system repository"
84
+ destination = File.join(thor_root, thor_yaml[as][:filename])
85
+
86
+ if package == :file
87
+ File.open(destination, "w") { |f| f.puts contents }
88
+ else
89
+ FileUtils.cp_r(name, destination)
68
90
  end
69
-
70
- yaml[as][:filename] # Indicate sucess
91
+
92
+ thor_yaml[as][:filename] # Indicate success
71
93
  end
72
-
73
- desc "uninstall NAME", "uninstall a named Thor module"
94
+
95
+ desc "uninstall NAME", "Uninstall a named Thor module"
74
96
  def uninstall(name)
75
- yaml = thor_yaml
76
- raise Error, "Can't find module `#{name}'" unless yaml[name]
77
-
78
- puts "Uninstalling #{name}."
79
-
80
- file = File.join(thor_root, "#{yaml[name][:filename]}")
81
- File.delete(file)
82
- yaml.delete(name)
83
- save_yaml(yaml)
84
-
97
+ raise Error, "Can't find module '#{name}'" unless thor_yaml[name]
98
+ say "Uninstalling #{name}."
99
+ FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}"))
100
+
101
+ thor_yaml.delete(name)
102
+ save_yaml(thor_yaml)
103
+
85
104
  puts "Done."
86
105
  end
87
-
88
- desc "update NAME", "update a Thor file from its original location"
106
+
107
+ desc "update NAME", "Update a Thor file from its original location"
89
108
  def update(name)
90
- yaml = thor_yaml
91
- raise Error, "Can't find module `#{name}'" if !yaml[name] || !yaml[name][:location]
109
+ raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location]
92
110
 
93
- puts "Updating `#{name}' from #{yaml[name][:location]}"
94
- old_filename = yaml[name][:filename]
111
+ say "Updating '#{name}' from #{thor_yaml[name][:location]}"
112
+
113
+ old_filename = thor_yaml[name][:filename]
95
114
  self.options = self.options.merge("as" => name)
96
- filename = install(yaml[name][:location])
115
+ filename = install(thor_yaml[name][:location])
116
+
97
117
  unless filename == old_filename
98
118
  File.delete(File.join(thor_root, old_filename))
99
119
  end
100
120
  end
101
-
102
- desc "installed", "list the installed Thor modules and tasks (--internal means list the built-in tasks as well)"
121
+
122
+ desc "installed", "List the installed Thor modules and tasks"
103
123
  method_options :internal => :boolean
104
124
  def installed
105
- thor_root_glob.each do |f|
106
- next if f =~ /thor\.yml$/
107
- load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))
108
- end
125
+ initialize_thorfiles(nil, true)
126
+
127
+ klasses = Thor::Base.subclasses
128
+ klasses -= [Thor, Thor::Runner] unless options["internal"]
109
129
 
110
- klasses = Thor.subclasses
111
- klasses -= [Thor, Thor::Runner] unless options['internal']
112
130
  display_klasses(true, klasses)
113
131
  end
114
-
115
- desc "list [SEARCH]", "list the available thor tasks (--substring means SEARCH can be anywhere in the module)"
116
- method_options :substring => :boolean,
117
- :group => :optional,
118
- :all => :boolean
119
- def list(search = "")
132
+
133
+ desc "list [SEARCH]",
134
+ "List the available thor tasks (--substring means SEARCH anywhere in the namespace)"
135
+ method_options :substring => :boolean, :group => :string, :all => :boolean
136
+ def list(search="")
120
137
  initialize_thorfiles
138
+
121
139
  search = ".*#{search}" if options["substring"]
122
140
  search = /^#{search}.*/i
123
- group = options[:group] || 'standard'
124
-
125
- classes = Thor.subclasses.select do |k|
126
- (options[:all] || k.group_name == group) &&
127
- Thor::Util.constant_to_thor_path(k.name) =~ search
141
+ group = options[:group] || "standard"
142
+
143
+ klasses = Thor::Base.subclasses.select do |k|
144
+ (options[:all] || k.group == group) && k.namespace =~ search
128
145
  end
129
- display_klasses(false, classes)
130
- end
131
146
 
132
- # Override Thor#help so we can give info about not-yet-loaded tasks
133
- def help(task = nil)
134
- initialize_thorfiles(task) if task && task.include?(?:)
135
- super
147
+ display_klasses(false, klasses)
136
148
  end
137
-
138
- def method_missing(meth, *args)
139
- meth = meth.to_s
140
- super(meth.to_sym, *args) unless meth.include? ?:
141
149
 
142
- initialize_thorfiles(meth)
143
- task = Thor[meth]
144
- task.parse task.klass.new, ARGV[1..-1]
145
- end
150
+ private
146
151
 
147
- def self.thor_root
148
- File.join(ENV["HOME"] || ENV["APPDATA"], ".thor")
149
- end
152
+ def thor_root
153
+ Thor::Util.thor_root
154
+ end
150
155
 
151
- def self.thor_root_glob
152
- # On Windows thor_root will be something like this:
153
- #
154
- # C:\Documents and Settings\james\.thor
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 || {}
161
+ end
162
+ end
163
+
164
+ # Save the yaml file. If none exists in thor root, creates one.
155
165
  #
156
- # If we don't #gsub the \ character, Dir.glob will fail.
157
- Dir["#{thor_root.gsub(/\\/, '/')}/**/*"]
158
- end
159
-
160
- private
161
- def thor_root
162
- self.class.thor_root
163
- end
166
+ def save_yaml(yaml)
167
+ yaml_file = File.join(thor_root, "thor.yml")
164
168
 
165
- def thor_root_glob
166
- self.class.thor_root_glob
167
- end
168
-
169
- def thor_yaml
170
- yaml_file = File.join(thor_root, "thor.yml")
171
- yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file)
172
- yaml || {}
173
- end
174
-
175
- def save_yaml(yaml)
176
- yaml_file = File.join(thor_root, "thor.yml")
177
- File.open(yaml_file, "w") {|f| f.puts yaml.to_yaml }
178
- end
179
-
180
- def display_klasses(with_modules = false, klasses = Thor.subclasses)
181
- klasses -= [Thor, Thor::Runner] unless with_modules
182
- raise Error, "No Thor tasks available" if klasses.empty?
183
-
184
- if with_modules && !(yaml = thor_yaml).empty?
185
- max_name = yaml.max {|(xk,xv),(yk,yv)| xk.to_s.size <=> yk.to_s.size }.first.size
186
- modules_label = "Modules"
187
- namespaces_label = "Namespaces"
188
- column_width = [max_name + 4, modules_label.size + 1].max
189
-
190
- print "%-#{column_width}s" % modules_label
191
- puts namespaces_label
192
- print "%-#{column_width}s" % ("-" * modules_label.size)
193
- puts "-" * namespaces_label.size
194
-
195
- yaml.each do |name, info|
196
- print "%-#{column_width}s" % name
197
- puts info[:constants].map {|c| Thor::Util.constant_to_thor_path(c)}.join(", ")
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)
198
173
  end
199
-
200
- puts
201
- end
202
-
203
- # Calculate the largest base class name
204
- max_base = klasses.max do |x,y|
205
- Thor::Util.constant_to_thor_path(x.name).size <=> Thor::Util.constant_to_thor_path(y.name).size
206
- end.name.size
207
-
208
- # Calculate the size of the largest option description
209
- max_left_item = klasses.max do |x,y|
210
- (x.maxima.usage + x.maxima.opt).to_i <=> (y.maxima.usage + y.maxima.opt).to_i
211
- end
212
-
213
- max_left = max_left_item.maxima.usage + max_left_item.maxima.opt
214
-
215
- unless klasses.empty?
216
- puts # add some spacing
217
- klasses.each { |k| display_tasks(k, max_base, max_left); }
218
- else
219
- puts "\033[1;34mNo Thor tasks available\033[0m"
174
+
175
+ File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml }
220
176
  end
221
- end
222
-
223
- def display_tasks(klass, max_base, max_left)
224
- if klass.tasks.values.length > 1
225
-
226
- base = Thor::Util.constant_to_thor_path(klass.name)
227
-
228
- if base.to_a.empty?
229
- base = 'default'
230
- puts "\033[1;35m#{base}\033[0m"
231
- else
232
- 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))
233
188
  end
234
- puts "-" * base.length
235
-
236
- klass.tasks.each true do |name, task|
237
- format_string = "%-#{max_left + max_base + 5}s"
238
- print format_string % task.formatted_usage(true)
239
- 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
240
224
  end
241
-
242
- unless klass.opts.empty?
243
- 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
244
232
  end
245
-
246
- puts # add some spacing
247
233
  end
248
- end
249
234
 
250
- def initialize_thorfiles(relevant_to = nil)
251
- thorfiles(relevant_to).each {|f| load_thorfile f unless Thor.subclass_files.keys.include?(File.expand_path(f))}
252
- end
253
-
254
- def load_thorfile(path)
255
- begin
256
- load path
257
- rescue Object => e
258
- $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
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]}") }
259
247
  end
260
- end
261
-
262
- def thorfiles(relevant_to = nil)
263
- path = Dir.pwd
264
- thorfiles = []
265
-
266
- # Look for Thorfile or *.thor in the current directory or a parent directory, until the root
267
- while thorfiles.empty?
268
- thorfiles = Thor::Runner.globs_for(path).map {|g| Dir[g]}.flatten
269
- path = File.dirname(path)
270
- break if path == "/"
248
+
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.each { |k| display_tasks(k) }
273
+ else
274
+ say "\033[1;34mNo Thor tasks available\033[0m"
275
+ end
271
276
  end
272
277
 
273
- # We want to load system-wide Thorfiles first
274
- # so the local Thorfiles will override them.
275
- (relevant_to ? thorfiles_relevant_to(relevant_to) :
276
- thor_root_glob) + thorfiles - ["#{thor_root}/thor.yml"]
277
- end
278
+ # Display tasks from the given Thor class.
279
+ #
280
+ def display_tasks(klass)
281
+ unless klass.tasks.empty?
282
+ base = klass.namespace
278
283
 
279
- def thorfiles_relevant_to(meth)
280
- klass_str = Thor::Util.to_constant(meth.split(":")[0...-1].join(":"))
281
- thor_yaml.select do |k, v|
282
- v[:constants] && v[:constants].include?(klass_str)
283
- end.map { |k, v| File.join(thor_root, "#{v[:filename]}") }
284
- end
284
+ if base == "default"
285
+ say "\033[1;35m#{base}\033[0m"
286
+ else
287
+ say "\033[1;34m#{base}\033[0m"
288
+ end
289
+ say "-" * base.length
285
290
 
291
+ klass.help(shell, :short => true, :namespace => true)
292
+ say
293
+ end
294
+ end
286
295
  end