crazy-yard 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +438 -0
  4. data/bin/ey +9 -0
  5. data/lib/engineyard.rb +9 -0
  6. data/lib/engineyard/cli.rb +816 -0
  7. data/lib/engineyard/cli/api.rb +98 -0
  8. data/lib/engineyard/cli/recipes.rb +129 -0
  9. data/lib/engineyard/cli/ui.rb +275 -0
  10. data/lib/engineyard/cli/web.rb +85 -0
  11. data/lib/engineyard/config.rb +158 -0
  12. data/lib/engineyard/deploy_config.rb +65 -0
  13. data/lib/engineyard/deploy_config/ref.rb +56 -0
  14. data/lib/engineyard/error.rb +82 -0
  15. data/lib/engineyard/eyrc.rb +59 -0
  16. data/lib/engineyard/repo.rb +105 -0
  17. data/lib/engineyard/serverside_runner.rb +159 -0
  18. data/lib/engineyard/templates.rb +6 -0
  19. data/lib/engineyard/templates/ey.yml.erb +196 -0
  20. data/lib/engineyard/templates/ey_yml.rb +119 -0
  21. data/lib/engineyard/thor.rb +215 -0
  22. data/lib/engineyard/version.rb +4 -0
  23. data/lib/vendor/thor/Gemfile +15 -0
  24. data/lib/vendor/thor/LICENSE.md +20 -0
  25. data/lib/vendor/thor/README.md +35 -0
  26. data/lib/vendor/thor/lib/thor.rb +473 -0
  27. data/lib/vendor/thor/lib/thor/actions.rb +318 -0
  28. data/lib/vendor/thor/lib/thor/actions/create_file.rb +105 -0
  29. data/lib/vendor/thor/lib/thor/actions/create_link.rb +60 -0
  30. data/lib/vendor/thor/lib/thor/actions/directory.rb +119 -0
  31. data/lib/vendor/thor/lib/thor/actions/empty_directory.rb +137 -0
  32. data/lib/vendor/thor/lib/thor/actions/file_manipulation.rb +314 -0
  33. data/lib/vendor/thor/lib/thor/actions/inject_into_file.rb +109 -0
  34. data/lib/vendor/thor/lib/thor/base.rb +652 -0
  35. data/lib/vendor/thor/lib/thor/command.rb +136 -0
  36. data/lib/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +80 -0
  37. data/lib/vendor/thor/lib/thor/core_ext/io_binary_read.rb +12 -0
  38. data/lib/vendor/thor/lib/thor/core_ext/ordered_hash.rb +100 -0
  39. data/lib/vendor/thor/lib/thor/error.rb +28 -0
  40. data/lib/vendor/thor/lib/thor/group.rb +282 -0
  41. data/lib/vendor/thor/lib/thor/invocation.rb +172 -0
  42. data/lib/vendor/thor/lib/thor/parser.rb +4 -0
  43. data/lib/vendor/thor/lib/thor/parser/argument.rb +74 -0
  44. data/lib/vendor/thor/lib/thor/parser/arguments.rb +171 -0
  45. data/lib/vendor/thor/lib/thor/parser/option.rb +121 -0
  46. data/lib/vendor/thor/lib/thor/parser/options.rb +218 -0
  47. data/lib/vendor/thor/lib/thor/rake_compat.rb +72 -0
  48. data/lib/vendor/thor/lib/thor/runner.rb +322 -0
  49. data/lib/vendor/thor/lib/thor/shell.rb +88 -0
  50. data/lib/vendor/thor/lib/thor/shell/basic.rb +393 -0
  51. data/lib/vendor/thor/lib/thor/shell/color.rb +148 -0
  52. data/lib/vendor/thor/lib/thor/shell/html.rb +127 -0
  53. data/lib/vendor/thor/lib/thor/util.rb +270 -0
  54. data/lib/vendor/thor/lib/thor/version.rb +3 -0
  55. data/lib/vendor/thor/thor.gemspec +24 -0
  56. data/spec/engineyard/cli/api_spec.rb +50 -0
  57. data/spec/engineyard/cli_spec.rb +28 -0
  58. data/spec/engineyard/config_spec.rb +61 -0
  59. data/spec/engineyard/deploy_config_spec.rb +194 -0
  60. data/spec/engineyard/eyrc_spec.rb +76 -0
  61. data/spec/engineyard/repo_spec.rb +83 -0
  62. data/spec/engineyard_spec.rb +7 -0
  63. data/spec/ey/console_spec.rb +57 -0
  64. data/spec/ey/deploy_spec.rb +435 -0
  65. data/spec/ey/ey_spec.rb +23 -0
  66. data/spec/ey/init_spec.rb +123 -0
  67. data/spec/ey/list_environments_spec.rb +120 -0
  68. data/spec/ey/login_spec.rb +33 -0
  69. data/spec/ey/logout_spec.rb +24 -0
  70. data/spec/ey/logs_spec.rb +36 -0
  71. data/spec/ey/rebuild_spec.rb +18 -0
  72. data/spec/ey/recipes/apply_spec.rb +29 -0
  73. data/spec/ey/recipes/download_spec.rb +43 -0
  74. data/spec/ey/recipes/upload_spec.rb +99 -0
  75. data/spec/ey/rollback_spec.rb +73 -0
  76. data/spec/ey/scp_spec.rb +176 -0
  77. data/spec/ey/servers_spec.rb +209 -0
  78. data/spec/ey/ssh_spec.rb +273 -0
  79. data/spec/ey/status_spec.rb +45 -0
  80. data/spec/ey/timeout_deploy_spec.rb +18 -0
  81. data/spec/ey/web/disable_spec.rb +21 -0
  82. data/spec/ey/web/enable_spec.rb +26 -0
  83. data/spec/ey/web/restart_spec.rb +21 -0
  84. data/spec/ey/whoami_spec.rb +30 -0
  85. data/spec/spec_helper.rb +84 -0
  86. data/spec/support/bundled_ey +7 -0
  87. data/spec/support/fixture_recipes.tgz +0 -0
  88. data/spec/support/git_repos.rb +115 -0
  89. data/spec/support/helpers.rb +330 -0
  90. data/spec/support/matchers.rb +16 -0
  91. data/spec/support/ruby_ext.rb +13 -0
  92. data/spec/support/shared_behavior.rb +278 -0
  93. metadata +411 -0
@@ -0,0 +1,119 @@
1
+ require 'erb'
2
+
3
+ module EY
4
+ module Templates
5
+ class EyYml
6
+ PATH = Pathname.new(__FILE__).dirname.join('ey.yml.erb').freeze
7
+
8
+ attr_reader :existing_config, :existing, :config, :template
9
+
10
+ def initialize(existing, template=PATH)
11
+ @template = template
12
+
13
+ @existing = existing.dup
14
+ @environments = @existing.delete('environments') || {}
15
+ @existing_config = @existing.delete('defaults') || {}
16
+ @config = defaults.merge(@existing_config)
17
+
18
+ fix_config!
19
+ end
20
+
21
+ def to_str
22
+ ERB.new(template.read, 0, "<>").result(binding)
23
+ end
24
+ alias to_s to_str
25
+
26
+ def write(dest)
27
+ dest = Pathname.new(dest)
28
+ dir = dest.dirname
29
+ temp = dir.join("ey.yml.tmp")
30
+
31
+ # generate first so we don't overwrite with a failed generation
32
+ output = to_str
33
+ temp.open('w') { |f| f << output }
34
+
35
+ FileUtils.mv(dest, dir.join("ey.yml.backup")) if dest.exist?
36
+ FileUtils.mv(temp, dest)
37
+ end
38
+
39
+ protected
40
+
41
+ def fix_config!
42
+ if config['migrate'] == nil && existing_config['migration_command']
43
+ config['migrate'] = true
44
+ end
45
+ end
46
+
47
+ def defaults
48
+ {
49
+ "migrate" => Pathname.new('db/migrate').exist?,
50
+ "migration_command" => "rake db:migrate --trace",
51
+ "precompile_assets" => Pathname.new('app/assets').exist?,
52
+ "precompile_assets_task" => "assets:precompile",
53
+ "asset_dependencies" => nil, # %w[app/assets lib/assets vendor/assets Gemfile.lock config/application.rb config/routes.rb],
54
+ "asset_strategy" => "shifting",
55
+ "precompile_unchanged_assets" => false,
56
+ "bundle_without" => nil,
57
+ "bundle_options" => nil,
58
+ "maintenance_on_migrate" => true,
59
+ "maintenance_on_restart" => nil,
60
+ "verbose" => false,
61
+ "ignore_database_adapter_warning" => false,
62
+ }
63
+ end
64
+
65
+ def option_unless_default(key)
66
+ value = config[key]
67
+ if value != nil && value != defaults[key]
68
+ option(key, value)
69
+ else
70
+ commented_option key
71
+ end
72
+ end
73
+
74
+ def option(key, value = nil)
75
+ value ||= config[key]
76
+ dump_indented_yaml key => value
77
+ end
78
+
79
+ def commented_option(key)
80
+ data = {key => defaults[key]}
81
+ " ##{dump_indented_yaml(data, 0)}"
82
+ end
83
+
84
+ def extra_root_options
85
+ out = ""
86
+
87
+ extra_defaults = config.reject { |k,v| defaults.key?(k) }
88
+ extra_defaults.each do |key,val|
89
+ out << option(key, val) << "\n"
90
+ end
91
+
92
+ unless existing.empty?
93
+ out << dump_indented_yaml(existing, 0)
94
+ end
95
+
96
+ out
97
+ end
98
+
99
+ def environment_options
100
+ if @environments && !@environments.empty?
101
+ dump_indented_yaml(@environments)
102
+ end
103
+ end
104
+
105
+ def dump_indented_yaml(data, indent=2)
106
+ YAML.dump(data).sub(/^---/, '').lstrip.gsub(/^/,' '*indent)
107
+ end
108
+
109
+ def string_to_boolean(str)
110
+ case str
111
+ when "true" then true
112
+ when "false" then false
113
+ else str
114
+ end
115
+ end
116
+
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,215 @@
1
+ $:.unshift(File.expand_path('../vendor/thor/lib/', File.dirname(__FILE__)))
2
+ require 'thor'
3
+
4
+ module EY
5
+ module UtilityMethods
6
+ protected
7
+ def api
8
+ @api ||= EY::CLI::API.new(config.endpoint, ui, options[:api_token])
9
+ end
10
+
11
+ def config
12
+ @config ||= EY::Config.new
13
+ end
14
+
15
+ # engineyard gem uses ui everywhere, thore supplies shell
16
+ def ui
17
+ shell
18
+ end
19
+
20
+ def in_repo?
21
+ EY::Repo.exist?
22
+ end
23
+
24
+ def repo
25
+ @repo ||= EY::Repo.new
26
+ end
27
+
28
+ def serverside_version
29
+ respond_to?(:options) && options[:serverside_version] || EY::ENGINEYARD_SERVERSIDE_VERSION
30
+ end
31
+
32
+ def serverside_runner(app_env, verbose, serverside_version = serverside_version(), ignore_bad_bridge = false)
33
+ ServersideRunner.new({
34
+ bridge: app_env.environment.bridge!(ignore_bad_bridge).hostname,
35
+ app: app_env.app,
36
+ environment: app_env.environment,
37
+ verbose: verbose,
38
+ serverside_version: serverside_version
39
+ })
40
+ end
41
+
42
+ def use_default_environment
43
+ if env = config.default_environment
44
+ ui.info "Using default environment #{config.default_environment.inspect} from ey.yml."
45
+ env
46
+ end
47
+ end
48
+
49
+ def fetch_environment(environment_name, account_name)
50
+ ui.info "Loading application data from Engine Yard Cloud..."
51
+
52
+ environment_name ||= use_default_environment
53
+ remotes = repo.remotes if in_repo?
54
+ constraints = {
55
+ environment_name: environment_name,
56
+ account_name: account_name,
57
+ remotes: remotes,
58
+ }
59
+
60
+ resolver = api.resolve_environments(constraints)
61
+
62
+ resolver.one_match { |match| return match }
63
+
64
+ resolver.no_matches do |errors, suggestions|
65
+ raise_no_matches(errors, suggestions)
66
+ end
67
+
68
+ resolver.many_matches do |matches|
69
+ if environment_name
70
+ message = "Multiple environments possible, please be more specific:\n\n"
71
+ matches.each do |env|
72
+ message << "\t#{env.name.ljust(25)} # ey <command> --environment='#{env.name}' --account='#{env.account.name}'\n"
73
+ end
74
+ raise EY::MultipleMatchesError.new(message)
75
+ else
76
+ raise EY::AmbiguousEnvironmentGitUriError.new(matches)
77
+ end
78
+ end
79
+ end
80
+
81
+ def fetch_app_environment(app_name, environment_name, account_name)
82
+ ui.info "Loading application data from Engine Yard Cloud..."
83
+
84
+ environment_name ||= use_default_environment
85
+ remotes = repo.remotes if in_repo?
86
+ constraints = {
87
+ app_name: app_name,
88
+ environment_name: environment_name,
89
+ account_name: account_name,
90
+ remotes: remotes,
91
+ }
92
+
93
+ if constraints.all? { |k,v| v.nil? || v.empty? || v.to_s.empty? }
94
+ raise EY::NoMatchesError.new <<-ERROR
95
+ Unable to find application without a git remote URI or app name.
96
+
97
+ Please specify --app=app_name or add this application at #{config.endpoint}"
98
+ ERROR
99
+ end
100
+
101
+ resolver = api.resolve_app_environments(constraints)
102
+
103
+ resolver.one_match { |match| return match }
104
+ resolver.no_matches do |errors, suggestions|
105
+ raise_no_matches(errors, suggestions)
106
+ end
107
+ resolver.many_matches do |app_envs|
108
+ raise EY::MultipleMatchesError.new(too_many_app_environments_error(app_envs))
109
+ end
110
+ end
111
+
112
+ def raise_no_matches(errors, suggestions)
113
+ message = "We found the following suggestions:\n" if suggestions.any?
114
+
115
+ suggestions.each do |suggest|
116
+ message << " # ey <command> --account='#{suggest['account_name']}' --app='#{suggest['app_name']}' --environment='#{suggest['env_name']}'\n"
117
+ end
118
+
119
+ raise EY::NoMatchesError.new([errors,message].compact.join("\n").strip)
120
+ end
121
+
122
+ def too_many_app_environments_error(app_envs)
123
+ message = "Multiple application environments possible, please be more specific:\n\n"
124
+
125
+ app_envs.group_by do |app_env|
126
+ [app_env.account_name, app_env.app_name]
127
+ end.sort_by { |k,v| k.join }.each do |(account_name, app_name), grouped_app_envs|
128
+ message << "\n"
129
+ message << account_name << "/" << app_name << "\n"
130
+ grouped_app_envs.map { |ae| ae.environment_name }.uniq.sort.each do |env_name|
131
+ message << "\t#{env_name.ljust(25)}"
132
+ message << " # ey <command> --account='#{account_name}' --app='#{app_name}' --environment='#{env_name}'\n"
133
+ end
134
+ end
135
+ message
136
+ end
137
+ end # UtilityMethods
138
+
139
+ class Thor < ::Thor
140
+ include UtilityMethods
141
+
142
+ check_unknown_options!
143
+
144
+ no_tasks do
145
+ def self.subcommand_help(cmd)
146
+ desc "#{cmd} help [COMMAND]", "Describe all subcommands or one specific subcommand."
147
+ class_eval <<-RUBY
148
+ def help(*args)
149
+ if args.empty?
150
+ ui.say "usage: #{banner_base} #{cmd} COMMAND"
151
+ ui.say
152
+ subcommands = self.class.printable_tasks.sort_by{|s| s[0] }
153
+ subcommands.reject!{|t| t[0] =~ /#{cmd} help$/}
154
+ ui.print_help(subcommands)
155
+ ui.say self.class.send(:class_options_help, ui)
156
+ ui.say "See #{banner_base} #{cmd} help COMMAND" +
157
+ " for more information on a specific subcommand." if args.empty?
158
+ else
159
+ super
160
+ end
161
+ end
162
+ RUBY
163
+ end
164
+
165
+ def self.banner_base
166
+ "ey"
167
+ end
168
+
169
+ def self.banner(task, task_help = false, subcommand = false)
170
+ subcommand_banner = to_s.split(/::/).map{|s| s.downcase}[2..-1]
171
+ subcommand_banner = if subcommand_banner.size > 0
172
+ subcommand_banner.join(' ')
173
+ else
174
+ nil
175
+ end
176
+
177
+ task = (task_help ? task.formatted_usage(self, false, subcommand) : task.name)
178
+ [banner_base, subcommand_banner, task].compact.join(" ")
179
+ end
180
+
181
+ def self.handle_no_task_error(task)
182
+ raise UndefinedTaskError, "Could not find command #{task.inspect}."
183
+ end
184
+
185
+ def self.subcommand(name, klass)
186
+ @@subcommand_class_for ||= {}
187
+ @@subcommand_class_for[name] = klass
188
+ super
189
+ end
190
+
191
+ def self.subcommand_class_for(name)
192
+ @@subcommand_class_for ||= {}
193
+ @@subcommand_class_for[name]
194
+ end
195
+
196
+ end
197
+
198
+ protected
199
+
200
+ def self.exit_on_failure?
201
+ true
202
+ end
203
+
204
+ end
205
+
206
+ # patch handle_no_method_error? to work with rubinius' error text.
207
+ class ::Thor::Task
208
+ def handle_no_method_error?(instance, error, caller)
209
+ not_debugging?(instance) && (
210
+ error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/ ||
211
+ error.message =~ /undefined method `#{name}' on an instance of #{Regexp.escape(instance.class.name)}/
212
+ )
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,4 @@
1
+ module EY
2
+ VERSION = '3.2.2'
3
+ ENGINEYARD_SERVERSIDE_VERSION = ENV['ENGINEYARD_SERVERSIDE_VERSION'] || '2.6.3'
4
+ end
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ platforms :mri_18 do
6
+ gem 'ruby-debug', '>= 0.10.3'
7
+ end
8
+
9
+ platforms :mri_19 do
10
+ gem 'ruby-debug19'
11
+ end
12
+
13
+ group :development do
14
+ gem 'pry'
15
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Yehuda Katz, Eric Hodel, et al.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,35 @@
1
+ [![Gem Version](https://badge.fury.io/rb/thor.png)](https://rubygems.org/gems/thor)
2
+ [![Build Status](https://secure.travis-ci.org/wycats/thor.png?branch=master)](http://travis-ci.org/wycats/thor)
3
+ [![Dependency Status](https://gemnasium.com/wycats/thor.png?travis)](https://gemnasium.com/wycats/thor)
4
+ [![Code Climate](https://codeclimate.com/github/wycats/thor.png)](https://codeclimate.com/github/wycats/thor)
5
+ [![Coverage Status](https://coveralls.io/repos/wycats/thor/badge.png?branch=master)](https://coveralls.io/r/wycats/thor)
6
+
7
+ Thor
8
+ ====
9
+
10
+ Description
11
+ -----------
12
+ Thor is a simple and efficient tool for building self-documenting command line
13
+ utilities. It removes the pain of parsing command line options, writing
14
+ "USAGE:" banners, and can also be used as an alternative to the [Rake][rake]
15
+ build tool. The syntax is Rake-like, so it should be familiar to most Rake
16
+ users.
17
+
18
+ [rake]: https://github.com/jimweirich/rake
19
+
20
+ Installation
21
+ ------------
22
+ gem install thor
23
+
24
+ Usage and documentation
25
+ -----------------------
26
+ Please see the [wiki][] for basic usage and other documentation on using Thor. You can also checkout the [official homepage][homepage].
27
+
28
+ [wiki]: https://github.com/wycats/thor/wiki
29
+ [homepage]: http://whatisthor.com/
30
+
31
+ License
32
+ -------
33
+ Released under the MIT License. See the [LICENSE][] file for further details.
34
+
35
+ [license]: LICENSE.md
@@ -0,0 +1,473 @@
1
+ require 'set'
2
+ require 'thor/base'
3
+
4
+ class Thor
5
+ class << self
6
+ # Allows for custom "Command" package naming.
7
+ #
8
+ # === Parameters
9
+ # name<String>
10
+ # options<Hash>
11
+ #
12
+ def package_name(name, options={})
13
+ @package_name = name.nil? || name == '' ? nil : name
14
+ end
15
+
16
+ # Sets the default command when thor is executed without an explicit command to be called.
17
+ #
18
+ # ==== Parameters
19
+ # meth<Symbol>:: name of the default command
20
+ #
21
+ def default_command(meth=nil)
22
+ @default_command = case meth
23
+ when :none
24
+ 'help'
25
+ when nil
26
+ @default_command || from_superclass(:default_command, 'help')
27
+ else
28
+ meth.to_s
29
+ end
30
+ end
31
+ alias default_task default_command
32
+
33
+ # Registers another Thor subclass as a command.
34
+ #
35
+ # ==== Parameters
36
+ # klass<Class>:: Thor subclass to register
37
+ # command<String>:: Subcommand name to use
38
+ # usage<String>:: Short usage for the subcommand
39
+ # description<String>:: Description for the subcommand
40
+ def register(klass, subcommand_name, usage, description, options={})
41
+ if klass <= Thor::Group
42
+ desc usage, description, options
43
+ define_method(subcommand_name) { |*args| invoke(klass, args) }
44
+ else
45
+ desc usage, description, options
46
+ subcommand subcommand_name, klass
47
+ end
48
+ end
49
+
50
+ # Defines the usage and the description of the next command.
51
+ #
52
+ # ==== Parameters
53
+ # usage<String>
54
+ # description<String>
55
+ # options<String>
56
+ #
57
+ def desc(usage, description, options={})
58
+ if options[:for]
59
+ command = find_and_refresh_command(options[:for])
60
+ command.usage = usage if usage
61
+ command.description = description if description
62
+ else
63
+ @usage, @desc, @hide = usage, description, options[:hide] || false
64
+ end
65
+ end
66
+
67
+ # Defines the long description of the next command.
68
+ #
69
+ # ==== Parameters
70
+ # long description<String>
71
+ #
72
+ def long_desc(long_description, options={})
73
+ if options[:for]
74
+ command = find_and_refresh_command(options[:for])
75
+ command.long_description = long_description if long_description
76
+ else
77
+ @long_desc = long_description
78
+ end
79
+ end
80
+
81
+ # Maps an input to a command. If you define:
82
+ #
83
+ # map "-T" => "list"
84
+ #
85
+ # Running:
86
+ #
87
+ # thor -T
88
+ #
89
+ # Will invoke the list command.
90
+ #
91
+ # ==== Parameters
92
+ # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given command.
93
+ #
94
+ def map(mappings=nil)
95
+ @map ||= from_superclass(:map, {})
96
+
97
+ if mappings
98
+ mappings.each do |key, value|
99
+ if key.respond_to?(:each)
100
+ key.each {|subkey| @map[subkey] = value}
101
+ else
102
+ @map[key] = value
103
+ end
104
+ end
105
+ end
106
+
107
+ @map
108
+ end
109
+
110
+ # Declares the options for the next command to be declared.
111
+ #
112
+ # ==== Parameters
113
+ # Hash[Symbol => Object]:: The hash key is the name of the option and the value
114
+ # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric
115
+ # or :required (string). If you give a value, the type of the value is used.
116
+ #
117
+ def method_options(options=nil)
118
+ @method_options ||= {}
119
+ build_options(options, @method_options) if options
120
+ @method_options
121
+ end
122
+
123
+ alias options method_options
124
+
125
+ # Adds an option to the set of method options. If :for is given as option,
126
+ # it allows you to change the options from a previous defined command.
127
+ #
128
+ # def previous_command
129
+ # # magic
130
+ # end
131
+ #
132
+ # method_option :foo => :bar, :for => :previous_command
133
+ #
134
+ # def next_command
135
+ # # magic
136
+ # end
137
+ #
138
+ # ==== Parameters
139
+ # name<Symbol>:: The name of the argument.
140
+ # options<Hash>:: Described below.
141
+ #
142
+ # ==== Options
143
+ # :desc - Description for the argument.
144
+ # :required - If the argument is required or not.
145
+ # :default - Default value for this argument. It cannot be required and have default values.
146
+ # :aliases - Aliases for this option.
147
+ # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
148
+ # :banner - String to show on usage notes.
149
+ # :hide - If you want to hide this option from the help.
150
+ #
151
+ def method_option(name, options={})
152
+ scope = if options[:for]
153
+ find_and_refresh_command(options[:for]).options
154
+ else
155
+ method_options
156
+ end
157
+
158
+ build_option(name, options, scope)
159
+ end
160
+ alias option method_option
161
+
162
+ # Prints help information for the given command.
163
+ #
164
+ # ==== Parameters
165
+ # shell<Thor::Shell>
166
+ # command_name<String>
167
+ #
168
+ def command_help(shell, command_name)
169
+ meth = normalize_command_name(command_name)
170
+ command = all_commands[meth]
171
+ handle_no_command_error(meth) unless command
172
+
173
+ shell.say "Usage:"
174
+ shell.say " #{banner(command)}"
175
+ shell.say
176
+ class_options_help(shell, nil => command.options.map { |_, o| o })
177
+ if command.long_description
178
+ shell.say "Description:"
179
+ shell.print_wrapped(command.long_description, :indent => 2)
180
+ else
181
+ shell.say command.description
182
+ end
183
+ end
184
+ alias task_help command_help
185
+
186
+ # Prints help information for this class.
187
+ #
188
+ # ==== Parameters
189
+ # shell<Thor::Shell>
190
+ #
191
+ def help(shell, subcommand = false)
192
+ list = printable_commands(true, subcommand)
193
+ Thor::Util.thor_classes_in(self).each do |klass|
194
+ list += klass.printable_commands(false)
195
+ end
196
+ list.sort!{ |a,b| a[0] <=> b[0] }
197
+
198
+ if @package_name
199
+ shell.say "#{@package_name} commands:"
200
+ else
201
+ shell.say "Commands:"
202
+ end
203
+
204
+ shell.print_table(list, :indent => 2, :truncate => true)
205
+ shell.say
206
+ class_options_help(shell)
207
+ end
208
+
209
+ # Returns commands ready to be printed.
210
+ def printable_commands(all = true, subcommand = false)
211
+ (all ? all_commands : commands).map do |_, command|
212
+ next if command.hidden?
213
+ item = []
214
+ item << banner(command, false, subcommand)
215
+ item << (command.description ? "# #{command.description.gsub(/\s+/m,' ')}" : "")
216
+ item
217
+ end.compact
218
+ end
219
+ alias printable_tasks printable_commands
220
+
221
+ def subcommands
222
+ @subcommands ||= from_superclass(:subcommands, [])
223
+ end
224
+ alias subtasks subcommands
225
+
226
+ def subcommand(subcommand, subcommand_class)
227
+ self.subcommands << subcommand.to_s
228
+ subcommand_class.subcommand_help subcommand
229
+
230
+ define_method(subcommand) do |*args|
231
+ args, opts = Thor::Arguments.split(args)
232
+ invoke subcommand_class, args, opts, :invoked_via_subcommand => true, :class_options => options
233
+ end
234
+ end
235
+ alias subtask subcommand
236
+
237
+ # Extend check unknown options to accept a hash of conditions.
238
+ #
239
+ # === Parameters
240
+ # options<Hash>: A hash containing :only and/or :except keys
241
+ def check_unknown_options!(options={})
242
+ @check_unknown_options ||= Hash.new
243
+ options.each do |key, value|
244
+ if value
245
+ @check_unknown_options[key] = Array(value)
246
+ else
247
+ @check_unknown_options.delete(key)
248
+ end
249
+ end
250
+ @check_unknown_options
251
+ end
252
+
253
+ # Overwrite check_unknown_options? to take subcommands and options into account.
254
+ def check_unknown_options?(config) #:nodoc:
255
+ options = check_unknown_options
256
+ return false unless options
257
+
258
+ command = config[:current_command]
259
+ return true unless command
260
+
261
+ name = command.name
262
+
263
+ if subcommands.include?(name)
264
+ false
265
+ elsif options[:except]
266
+ !options[:except].include?(name.to_sym)
267
+ elsif options[:only]
268
+ options[:only].include?(name.to_sym)
269
+ else
270
+ true
271
+ end
272
+ end
273
+
274
+ # Stop parsing of options as soon as an unknown option or a regular
275
+ # argument is encountered. All remaining arguments are passed to the command.
276
+ # This is useful if you have a command that can receive arbitrary additional
277
+ # options, and where those additional options should not be handled by
278
+ # Thor.
279
+ #
280
+ # ==== Example
281
+ #
282
+ # To better understand how this is useful, let's consider a command that calls
283
+ # an external command. A user may want to pass arbitrary options and
284
+ # arguments to that command. The command itself also accepts some options,
285
+ # which should be handled by Thor.
286
+ #
287
+ # class_option "verbose", :type => :boolean
288
+ # stop_on_unknown_option! :exec
289
+ # check_unknown_options! :except => :exec
290
+ #
291
+ # desc "exec", "Run a shell command"
292
+ # def exec(*args)
293
+ # puts "diagnostic output" if options[:verbose]
294
+ # Kernel.exec(*args)
295
+ # end
296
+ #
297
+ # Here +exec+ can be called with +--verbose+ to get diagnostic output,
298
+ # e.g.:
299
+ #
300
+ # $ thor exec --verbose echo foo
301
+ # diagnostic output
302
+ # foo
303
+ #
304
+ # But if +--verbose+ is given after +echo+, it is passed to +echo+ instead:
305
+ #
306
+ # $ thor exec echo --verbose foo
307
+ # --verbose foo
308
+ #
309
+ # ==== Parameters
310
+ # Symbol ...:: A list of commands that should be affected.
311
+ def stop_on_unknown_option!(*command_names)
312
+ @stop_on_unknown_option ||= Set.new
313
+ @stop_on_unknown_option.merge(command_names)
314
+ end
315
+
316
+ def stop_on_unknown_option?(command) #:nodoc:
317
+ !!@stop_on_unknown_option && @stop_on_unknown_option.include?(command.name.to_sym)
318
+ end
319
+
320
+ protected
321
+
322
+ # The method responsible for dispatching given the args.
323
+ def dispatch(meth, given_args, given_opts, config) #:nodoc:
324
+ # There is an edge case when dispatching from a subcommand.
325
+ # A problem occurs invoking the default command. This case occurs
326
+ # when arguments are passed and a default command is defined, and
327
+ # the first given_args does not match the default command.
328
+ # Thor use "help" by default so we skip that case.
329
+ # Note the call to retrieve_command_name. It's called with
330
+ # given_args.dup since that method calls args.shift. Then lookup
331
+ # the command normally. If the first item in given_args is not
332
+ # a command then use the default command. The given_args will be
333
+ # intact later since dup was used.
334
+ if config[:invoked_via_subcommand] && given_args.size >= 1 && default_command != "help" && given_args.first != default_command
335
+ meth ||= retrieve_command_name(given_args.dup)
336
+ command = all_commands[normalize_command_name(meth)]
337
+ command ||= all_commands[normalize_command_name(default_command)]
338
+ else
339
+ meth ||= retrieve_command_name(given_args)
340
+ command = all_commands[normalize_command_name(meth)]
341
+ end
342
+
343
+ if command
344
+ args, opts = Thor::Options.split(given_args)
345
+ if stop_on_unknown_option?(command) && !args.empty?
346
+ # given_args starts with a non-option, so we treat everything as
347
+ # ordinary arguments
348
+ args.concat opts
349
+ opts.clear
350
+ end
351
+ else
352
+ args, opts = given_args, nil
353
+ command = Thor::DynamicCommand.new(meth)
354
+ end
355
+
356
+ opts = given_opts || opts || []
357
+ config.merge!(:current_command => command, :command_options => command.options)
358
+
359
+ instance = new(args, opts, config)
360
+ yield instance if block_given?
361
+ args = instance.args
362
+ trailing = args[Range.new(arguments.size, -1)]
363
+ instance.invoke_command(command, trailing || [])
364
+ end
365
+
366
+ # The banner for this class. You can customize it if you are invoking the
367
+ # thor class by another ways which is not the Thor::Runner. It receives
368
+ # the command that is going to be invoked and a boolean which indicates if
369
+ # the namespace should be displayed as arguments.
370
+ #
371
+ def banner(command, namespace = nil, subcommand = false)
372
+ "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}"
373
+ end
374
+
375
+ def baseclass #:nodoc:
376
+ Thor
377
+ end
378
+
379
+ def create_command(meth) #:nodoc:
380
+ if @usage && @desc
381
+ base_class = @hide ? Thor::HiddenCommand : Thor::Command
382
+ commands[meth] = base_class.new(meth, @desc, @long_desc, @usage, method_options)
383
+ @usage, @desc, @long_desc, @method_options, @hide = nil
384
+ true
385
+ elsif self.all_commands[meth] || meth == "method_missing"
386
+ true
387
+ else
388
+ puts "[WARNING] Attempted to create command #{meth.inspect} without usage or description. " <<
389
+ "Call desc if you want this method to be available as command or declare it inside a " <<
390
+ "no_commands{} block. Invoked from #{caller[1].inspect}."
391
+ false
392
+ end
393
+ end
394
+ alias create_task create_command
395
+
396
+ def initialize_added #:nodoc:
397
+ class_options.merge!(method_options)
398
+ @method_options = nil
399
+ end
400
+
401
+ # Retrieve the command name from given args.
402
+ def retrieve_command_name(args) #:nodoc:
403
+ meth = args.first.to_s unless args.empty?
404
+ if meth && (map[meth] || meth !~ /^\-/)
405
+ args.shift
406
+ else
407
+ nil
408
+ end
409
+ end
410
+ alias retrieve_task_name retrieve_command_name
411
+
412
+ # receives a (possibly nil) command name and returns a name that is in
413
+ # the commands hash. In addition to normalizing aliases, this logic
414
+ # will determine if a shortened command is an unambiguous substring of
415
+ # a command or alias.
416
+ #
417
+ # +normalize_command_name+ also converts names like +animal-prison+
418
+ # into +animal_prison+.
419
+ def normalize_command_name(meth) #:nodoc:
420
+ return default_command.to_s.gsub('-', '_') unless meth
421
+
422
+ possibilities = find_command_possibilities(meth)
423
+ if possibilities.size > 1
424
+ raise ArgumentError, "Ambiguous command #{meth} matches [#{possibilities.join(', ')}]"
425
+ elsif possibilities.size < 1
426
+ meth = meth || default_command
427
+ elsif map[meth]
428
+ meth = map[meth]
429
+ else
430
+ meth = possibilities.first
431
+ end
432
+
433
+ meth.to_s.gsub('-','_') # treat foo-bar as foo_bar
434
+ end
435
+ alias normalize_task_name normalize_command_name
436
+
437
+ # this is the logic that takes the command name passed in by the user
438
+ # and determines whether it is an unambiguous substrings of a command or
439
+ # alias name.
440
+ def find_command_possibilities(meth)
441
+ len = meth.to_s.length
442
+ possibilities = all_commands.merge(map).keys.select { |n| meth == n[0, len] }.sort
443
+ unique_possibilities = possibilities.map { |k| map[k] || k }.uniq
444
+
445
+ if possibilities.include?(meth)
446
+ [meth]
447
+ elsif unique_possibilities.size == 1
448
+ unique_possibilities
449
+ else
450
+ possibilities
451
+ end
452
+ end
453
+ alias find_task_possibilities find_command_possibilities
454
+
455
+ def subcommand_help(cmd)
456
+ desc "help [COMMAND]", "Describe subcommands or one specific subcommand"
457
+ class_eval <<-RUBY
458
+ def help(command = nil, subcommand = true); super; end
459
+ RUBY
460
+ end
461
+ alias subtask_help subcommand_help
462
+
463
+ end
464
+
465
+ include Thor::Base
466
+
467
+ map HELP_MAPPINGS => :help
468
+
469
+ desc "help [COMMAND]", "Describe available commands or one specific command"
470
+ def help(command = nil, subcommand = false)
471
+ command ? self.class.command_help(shell, command) : self.class.help(shell, subcommand)
472
+ end
473
+ end