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,59 @@
|
|
1
|
+
class Thor
|
2
|
+
module CoreExt
|
3
|
+
|
4
|
+
# A hash with indifferent access and magic predicates.
|
5
|
+
#
|
6
|
+
# hash = Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true
|
7
|
+
#
|
8
|
+
# hash[:foo] #=> 'bar'
|
9
|
+
# hash['foo'] #=> 'bar'
|
10
|
+
# hash.foo? #=> true
|
11
|
+
#
|
12
|
+
class HashWithIndifferentAccess < ::Hash
|
13
|
+
|
14
|
+
def initialize(hash)
|
15
|
+
super()
|
16
|
+
|
17
|
+
hash.each do |key, value|
|
18
|
+
if key.is_a?(Symbol)
|
19
|
+
self[key.to_s] = value
|
20
|
+
else
|
21
|
+
self[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
super(convert_key(key))
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(key)
|
31
|
+
super(convert_key(key))
|
32
|
+
end
|
33
|
+
|
34
|
+
def values_at(*indices)
|
35
|
+
indices.collect { |key| self[convert_key(key)] }
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def convert_key(key)
|
41
|
+
key.is_a?(Symbol) ? key.to_s : key
|
42
|
+
end
|
43
|
+
|
44
|
+
# Magic predicates. For instance:
|
45
|
+
#
|
46
|
+
# options.force? # => !!options['force']
|
47
|
+
#
|
48
|
+
def method_missing(method, *args, &block)
|
49
|
+
method = method.to_s
|
50
|
+
if method =~ /^(\w+)\?$/
|
51
|
+
!!self[$1]
|
52
|
+
else
|
53
|
+
self[method]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
class Thor #:nodoc:
|
2
|
+
module CoreExt #:nodoc:
|
3
|
+
|
4
|
+
# This class is based on the Ruby 1.9 ordered hashes.
|
5
|
+
#
|
6
|
+
# It keeps the semantics and most of the efficiency of normal hashes
|
7
|
+
# while also keeping track of the order in which elements were set.
|
8
|
+
#
|
9
|
+
class OrderedHash #:nodoc:
|
10
|
+
Node = Struct.new(:key, :value, :next, :prev)
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@hash = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Called on clone. It gets all the notes from the cloned object, dup them
|
18
|
+
# and assign the duped objects siblings.
|
19
|
+
#
|
20
|
+
def initialize_copy(other)
|
21
|
+
@hash = {}
|
22
|
+
|
23
|
+
array = []
|
24
|
+
other.each do |key, value|
|
25
|
+
array << (@hash[key] = Node.new(key, value))
|
26
|
+
end
|
27
|
+
|
28
|
+
array.each_with_index do |node, i|
|
29
|
+
node.next = array[i + 1]
|
30
|
+
node.prev = array[i - 1] if i > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
@first = array.first
|
34
|
+
@last = array.last
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](key)
|
38
|
+
@hash[key] && @hash[key].value
|
39
|
+
end
|
40
|
+
|
41
|
+
def []=(key, value)
|
42
|
+
if old = @hash[key]
|
43
|
+
node = old.dup
|
44
|
+
node.value = value
|
45
|
+
|
46
|
+
@first = node if @first == old
|
47
|
+
@last = node if @last == old
|
48
|
+
|
49
|
+
old.prev.next = node if old.prev
|
50
|
+
old.next.prev = node if old.next
|
51
|
+
else
|
52
|
+
node = Node.new(key, value)
|
53
|
+
|
54
|
+
if @first.nil?
|
55
|
+
@first = @last = node
|
56
|
+
else
|
57
|
+
node.prev = @last
|
58
|
+
@last.next = node
|
59
|
+
@last = node
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@hash[key] = node
|
64
|
+
value
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete(key)
|
68
|
+
if node = @hash[key]
|
69
|
+
prev_node = node.prev
|
70
|
+
next_node = node.next
|
71
|
+
|
72
|
+
next_node.prev = prev_node if next_node
|
73
|
+
prev_node.next = next_node if prev_node
|
74
|
+
|
75
|
+
@first = next_node if @first == node
|
76
|
+
@last = prev_node if @last == node
|
77
|
+
|
78
|
+
value = node.value
|
79
|
+
end
|
80
|
+
|
81
|
+
@hash[key] = nil
|
82
|
+
value
|
83
|
+
end
|
84
|
+
|
85
|
+
def each
|
86
|
+
return unless @first
|
87
|
+
yield [@first.key, @first.value]
|
88
|
+
node = @first
|
89
|
+
yield [node.key, node.value] while node = node.next
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
def keys
|
94
|
+
self.map { |k, v| k }
|
95
|
+
end
|
96
|
+
|
97
|
+
def values
|
98
|
+
self.map { |k, v| v }
|
99
|
+
end
|
100
|
+
|
101
|
+
def merge(other)
|
102
|
+
new = clone
|
103
|
+
other.each do |key, value|
|
104
|
+
new[key] = value
|
105
|
+
end
|
106
|
+
new
|
107
|
+
end
|
108
|
+
|
109
|
+
def merge!(other)
|
110
|
+
other.each do |key, value|
|
111
|
+
self[key] = value
|
112
|
+
end
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def empty?
|
117
|
+
@hash.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_a
|
121
|
+
inject([]) do |array, (key, value)|
|
122
|
+
array << [key, value]
|
123
|
+
array
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def to_s
|
128
|
+
to_a.inspect
|
129
|
+
end
|
130
|
+
alias :inspect :to_s
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/thor/error.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class Thor
|
2
|
+
# Thor::Error is raised when it's caused by the user invoking the task and
|
3
|
+
# only errors that inherit from it are rescued.
|
4
|
+
#
|
5
|
+
# So, for example, if the developer declares a required argument after an
|
6
|
+
# option, it should raise an ::ArgumentError and not ::Thor::ArgumentError,
|
7
|
+
# because it was caused by the developer and not the "final user".
|
8
|
+
#
|
9
|
+
class Error < StandardError #:nodoc:
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raised when a task was not found.
|
13
|
+
#
|
14
|
+
class UndefinedTaskError < Error #:nodoc:
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when a task was found, but not invoked properly.
|
18
|
+
#
|
19
|
+
class InvocationError < Error #:nodoc:
|
20
|
+
end
|
21
|
+
|
22
|
+
class RequiredArgumentMissingError < InvocationError #:nodoc:
|
23
|
+
end
|
24
|
+
|
25
|
+
class MalformattedArgumentError < InvocationError #:nodoc:
|
26
|
+
end
|
27
|
+
end
|
data/lib/thor/group.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
class Thor::Group
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# The descrition for this Thor::Group as a whole.
|
6
|
+
#
|
7
|
+
# ==== Parameters
|
8
|
+
# description<String>:: The description for this Thor::Group.
|
9
|
+
#
|
10
|
+
def desc(description=nil)
|
11
|
+
case description
|
12
|
+
# TODO When a symbol is given, read a file in the current directory
|
13
|
+
# when Symbol
|
14
|
+
# @desc = File.read
|
15
|
+
when nil
|
16
|
+
@desc ||= from_superclass(:desc, nil)
|
17
|
+
else
|
18
|
+
@desc = description
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Start in Thor::Group works differently. It invokes all tasks inside the
|
23
|
+
# class and does not have to parse task options.
|
24
|
+
#
|
25
|
+
def start(args=ARGV, config={})
|
26
|
+
config[:shell] ||= Thor::Base.shell.new
|
27
|
+
|
28
|
+
if Thor::HELP_MAPPINGS.include?(args.first)
|
29
|
+
help(config[:shell])
|
30
|
+
else
|
31
|
+
opts = Thor::Options.new(class_options)
|
32
|
+
opts.parse(args)
|
33
|
+
|
34
|
+
new(opts.arguments, opts.options, config).invoke_all
|
35
|
+
end
|
36
|
+
rescue Thor::Error => e
|
37
|
+
config[:shell].error e.message
|
38
|
+
end
|
39
|
+
|
40
|
+
# Prints help information.
|
41
|
+
#
|
42
|
+
# ==== Options
|
43
|
+
# short:: When true, shows only usage.
|
44
|
+
#
|
45
|
+
def help(shell, options={})
|
46
|
+
if options[:short]
|
47
|
+
shell.say "#{self.namespace} #{self.class_options.map {|_,o| o.usage}.join(' ')}"
|
48
|
+
else
|
49
|
+
shell.say "Usage:"
|
50
|
+
shell.say " #{self.namespace} #{self.arguments.map{|o| o.usage}.join(' ')}"
|
51
|
+
shell.say
|
52
|
+
|
53
|
+
list = self.class_options.map do |_, option|
|
54
|
+
next if option.argument?
|
55
|
+
[ option.usage, option.description || '' ]
|
56
|
+
end.compact
|
57
|
+
|
58
|
+
unless list.empty?
|
59
|
+
shell.say "Global options:"
|
60
|
+
shell.print_table(list, :emphasize_last => true, :ident => 2)
|
61
|
+
shell.say
|
62
|
+
end
|
63
|
+
|
64
|
+
shell.say self.desc if self.desc
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def baseclass #:nodoc:
|
71
|
+
Thor::Group
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid_task?(meth) #:nodoc:
|
75
|
+
public_instance_methods.include?(meth)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_task(meth) #:nodoc:
|
79
|
+
tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Invokes all tasks in the instance.
|
84
|
+
#
|
85
|
+
def invoke_all
|
86
|
+
self.class.all_tasks.map { |_, task| task.run(self) }
|
87
|
+
end
|
88
|
+
|
89
|
+
include Thor::Base
|
90
|
+
end
|
data/lib/thor/option.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
class Thor
|
2
|
+
class Option
|
3
|
+
attr_reader :name, :description, :required, :type, :default, :aliases
|
4
|
+
|
5
|
+
VALID_TYPES = [:boolean, :numeric, :hash, :array, :string, :default]
|
6
|
+
|
7
|
+
def initialize(name, description=nil, required=nil, type=nil, default=nil, aliases=nil)
|
8
|
+
raise ArgumentError, "Option name can't be nil." if name.nil?
|
9
|
+
raise ArgumentError, "Option cannot be required and have default values." if required && !default.nil?
|
10
|
+
raise ArgumentError, "Type :#{type} is not valid for options." if type && !VALID_TYPES.include?(type.to_sym)
|
11
|
+
|
12
|
+
@name = name.to_s
|
13
|
+
@description = description
|
14
|
+
@required = required || false
|
15
|
+
@type = (type || :default).to_sym
|
16
|
+
@default = default
|
17
|
+
@aliases = [*aliases].compact
|
18
|
+
end
|
19
|
+
|
20
|
+
# This parse quick options given as method_options. It makes several
|
21
|
+
# assumptions, but you can be more specific using the option method.
|
22
|
+
#
|
23
|
+
# parse :foo => "bar"
|
24
|
+
# #=> Option foo with default value bar
|
25
|
+
#
|
26
|
+
# parse [:foo, :baz] => "bar"
|
27
|
+
# #=> Option foo with default value bar and alias :baz
|
28
|
+
#
|
29
|
+
# parse :foo => :required
|
30
|
+
# #=> Required option foo without default value
|
31
|
+
#
|
32
|
+
# parse :foo => :optional
|
33
|
+
# #=> Optional foo without default value
|
34
|
+
#
|
35
|
+
# parse :foo => 2
|
36
|
+
# #=> Option foo with default value 2 and type numeric
|
37
|
+
#
|
38
|
+
# parse :foo => :numeric
|
39
|
+
# #=> Option foo without default value and type numeric
|
40
|
+
#
|
41
|
+
# parse :foo => true
|
42
|
+
# #=> Option foo with default value true and type boolean
|
43
|
+
#
|
44
|
+
# The valid types are :boolean, :numeric, :hash, :array and :string. If none
|
45
|
+
# is given a default type is assumed. This default type accepts arguments as
|
46
|
+
# string (--foo=value) or booleans (just --foo).
|
47
|
+
#
|
48
|
+
# By default all options are optional, unless :required is given.
|
49
|
+
#
|
50
|
+
def self.parse(key, value)
|
51
|
+
if key.is_a?(Array)
|
52
|
+
name, *aliases = key
|
53
|
+
else
|
54
|
+
name, aliases = key, []
|
55
|
+
end
|
56
|
+
|
57
|
+
name = name.to_s
|
58
|
+
default = value
|
59
|
+
|
60
|
+
type = case value
|
61
|
+
when Symbol
|
62
|
+
default = nil
|
63
|
+
|
64
|
+
if VALID_TYPES.include?(value)
|
65
|
+
value
|
66
|
+
elsif required = (value == :required)
|
67
|
+
:string
|
68
|
+
end
|
69
|
+
when TrueClass, FalseClass
|
70
|
+
:boolean
|
71
|
+
when Numeric
|
72
|
+
:numeric
|
73
|
+
when Hash, Array, String
|
74
|
+
value.class.name.downcase.to_sym
|
75
|
+
end
|
76
|
+
|
77
|
+
self.new(name.to_s, nil, required, type, default, aliases)
|
78
|
+
end
|
79
|
+
|
80
|
+
def argument?
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def required?
|
85
|
+
required
|
86
|
+
end
|
87
|
+
|
88
|
+
def optional?
|
89
|
+
!required
|
90
|
+
end
|
91
|
+
|
92
|
+
def <=>(other)
|
93
|
+
self.position <=> other.position
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns true if this type requires an input to be given. Just :default
|
97
|
+
# and :boolean does not require an input.
|
98
|
+
#
|
99
|
+
def input_required?
|
100
|
+
[ :numeric, :hash, :array, :string ].include?(type)
|
101
|
+
end
|
102
|
+
|
103
|
+
def switch_name
|
104
|
+
@switch_name ||= dasherized? ? name : dasherize(name)
|
105
|
+
end
|
106
|
+
|
107
|
+
def human_name
|
108
|
+
@human_name ||= dasherized? ? undasherize(name) : name
|
109
|
+
end
|
110
|
+
|
111
|
+
def dasherized?
|
112
|
+
name.index('-') == 0
|
113
|
+
end
|
114
|
+
|
115
|
+
def undasherize(str)
|
116
|
+
str.sub(/^-{1,2}/, '')
|
117
|
+
end
|
118
|
+
|
119
|
+
def dasherize(str)
|
120
|
+
(str.length > 1 ? "--" : "-") + str
|
121
|
+
end
|
122
|
+
|
123
|
+
def usage
|
124
|
+
sample = formatted_default || formatted_value
|
125
|
+
|
126
|
+
sample = if sample
|
127
|
+
"#{switch_name}=#{sample}"
|
128
|
+
else
|
129
|
+
switch_name
|
130
|
+
end
|
131
|
+
|
132
|
+
sample = "[#{sample}]" unless required?
|
133
|
+
sample = "#{aliases.join(', ')}, #{sample}" unless aliases.empty?
|
134
|
+
sample
|
135
|
+
end
|
136
|
+
|
137
|
+
protected
|
138
|
+
|
139
|
+
def position
|
140
|
+
if argument?
|
141
|
+
-1
|
142
|
+
elsif required?
|
143
|
+
0
|
144
|
+
else
|
145
|
+
1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def formatted_default
|
150
|
+
return unless default
|
151
|
+
|
152
|
+
case type
|
153
|
+
when :boolean
|
154
|
+
nil
|
155
|
+
when :numeric
|
156
|
+
default.to_s
|
157
|
+
when :string, :default
|
158
|
+
default.empty? ? formatted_value : default.to_s
|
159
|
+
when :hash
|
160
|
+
if default.empty?
|
161
|
+
formatted_value
|
162
|
+
else
|
163
|
+
default.inject([]) do |mem, (key, value)|
|
164
|
+
mem << "#{key}:#{value}".gsub(/\s/, '_')
|
165
|
+
mem
|
166
|
+
end.join(' ')
|
167
|
+
end
|
168
|
+
when :array
|
169
|
+
default.empty? ? formatted_value : default.join(" ")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def formatted_value
|
174
|
+
case type
|
175
|
+
when :boolean
|
176
|
+
nil
|
177
|
+
when :string, :default
|
178
|
+
human_name.upcase
|
179
|
+
when :numeric
|
180
|
+
"N"
|
181
|
+
when :hash
|
182
|
+
"key:value"
|
183
|
+
when :array
|
184
|
+
"one two three"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Argument is a subset of option. It does not support :boolean and :default
|
190
|
+
# as types.
|
191
|
+
#
|
192
|
+
class Argument < Option
|
193
|
+
VALID_TYPES = [:numeric, :hash, :array, :string]
|
194
|
+
|
195
|
+
def initialize(name, description=nil, required=true, type=:string, default=nil)
|
196
|
+
raise ArgumentError, "Argument name can't be nil." if name.nil?
|
197
|
+
raise ArgumentError, "Type :#{type} is not valid for arguments." if type && !VALID_TYPES.include?(type.to_sym)
|
198
|
+
|
199
|
+
super(name, description, required, type || :string, default, [])
|
200
|
+
end
|
201
|
+
|
202
|
+
def argument?
|
203
|
+
true
|
204
|
+
end
|
205
|
+
|
206
|
+
def usage
|
207
|
+
required? ? formatted_value : "[#{formatted_value}]"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|