thor 0.9.9 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/CHANGELOG.rdoc +29 -4
  2. data/README.rdoc +234 -0
  3. data/Thorfile +57 -0
  4. data/VERSION +1 -0
  5. data/bin/rake2thor +4 -0
  6. data/bin/thor +1 -1
  7. data/lib/thor.rb +216 -119
  8. data/lib/thor/actions.rb +272 -0
  9. data/lib/thor/actions/create_file.rb +102 -0
  10. data/lib/thor/actions/directory.rb +87 -0
  11. data/lib/thor/actions/empty_directory.rb +133 -0
  12. data/lib/thor/actions/file_manipulation.rb +195 -0
  13. data/lib/thor/actions/inject_into_file.rb +78 -0
  14. data/lib/thor/base.rb +510 -0
  15. data/lib/thor/core_ext/hash_with_indifferent_access.rb +75 -0
  16. data/lib/thor/core_ext/ordered_hash.rb +100 -0
  17. data/lib/thor/error.rb +25 -1
  18. data/lib/thor/group.rb +263 -0
  19. data/lib/thor/invocation.rb +178 -0
  20. data/lib/thor/parser.rb +4 -0
  21. data/lib/thor/parser/argument.rb +67 -0
  22. data/lib/thor/parser/arguments.rb +145 -0
  23. data/lib/thor/parser/option.rb +132 -0
  24. data/lib/thor/parser/options.rb +142 -0
  25. data/lib/thor/rake_compat.rb +67 -0
  26. data/lib/thor/runner.rb +232 -242
  27. data/lib/thor/shell.rb +72 -0
  28. data/lib/thor/shell/basic.rb +220 -0
  29. data/lib/thor/shell/color.rb +108 -0
  30. data/lib/thor/task.rb +97 -60
  31. data/lib/thor/util.rb +230 -55
  32. data/spec/actions/create_file_spec.rb +170 -0
  33. data/spec/actions/directory_spec.rb +118 -0
  34. data/spec/actions/empty_directory_spec.rb +91 -0
  35. data/spec/actions/file_manipulation_spec.rb +242 -0
  36. data/spec/actions/inject_into_file_spec.rb +80 -0
  37. data/spec/actions_spec.rb +291 -0
  38. data/spec/base_spec.rb +236 -0
  39. data/spec/core_ext/hash_with_indifferent_access_spec.rb +43 -0
  40. data/spec/core_ext/ordered_hash_spec.rb +115 -0
  41. data/spec/fixtures/bundle/execute.rb +6 -0
  42. data/spec/fixtures/doc/config.rb +1 -0
  43. data/spec/group_spec.rb +177 -0
  44. data/spec/invocation_spec.rb +107 -0
  45. data/spec/parser/argument_spec.rb +47 -0
  46. data/spec/parser/arguments_spec.rb +64 -0
  47. data/spec/parser/option_spec.rb +212 -0
  48. data/spec/parser/options_spec.rb +255 -0
  49. data/spec/rake_compat_spec.rb +64 -0
  50. data/spec/runner_spec.rb +204 -0
  51. data/spec/shell/basic_spec.rb +206 -0
  52. data/spec/shell/color_spec.rb +41 -0
  53. data/spec/shell_spec.rb +25 -0
  54. data/spec/spec_helper.rb +52 -0
  55. data/spec/task_spec.rb +82 -0
  56. data/spec/thor_spec.rb +234 -0
  57. data/spec/util_spec.rb +196 -0
  58. metadata +69 -25
  59. data/README.markdown +0 -76
  60. data/Rakefile +0 -6
  61. data/lib/thor/options.rb +0 -242
  62. data/lib/thor/ordered_hash.rb +0 -64
  63. data/lib/thor/task_hash.rb +0 -22
  64. data/lib/thor/tasks.rb +0 -77
  65. data/lib/thor/tasks/package.rb +0 -18
@@ -0,0 +1,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