josevalim-thor 0.10.0

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.
@@ -0,0 +1,198 @@
1
+ require 'tempfile'
2
+
3
+ class Thor
4
+ module Shell
5
+ class Basic
6
+ attr_accessor :base
7
+
8
+ # Ask something to the user and receives a response.
9
+ #
10
+ # ==== Example
11
+ # ask("What is your name?")
12
+ #
13
+ def ask(statement, color=nil)
14
+ say("#{statement} ", color)
15
+ $stdin.gets.strip
16
+ end
17
+
18
+ # Say (print) something to the user. If the sentence ends with a whitespace
19
+ # or tab character, a new line is not appended (print + flush). Otherwise
20
+ # are passed straight to puts (behavior got from Highline).
21
+ #
22
+ # ==== Example
23
+ # say("I know you knew that.")
24
+ #
25
+ def say(statement="", color=nil)
26
+ statement = statement.to_s
27
+
28
+ if statement[-1, 1] == " " || statement[-1, 1] == "\t"
29
+ $stdout.print(statement)
30
+ $stdout.flush
31
+ else
32
+ $stdout.puts(statement)
33
+ end
34
+ end
35
+
36
+ # Say a status with the given color and appends the message.
37
+ # It does not show the status if the base is set to quiet.
38
+ #
39
+ def say_status(status, message, color=nil)
40
+ return if base && base.options[:quiet]
41
+
42
+ status_flag = "[#{status.to_s.upcase}]".rjust(12)
43
+ say "#{status_flag} #{message}"
44
+ end
45
+
46
+ # Make a question the to user and returns true if the user replies "y" or
47
+ # "yes".
48
+ #
49
+ def yes?(statement, color=nil)
50
+ ask(statement, color) =~ is?(:yes)
51
+ end
52
+
53
+ # Make a question the to user and returns true if the user replies "n" or
54
+ # "no".
55
+ #
56
+ def no?(statement, color=nil)
57
+ !yes?(statement, color)
58
+ end
59
+
60
+ # Prints a list of items.
61
+ #
62
+ # ==== Parameters
63
+ # list<Array[String, String, ...]>
64
+ # mode<Symbol>:: Can be :rows or :inline. Defaults to :rows.
65
+ #
66
+ def print_list(list, mode=:rows)
67
+ return if list.empty?
68
+
69
+ content = case mode
70
+ when :inline
71
+ last = list.pop
72
+ "#{list.join(", ")}, and #{last}"
73
+ else # rows
74
+ list.join("\n")
75
+ end
76
+
77
+ $stdout.puts content
78
+ end
79
+
80
+ # Prints a table.
81
+ #
82
+ # ==== Parameters
83
+ # Array[Array[String, String, ...]]
84
+ #
85
+ # ==== Options
86
+ # ident<Integer>:: Ident the first column by ident value.
87
+ # emphasize_last<Boolean>:: When true, add a different behavior to the last column.
88
+ #
89
+ def print_table(table, options={})
90
+ return if table.empty?
91
+
92
+ formats = []
93
+ 0.upto(table.first.length - 2) do |i|
94
+ maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size
95
+ formats << "%-#{maxima + 2}s"
96
+ end
97
+
98
+ formats[0] = formats[0].insert(0, " " * options[:ident]) if options[:ident]
99
+ formats << "%s"
100
+
101
+ if options[:emphasize_last]
102
+ table.each do |row|
103
+ next if row[-1].empty?
104
+ row[-1] = "# #{row[-1]}"
105
+ end
106
+ end
107
+
108
+ table.each do |row|
109
+ row.each_with_index do |column, i|
110
+ $stdout.print formats[i] % column.to_s
111
+ end
112
+ $stdout.puts
113
+ end
114
+ end
115
+
116
+ # Deals with file collision and returns true if the file should be
117
+ # overwriten and false otherwise. If a block is given, it uses the block
118
+ # response as the content for the diff.
119
+ #
120
+ # ==== Parameters
121
+ # destination<String>:: the destination file to solve conflicts
122
+ # block<Proc>:: an optional proc that returns the value to be used in diff
123
+ #
124
+ def file_collision(destination)
125
+ return true if @always_force
126
+
127
+ options = block_given? ? "[Ynaqdh]" : "[Ynaqh]"
128
+ answer = ask %[Overwrite #{destination}? (enter "h" for help) #{options}]
129
+
130
+ case answer
131
+ when is?(:yes), is?(:force)
132
+ true
133
+ when is?(:no), is?(:skip)
134
+ false
135
+ when is?(:always)
136
+ @always_force = true
137
+ when is?(:quit)
138
+ say 'Aborting...'
139
+ raise SystemExit
140
+ when is?(:diff)
141
+ show_diff(destination, yield) if block_given?
142
+ say 'Retrying...'
143
+ raise ScriptError
144
+ else
145
+ say file_collision_help
146
+ raise ScriptError
147
+ end
148
+ rescue ScriptError
149
+ retry
150
+ end
151
+
152
+ # Called if something goes wrong during the execution. This is used by Thor
153
+ # internally and should not be used inside your scripts. If someone went
154
+ # wrong, you can always raise an exception. If you raise a Thor::Error, it
155
+ # will be rescued and wrapped in the method below.
156
+ #
157
+ def error(statement) #:nodoc:
158
+ $stderr.puts statement
159
+ end
160
+
161
+ protected
162
+
163
+ def is?(value)
164
+ value = value.to_s
165
+
166
+ if value.size == 1
167
+ /\A#{value}\z/i
168
+ else
169
+ /\A(#{value}|#{value[0,1]})\z/i
170
+ end
171
+ end
172
+
173
+ def file_collision_help
174
+ <<HELP
175
+ Y - yes, overwrite
176
+ n - no, do not overwrite
177
+ a - all, overwrite this and all others
178
+ q - quit, abort
179
+ d - diff, show the differences between the old and the new
180
+ h - help, show this help
181
+ HELP
182
+ end
183
+
184
+ def show_diff(destination, content)
185
+ Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp|
186
+ temp.write content
187
+ temp.rewind
188
+ say `#{diff_cmd} "#{destination}" "#{temp.path}"`
189
+ end
190
+ end
191
+
192
+ def diff_cmd
193
+ ENV['THOR_DIFF'] || ENV['RAILS_DIFF'] || 'diff -u'
194
+ end
195
+
196
+ end
197
+ end
198
+ end
data/lib/thor/task.rb ADDED
@@ -0,0 +1,85 @@
1
+ class Thor
2
+ class Task < Struct.new(:name, :description, :usage, :options)
3
+
4
+ # Creates a dynamic task. Dynamic tasks are created on demand to allow method
5
+ # missing calls (since a method missing does not have a task object for it).
6
+ #
7
+ def self.dynamic(name)
8
+ new(name, "A dynamically-generated task", name.to_s, nil)
9
+ end
10
+
11
+ def initialize(name, description, usage, options)
12
+ super(name, description, usage, options || {})
13
+ end
14
+
15
+ # Dup the options hash on clone.
16
+ #
17
+ def initialize_copy(other)
18
+ super(other)
19
+ self.options = other.options.dup if other.options
20
+ end
21
+
22
+ # By default, a task invokes a method in the thor class. You can change this
23
+ # implementation to create custom tasks.
24
+ #
25
+ def run(instance, *args)
26
+ raise UndefinedTaskError, "the '#{name}' task of #{instance.class} is private" unless public_method?(instance)
27
+ instance.send(name, *args)
28
+ rescue ArgumentError => e
29
+ backtrace = sans_backtrace(e.backtrace, caller)
30
+
31
+ if instance.is_a?(Thor) && backtrace.empty?
32
+ raise InvocationError, "'#{name}' was called incorrectly. Call as '#{formatted_usage(instance.class)}'"
33
+ else
34
+ raise e
35
+ end
36
+ rescue NoMethodError => e
37
+ if e.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
38
+ raise UndefinedTaskError, "The #{instance.class.namespace} namespace doesn't have a '#{name}' task"
39
+ else
40
+ raise e
41
+ end
42
+ end
43
+
44
+ # Returns the first line of the given description.
45
+ #
46
+ def short_description
47
+ description.split("\n").first if description
48
+ end
49
+
50
+ # Returns the formatted usage. If a klass is given, the klass default options
51
+ # are merged with the task options providinf a full description.
52
+ #
53
+ def formatted_usage(klass=nil)
54
+ formatted = ''
55
+ formatted << "#{klass.namespace.gsub(/^default/,'')}:" if klass
56
+ formatted << usage.to_s
57
+ formatted << " #{formatted_options}"
58
+ formatted.strip!
59
+ formatted
60
+ end
61
+
62
+ # Returns the options usage for this task.
63
+ #
64
+ def formatted_options
65
+ @formatted_options ||= options.values.sort.map{ |o| o.usage }.join(" ")
66
+ end
67
+
68
+ protected
69
+
70
+ # Given a target, checks if this class name is not a private/protected method.
71
+ #
72
+ def public_method?(instance)
73
+ !(instance.private_methods + instance.protected_methods).include?(name.to_s)
74
+ end
75
+
76
+ # Clean everything that comes from the Thor gempath and remove the caller.
77
+ #
78
+ def sans_backtrace(backtrace, caller)
79
+ dirname = /^#{Regexp.escape(File.dirname(__FILE__))}/
80
+ saned = backtrace.reject { |frame| frame =~ dirname }
81
+ saned -= caller
82
+ end
83
+
84
+ end
85
+ end
data/lib/thor/tasks.rb ADDED
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), "tasks", "*.rb")].each do |task|
2
+ require task
3
+ end
@@ -0,0 +1,35 @@
1
+ class Thor
2
+ # Creates an install task.
3
+ #
4
+ # ==== Parameters
5
+ # spec<Gem::Specification>
6
+ #
7
+ # ==== Options
8
+ # :dir - The directory where the package is hold before installation. Defaults to ./pkg.
9
+ #
10
+ def self.install_task(spec, options={})
11
+ package_task(spec, options)
12
+ tasks['install'] = Thor::InstallTask.new(spec, options)
13
+ end
14
+
15
+ class InstallTask < Task
16
+ attr_accessor :spec, :config
17
+
18
+ def initialize(gemspec, config={})
19
+ super(:install, "Install the gem", "install", {})
20
+ @spec = gemspec
21
+ @config = { :dir => File.join(Dir.pwd, "pkg") }.merge(config)
22
+ end
23
+
24
+ def run(instance, args=[])
25
+ null, sudo, gem = RUBY_PLATFORM =~ /mswin|mingw/ ? ['NUL', '', 'gem.bat'] :
26
+ ['/dev/null', 'sudo', 'gem']
27
+
28
+ old_stderr, $stderr = $stderr.dup, File.open(null, "w")
29
+ instance.invoke(:package)
30
+ $stderr = old_stderr
31
+
32
+ system %{#{sudo} #{Gem.ruby} -S #{gem} install #{config[:dir]}/#{spec.name}-#{spec.version} --no-rdoc --no-ri --no-update-sources}
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ require "fileutils"
2
+
3
+ class Thor
4
+ # Creates a package task.
5
+ #
6
+ # ==== Parameters
7
+ # spec<Gem::Specification>
8
+ #
9
+ # ==== Options
10
+ # :dir - The package directory. Defaults to ./pkg.
11
+ #
12
+ def self.package_task(spec, options={})
13
+ tasks['package'] = Thor::PackageTask.new(spec, options)
14
+ end
15
+
16
+ class PackageTask < Task
17
+ attr_accessor :spec, :config
18
+
19
+ def initialize(gemspec, config={})
20
+ super(:package, "Build a gem package", "package", {})
21
+ @spec = gemspec
22
+ @config = {:dir => File.join(Dir.pwd, "pkg")}.merge(config)
23
+ end
24
+
25
+ def run(instance, args=[])
26
+ FileUtils.mkdir_p(config[:dir])
27
+ Gem::Builder.new(spec).build
28
+ FileUtils.mv(spec.file_name, File.join(config[:dir], spec.file_name))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ require "fileutils"
2
+
3
+ class Thor
4
+ # Creates a spec task.
5
+ #
6
+ # ==== Parameters
7
+ # files<Array> - Array of files to spec
8
+ #
9
+ # ==== Options
10
+ # :name - The name of the task. It can be rcov or spec. Spec is the default.
11
+ # :rcov - A hash with rcov specific options.
12
+ # :rcov_dir - Where rcov reports should be printed.
13
+ # :verbose - Sets the default value for verbose, although it can be specified
14
+ # also through the command line.
15
+ #
16
+ # All other options are added to rspec.
17
+ #
18
+ def self.spec_task(files, options={})
19
+ name = (options.delete(:name) || 'spec').to_s
20
+ tasks[name] = Thor::SpecTask.new(name, files, options)
21
+ end
22
+
23
+ class SpecTask < Task
24
+ attr_accessor :name, :files, :rcov_dir, :rcov_config, :spec_config
25
+
26
+ def initialize(name, files, config={})
27
+ options = { :verbose => Thor::Option.parse(:verbose, config.delete(:verbose) || false) }
28
+ super(name, "#{name.capitalize} task", name, options)
29
+
30
+ @name = name
31
+ @files = files.map{ |f| %["#{f}"] }.join(" ")
32
+ @rcov_dir = config.delete(:rdoc_dir) || File.join(Dir.pwd, 'coverage')
33
+ @rcov_config = config.delete(:rcov) || {}
34
+ @spec_config = { :format => 'specdoc', :color => true }.merge(config)
35
+ end
36
+
37
+ def run(instance, args=[])
38
+ rcov_opts = Thor::Options.to_switches(rcov_config)
39
+ spec_opts = Thor::Options.to_switches(spec_config)
40
+
41
+ require 'rbconfig'
42
+ cmd = RbConfig::CONFIG['ruby_install_name'] << " "
43
+
44
+ if rcov?
45
+ FileUtils.rm_rf(rcov_dir)
46
+ cmd << "-S #{where('rcov')} -o #{rcov_dir} #{rcov_opts} "
47
+ end
48
+
49
+ cmd << [where('spec'), rcov? ? " -- " : nil, files, spec_opts].join(" ")
50
+
51
+ puts cmd if instance.options.verbose?
52
+ system(cmd)
53
+ exit($?.exitstatus)
54
+ end
55
+
56
+ private
57
+
58
+ def rcov?
59
+ name == "rcov"
60
+ end
61
+
62
+ def where(file)
63
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
64
+ file_with_path = File.join(path, file)
65
+ next unless File.exist?(file_with_path) && File.executable?(file_with_path)
66
+ return File.expand_path(file_with_path)
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/thor/util.rb ADDED
@@ -0,0 +1,209 @@
1
+ class Thor
2
+ module Sandbox; end
3
+
4
+ # This module holds several utilities:
5
+ #
6
+ # 1) Methods to convert thor namespaces to constants and vice-versa.
7
+ #
8
+ # Thor::Utils.constant_to_namespace(Foo::Bar::Baz) #=> "foo:bar:baz"
9
+ # Thor::Utils.namespace_to_constant("foo:bar:baz") #=> Foo::Bar::Baz
10
+ #
11
+ # 2) Loading thor files and sandboxing:
12
+ #
13
+ # Thor::Utils.load_thorfile("~/.thor/foo")
14
+ #
15
+ module Util
16
+
17
+ # Receives a namespace and search for it in the Thor::Base subclasses.
18
+ #
19
+ # ==== Parameters
20
+ # namespace<String>:: The namespace to search for.
21
+ #
22
+ def self.find_by_namespace(namespace)
23
+ namespace = 'default' if namespace.empty?
24
+
25
+ Thor::Base.subclasses.find do |klass|
26
+ klass.namespace == namespace
27
+ end
28
+ end
29
+
30
+ # Receives a constant and converts it to a Thor namespace. Since Thor tasks
31
+ # can be added to a sandbox, this method is also responsable for removing
32
+ # the sandbox namespace.
33
+ #
34
+ # This method should not be used in general because it's used to deal with
35
+ # older versions of Thor. On current versions, if you need to get the
36
+ # namespace from a class, just call namespace on it.
37
+ #
38
+ # TODO Deprecate this method in the future.
39
+ #
40
+ # ==== Parameters
41
+ # constant<Object>:: The constant to be converted to the thor path.
42
+ #
43
+ # ==== Returns
44
+ # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz"
45
+ #
46
+ def self.constant_to_namespace(constant, remove_default=true)
47
+ constant = constant.to_s.gsub(/^Thor::Sandbox::/, "")
48
+ constant = snake_case(constant).squeeze(":")
49
+ constant.gsub!(/^default/, '') if remove_default
50
+ constant
51
+ end
52
+
53
+ # Given the contents, evaluate it inside the sandbox and returns the thor
54
+ # classes defined in the sandbox.
55
+ #
56
+ # ==== Parameters
57
+ # contents<String>
58
+ #
59
+ # ==== Returns
60
+ # Array[Object]
61
+ #
62
+ def self.namespaces_in_contents(contents, file=__FILE__)
63
+ old_constants = Thor::Base.subclasses.dup
64
+ Thor::Base.subclasses.clear
65
+
66
+ load_thorfile(file, contents)
67
+
68
+ new_constants = Thor::Base.subclasses.dup
69
+ Thor::Base.subclasses.replace(old_constants)
70
+
71
+ new_constants.map!{ |c| c.namespace }
72
+ new_constants.compact!
73
+ new_constants
74
+ end
75
+
76
+ # Receives a string and convert it to snake case. SnakeCase returns snake_case.
77
+ #
78
+ # ==== Parameters
79
+ # String
80
+ #
81
+ # ==== Returns
82
+ # String
83
+ #
84
+ def self.snake_case(str)
85
+ return str.downcase if str =~ /^[A-Z_]+$/
86
+ str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
87
+ return $+.downcase
88
+ end
89
+
90
+ # Receives a namespace and tries to retrieve a Thor or Thor::Group class
91
+ # from it. It first searches for a class using the all the given namespace,
92
+ # if it's not found, removes the highest entry and searches for the class
93
+ # again. If found, returns the highest entry as the class name.
94
+ #
95
+ # ==== Examples
96
+ #
97
+ # class Foo::Bar < Thor
98
+ # def baz
99
+ # end
100
+ # end
101
+ #
102
+ # class Baz::Foo < Thor::Group
103
+ # end
104
+ #
105
+ # Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task
106
+ # Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil
107
+ # Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz"
108
+ #
109
+ # ==== Parameters
110
+ # namespace<String>
111
+ #
112
+ # ==== Errors
113
+ # Thor::Error:: raised if the namespace cannot be found.
114
+ #
115
+ # Thor::Error:: raised if the namespace evals to a class which does not
116
+ # inherit from Thor or Thor::Group.
117
+ #
118
+ def self.namespace_to_thor_class(namespace)
119
+ klass, task_name = Thor::Util.find_by_namespace(namespace), nil
120
+
121
+ if klass.nil? && namespace.include?(?:)
122
+ namespace = namespace.split(":")
123
+ task_name = namespace.pop
124
+ klass = Thor::Util.find_by_namespace(namespace.join(":"))
125
+ end
126
+
127
+ raise Error, "could not find Thor class or task '#{namespace}'" unless klass
128
+
129
+ return klass, task_name
130
+ end
131
+
132
+ # Receives a path and load the thor file in the path. The file is evaluated
133
+ # inside the sandbox to avoid namespacing conflicts.
134
+ #
135
+ def self.load_thorfile(path, content=nil)
136
+ content ||= File.read(path)
137
+
138
+ begin
139
+ Thor::Sandbox.class_eval(content, path)
140
+ rescue Exception => e
141
+ $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
142
+ end
143
+ end
144
+
145
+ # Receives a yaml (hash) and updates all constants entries to namespace.
146
+ # This was added to deal with deprecated versions of Thor.
147
+ #
148
+ # TODO Deprecate this method in the future.
149
+ #
150
+ # ==== Returns
151
+ # TrueClass|FalseClass:: Returns true if any change to the yaml file was made.
152
+ #
153
+ def self.convert_constants_to_namespaces(yaml)
154
+ yaml_changed = false
155
+
156
+ yaml.each do |k, v|
157
+ next unless v[:constants] && v[:namespaces].nil?
158
+ yaml_changed = true
159
+ yaml[k][:namespaces] = v[:constants].map{|c| Thor::Util.constant_to_namespace(c)}
160
+ end
161
+
162
+ yaml_changed
163
+ end
164
+
165
+ # Returns the root where thor files are located, dependending on the OS.
166
+ #
167
+ def self.thor_root
168
+ return File.join(ENV["HOME"], '.thor') if ENV["HOME"]
169
+
170
+ if ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
171
+ return File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"], '.thor')
172
+ end
173
+
174
+ return File.join(ENV["APPDATA"], '.thor') if ENV["APPDATA"]
175
+
176
+ begin
177
+ File.expand_path("~")
178
+ rescue
179
+ if File::ALT_SEPARATOR
180
+ "C:/"
181
+ else
182
+ "/"
183
+ end
184
+ end
185
+ end
186
+
187
+ # Returns the files in the thor root. On Windows thor_root will be something
188
+ # like this:
189
+ #
190
+ # C:\Documents and Settings\james\.thor
191
+ #
192
+ # If we don't #gsub the \ character, Dir.glob will fail.
193
+ #
194
+ def self.thor_root_glob
195
+ files = Dir["#{thor_root.gsub(/\\/, '/')}/*"]
196
+
197
+ files.map! do |file|
198
+ File.directory?(file) ? File.join(file, "main.thor") : file
199
+ end
200
+ end
201
+
202
+ # Where to look for Thor files.
203
+ #
204
+ def self.globs_for(path)
205
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
206
+ end
207
+
208
+ end
209
+ end