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,133 @@
1
+ class Thor
2
+ module Actions
3
+
4
+ # Creates an empty directory.
5
+ #
6
+ # ==== Parameters
7
+ # destination<String>:: the relative path to the destination root.
8
+ # config<Hash>:: give :verbose => false to not log the status.
9
+ #
10
+ # ==== Examples
11
+ #
12
+ # empty_directory "doc"
13
+ #
14
+ def empty_directory(destination, config={})
15
+ action EmptyDirectory.new(self, destination, config)
16
+ end
17
+
18
+ # Class which holds create directory logic. This is the base class for
19
+ # other actions like create_file and directory.
20
+ #
21
+ # This implementation is based in Templater actions, created by Jonas Nicklas
22
+ # and Michael S. Klishin under MIT LICENSE.
23
+ #
24
+ class EmptyDirectory #:nodoc:
25
+ attr_reader :base, :destination, :given_destination, :relative_destination, :config
26
+
27
+ # Initializes given the source and destination.
28
+ #
29
+ # ==== Parameters
30
+ # base<Thor::Base>:: A Thor::Base instance
31
+ # source<String>:: Relative path to the source of this file
32
+ # destination<String>:: Relative path to the destination of this file
33
+ # config<Hash>:: give :verbose => false to not log the status.
34
+ #
35
+ def initialize(base, destination, config={})
36
+ @base, @config = base, { :verbose => true }.merge(config)
37
+ self.destination = destination
38
+ end
39
+
40
+ # Checks if the destination file already exists.
41
+ #
42
+ # ==== Returns
43
+ # Boolean:: true if the file exists, false otherwise.
44
+ #
45
+ def exists?
46
+ ::File.exists?(destination)
47
+ end
48
+
49
+ def invoke!
50
+ invoke_with_conflict_check do
51
+ ::FileUtils.mkdir_p(destination)
52
+ end
53
+ end
54
+
55
+ def revoke!
56
+ say_status :remove, :red
57
+ ::FileUtils.rm_rf(destination) if !pretend? && exists?
58
+ end
59
+
60
+ protected
61
+
62
+ # Shortcut for pretend.
63
+ #
64
+ def pretend?
65
+ base.options[:pretend]
66
+ end
67
+
68
+ # Sets the absolute destination value from a relative destination value.
69
+ # It also stores the given and relative destination. Let's suppose our
70
+ # script is being executed on "dest", it sets the destination root to
71
+ # "dest". The destination, given_destination and relative_destination
72
+ # are related in the following way:
73
+ #
74
+ # inside "bar" do
75
+ # empty_directory "baz"
76
+ # end
77
+ #
78
+ # destination #=> dest/bar/baz
79
+ # relative_destination #=> bar/baz
80
+ # given_destination #=> baz
81
+ #
82
+ def destination=(destination)
83
+ if destination
84
+ @given_destination = convert_encoded_instructions(destination.to_s)
85
+ @destination = ::File.expand_path(@given_destination, base.destination_root)
86
+ @relative_destination = base.relative_to_original_destination_root(@destination)
87
+ end
88
+ end
89
+
90
+ # Filenames in the encoded form are converted. If you have a file:
91
+ #
92
+ # %class_name%.rb
93
+ #
94
+ # It gets the class name from the base and replace it:
95
+ #
96
+ # user.rb
97
+ #
98
+ def convert_encoded_instructions(filename)
99
+ filename.gsub(/%(.*?)%/) do |string|
100
+ instruction = $1.strip
101
+ base.respond_to?(instruction) ? base.send(instruction) : string
102
+ end
103
+ end
104
+
105
+ # Receives a hash of options and just execute the block if some
106
+ # conditions are met.
107
+ #
108
+ def invoke_with_conflict_check(&block)
109
+ if exists?
110
+ on_conflict_behavior(&block)
111
+ else
112
+ say_status :create, :green
113
+ block.call unless pretend?
114
+ end
115
+
116
+ destination
117
+ end
118
+
119
+ # What to do when the destination file already exists.
120
+ #
121
+ def on_conflict_behavior(&block)
122
+ say_status :exist, :blue
123
+ end
124
+
125
+ # Shortcut to say_status shell method.
126
+ #
127
+ def say_status(status, color)
128
+ base.shell.say_status status, relative_destination, color if config[:verbose]
129
+ end
130
+
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,195 @@
1
+ require 'erb'
2
+ require 'open-uri'
3
+
4
+ class Thor
5
+ module Actions
6
+
7
+ # Copies the file from the relative source to the relative destination. If
8
+ # the destination is not given it's assumed to be equal to the source.
9
+ #
10
+ # ==== Parameters
11
+ # source<String>:: the relative path to the source root.
12
+ # destination<String>:: the relative path to the destination root.
13
+ # config<Hash>:: give :verbose => false to not log the status.
14
+ #
15
+ # ==== Examples
16
+ #
17
+ # copy_file "README", "doc/README"
18
+ #
19
+ # copy_file "doc/README"
20
+ #
21
+ def copy_file(source, destination=nil, config={})
22
+ destination ||= source
23
+ source = File.expand_path(find_in_source_paths(source.to_s))
24
+
25
+ create_file destination, nil, config do
26
+ File.read(source)
27
+ end
28
+ end
29
+
30
+ # Gets the content at the given address and places it at the given relative
31
+ # destination. If a block is given instead of destination, the content of
32
+ # the url is yielded and used as location.
33
+ #
34
+ # ==== Parameters
35
+ # source<String>:: the address of the given content.
36
+ # destination<String>:: the relative path to the destination root.
37
+ # config<Hash>:: give :verbose => false to not log the status.
38
+ #
39
+ # ==== Examples
40
+ #
41
+ # get "http://gist.github.com/103208", "doc/README"
42
+ #
43
+ # get "http://gist.github.com/103208" do |content|
44
+ # content.split("\n").first
45
+ # end
46
+ #
47
+ def get(source, destination=nil, config={}, &block)
48
+ source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ /^http\:\/\//
49
+ render = open(source).read
50
+
51
+ destination ||= if block_given?
52
+ block.arity == 1 ? block.call(render) : block.call
53
+ else
54
+ File.basename(source)
55
+ end
56
+
57
+ create_file destination, render, config
58
+ end
59
+
60
+ # Gets an ERB template at the relative source, executes it and makes a copy
61
+ # at the relative destination. If the destination is not given it's assumed
62
+ # to be equal to the source removing .tt from the filename.
63
+ #
64
+ # ==== Parameters
65
+ # source<String>:: the relative path to the source root.
66
+ # destination<String>:: the relative path to the destination root.
67
+ # config<Hash>:: give :verbose => false to not log the status.
68
+ #
69
+ # ==== Examples
70
+ #
71
+ # template "README", "doc/README"
72
+ #
73
+ # template "doc/README"
74
+ #
75
+ def template(source, destination=nil, config={})
76
+ destination ||= source
77
+ source = File.expand_path(find_in_source_paths(source.to_s))
78
+ context = instance_eval('binding')
79
+
80
+ create_file destination, nil, config do
81
+ ERB.new(::File.read(source), nil, '-').result(context)
82
+ end
83
+ end
84
+
85
+ # Changes the mode of the given file or directory.
86
+ #
87
+ # ==== Parameters
88
+ # mode<Integer>:: the file mode
89
+ # path<String>:: the name of the file to change mode
90
+ # config<Hash>:: give :verbose => false to not log the status.
91
+ #
92
+ # ==== Example
93
+ #
94
+ # chmod "script/*", 0755
95
+ #
96
+ def chmod(path, mode, config={})
97
+ return unless behavior == :invoke
98
+ path = File.expand_path(path, destination_root)
99
+ say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
100
+ FileUtils.chmod_R(mode, path) unless options[:pretend]
101
+ end
102
+
103
+ # Prepend text to a file.
104
+ #
105
+ # ==== Parameters
106
+ # path<String>:: path of the file to be changed
107
+ # data<String>:: the data to prepend to the file, can be also given as a block.
108
+ # config<Hash>:: give :verbose => false to not log the status.
109
+ #
110
+ # ==== Example
111
+ #
112
+ # prepend_file 'config/environments/test.rb', 'config.gem "rspec"'
113
+ #
114
+ def prepend_file(path, data=nil, config={}, &block)
115
+ return unless behavior == :invoke
116
+ path = File.expand_path(path, destination_root)
117
+ say_status :prepend, relative_to_original_destination_root(path), config.fetch(:verbose, true)
118
+
119
+ unless options[:pretend]
120
+ content = data || block.call
121
+ content << File.read(path)
122
+ File.open(path, 'wb') { |file| file.write(content) }
123
+ end
124
+ end
125
+
126
+ # Append text to a file.
127
+ #
128
+ # ==== Parameters
129
+ # path<String>:: path of the file to be changed
130
+ # data<String>:: the data to append to the file, can be also given as a block.
131
+ # config<Hash>:: give :verbose => false to not log the status.
132
+ #
133
+ # ==== Example
134
+ #
135
+ # append_file 'config/environments/test.rb', 'config.gem "rspec"'
136
+ #
137
+ def append_file(path, data=nil, config={}, &block)
138
+ return unless behavior == :invoke
139
+ path = File.expand_path(path, destination_root)
140
+ say_status :append, relative_to_original_destination_root(path), config.fetch(:verbose, true)
141
+ File.open(path, 'ab') { |file| file.write(data || block.call) } unless options[:pretend]
142
+ end
143
+
144
+ # Run a regular expression replacement on a file.
145
+ #
146
+ # ==== Parameters
147
+ # path<String>:: path of the file to be changed
148
+ # flag<Regexp|String>:: the regexp or string to be replaced
149
+ # replacement<String>:: the replacement, can be also given as a block
150
+ # config<Hash>:: give :verbose => false to not log the status.
151
+ #
152
+ # ==== Example
153
+ #
154
+ # gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'
155
+ #
156
+ # gsub_file 'README', /rake/, :green do |match|
157
+ # match << " no more. Use thor!"
158
+ # end
159
+ #
160
+ def gsub_file(path, flag, *args, &block)
161
+ return unless behavior == :invoke
162
+ config = args.last.is_a?(Hash) ? args.pop : {}
163
+
164
+ path = File.expand_path(path, destination_root)
165
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
166
+
167
+ unless options[:pretend]
168
+ content = File.read(path)
169
+ content.gsub!(flag, *args, &block)
170
+ File.open(path, 'wb') { |file| file.write(content) }
171
+ end
172
+ end
173
+
174
+ # Removes a file at the given location.
175
+ #
176
+ # ==== Parameters
177
+ # path<String>:: path of the file to be changed
178
+ # config<Hash>:: give :verbose => false to not log the status.
179
+ #
180
+ # ==== Example
181
+ #
182
+ # remove_file 'README'
183
+ # remove_file 'app/controllers/application_controller.rb'
184
+ #
185
+ def remove_file(path, config={})
186
+ return unless behavior == :invoke
187
+ path = File.expand_path(path, destination_root)
188
+
189
+ say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
190
+ ::FileUtils.rm_rf(path) if !options[:pretend] && File.exists?(path)
191
+ end
192
+ alias :remove_dir :remove_file
193
+
194
+ end
195
+ end
@@ -0,0 +1,78 @@
1
+ require 'thor/actions/empty_directory'
2
+
3
+ class Thor
4
+ module Actions
5
+
6
+ # Injects the given content into a file. Different from append_file,
7
+ # prepend_file and gsub_file, this method is reversible. By this reason,
8
+ # the flag can only be strings. gsub_file is your friend if you need to
9
+ # deal with more complex cases.
10
+ #
11
+ # ==== Parameters
12
+ # destination<String>:: Relative path to the destination root
13
+ # data<String>:: Data to add to the file. Can be given as a block.
14
+ # config<Hash>:: give :verbose => false to not log the status and the flag
15
+ # for injection (:after or :before).
16
+ #
17
+ # ==== Examples
18
+ #
19
+ # inject_into_file "config/environment.rb", "config.gem thor", :after => "Rails::Initializer.run do |config|\n"
20
+ #
21
+ # inject_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
22
+ # gems = ask "Which gems would you like to add?"
23
+ # gems.split(" ").map{ |gem| " config.gem #{gem}" }.join("\n")
24
+ # end
25
+ #
26
+ def inject_into_file(destination, *args, &block)
27
+ if block_given?
28
+ data, config = block, args.shift
29
+ else
30
+ data, config = args.shift, args.shift
31
+ end
32
+
33
+ log_status = args.empty? || args.pop
34
+ action InjectIntoFile.new(self, destination, data, config)
35
+ end
36
+
37
+ class InjectIntoFile < EmptyDirectory #:nodoc:
38
+ attr_reader :flag, :replacement
39
+
40
+ def initialize(base, destination, data, config)
41
+ super(base, destination, { :verbose => true }.merge(config))
42
+
43
+ data = data.call if data.is_a?(Proc)
44
+
45
+ @replacement = if @config.key?(:after)
46
+ @flag = @config.delete(:after)
47
+ @flag + data
48
+ else
49
+ @flag = @config.delete(:before)
50
+ data + @flag
51
+ end
52
+ end
53
+
54
+ def invoke!
55
+ say_status :inject, config[:verbose]
56
+ replace!(flag, replacement)
57
+ end
58
+
59
+ def revoke!
60
+ say_status :deinject, config[:verbose]
61
+ replace!(replacement, flag)
62
+ end
63
+
64
+ protected
65
+
66
+ # Adds the content to the file.
67
+ #
68
+ def replace!(regexp, string)
69
+ unless base.options[:pretend]
70
+ content = File.read(destination)
71
+ content.gsub!(regexp, string)
72
+ File.open(destination, 'wb') { |file| file.write(content) }
73
+ end
74
+ end
75
+
76
+ end
77
+ end
78
+ end
data/lib/thor/base.rb ADDED
@@ -0,0 +1,510 @@
1
+ require 'thor/core_ext/hash_with_indifferent_access'
2
+ require 'thor/core_ext/ordered_hash'
3
+ require 'thor/error'
4
+ require 'thor/shell'
5
+ require 'thor/invocation'
6
+ require 'thor/parser'
7
+ require 'thor/task'
8
+ require 'thor/util'
9
+
10
+ class Thor
11
+ # Shortcuts for help.
12
+ HELP_MAPPINGS = %w(-h -? --help -D)
13
+
14
+ # Thor methods that should not be overwritten by the user.
15
+ THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root
16
+ action add_file create_file in_root inside run run_ruby_script)
17
+
18
+ module Base
19
+ attr_accessor :options
20
+
21
+ # It receives arguments in an Array and two hashes, one for options and
22
+ # other for configuration.
23
+ #
24
+ # Notice that it does not check if all required arguments were supplied.
25
+ # It should be done by the parser.
26
+ #
27
+ # ==== Parameters
28
+ # args<Array[Object]>:: An array of objects. The objects are applied to their
29
+ # respective accessors declared with <tt>argument</tt>.
30
+ #
31
+ # options<Hash>:: An options hash that will be available as self.options.
32
+ # The hash given is converted to a hash with indifferent
33
+ # access, magic predicates (options.skip?) and then frozen.
34
+ #
35
+ # config<Hash>:: Configuration for this Thor class.
36
+ #
37
+ def initialize(args=[], options={}, config={})
38
+ Thor::Arguments.parse(self.class.arguments, args).each do |key, value|
39
+ send("#{key}=", value)
40
+ end
41
+
42
+ parse_options = self.class.class_options
43
+
44
+ if options.is_a?(Array)
45
+ task_options = config.delete(:task_options) # hook for start
46
+ parse_options = parse_options.merge(task_options) if task_options
47
+ array_options, hash_options = options, {}
48
+ else
49
+ array_options, hash_options = [], options
50
+ end
51
+
52
+ options = Thor::Options.parse(parse_options, array_options)
53
+ self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).merge!(hash_options)
54
+ self.options.freeze
55
+ end
56
+
57
+ class << self
58
+ def included(base) #:nodoc:
59
+ base.send :extend, ClassMethods
60
+ base.send :include, Invocation
61
+ base.send :include, Shell
62
+ end
63
+
64
+ # Returns the classes that inherits from Thor or Thor::Group.
65
+ #
66
+ # ==== Returns
67
+ # Array[Class]
68
+ #
69
+ def subclasses
70
+ @subclasses ||= []
71
+ end
72
+
73
+ # Returns the files where the subclasses are kept.
74
+ #
75
+ # ==== Returns
76
+ # Hash[path<String> => Class]
77
+ #
78
+ def subclass_files
79
+ @subclass_files ||= Hash.new{ |h,k| h[k] = [] }
80
+ end
81
+
82
+ # Whenever a class inherits from Thor or Thor::Group, we should track the
83
+ # class and the file on Thor::Base. This is the method responsable for it.
84
+ #
85
+ def register_klass_file(klass) #:nodoc:
86
+ file = caller[1].match(/(.*):\d+/)[1]
87
+ Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass)
88
+
89
+ file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
90
+ file_subclasses << klass unless file_subclasses.include?(klass)
91
+ end
92
+ end
93
+
94
+ module ClassMethods
95
+ # Adds an argument to the class and creates an attr_accessor for it.
96
+ #
97
+ # Arguments are different from options in several aspects. The first one
98
+ # is how they are parsed from the command line, arguments are retrieved
99
+ # from position:
100
+ #
101
+ # thor task NAME
102
+ #
103
+ # Instead of:
104
+ #
105
+ # thor task --name=NAME
106
+ #
107
+ # Besides, arguments are used inside your code as an accessor (self.argument),
108
+ # while options are all kept in a hash (self.options).
109
+ #
110
+ # Finally, arguments cannot have type :default or :boolean but can be
111
+ # optional (supplying :optional => :true or :required => false), although
112
+ # you cannot have a required argument after a non-required argument. If you
113
+ # try it, an error is raised.
114
+ #
115
+ # ==== Parameters
116
+ # name<Symbol>:: The name of the argument.
117
+ # options<Hash>:: Described below.
118
+ #
119
+ # ==== Options
120
+ # :desc - Description for the argument.
121
+ # :required - If the argument is required or not.
122
+ # :optional - If the argument is optional or not.
123
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric.
124
+ # :default - Default value for this argument. It cannot be required and have default values.
125
+ # :banner - String to show on usage notes.
126
+ #
127
+ # ==== Errors
128
+ # ArgumentError:: Raised if you supply a required argument after a non required one.
129
+ #
130
+ def argument(name, options={})
131
+ is_thor_reserved_word?(name, :argument)
132
+ no_tasks { attr_accessor name }
133
+
134
+ required = if options.key?(:optional)
135
+ !options[:optional]
136
+ elsif options.key?(:required)
137
+ options[:required]
138
+ else
139
+ options[:default].nil?
140
+ end
141
+
142
+ remove_argument name
143
+
144
+ arguments.each do |argument|
145
+ next if argument.required?
146
+ raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " <<
147
+ "the non-required argument #{argument.human_name.inspect}."
148
+ end if required
149
+
150
+ arguments << Thor::Argument.new(name, options[:desc], required, options[:type],
151
+ options[:default], options[:banner])
152
+ end
153
+
154
+ # Returns this class arguments, looking up in the ancestors chain.
155
+ #
156
+ # ==== Returns
157
+ # Array[Thor::Argument]
158
+ #
159
+ def arguments
160
+ @arguments ||= from_superclass(:arguments, [])
161
+ end
162
+
163
+ # Adds a bunch of options to the set of class options.
164
+ #
165
+ # class_options :foo => false, :bar => :required, :baz => :string
166
+ #
167
+ # If you prefer more detailed declaration, check class_option.
168
+ #
169
+ # ==== Parameters
170
+ # Hash[Symbol => Object]
171
+ #
172
+ def class_options(options=nil)
173
+ @class_options ||= from_superclass(:class_options, {})
174
+ build_options(options, @class_options) if options
175
+ @class_options
176
+ end
177
+
178
+ # Adds an option to the set of class options
179
+ #
180
+ # ==== Parameters
181
+ # name<Symbol>:: The name of the argument.
182
+ # options<Hash>:: Described below.
183
+ #
184
+ # ==== Options
185
+ # :desc - Description for the argument.
186
+ # :required - If the argument is required or not.
187
+ # :default - Default value for this argument.
188
+ # :group - The group for this options. Use by class options to output options in different levels.
189
+ # :aliases - Aliases for this option.
190
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
191
+ # :banner - String to show on usage notes.
192
+ #
193
+ def class_option(name, options={})
194
+ build_option(name, options, class_options)
195
+ end
196
+
197
+ # Removes a previous defined argument. If :undefine is given, undefine
198
+ # accessors as well.
199
+ #
200
+ # ==== Paremeters
201
+ # names<Array>:: Arguments to be removed
202
+ #
203
+ # ==== Examples
204
+ #
205
+ # remove_argument :foo
206
+ # remove_argument :foo, :bar, :baz, :undefine => true
207
+ #
208
+ def remove_argument(*names)
209
+ options = names.last.is_a?(Hash) ? names.pop : {}
210
+
211
+ names.each do |name|
212
+ arguments.delete_if { |a| a.name == name.to_s }
213
+ undef_method name, "#{name}=" if options[:undefine]
214
+ end
215
+ end
216
+
217
+ # Removes a previous defined class option.
218
+ #
219
+ # ==== Paremeters
220
+ # names<Array>:: Class options to be removed
221
+ #
222
+ # ==== Examples
223
+ #
224
+ # remove_class_option :foo
225
+ # remove_class_option :foo, :bar, :baz
226
+ #
227
+ def remove_class_option(*names)
228
+ names.each do |name|
229
+ class_options.delete(name)
230
+ end
231
+ end
232
+
233
+ # Defines the group. This is used when thor list is invoked so you can specify
234
+ # that only tasks from a pre-defined group will be shown. Defaults to standard.
235
+ #
236
+ # ==== Parameters
237
+ # name<String|Symbol>
238
+ #
239
+ def group(name=nil)
240
+ case name
241
+ when nil
242
+ @group ||= from_superclass(:group, 'standard')
243
+ else
244
+ @group = name.to_s
245
+ end
246
+ end
247
+
248
+ # Returns the tasks for this Thor class.
249
+ #
250
+ # ==== Returns
251
+ # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task
252
+ # objects as values.
253
+ #
254
+ def tasks
255
+ @tasks ||= Thor::CoreExt::OrderedHash.new
256
+ end
257
+
258
+ # Returns the tasks for this Thor class and all subclasses.
259
+ #
260
+ # ==== Returns
261
+ # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task
262
+ # objects as values.
263
+ #
264
+ def all_tasks
265
+ @all_tasks ||= from_superclass(:all_tasks, Thor::CoreExt::OrderedHash.new)
266
+ @all_tasks.merge(tasks)
267
+ end
268
+
269
+ # Removes a given task from this Thor class. This is usually done if you
270
+ # are inheriting from another class and don't want it to be available
271
+ # anymore.
272
+ #
273
+ # By default it only remove the mapping to the task. But you can supply
274
+ # :undefine => true to undefine the method from the class as well.
275
+ #
276
+ # ==== Parameters
277
+ # name<Symbol|String>:: The name of the task to be removed
278
+ # options<Hash>:: You can give :undefine => true if you want tasks the method
279
+ # to be undefined from the class as well.
280
+ #
281
+ def remove_task(*names)
282
+ options = names.last.is_a?(Hash) ? names.pop : {}
283
+
284
+ names.each do |name|
285
+ tasks.delete(name.to_s)
286
+ all_tasks.delete(name.to_s)
287
+ undef_method name if options[:undefine]
288
+ end
289
+ end
290
+
291
+ # All methods defined inside the given block are not added as tasks.
292
+ #
293
+ # So you can do:
294
+ #
295
+ # class MyScript < Thor
296
+ # no_tasks do
297
+ # def this_is_not_a_task
298
+ # end
299
+ # end
300
+ # end
301
+ #
302
+ # You can also add the method and remove it from the task list:
303
+ #
304
+ # class MyScript < Thor
305
+ # def this_is_not_a_task
306
+ # end
307
+ # remove_task :this_is_not_a_task
308
+ # end
309
+ #
310
+ def no_tasks
311
+ @no_tasks = true
312
+ yield
313
+ @no_tasks = false
314
+ end
315
+
316
+ # Sets the namespace for the Thor or Thor::Group class. By default the
317
+ # namespace is retrieved from the class name. If your Thor class is named
318
+ # Scripts::MyScript, the help method, for example, will be called as:
319
+ #
320
+ # thor scripts:my_script -h
321
+ #
322
+ # If you change the namespace:
323
+ #
324
+ # namespace :my_scripts
325
+ #
326
+ # You change how your tasks are invoked:
327
+ #
328
+ # thor my_scripts -h
329
+ #
330
+ # Finally, if you change your namespace to default:
331
+ #
332
+ # namespace :default
333
+ #
334
+ # Your tasks can be invoked with a shortcut. Instead of:
335
+ #
336
+ # thor :my_task
337
+ #
338
+ def namespace(name=nil)
339
+ case name
340
+ when nil
341
+ @namespace ||= Thor::Util.namespace_from_thor_class(self, false)
342
+ else
343
+ @namespace = name.to_s
344
+ end
345
+ end
346
+
347
+ # Default way to start generators from the command line.
348
+ #
349
+ def start(given_args=ARGV, config={})
350
+ config[:shell] ||= Thor::Base.shell.new
351
+ yield
352
+ rescue Thor::Error => e
353
+ if given_args.include?("--debug")
354
+ raise e
355
+ else
356
+ config[:shell].error e.message
357
+ end
358
+ end
359
+
360
+ protected
361
+
362
+ # Prints the class options per group. If an option does not belong to
363
+ # any group, it uses the ungrouped name value. This method provide to
364
+ # hooks to add extra options, one of them if the third argument called
365
+ # extra_group that should be a hash in the format :group => Array[Options].
366
+ #
367
+ # The second is by returning a lambda used to print values. The lambda
368
+ # requires two options: the group name and the array of options.
369
+ #
370
+ def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
371
+ groups = {}
372
+
373
+ class_options.each do |_, value|
374
+ groups[value.group] ||= []
375
+ groups[value.group] << value
376
+ end
377
+
378
+ printer = proc do |group_name, options|
379
+ list = []
380
+ padding = options.collect{ |o| o.aliases.size }.max.to_i * 4
381
+
382
+ options.each do |option|
383
+ item = [ option.usage(padding) ]
384
+ item.push(option.description ? "# #{option.description}" : "")
385
+
386
+ list << item
387
+ list << [ "", "# Default: #{option.default}" ] if option.show_default?
388
+ end
389
+
390
+ unless list.empty?
391
+ shell.say(group_name ? "#{group_name} options:" : "Options:")
392
+ shell.print_table(list, :ident => 2)
393
+ shell.say ""
394
+ end
395
+ end
396
+
397
+ # Deal with default group
398
+ global_options = groups.delete(nil) || []
399
+ printer.call(ungrouped_name, global_options) if global_options
400
+
401
+ # Print all others
402
+ groups = extra_group.merge(groups) if extra_group
403
+ groups.each(&printer)
404
+ printer
405
+ end
406
+
407
+ # Raises an error if the word given is a Thor reserved word.
408
+ #
409
+ def is_thor_reserved_word?(word, type) #:nodoc:
410
+ return false unless THOR_RESERVED_WORDS.include?(word.to_s)
411
+ raise "#{word.inspect} is a Thor reserved word and cannot be defined as #{type}"
412
+ end
413
+
414
+ # Build an option and adds it to the given scope.
415
+ #
416
+ # ==== Parameters
417
+ # name<Symbol>:: The name of the argument.
418
+ # options<Hash>:: Described in both class_option and method_option.
419
+ #
420
+ def build_option(name, options, scope) #:nodoc:
421
+ scope[name] = Thor::Option.new(name, options[:desc], options[:required],
422
+ options[:type], options[:default], options[:banner],
423
+ options[:group], options[:aliases])
424
+ end
425
+
426
+ # Receives a hash of options, parse them and add to the scope. This is a
427
+ # fast way to set a bunch of options:
428
+ #
429
+ # build_options :foo => true, :bar => :required, :baz => :string
430
+ #
431
+ # ==== Parameters
432
+ # Hash[Symbol => Object]
433
+ #
434
+ def build_options(options, scope) #:nodoc:
435
+ options.each do |key, value|
436
+ scope[key] = Thor::Option.parse(key, value)
437
+ end
438
+ end
439
+
440
+ # Finds a task with the given name. If the task belongs to the current
441
+ # class, just return it, otherwise dup it and add the fresh copy to the
442
+ # current task hash.
443
+ #
444
+ def find_and_refresh_task(name) #:nodoc:
445
+ task = if task = tasks[name.to_s]
446
+ task
447
+ elsif task = all_tasks[name.to_s]
448
+ tasks[name.to_s] = task.clone
449
+ else
450
+ raise ArgumentError, "You supplied :for => #{name.inspect}, but the task #{name.inspect} could not be found."
451
+ end
452
+ end
453
+
454
+ # Everytime someone inherits from a Thor class, register the klass
455
+ # and file into baseclass.
456
+ #
457
+ def inherited(klass)
458
+ Thor::Base.register_klass_file(klass)
459
+ end
460
+
461
+ # Fire this callback whenever a method is added. Added methods are
462
+ # tracked as tasks by invoking the create_task method.
463
+ #
464
+ def method_added(meth)
465
+ meth = meth.to_s
466
+
467
+ if meth == "initialize"
468
+ initialize_added
469
+ return
470
+ end
471
+
472
+ # Return if it's not a public instance method
473
+ return unless public_instance_methods.include?(meth) ||
474
+ public_instance_methods.include?(meth.to_sym)
475
+
476
+ return if @no_tasks || !create_task(meth)
477
+
478
+ is_thor_reserved_word?(meth, :task)
479
+ Thor::Base.register_klass_file(self)
480
+ end
481
+
482
+ # Retrieves a value from superclass. If it reaches the baseclass,
483
+ # returns default.
484
+ #
485
+ def from_superclass(method, default=nil)
486
+ if self == baseclass || !superclass.respond_to?(method, true)
487
+ default
488
+ else
489
+ value = superclass.send(method)
490
+ value.dup if value
491
+ end
492
+ end
493
+
494
+ # SIGNATURE: Sets the baseclass. This is where the superclass lookup
495
+ # finishes.
496
+ def baseclass #:nodoc:
497
+ end
498
+
499
+ # SIGNATURE: Creates a new task if valid_task? is true. This method is
500
+ # called when a new method is added to the class.
501
+ def create_task(meth) #:nodoc:
502
+ end
503
+
504
+ # SIGNATURE: Defines behavior when the initialize method is added to the
505
+ # class.
506
+ def initialize_added #:nodoc:
507
+ end
508
+ end
509
+ end
510
+ end