engineyard 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
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