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.
- data/CHANGELOG.rdoc +73 -0
- data/LICENSE +20 -0
- data/README.markdown +76 -0
- data/Rakefile +6 -0
- data/bin/rake2thor +87 -0
- data/bin/thor +7 -0
- data/lib/thor.rb +229 -0
- data/lib/thor/actions.rb +147 -0
- data/lib/thor/actions/commands.rb +61 -0
- data/lib/thor/actions/copy_file.rb +32 -0
- data/lib/thor/actions/create_file.rb +48 -0
- data/lib/thor/actions/directory.rb +36 -0
- data/lib/thor/actions/empty_directory.rb +30 -0
- data/lib/thor/actions/get.rb +58 -0
- data/lib/thor/actions/gsub_file.rb +77 -0
- data/lib/thor/actions/inject_into_file.rb +93 -0
- data/lib/thor/actions/template.rb +37 -0
- data/lib/thor/actions/templater.rb +163 -0
- data/lib/thor/base.rb +447 -0
- data/lib/thor/core_ext/hash_with_indifferent_access.rb +59 -0
- data/lib/thor/core_ext/ordered_hash.rb +133 -0
- data/lib/thor/error.rb +27 -0
- data/lib/thor/group.rb +90 -0
- data/lib/thor/option.rb +210 -0
- data/lib/thor/options.rb +282 -0
- data/lib/thor/runner.rb +296 -0
- data/lib/thor/shell/basic.rb +198 -0
- data/lib/thor/task.rb +85 -0
- data/lib/thor/tasks.rb +3 -0
- data/lib/thor/tasks/install.rb +35 -0
- data/lib/thor/tasks/package.rb +31 -0
- data/lib/thor/tasks/spec.rb +70 -0
- data/lib/thor/util.rb +209 -0
- metadata +93 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
class Thor
|
2
|
+
module Actions
|
3
|
+
|
4
|
+
# Injects the given content into a file. Different from append_file,
|
5
|
+
# prepend_file and gsub_file, this method is reversible. By this reason,
|
6
|
+
# the flag can only be strings. gsub_file is your friend if you need to
|
7
|
+
# deal with more complex cases.
|
8
|
+
#
|
9
|
+
# ==== Parameters
|
10
|
+
# destination<String>:: Relative path to the destination root
|
11
|
+
# data<String>:: Data to add to the file. Can be given as a block.
|
12
|
+
# flag<String>:: Flag of where to add the changes.
|
13
|
+
# log_status<Boolean>:: If false, does not log the status. True by default.
|
14
|
+
# If a symbol is given, uses it as the output color.
|
15
|
+
#
|
16
|
+
# ==== Examples
|
17
|
+
#
|
18
|
+
# inject_into_file "config/environment.rb", "config.gem thor", :after => "Rails::Initializer.run do |config|\n"
|
19
|
+
#
|
20
|
+
# inject_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do
|
21
|
+
# gems = ask "Which gems would you like to add?"
|
22
|
+
# gems.split(" ").map{ |gem| " config.gem #{gem}" }.join("\n")
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
def inject_into_file(destination, *args, &block)
|
26
|
+
if block_given?
|
27
|
+
data, flag = block, args.shift
|
28
|
+
else
|
29
|
+
data, flag = args.shift, args.shift
|
30
|
+
end
|
31
|
+
|
32
|
+
log_status = args.empty? || args.pop
|
33
|
+
action InjectIntoFile.new(self, destination, data, flag, log_status)
|
34
|
+
end
|
35
|
+
|
36
|
+
class InjectIntoFile #:nodoc:
|
37
|
+
attr_reader :base, :destination, :relative_destination, :flag, :replacement
|
38
|
+
|
39
|
+
def initialize(base, destination, data, flag, log_status=true)
|
40
|
+
@base, @log_status = base, log_status
|
41
|
+
behavior, @flag = flag.keys.first, flag.values.first
|
42
|
+
|
43
|
+
self.destination = destination
|
44
|
+
data = data.call if data.is_a?(Proc)
|
45
|
+
|
46
|
+
@replacement = if behavior == :after
|
47
|
+
@flag + data
|
48
|
+
else
|
49
|
+
data + @flag
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def invoke!
|
54
|
+
say_status :inject
|
55
|
+
replace!(flag, replacement)
|
56
|
+
end
|
57
|
+
|
58
|
+
def revoke!
|
59
|
+
say_status :deinject
|
60
|
+
replace!(replacement, flag)
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
# Sets the destination value from a relative destination value. The
|
66
|
+
# relative destination is kept to be used in output messages.
|
67
|
+
#
|
68
|
+
def destination=(destination)
|
69
|
+
if destination
|
70
|
+
@destination = ::File.expand_path(destination.to_s, base.destination_root)
|
71
|
+
@relative_destination = base.relative_to_absolute_root(@destination)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Shortcut to say_status base method.
|
76
|
+
#
|
77
|
+
def say_status(status)
|
78
|
+
base.send :say_status_if_log, status, relative_destination, @log_status
|
79
|
+
end
|
80
|
+
|
81
|
+
# Adds the content to the file.
|
82
|
+
#
|
83
|
+
def replace!(regexp, string)
|
84
|
+
unless base.options[:pretend]
|
85
|
+
content = File.read(destination)
|
86
|
+
content.gsub!(regexp, string)
|
87
|
+
File.open(destination, 'wb') { |file| file.write(content) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'thor/actions/templater'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
class Thor
|
5
|
+
module Actions
|
6
|
+
|
7
|
+
# Gets an ERB template at the relative source, executes it and makes a copy
|
8
|
+
# at the relative destination. If the destination is not given it's assumed
|
9
|
+
# to be equal to the source.
|
10
|
+
#
|
11
|
+
# ==== Parameters
|
12
|
+
# source<String>:: the relative path to the source root
|
13
|
+
# destination<String>:: the relative path to the destination root
|
14
|
+
# log_status<Boolean>:: if false, does not log the status. True by default.
|
15
|
+
#
|
16
|
+
# ==== Examples
|
17
|
+
#
|
18
|
+
# template "README", "doc/README"
|
19
|
+
#
|
20
|
+
# template "doc/README"
|
21
|
+
#
|
22
|
+
def template(source, destination=nil, log_status=true)
|
23
|
+
action Template.new(self, source, destination || source, log_status)
|
24
|
+
end
|
25
|
+
|
26
|
+
class Template < Templater #:nodoc:
|
27
|
+
|
28
|
+
def render
|
29
|
+
@render ||= begin
|
30
|
+
context = base.instance_eval('binding')
|
31
|
+
ERB.new(::File.read(source), nil, '-').result(context)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
class Thor
|
2
|
+
module Actions
|
3
|
+
|
4
|
+
# This is the base class for templater actions, ie. that copies something
|
5
|
+
# from some directory (source) to another (destination).
|
6
|
+
#
|
7
|
+
# This implementation is completely based in Templater actions, created
|
8
|
+
# by Jonas Nicklas and Michael S. Klishin under MIT LICENSE.
|
9
|
+
#
|
10
|
+
class Templater #:nodoc:
|
11
|
+
attr_reader :base, :source, :destination, :relative_destination
|
12
|
+
|
13
|
+
# Initializes given the source and destination.
|
14
|
+
#
|
15
|
+
# ==== Parameters
|
16
|
+
# base<Thor::Base>:: A Thor::Base instance
|
17
|
+
# source<String>:: Relative path to the source of this file
|
18
|
+
# destination<String>:: Relative path to the destination of this file
|
19
|
+
# log_status<Boolean>:: If false, does not log the status. True by default.
|
20
|
+
# Templater log status does not accept color.
|
21
|
+
#
|
22
|
+
def initialize(base, source, destination, log_status=true)
|
23
|
+
@base, @log_status = base, log_status
|
24
|
+
self.source = source
|
25
|
+
self.destination = destination
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the contents of the source file as a String. If render is
|
29
|
+
# available, a diff option is shown in the file collision menu.
|
30
|
+
#
|
31
|
+
# ==== Returns
|
32
|
+
# String:: The source file.
|
33
|
+
#
|
34
|
+
# def render
|
35
|
+
# end
|
36
|
+
|
37
|
+
# Checks if the destination file already exists.
|
38
|
+
#
|
39
|
+
# ==== Returns
|
40
|
+
# Boolean:: true if the file exists, false otherwise.
|
41
|
+
#
|
42
|
+
def exists?
|
43
|
+
::File.exists?(destination)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks if the content of the file at the destination is identical to the rendered result.
|
47
|
+
#
|
48
|
+
# ==== Returns
|
49
|
+
# Boolean:: true if it is identical, false otherwise.
|
50
|
+
#
|
51
|
+
def identical?
|
52
|
+
exists? && (is_not_comparable? || ::File.read(destination) == render)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Invokes the action. By default it adds to the file the content rendered,
|
56
|
+
# but you can modify in the subclass.
|
57
|
+
#
|
58
|
+
def invoke!
|
59
|
+
invoke_with_options!(base.options) do
|
60
|
+
::FileUtils.mkdir_p(::File.dirname(destination))
|
61
|
+
::File.open(destination, 'w'){ |f| f.write render }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Revokes the action.
|
66
|
+
#
|
67
|
+
def revoke!
|
68
|
+
say_status :deleted, :green
|
69
|
+
::FileUtils.rm_rf(destination) unless pretend?
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Shortcut for pretend.
|
75
|
+
#
|
76
|
+
def pretend?
|
77
|
+
base.options[:pretend]
|
78
|
+
end
|
79
|
+
|
80
|
+
# A templater is comparable if responds to render. In such cases, we have
|
81
|
+
# to show the conflict menu to the user unless the files are identical.
|
82
|
+
#
|
83
|
+
def is_not_comparable?
|
84
|
+
!respond_to?(:render)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sets the source value from a relative source value.
|
88
|
+
#
|
89
|
+
def source=(source)
|
90
|
+
if source
|
91
|
+
@source = ::File.expand_path(source.to_s, base.source_root)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Sets the destination value from a relative destination value. The
|
96
|
+
# relative destination is kept to be used in output messages.
|
97
|
+
#
|
98
|
+
def destination=(destination)
|
99
|
+
if destination
|
100
|
+
@destination = ::File.expand_path(destination.to_s, base.destination_root)
|
101
|
+
@relative_destination = base.relative_to_absolute_root(@destination)
|
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_options!(options, &block)
|
109
|
+
if exists?
|
110
|
+
if is_not_comparable?
|
111
|
+
say_status :exist, :blue
|
112
|
+
elsif identical?
|
113
|
+
say_status :identical, :blue
|
114
|
+
else
|
115
|
+
force_or_skip_or_conflict(options[:force], options[:skip], &block)
|
116
|
+
end
|
117
|
+
else
|
118
|
+
say_status :create, :green
|
119
|
+
block.call unless pretend?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# If force is true, run the action, otherwise check if it's not being
|
124
|
+
# skipped. If both are false, show the file_collision menu, if the menu
|
125
|
+
# returns true, force it, otherwise skip.
|
126
|
+
#
|
127
|
+
def force_or_skip_or_conflict(force, skip, &block)
|
128
|
+
if force
|
129
|
+
say_status :force, :yellow
|
130
|
+
block.call unless pretend?
|
131
|
+
elsif skip
|
132
|
+
say_status :skip, :yellow
|
133
|
+
else
|
134
|
+
say_status :conflict, :red
|
135
|
+
force_or_skip_or_conflict(force_on_collision?, true, &block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Shows the file collision menu to the user and gets the result.
|
140
|
+
#
|
141
|
+
def force_on_collision?
|
142
|
+
base.shell.file_collision(destination){ render }
|
143
|
+
end
|
144
|
+
|
145
|
+
# Shortcut to say_status shell method.
|
146
|
+
#
|
147
|
+
def say_status(status, color)
|
148
|
+
base.shell.say_status status, relative_destination, color if @log_status
|
149
|
+
end
|
150
|
+
|
151
|
+
# TODO Add this behavior to all actions.
|
152
|
+
#
|
153
|
+
def after_invoke
|
154
|
+
# Optionally change permissions.
|
155
|
+
FileUtils.chmod(base.options[:chmod], destination) if base.options[:chmod]
|
156
|
+
|
157
|
+
# Optionally add file to subversion or git
|
158
|
+
system("git add -v #{relative_destination}") if options[:git]
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/thor/base.rb
ADDED
@@ -0,0 +1,447 @@
|
|
1
|
+
require 'thor/core_ext/hash_with_indifferent_access'
|
2
|
+
require 'thor/core_ext/ordered_hash'
|
3
|
+
require 'thor/shell/basic'
|
4
|
+
require 'thor/error'
|
5
|
+
require 'thor/options'
|
6
|
+
require 'thor/task'
|
7
|
+
require 'thor/util'
|
8
|
+
|
9
|
+
class Thor
|
10
|
+
HELP_MAPPINGS = ["-h", "-?", "--help", "-D"]
|
11
|
+
|
12
|
+
class Maxima < Struct.new(:usage, :options, :class_options)
|
13
|
+
end
|
14
|
+
|
15
|
+
module Base
|
16
|
+
|
17
|
+
def self.included(base) #:nodoc:
|
18
|
+
base.send :extend, ClassMethods
|
19
|
+
base.send :include, SingletonMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the classes that inherits from Thor or Thor::Group.
|
23
|
+
#
|
24
|
+
# ==== Returns
|
25
|
+
# Array[Class]
|
26
|
+
#
|
27
|
+
def self.subclasses
|
28
|
+
@subclasses ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the files where the subclasses are kept.
|
32
|
+
#
|
33
|
+
# ==== Returns
|
34
|
+
# Hash[path<String> => Class]
|
35
|
+
#
|
36
|
+
def self.subclass_files
|
37
|
+
@subclass_files ||= Hash.new{ |h,k| h[k] = [] }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the shell used in all Thor classes.
|
41
|
+
#
|
42
|
+
def self.shell
|
43
|
+
@shell || Thor::Shell::Basic
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets the shell used in all Thor classes.
|
47
|
+
#
|
48
|
+
def self.shell=(klass)
|
49
|
+
@shell = klass
|
50
|
+
end
|
51
|
+
|
52
|
+
# Whenever a class inherits from Thor or Thor::Group, we should track the
|
53
|
+
# class and the file on Thor::Base. This is the method responsable for it.
|
54
|
+
#
|
55
|
+
def self.register_klass_file(klass) #:nodoc:
|
56
|
+
file = caller[1].match(/(.*):\d+/)[1]
|
57
|
+
Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass)
|
58
|
+
|
59
|
+
file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
|
60
|
+
file_subclasses << klass unless file_subclasses.include?(klass)
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
# Adds an argument to the class and creates an attr_accessor for it.
|
65
|
+
#
|
66
|
+
# Arguments are different from options in several aspects. The first one
|
67
|
+
# is how they are parsed from the command line, arguments are retrieved
|
68
|
+
# from position:
|
69
|
+
#
|
70
|
+
# thor task NAME
|
71
|
+
#
|
72
|
+
# Instead of:
|
73
|
+
#
|
74
|
+
# thor task --name=NAME
|
75
|
+
#
|
76
|
+
# Besides, arguments are used inside your code as an accessor (self.argument),
|
77
|
+
# while options are all kept in a hash (self.options).
|
78
|
+
#
|
79
|
+
# Finally, arguments cannot have type :default or :boolean but can be
|
80
|
+
# optional (supplying :optional => :true or :required => false), although
|
81
|
+
# you cannot have a required argument after a non-required argument. If you
|
82
|
+
# try it, an error is raised.
|
83
|
+
#
|
84
|
+
# ==== Parameters
|
85
|
+
# name<Symbol>:: The name of the argument.
|
86
|
+
# options<Hash>:: The description, type, default value for this argument.
|
87
|
+
# The type can be :string, :numeric, :hash or :array. If none, string is assumed.
|
88
|
+
#
|
89
|
+
# ==== Errors
|
90
|
+
# ArgumentError:: Raised if you supply a required argument after a non required one.
|
91
|
+
#
|
92
|
+
def argument(name, options={})
|
93
|
+
no_tasks { attr_accessor name }
|
94
|
+
|
95
|
+
required = if options.key?(:optional)
|
96
|
+
!options[:optional]
|
97
|
+
elsif options.key?(:required)
|
98
|
+
options[:required]
|
99
|
+
else
|
100
|
+
options[:default].nil?
|
101
|
+
end
|
102
|
+
|
103
|
+
class_options.values.each do |option|
|
104
|
+
next unless option.argument? && !option.required?
|
105
|
+
raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " <<
|
106
|
+
"the non-required argument #{option.human_name.inspect}."
|
107
|
+
end if required
|
108
|
+
|
109
|
+
class_options[name] = Thor::Argument.new(name, options[:desc], required,
|
110
|
+
options[:type], options[:default])
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns this class arguments, looking up in the ancestors chain.
|
114
|
+
#
|
115
|
+
# ==== Returns
|
116
|
+
# Array[Thor::Argument]
|
117
|
+
#
|
118
|
+
def arguments
|
119
|
+
class_options.values.select{ |o| o.argument? }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds a bunch of options to the set of class options.
|
123
|
+
#
|
124
|
+
# class_options :foo => :optional, :bar => :required, :baz => :string
|
125
|
+
#
|
126
|
+
# If you prefer more detailed declaration, check class_option.
|
127
|
+
#
|
128
|
+
# ==== Parameters
|
129
|
+
# Hash[Symbol => Object]
|
130
|
+
#
|
131
|
+
def class_options(options=nil)
|
132
|
+
@class_options ||= from_superclass(:class_options, Thor::CoreExt::OrderedHash.new)
|
133
|
+
build_options(options, @class_options) if options
|
134
|
+
@class_options
|
135
|
+
end
|
136
|
+
|
137
|
+
# Adds an option to the set of class options
|
138
|
+
#
|
139
|
+
# ==== Parameters
|
140
|
+
# name<Symbol>:: The name of the argument.
|
141
|
+
# options<Hash>:: The description, type, default value, aliases and if this option is required or not.
|
142
|
+
# The type can be :string, :boolean, :numeric, :hash or :array. If none is given
|
143
|
+
# a default type which accepts both (--name and --name=NAME) entries is assumed.
|
144
|
+
#
|
145
|
+
def class_option(name, options)
|
146
|
+
build_option(name, options, class_options)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Defines the group. This is used when thor list is invoked so you can specify
|
150
|
+
# that only tasks from a pre-defined group will be shown. Defaults to standard.
|
151
|
+
#
|
152
|
+
# ==== Parameters
|
153
|
+
# name<String|Symbol>
|
154
|
+
#
|
155
|
+
def group(name)
|
156
|
+
@group_name = name.to_s
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the group name.
|
160
|
+
#
|
161
|
+
# ==== Returns
|
162
|
+
# String
|
163
|
+
#
|
164
|
+
def group_name
|
165
|
+
@group_name ||= from_superclass(:group_name, 'standard')
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the tasks for this Thor class.
|
169
|
+
#
|
170
|
+
# ==== Returns
|
171
|
+
# OrderedHash:: An ordered hash with this class tasks.
|
172
|
+
#
|
173
|
+
def tasks
|
174
|
+
@tasks ||= Thor::CoreExt::OrderedHash.new
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns the tasks for this Thor class and all subclasses.
|
178
|
+
#
|
179
|
+
# ==== Returns
|
180
|
+
# OrderedHash
|
181
|
+
#
|
182
|
+
def all_tasks
|
183
|
+
@all_tasks ||= from_superclass(:all_tasks, Thor::CoreExt::OrderedHash.new)
|
184
|
+
@all_tasks.merge(tasks)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Removes a given task from this Thor class. This is usually done if you
|
188
|
+
# are inheriting from another class and don't want it to be available
|
189
|
+
# anymore.
|
190
|
+
#
|
191
|
+
# By default it only remove the mapping to the task. But you can supply
|
192
|
+
# :undefine => true to undefine the method from the class as well.
|
193
|
+
#
|
194
|
+
# ==== Parameters
|
195
|
+
# name<Symbol|String>:: The name of the task to be removed
|
196
|
+
# options<Hash>:: You can give :undefine => true if you want tasks the method
|
197
|
+
# to be undefined from the class as well.
|
198
|
+
#
|
199
|
+
def remove_task(*names)
|
200
|
+
options = names.last.is_a?(Hash) ? names.pop : {}
|
201
|
+
|
202
|
+
names.each do |name|
|
203
|
+
tasks.delete(name.to_s)
|
204
|
+
all_tasks.delete(name.to_s)
|
205
|
+
undef_method name if options[:undefine]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Retrieve a specific task from this Thor class. If the desired Task cannot
|
210
|
+
# be found, returns a dynamic Thor::Task that will map to the given method.
|
211
|
+
#
|
212
|
+
# ==== Parameters
|
213
|
+
# meth<Symbol>:: the name of the task to be retrieved
|
214
|
+
#
|
215
|
+
# ==== Returns
|
216
|
+
# Task
|
217
|
+
#
|
218
|
+
def [](meth)
|
219
|
+
all_tasks[meth.to_s] || Thor::Task.dynamic(meth)
|
220
|
+
end
|
221
|
+
|
222
|
+
# All methods defined inside the given block are not added as tasks.
|
223
|
+
#
|
224
|
+
# So you can do:
|
225
|
+
#
|
226
|
+
# class MyScript < Thor
|
227
|
+
# no_tasks do
|
228
|
+
# def this_is_not_a_task
|
229
|
+
# end
|
230
|
+
# end
|
231
|
+
# end
|
232
|
+
#
|
233
|
+
# You can also add the method and remove it from the task list:
|
234
|
+
#
|
235
|
+
# class MyScript < Thor
|
236
|
+
# def this_is_not_a_task
|
237
|
+
# end
|
238
|
+
# remove_task :this_is_not_a_task
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
def no_tasks
|
242
|
+
@no_tasks = true
|
243
|
+
yield
|
244
|
+
@no_tasks = false
|
245
|
+
end
|
246
|
+
|
247
|
+
# Sets the namespace for the Thor or Thor::Group class. By default the
|
248
|
+
# namespace is retrieved from the class name. If your Thor class is named
|
249
|
+
# Scripts::MyScript, the help method, for example, will be called as:
|
250
|
+
#
|
251
|
+
# thor scripts:my_script -h
|
252
|
+
#
|
253
|
+
# If you change the namespace:
|
254
|
+
#
|
255
|
+
# namespace :my_scripts
|
256
|
+
#
|
257
|
+
# You change how your tasks are invoked:
|
258
|
+
#
|
259
|
+
# thor my_scripts -h
|
260
|
+
#
|
261
|
+
# Finally, if you change your namespace to default:
|
262
|
+
#
|
263
|
+
# namespace :default
|
264
|
+
#
|
265
|
+
# Your tasks can be invoked with a shortcut. Instead of:
|
266
|
+
#
|
267
|
+
# thor :my_task
|
268
|
+
#
|
269
|
+
def namespace(name=nil)
|
270
|
+
case name
|
271
|
+
when nil
|
272
|
+
@namespace ||= Thor::Util.constant_to_namespace(self, false)
|
273
|
+
else
|
274
|
+
@namespace = name.to_s
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
protected
|
279
|
+
|
280
|
+
# Build an option and adds it to the given scope.
|
281
|
+
#
|
282
|
+
# ==== Parameters
|
283
|
+
# name<Symbol>:: The name of the argument.
|
284
|
+
# options<Hash>:: The desc, type, default value and aliases for this option.
|
285
|
+
# The type can be :string, :boolean, :numeric, :hash or :array. If none is given
|
286
|
+
# a default type which accepts both (--name and --name=NAME) entries is assumed.
|
287
|
+
#
|
288
|
+
def build_option(name, options, scope)
|
289
|
+
scope[name] = Thor::Option.new(name, options[:desc], options[:required],
|
290
|
+
options[:type], options[:default], options[:aliases])
|
291
|
+
end
|
292
|
+
|
293
|
+
# Receives a hash of options, parse them and add to the scope. This is a
|
294
|
+
# fast way to set a bunch of options:
|
295
|
+
#
|
296
|
+
# build_options :foo => :optional, :bar => :required, :baz => :string
|
297
|
+
#
|
298
|
+
# ==== Parameters
|
299
|
+
# Hash[Symbol => Object]
|
300
|
+
#
|
301
|
+
def build_options(options, scope)
|
302
|
+
options.each do |key, value|
|
303
|
+
scope[key] = Thor::Option.parse(key, value)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Finds a task with the given name. If the task belongs to the current
|
308
|
+
# class, just return it, otherwise dup it and add the fresh copy to the
|
309
|
+
# current task hash.
|
310
|
+
#
|
311
|
+
def find_and_refresh_task(name)
|
312
|
+
task = if task = tasks[name.to_s]
|
313
|
+
task
|
314
|
+
elsif task = all_tasks[name.to_s]
|
315
|
+
tasks[name.to_s] = task.clone
|
316
|
+
else
|
317
|
+
raise ArgumentError, "You supplied :for => #{name.inspect}, but the task #{name.inspect} could not be found."
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Everytime someone inherits from a Thor class, register the klass
|
322
|
+
# and file into baseclass.
|
323
|
+
#
|
324
|
+
def inherited(klass)
|
325
|
+
Thor::Base.register_klass_file(klass)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Fire this callback whenever a method is added. Added methods are
|
329
|
+
# tracked as tasks if the requirements set by valid_task? are valid.
|
330
|
+
#
|
331
|
+
def method_added(meth)
|
332
|
+
meth = meth.to_s
|
333
|
+
|
334
|
+
if meth == "initialize"
|
335
|
+
initialize_added
|
336
|
+
return
|
337
|
+
end
|
338
|
+
|
339
|
+
return if @no_tasks || !valid_task?(meth)
|
340
|
+
Thor::Base.register_klass_file(self)
|
341
|
+
create_task(meth)
|
342
|
+
end
|
343
|
+
|
344
|
+
def from_superclass(method, default=nil)
|
345
|
+
self == baseclass ? default : superclass.send(method).dup
|
346
|
+
end
|
347
|
+
|
348
|
+
# SIGNATURE: Sets the baseclass. This is where the superclass lookup
|
349
|
+
# finishes.
|
350
|
+
def baseclass #:nodoc:
|
351
|
+
end
|
352
|
+
|
353
|
+
# SIGNATURE: Defines if a given method is a valid_task?. This method is
|
354
|
+
# called before a new method is added to the class.
|
355
|
+
def valid_task?(meth) #:nodoc:
|
356
|
+
end
|
357
|
+
|
358
|
+
# SIGNATURE: Creates a new task if valid_task? is true. This method is
|
359
|
+
# called when a new method is added to the class.
|
360
|
+
def create_task(meth) #:nodoc:
|
361
|
+
end
|
362
|
+
|
363
|
+
# SIGNATURE: Defines behavior when the initialize method is added to the
|
364
|
+
# class.
|
365
|
+
def initialize_added #:nodoc:
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
module SingletonMethods
|
370
|
+
attr_accessor :options
|
371
|
+
|
372
|
+
SHELL_DELEGATED_METHODS = [:ask, :yes?, :no?, :say, :say_status, :print_list, :print_table]
|
373
|
+
|
374
|
+
# It receives arguments in an Array and two hashes, one for options and
|
375
|
+
# other for configuration.
|
376
|
+
#
|
377
|
+
# Notice that it does not check arguments type neither if all required
|
378
|
+
# arguments were supplied. It should be done by the parser.
|
379
|
+
#
|
380
|
+
# ==== Parameters
|
381
|
+
# args<Array[Object]>:: An array of objects. The objects are applied to their
|
382
|
+
# respective accessors declared with <tt>argument</tt>.
|
383
|
+
#
|
384
|
+
# options<Hash>:: An options hash that will be available as self.options.
|
385
|
+
# The hash given is converted to a hash with indifferent
|
386
|
+
# access, magic predicates (options.skip?) and then frozen.
|
387
|
+
#
|
388
|
+
# config<Hash>:: Configuration for this Thor class.
|
389
|
+
#
|
390
|
+
# ==== Configuration
|
391
|
+
# shell<Object>:: An instance of the shell to be used.
|
392
|
+
#
|
393
|
+
# ==== Examples
|
394
|
+
#
|
395
|
+
# class MyScript < Thor
|
396
|
+
# argument :first, :type => :numeric
|
397
|
+
# end
|
398
|
+
#
|
399
|
+
# MyScript.new [1.0], { :foo => :bar }, :shell => Thor::Shell::Basic.new
|
400
|
+
#
|
401
|
+
def initialize(args=[], options={}, config={})
|
402
|
+
self.class.arguments.zip(args).each do |argument, value|
|
403
|
+
send("#{argument.human_name}=", value)
|
404
|
+
end
|
405
|
+
|
406
|
+
self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).freeze
|
407
|
+
self.shell = config[:shell]
|
408
|
+
|
409
|
+
# Add base to shell if an accessor is provided.
|
410
|
+
self.shell.base = self if self.shell.respond_to?(:base)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Common methods that are delegated to the shell.
|
414
|
+
#
|
415
|
+
SHELL_DELEGATED_METHODS.each do |method|
|
416
|
+
module_eval <<-METHOD, __FILE__, __LINE__
|
417
|
+
def #{method}(*args)
|
418
|
+
shell.#{method}(*args)
|
419
|
+
end
|
420
|
+
METHOD
|
421
|
+
end
|
422
|
+
|
423
|
+
# Holds the shell for the given Thor instance. If no shell is given,
|
424
|
+
# it gets a default shell from Thor::Base.shell.
|
425
|
+
#
|
426
|
+
def shell
|
427
|
+
@shell ||= Thor::Base.shell.new
|
428
|
+
end
|
429
|
+
|
430
|
+
# Sets the shell for this thor class.
|
431
|
+
#
|
432
|
+
def shell=(shell)
|
433
|
+
@shell = shell
|
434
|
+
end
|
435
|
+
|
436
|
+
# Finds a task with the name given and invokes it with the given arguments.
|
437
|
+
# This is the default interface to invoke tasks. You can always run a task
|
438
|
+
# directly, but the invocation system will be implemented in a fashion
|
439
|
+
# that a same task cannot be invoked twice (a la rake).
|
440
|
+
#
|
441
|
+
def invoke(name, *args)
|
442
|
+
self.class[name].run(self, *args)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
end
|
447
|
+
end
|