engineyard 0.5.3 → 0.5.4

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 (43) hide show
  1. data/lib/engineyard/cli.rb +71 -44
  2. data/lib/engineyard/cli/recipes.rb +17 -17
  3. data/lib/engineyard/cli/ui.rb +4 -8
  4. data/lib/engineyard/cli/web.rb +14 -15
  5. data/lib/engineyard/model/environment.rb +1 -1
  6. data/lib/engineyard/model/instance.rb +16 -4
  7. data/lib/engineyard/thor.rb +24 -18
  8. data/lib/engineyard/vendor/thor.rb +270 -0
  9. data/lib/engineyard/vendor/thor/actions.rb +297 -0
  10. data/lib/engineyard/vendor/thor/actions/create_file.rb +105 -0
  11. data/lib/engineyard/vendor/thor/actions/directory.rb +93 -0
  12. data/lib/engineyard/vendor/thor/actions/empty_directory.rb +134 -0
  13. data/lib/engineyard/vendor/thor/actions/file_manipulation.rb +229 -0
  14. data/lib/engineyard/vendor/thor/actions/inject_into_file.rb +104 -0
  15. data/lib/engineyard/vendor/thor/base.rb +540 -0
  16. data/lib/engineyard/vendor/thor/core_ext/file_binary_read.rb +9 -0
  17. data/lib/engineyard/vendor/thor/core_ext/hash_with_indifferent_access.rb +75 -0
  18. data/lib/engineyard/vendor/thor/core_ext/ordered_hash.rb +100 -0
  19. data/lib/engineyard/vendor/thor/error.rb +30 -0
  20. data/lib/engineyard/vendor/thor/group.rb +271 -0
  21. data/lib/engineyard/vendor/thor/invocation.rb +180 -0
  22. data/lib/engineyard/vendor/thor/parser.rb +4 -0
  23. data/lib/engineyard/vendor/thor/parser/argument.rb +67 -0
  24. data/lib/engineyard/vendor/thor/parser/arguments.rb +161 -0
  25. data/lib/engineyard/vendor/thor/parser/option.rb +128 -0
  26. data/lib/engineyard/vendor/thor/parser/options.rb +164 -0
  27. data/lib/engineyard/vendor/thor/rake_compat.rb +66 -0
  28. data/lib/engineyard/vendor/thor/runner.rb +314 -0
  29. data/lib/engineyard/vendor/thor/shell.rb +83 -0
  30. data/lib/engineyard/vendor/thor/shell/basic.rb +268 -0
  31. data/lib/engineyard/vendor/thor/shell/color.rb +108 -0
  32. data/lib/engineyard/vendor/thor/task.rb +102 -0
  33. data/lib/engineyard/vendor/thor/util.rb +229 -0
  34. data/lib/engineyard/vendor/thor/version.rb +3 -0
  35. data/lib/engineyard/version.rb +1 -1
  36. data/spec/ey/deploy_spec.rb +24 -5
  37. data/spec/ey/ey_spec.rb +1 -1
  38. data/spec/ey/rollback_spec.rb +7 -0
  39. data/spec/spec_helper.rb +13 -0
  40. data/spec/support/git_repo.rb +10 -7
  41. data/spec/support/helpers.rb +2 -2
  42. metadata +30 -4
  43. data/lib/engineyard/cli/thor_fixes.rb +0 -26
@@ -17,17 +17,17 @@ module EY
17
17
  super
18
18
  end
19
19
 
20
- desc "deploy [--environment ENVIRONMENT] [--ref GIT-REF]", <<-DESC
21
- Deploy specified branch/tag/sha to specified environment.
22
-
23
- This command must be run with the current directory containing the app to be
24
- deployed. If ey.yml specifies a default branch then the ref parameter can be
25
- omitted. Furthermore, if a default branch is specified but a different command
26
- is supplied the deploy will fail unless --force is used.
27
-
28
- Migrations are run by default with 'rake db:migrate'. A different command can be
29
- specified via --migrate "ruby do_migrations.rb". Migrations can also be skipped
30
- entirely by using --no-migrate.
20
+ desc "deploy [--environment ENVIRONMENT] [--ref GIT-REF]",
21
+ "Deploy specified branch, tag, or sha to specified environment."
22
+ long_desc <<-DESC
23
+ This command must be run with the current directory containing the app to be
24
+ deployed. If ey.yml specifies a default branch then the ref parameter can be
25
+ omitted. Furthermore, if a default branch is specified but a different command
26
+ is supplied the deploy will fail unless --force is used.
27
+
28
+ Migrations are run by default with 'rake db:migrate'. A different command can be
29
+ specified via --migrate "ruby do_migrations.rb". Migrations can also be skipped
30
+ entirely by using --no-migrate.
31
31
  DESC
32
32
  method_option :force, :type => :boolean, :aliases => %w(-f),
33
33
  :desc => "Force a deploy of the specified branch even if a default is set"
@@ -70,11 +70,10 @@ entirely by using --no-migrate.
70
70
  raise exists ? EnvironmentUnlinkedError.new(options[:environment]) : e
71
71
  end
72
72
 
73
- desc "environments [--all]", <<-DESC
74
- List environments.
75
-
76
- By default, environments for this app are displayed. If the -all option is
77
- used, all environments are displayed instead.
73
+ desc "environments [--all]", "List environments."
74
+ long_desc <<-DESC
75
+ By default, environments for this app are displayed. The --all option will
76
+ display all environments, including those for this app.
78
77
  DESC
79
78
 
80
79
  method_option :all, :type => :boolean, :aliases => %(-a)
@@ -85,16 +84,15 @@ used, all environments are displayed instead.
85
84
  end
86
85
  map "envs" => :environments
87
86
 
88
- desc "rebuild [--environment ENVIRONMENT]", <<-DESC
89
- Rebuild specified environment.
87
+ desc "rebuild [--environment ENVIRONMENT]", "Rebuild specified environment."
88
+ long_desc <<-DESC
89
+ Engine Yard's main configuration run occurs on all servers. Mainly used to fix
90
+ failed configuration of new or existing servers, or to update servers to latest
91
+ Engine Yard stack (e.g. to apply an Engine Yard supplied security
92
+ patch).
90
93
 
91
- Engine Yard's main configuration run occurs on all servers. Mainly used to fix
92
- failed configuration of new or existing servers, or to update servers to latest
93
- Engine Yard stack (e.g. to apply an Engine Yard supplied security
94
- patch).
95
-
96
- Note that uploaded recipes are also run after the main configuration run has
97
- successfully completed.
94
+ Note that uploaded recipes are also run after the main configuration run has
95
+ successfully completed.
98
96
  DESC
99
97
 
100
98
  method_option :environment, :type => :string, :aliases => %w(-e),
@@ -105,12 +103,11 @@ successfully completed.
105
103
  env.rebuild
106
104
  end
107
105
 
108
- desc "rollback [--environment ENVIRONMENT]", <<-DESC
109
- Rollback to the previous deploy.
110
-
111
- Uses code from previous deploy in the "/data/APP_NAME/releases" directory on
112
- remote server(s) to restart application servers.
113
- DESC
106
+ desc "rollback [--environment ENVIRONMENT]", "Rollback to the previous deploy."
107
+ long_desc <<-DESC
108
+ Uses code from previous deploy in the "/data/APP_NAME/releases" directory on
109
+ remote server(s) to restart application servers.
110
+ DESC
114
111
  method_option :environment, :type => :string, :aliases => %w(-e),
115
112
  :desc => "Environment in which to roll back the current application"
116
113
  def rollback
@@ -127,11 +124,10 @@ remote server(s) to restart application servers.
127
124
  end
128
125
  end
129
126
 
130
- desc "ssh [--environment ENVIRONMENT]", <<-DESC
131
- Open an ssh session.
132
-
133
- If the environment contains just one server, a session to it will be opened. For
134
- environments with clusters, a session will be opened to the application master.
127
+ desc "ssh [--environment ENVIRONMENT]", "Open an ssh session."
128
+ long_desc <<-DESC
129
+ If the environment contains just one server, a session to it will be opened. For
130
+ environments with clusters, a session will be opened to the application master.
135
131
  DESC
136
132
  method_option :environment, :type => :string, :aliases => %w(-e),
137
133
  :desc => "Environment to ssh into"
@@ -145,12 +141,11 @@ environments with clusters, a session will be opened to the application master.
145
141
  end
146
142
  end
147
143
 
148
- desc "logs [--environment ENVIRONMENT]", <<-DESC
149
- Retrieve the latest logs for an environment.
150
-
151
- Displays Engine Yard configuration logs for all servers in the environment. If
152
- recipes were uploaded to the environment & run, their logs will also be
153
- displayed beneath the main configuration logs.
144
+ desc "logs [--environment ENVIRONMENT]", "Retrieve the latest logs for an environment."
145
+ long_desc <<-DESC
146
+ Displays Engine Yard configuration logs for all servers in the environment. If
147
+ recipes were uploaded to the environment & run, their logs will also be
148
+ displayed beneath the main configuration logs.
154
149
  DESC
155
150
  method_option :environment, :type => :string, :aliases => %w(-e),
156
151
  :desc => "Environment with the interesting logs"
@@ -186,13 +181,45 @@ displayed beneath the main configuration logs.
186
181
  desc "help [COMMAND]", "Describe all commands or one specific command."
187
182
  def help(*cmds)
188
183
  if cmds.empty?
189
- super
190
- EY.ui.say "See '#{self.class.send(:banner_base)} help [COMMAND]' for more information on a specific command."
184
+ base = self.class.send(:banner_base)
185
+ list = self.class.printable_tasks
186
+
187
+ EY.ui.say "Usage:"
188
+ EY.ui.say " #{base} [--help] [--version] COMMAND [ARGS]"
189
+ EY.ui.say
190
+
191
+ EY.ui.say "Deploy commands:"
192
+ deploy_cmds = %w(deploy environments logs rebuild rollback)
193
+ deploy_cmds.map! do |name|
194
+ list.find{|task| task[0] =~ /^#{base} #{name}/ }
195
+ end
196
+ list -= deploy_cmds
197
+ EY.ui.print_help(deploy_cmds)
198
+ EY.ui.say
199
+
200
+ EY::Thor.subcommands.each do |name, klass|
201
+ list.reject!{|cmd| cmd[0] =~ /^#{base} #{name}/}
202
+ EY.ui.say "#{name.capitalize} commands:"
203
+ tasks = klass.printable_tasks.reject{|t| t[0] =~ /help$/ }
204
+ EY.ui.print_help(tasks)
205
+ EY.ui.say
206
+ end
207
+
208
+ %w(help version).each{|n| list.reject!{|c| c[0] =~ /^#{base} #{n}/ } }
209
+ if list.any?
210
+ EY.ui.say "Other commands:"
211
+ EY.ui.print_help(list)
212
+ EY.ui.say
213
+ end
214
+
215
+ self.class.send(:class_options_help, shell)
216
+ EY.ui.say "See '#{base} help COMMAND' for more information on a specific command."
191
217
  elsif klass = EY::Thor.subcommands[cmds.first]
192
218
  klass.new.help(*cmds[1..-1])
193
219
  else
194
220
  super
195
221
  end
196
222
  end
223
+
197
224
  end # CLI
198
225
  end # EY
@@ -1,11 +1,11 @@
1
1
  module EY
2
2
  class CLI
3
3
  class Recipes < EY::Thor
4
- desc "recipes apply [ENVIRONMENT]", <<-DESC
5
- Run uploaded chef recipes on specified environment.
6
-
7
- This is similar to '#{banner_base} rebuild' except Engine Yard's main
8
- configuration step is skipped.
4
+ desc "apply [--environment ENVIRONMENT]",
5
+ "Run uploaded chef recipes on specified environment."
6
+ long_desc <<-DESC
7
+ This is similar to '#{banner_base} rebuild' except Engine Yard's main
8
+ configuration step is skipped.
9
9
  DESC
10
10
 
11
11
  method_option :environment, :type => :string, :aliases => %w(-e),
@@ -16,11 +16,11 @@ configuration step is skipped.
16
16
  EY.ui.say "Uploaded recipes started for #{environment.name}"
17
17
  end
18
18
 
19
- desc "recipes upload [ENVIRONMENT]", <<-DESC
20
- Upload custom chef recipes to specified environment.
21
-
22
- The current directory should contain a subdirectory named "cookbooks" to be
23
- uploaded.
19
+ desc "upload [--environment ENVIRONMENT]",
20
+ "Upload custom chef recipes to specified environment."
21
+ long_desc <<-DESC
22
+ The current directory should contain a subdirectory named "cookbooks" to be
23
+ uploaded.
24
24
  DESC
25
25
 
26
26
  method_option :environment, :type => :string, :aliases => %w(-e),
@@ -31,14 +31,14 @@ uploaded.
31
31
  EY.ui.say "Recipes uploaded successfully for #{environment.name}"
32
32
  end
33
33
 
34
- desc "recipes download [--environment ENVIRONMENT]", <<-DESC
35
- Download custom chef recipes from ENVIRONMENT into the current directory.
34
+ desc "download [--environment ENVIRONMENT]",
35
+ "Download custom chef recipes from ENVIRONMENT into the current directory."
36
+ long_desc <<-DESC
37
+ The recipes will be unpacked into a directory called "cookbooks" in the
38
+ current directory.
36
39
 
37
- The recipes will be unpacked into a directory called "cookbooks" in the
38
- current directory.
39
-
40
- If the cookbooks directory already exists, an error will be raised.
41
- DESC
40
+ If the cookbooks directory already exists, an error will be raised.
41
+ DESC
42
42
  method_option :environment, :type => :string, :aliases => %w(-e),
43
43
  :desc => "Environment for which to download the recipes"
44
44
  def download
@@ -43,14 +43,6 @@ module EY
43
43
  end
44
44
  end
45
45
 
46
- def say(*args)
47
- if args.first == "Tasks:"
48
- super "Commands:"
49
- else
50
- super
51
- end
52
- end
53
-
54
46
  def ask(message, password = false)
55
47
  begin
56
48
  require 'highline'
@@ -104,6 +96,10 @@ module EY
104
96
  end
105
97
  end
106
98
 
99
+ def print_help(table)
100
+ print_table(table, :ident => 2, :truncate => true, :colwidth => 20)
101
+ end
102
+
107
103
  def set_color(string, color, bold=false)
108
104
  $stdout.tty? ? super : string
109
105
  end
@@ -1,9 +1,8 @@
1
1
  module EY
2
2
  class CLI
3
3
  class Web < EY::Thor
4
- desc "web enable [ENVIRONMENT]", <<-HELP
5
- Take down the maintenance page for the current application in the specified environment.
6
- HELP
4
+ desc "enable [--environment/-e ENVIRONMENT]",
5
+ "Remove the maintenance page for this application in the given environment."
7
6
  method_option :environment, :type => :string, :aliases => %w(-e),
8
7
  :desc => "Environment on which to put up the maintenance page"
9
8
  def enable
@@ -14,19 +13,19 @@ Take down the maintenance page for the current application in the specified envi
14
13
  environment.take_down_maintenance_page(app)
15
14
  end
16
15
 
17
- desc "web disable [ENVIRONMENT]", <<-HELP
18
- Put up the maintenance page for the current application in the specified environment.
16
+ desc "disable [--environment/-e ENVIRONMENT]",
17
+ "Put up the maintenance page for this application in the given environment."
18
+ long_desc <<-DESC
19
+ The maintenance page is taken from the app currently being deployed. This means
20
+ that you can customize maintenance pages to tell users the reason for downtime
21
+ on every particular deploy.
19
22
 
20
- The maintenance page is taken from the app currently being deployed. This means
21
- that you can customize maintenance pages to tell users the reason for downtime
22
- on every particular deploy.
23
-
24
- Maintenance pages searched for in order of decreasing priority:
25
- * public/maintenance.html.custom
26
- * public/maintenance.html.tmp
27
- * public/maintenance.html
28
- * public/system/maintenance.html.default
29
- HELP
23
+ Maintenance pages searched for in order of decreasing priority:
24
+ * public/maintenance.html.custom
25
+ * public/maintenance.html.tmp
26
+ * public/maintenance.html
27
+ * public/system/maintenance.html.default
28
+ DESC
30
29
  method_option :environment, :type => :string, :aliases => %w(-e),
31
30
  :desc => "Environment on which to take down the maintenance page"
32
31
  def disable
@@ -1,6 +1,6 @@
1
1
  module EY
2
2
  module Model
3
- class Environment < ApiStruct.new(:id, :name, :instances, :instances_count, :apps, :app_master, :username, :api)
3
+ class Environment < ApiStruct.new(:id, :name, :instances, :instances_count, :apps, :app_master, :username, :stack_name, :api)
4
4
  def self.from_hash(hash)
5
5
  super.tap do |env|
6
6
  env.username = hash['ssh_username']
@@ -32,8 +32,9 @@ exit(17) # required_version < current_version
32
32
 
33
33
  def deploy(app, ref, migration_command=nil, extra_configuration=nil)
34
34
  deploy_args = [
35
- '--app', app.name,
36
- '--repo', app.repository_uri,
35
+ '--app', app.name,
36
+ '--repo', app.repository_uri,
37
+ '--stack', environment.stack_name,
37
38
  '--branch', ref,
38
39
  ]
39
40
 
@@ -49,7 +50,10 @@ exit(17) # required_version < current_version
49
50
  end
50
51
 
51
52
  def rollback(app, extra_configuration=nil)
52
- deploy_args = ['rollback', '--app', app.name]
53
+ deploy_args = ['rollback',
54
+ '--app', app.name,
55
+ '--stack', environment.stack_name,
56
+ ]
53
57
 
54
58
  if extra_configuration
55
59
  deploy_args << '--config' << extra_configuration.to_json
@@ -99,7 +103,15 @@ exit(17) # required_version < current_version
99
103
  end
100
104
 
101
105
  def install_ey_deploy
102
- ssh(Escape.shell_command(['sudo', gem_path, 'install', 'ey-deploy', '--no-rdoc', '--no-ri', '-v', EYSD_VERSION]))
106
+ ssh(Escape.shell_command([
107
+ 'sudo', 'sh', '-c',
108
+ # rubygems looks at *.gem in its current directory for
109
+ # installation candidates, so we have to make sure it
110
+ # runs from a directory with no gem files in it.
111
+ #
112
+ # rubygems help suggests that --remote will disable this
113
+ # behavior, but it doesn't.
114
+ "cd `mktemp -d` && #{gem_path} install ey-deploy --no-rdoc --no-ri -v '#{EYSD_VERSION}'"]))
103
115
  end
104
116
 
105
117
  def upgrade_ey_deploy
@@ -1,5 +1,5 @@
1
+ $LOAD_PATH.unshift File.expand_path("../vendor", __FILE__)
1
2
  require 'thor'
2
- require 'engineyard/cli/thor_fixes'
3
3
 
4
4
  module EY
5
5
  class Thor < ::Thor
@@ -21,17 +21,22 @@ module EY
21
21
  end
22
22
 
23
23
  def self.subcommand_help(cmd)
24
- desc "help #{cmd} [SUBCOMMAND]", "Describe all subcommands or one specific subcommand."
25
-
24
+ desc "#{cmd} help [COMMAND]", "Describe all subcommands or one specific subcommand."
26
25
  class_eval <<-RUBY
27
- def help(*args)
28
- super
29
- if args.empty?
30
- banner = "See '" + self.class.send(:banner_base) + " help #{cmd} [SUBCOMMAND]' "
31
- text = "for more information on a specific subcommand."
32
- EY.ui.say banner + text
33
- end
34
- end
26
+ def help(*args)
27
+ if args.empty?
28
+ EY.ui.say "usage: #{banner_base} #{cmd} COMMAND"
29
+ EY.ui.say
30
+ subcommands = self.class.printable_tasks.sort_by{|s| s[0] }
31
+ subcommands.reject!{|t| t[0] =~ /#{cmd} help$/}
32
+ EY.ui.print_help(subcommands)
33
+ EY.ui.say self.class.send(:class_options_help, EY.ui)
34
+ EY.ui.say "See #{banner_base} #{cmd} help COMMAND" +
35
+ " for more information on a specific subcommand." if args.empty?
36
+ else
37
+ super
38
+ end
39
+ end
35
40
  RUBY
36
41
  end
37
42
 
@@ -39,13 +44,14 @@ end
39
44
  @@original_args[1..-1]
40
45
  end
41
46
 
42
- def self.printable_tasks(all=true)
43
- (all ? all_tasks : tasks).map do |_, task|
44
- item = []
45
- item << banner(task)
46
- item << (task.description ? "# #{task.description.gsub(/\n.*/,'')}" : "")
47
- item
48
- end
47
+ def self.banner_base
48
+ "ey"
49
+ end
50
+
51
+ def self.banner(task, task_help = false)
52
+ scmd = EY::Thor.subcommands.invert[self]
53
+ task = (task_help ? task.formatted_usage(self, false) : task.name)
54
+ [banner_base, scmd, task].compact.join(" ")
49
55
  end
50
56
  end
51
57
 
@@ -0,0 +1,270 @@
1
+ require 'thor/base'
2
+
3
+ # TODO: Update thor to allow for git-style CLI (git bisect run)
4
+ class Thor
5
+ class << self
6
+ # Sets the default task when thor is executed without an explicit task to be called.
7
+ #
8
+ # ==== Parameters
9
+ # meth<Symbol>:: name of the defaut task
10
+ #
11
+ def default_task(meth=nil)
12
+ case meth
13
+ when :none
14
+ @default_task = 'help'
15
+ when nil
16
+ @default_task ||= from_superclass(:default_task, 'help')
17
+ else
18
+ @default_task = meth.to_s
19
+ end
20
+ end
21
+
22
+ # Defines the usage and the description of the next task.
23
+ #
24
+ # ==== Parameters
25
+ # usage<String>
26
+ # description<String>
27
+ #
28
+ def desc(usage, description, options={})
29
+ if options[:for]
30
+ task = find_and_refresh_task(options[:for])
31
+ task.usage = usage if usage
32
+ task.description = description if description
33
+ else
34
+ @usage, @desc = usage, description
35
+ end
36
+ end
37
+
38
+ # Defines the long description of the next task.
39
+ #
40
+ # ==== Parameters
41
+ # long description<String>
42
+ #
43
+ def long_desc(long_description, options={})
44
+ if options[:for]
45
+ task = find_and_refresh_task(options[:for])
46
+ task.long_description = long_description if long_description
47
+ else
48
+ @long_desc = long_description
49
+ end
50
+ end
51
+
52
+ # Maps an input to a task. If you define:
53
+ #
54
+ # map "-T" => "list"
55
+ #
56
+ # Running:
57
+ #
58
+ # thor -T
59
+ #
60
+ # Will invoke the list task.
61
+ #
62
+ # ==== Parameters
63
+ # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given task.
64
+ #
65
+ def map(mappings=nil)
66
+ @map ||= from_superclass(:map, {})
67
+
68
+ if mappings
69
+ mappings.each do |key, value|
70
+ if key.respond_to?(:each)
71
+ key.each {|subkey| @map[subkey] = value}
72
+ else
73
+ @map[key] = value
74
+ end
75
+ end
76
+ end
77
+
78
+ @map
79
+ end
80
+
81
+ # Declares the options for the next task to be declared.
82
+ #
83
+ # ==== Parameters
84
+ # Hash[Symbol => Object]:: The hash key is the name of the option and the value
85
+ # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric
86
+ # or :required (string). If you give a value, the type of the value is used.
87
+ #
88
+ def method_options(options=nil)
89
+ @method_options ||= {}
90
+ build_options(options, @method_options) if options
91
+ @method_options
92
+ end
93
+
94
+ # Adds an option to the set of method options. If :for is given as option,
95
+ # it allows you to change the options from a previous defined task.
96
+ #
97
+ # def previous_task
98
+ # # magic
99
+ # end
100
+ #
101
+ # method_option :foo => :bar, :for => :previous_task
102
+ #
103
+ # def next_task
104
+ # # magic
105
+ # end
106
+ #
107
+ # ==== Parameters
108
+ # name<Symbol>:: The name of the argument.
109
+ # options<Hash>:: Described below.
110
+ #
111
+ # ==== Options
112
+ # :desc - Description for the argument.
113
+ # :required - If the argument is required or not.
114
+ # :default - Default value for this argument. It cannot be required and have default values.
115
+ # :aliases - Aliases for this option.
116
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
117
+ # :banner - String to show on usage notes.
118
+ #
119
+ def method_option(name, options={})
120
+ scope = if options[:for]
121
+ find_and_refresh_task(options[:for]).options
122
+ else
123
+ method_options
124
+ end
125
+
126
+ build_option(name, options, scope)
127
+ end
128
+
129
+ # Parses the task and options from the given args, instantiate the class
130
+ # and invoke the task. This method is used when the arguments must be parsed
131
+ # from an array. If you are inside Ruby and want to use a Thor class, you
132
+ # can simply initialize it:
133
+ #
134
+ # script = MyScript.new(args, options, config)
135
+ # script.invoke(:task, first_arg, second_arg, third_arg)
136
+ #
137
+ def start(original_args=ARGV, config={})
138
+ super do |given_args|
139
+ meth = given_args.first.to_s
140
+
141
+ if !meth.empty? && (map[meth] || meth !~ /^\-/)
142
+ given_args.shift
143
+ else
144
+ meth = nil
145
+ end
146
+
147
+ meth = normalize_task_name(meth)
148
+ task = all_tasks[meth]
149
+
150
+ if task
151
+ args, opts = Thor::Options.split(given_args)
152
+ config.merge!(:task_options => task.options)
153
+ else
154
+ args, opts = given_args, {}
155
+ end
156
+
157
+ task ||= Thor::Task::Dynamic.new(meth)
158
+ trailing = args[Range.new(arguments.size, -1)]
159
+ new(args, opts, config).invoke(task, trailing || [])
160
+ end
161
+ end
162
+
163
+ # Prints help information for the given task.
164
+ #
165
+ # ==== Parameters
166
+ # shell<Thor::Shell>
167
+ # task_name<String>
168
+ #
169
+ def task_help(shell, task_name)
170
+ meth = normalize_task_name(task_name)
171
+ task = all_tasks[meth]
172
+ handle_no_task_error(meth) unless task
173
+
174
+ shell.say "Usage:"
175
+ shell.say " #{banner(task, true)}"
176
+ shell.say
177
+ class_options_help(shell, nil => task.options.map { |_, o| o })
178
+ if task.long_description
179
+ shell.say "Description:"
180
+ shell.print_wrapped(task.long_description, :ident => 2)
181
+ else
182
+ shell.say task.description
183
+ end
184
+ end
185
+
186
+ # Prints help information for this class.
187
+ #
188
+ # ==== Parameters
189
+ # shell<Thor::Shell>
190
+ #
191
+ def help(shell)
192
+ list = printable_tasks
193
+ Thor::Util.thor_classes_in(self).each do |klass|
194
+ list += klass.printable_tasks(false)
195
+ end
196
+ list.sort!{ |a,b| a[0] <=> b[0] }
197
+
198
+ shell.say "Tasks:"
199
+ shell.print_table(list, :ident => 2, :truncate => true)
200
+ shell.say
201
+ class_options_help(shell)
202
+ end
203
+
204
+ # Returns tasks ready to be printed.
205
+ def printable_tasks(all=true)
206
+ (all ? all_tasks : tasks).map do |_, task|
207
+ item = []
208
+ item << banner(task)
209
+ item << (task.description ? "# #{task.description.gsub(/\s+/m,' ')}" : "")
210
+ item
211
+ end
212
+ end
213
+
214
+ def handle_argument_error(task, error) #:nodoc:
215
+ raise InvocationError, "#{task.name.inspect} was called incorrectly. Call as #{task.formatted_usage(self, banner_base == "thor").inspect}."
216
+ end
217
+
218
+ protected
219
+
220
+ # The banner for this class. You can customize it if you are invoking the
221
+ # thor class by another ways which is not the Thor::Runner. It receives
222
+ # the task that is going to be invoked and a boolean which indicates if
223
+ # the namespace should be displayed as arguments.
224
+ #
225
+ def banner(task, task_help = false)
226
+ "#{banner_base} #{task.formatted_usage(self, banner_base == "thor")}"
227
+ end
228
+
229
+ def baseclass #:nodoc:
230
+ Thor
231
+ end
232
+
233
+ def create_task(meth) #:nodoc:
234
+ if @usage && @desc
235
+ tasks[meth.to_s] = Thor::Task.new(meth, @desc, @long_desc, @usage, method_options)
236
+ @usage, @desc, @long_desc, @method_options = nil
237
+ true
238
+ elsif self.all_tasks[meth.to_s] || meth.to_sym == :method_missing
239
+ true
240
+ else
241
+ puts "[WARNING] Attempted to create task #{meth.inspect} without usage or description. " <<
242
+ "Call desc if you want this method to be available as task or declare it inside a " <<
243
+ "no_tasks{} block. Invoked from #{caller[1].inspect}."
244
+ false
245
+ end
246
+ end
247
+
248
+ def initialize_added #:nodoc:
249
+ class_options.merge!(method_options)
250
+ @method_options = nil
251
+ end
252
+
253
+ # Receives a task name (can be nil), and try to get a map from it.
254
+ # If a map can't be found use the sent name or the default task.
255
+ #
256
+ def normalize_task_name(meth) #:nodoc:
257
+ meth = map[meth.to_s] || meth || default_task
258
+ meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
259
+ end
260
+ end
261
+
262
+ include Thor::Base
263
+
264
+ map HELP_MAPPINGS => :help
265
+
266
+ desc "help [TASK]", "Describe available tasks or one specific task"
267
+ def help(task=nil)
268
+ task ? self.class.task_help(shell, task) : self.class.help(shell)
269
+ end
270
+ end