rudy 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ RUDY, CHANGES
2
+
3
+ #### 0.2 (2009-02-23) ###############################
4
+
5
+ NOTE: This is a complete re-write from 0.1
6
+
7
+ * UPGRADE: drydock 0.3.3
8
+ * NEW: Commands: myaddress, addresses, images, instances, disks, connect release
9
+ * NEW: Metadata storage to SimpleDB
10
+ * NEW: Creates EBS volumes based on startup from metadata
11
+ * NEW: Automated release process
12
+ * NEW: Automated creation of machine images
13
+ * NEW: Partial support for regions and zones
14
+ * NEW: Manage system based on security groups.
15
+ * NEW: "rudy groups" overhaul. Display, creates, destroys groups.
16
+ * CHANGE: Added Environment variables
17
+
18
+
19
+ #### 0.1 (2009-02-06) ###############################
20
+
21
+ * Initial release
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Solutious Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,32 @@
1
+ = Rudy - v0.2 BETA!
2
+
3
+ Rudy is a handy staging and deployment tool for EC2.
4
+
5
+ NOTE: Rudy is not ready for general consumption. Come back in Q2 2009!
6
+
7
+ == Installation
8
+
9
+ # Soon!
10
+
11
+ == More Info
12
+
13
+ * GitHub[http://github.com/solutious/rudy]
14
+ * RubyForge[http://rubyforge.org/projects/rudy]
15
+ * Inspiration[http://www.youtube.com/watch?v=CgaiIW5Rzes]
16
+
17
+
18
+ == Credits
19
+
20
+ * Delano Mandelbaum (delano@solutious.com)
21
+ * Keshia Knight Pulliam (rudy@solutious.com)
22
+
23
+
24
+ == Thanks
25
+
26
+ * The Rilli[http://rilli.com/] team, for the initial use case and support.
27
+ * Alicia Eyre Vasconcelos, for the name ("H-e-l-l-o R.U.D.Y")
28
+
29
+
30
+ == License
31
+
32
+ See: LICENSE.txt
@@ -0,0 +1,68 @@
1
+ require 'rubygems'
2
+ require 'rake/clean'
3
+ require 'rake/gempackagetask'
4
+ require 'hanna/rdoctask'
5
+ require 'fileutils'
6
+ include FileUtils
7
+
8
+ task :default => :package
9
+
10
+ # SPECS ===============================================================
11
+
12
+ # None-yet!
13
+
14
+ # PACKAGE =============================================================
15
+
16
+ name = "rudy"
17
+ load "#{name}.gemspec"
18
+
19
+ version = @spec.version
20
+
21
+ Rake::GemPackageTask.new(@spec) do |p|
22
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
23
+ end
24
+
25
+ task :release => [ :rdoc, :package ]
26
+
27
+ task :install => [ :rdoc, :package ] do
28
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
29
+ end
30
+
31
+ task :uninstall => [ :clean ] do
32
+ sh %{sudo gem uninstall #{name}}
33
+ end
34
+
35
+
36
+ # Rubyforge Release / Publish Tasks ==================================
37
+
38
+ desc 'Publish website to rubyforge'
39
+ task 'publish:rdoc' => 'doc/index.html' do
40
+ sh "scp -rp doc/* rubyforge.org:/var/www/gforge-projects/#{name}/"
41
+ end
42
+
43
+ desc 'Public release to rubyforge'
44
+ task 'publish:gem' => [:package] do |t|
45
+ sh <<-end
46
+ rubyforge add_release -o Any -a CHANGES.txt -f -n README.rdoc #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.gem &&
47
+ rubyforge add_file -o Any -a CHANGES.txt -f -n README.rdoc #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.tgz
48
+ end
49
+ end
50
+
51
+
52
+ Rake::RDocTask.new do |t|
53
+ t.rdoc_dir = 'doc'
54
+ t.title = @spec.summary
55
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
56
+ t.options << '--charset' << 'utf-8'
57
+ t.rdoc_files.include('LICENSE.txt')
58
+ t.rdoc_files.include('README.rdoc')
59
+ t.rdoc_files.include('CHANGES.txt')
60
+ t.rdoc_files.include('bin/*')
61
+ t.rdoc_files.include('lib/*.rb')
62
+ t.rdoc_files.include('lib/**/*.rb')
63
+ end
64
+
65
+ CLEAN.include [ 'pkg', '*.gem', '.config', 'doc' ]
66
+
67
+
68
+
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # Rudy -- Your friend in staging and deploying to EC2
4
+ #
5
+ # See rudy -h for usage
6
+ #
7
+ # AWS commands need the access identifiers. If you don't want
8
+ # to include them as arguments, you can set the following variables
9
+ # in your environment (replace **** with the literal value):
10
+ #
11
+
12
+ #
13
+ # No Ruby 1.9.1 support. Only 1.8.x for now :[
14
+ unless RUBY_VERSION < "1.9"
15
+ puts "Sorry! We're using the right_aws gem and it doesn't support Ruby 1.9 yet."
16
+ exit 1
17
+ end
18
+
19
+ RUDY_HOME = File.join(File.dirname(__FILE__))
20
+ RUDY_LIB = File.join(RUDY_HOME, '..', 'lib')
21
+ $:.unshift RUDY_LIB # Put our local lib in first place
22
+
23
+ require 'rubygems'
24
+ require 'openssl'
25
+
26
+ require 'drydock'
27
+ require 'rudy'
28
+ extend Drydock
29
+
30
+ debug :off
31
+
32
+ default :commands
33
+
34
+
35
+ global_usage "rudy [global options] COMMAND [command options]"
36
+ global_option :A, :access_key, "AWS Access Key", String
37
+ global_option :S, :secret_key, "AWS Secret Access Key", String
38
+
39
+ global_option :p, :position, String, "Position in the machine in its group (default: 01)"
40
+ global_option :u, :user, String, "Provide a username (default: rudy)"
41
+ global_option :e, :environment, String, "Connect to the specified environment (default: stage)"
42
+ global_option :r, :role, String, "Connect to a machine with the specified role (defalt: app)"
43
+
44
+ global_option :V, :version, "Display version number" do
45
+ puts "Rudy version: #{Rudy::VERSION}"
46
+ exit 0
47
+ end
48
+ global_option :v, :verbose, "Increase verbosity of output (i.e. -v or -vv or -vvv)" do
49
+ @verbose ||= 0
50
+ @verbose += 1
51
+ end
52
+
53
+ command :commands do
54
+ puts "Rudy can do all of these things:", ""
55
+ command_names.sort.each do |cmd|
56
+ puts " %16s" % cmd
57
+ end
58
+ puts
59
+ puts "Try: rudy -h OR rudy COMMAND -h"
60
+ puts
61
+ end
62
+
63
+ usage "rudy init"
64
+ command :init => Rudy::Command::Metadata do |obj|
65
+ obj.setup
66
+ end
67
+
68
+ usage "rudy info"
69
+ command :info => Rudy::Command::Metadata do |obj|
70
+ obj.info
71
+ end
72
+
73
+ option :D, :destroy, "Destroy all metadata stored in SimpleDB"
74
+ option :u, :update, "Update the role or environment metadata for the given instance-IDs"
75
+ usage "rudy [global options] metadata instance-ID"
76
+ command :metadata => Rudy::Command::Metadata do |obj, argv|
77
+ obj.print_header
78
+
79
+ if obj.update
80
+ raise "No instance ID!" if argv.empty?
81
+ raise "Nothing to change (see global options -r or -e)" unless obj.role || obj.environment
82
+ obj.update_metadata(argv.first)
83
+ elsif obj.destroy
84
+ exit unless you_are_sure?
85
+ obj.destroy_metadata
86
+ else
87
+ obj.print_metadata(argv.first)
88
+ end
89
+ end
90
+
91
+
92
+ option :i, :instance, String, "Instance ID to associate the id"
93
+ option :A, :associate, "Associate an address to a running instance"
94
+ usage "rudy [global options] addresses [-A -i instance ID] [address]"
95
+ command :addresses => Rudy::Command::Addresses do |obj, argv|
96
+ obj.print_header
97
+
98
+ if obj.associate
99
+ obj.associate_address(argv.first)
100
+ else
101
+ obj.print_addresses
102
+ end
103
+ end
104
+
105
+
106
+
107
+ option :p, :print, "Only print the SSH command, don't connect"
108
+ usage "rudy [-e env] [-u user] connect [-p]"
109
+ command :connect => Rudy::Command::Environment do |obj|
110
+ capture(:stderr) do
111
+ obj.print_header
112
+ obj.connect
113
+ end
114
+ end
115
+
116
+
117
+
118
+ option :all, "Display all disk definitions"
119
+ option :p, :path, String, "The filesystem path to use as the mount point"
120
+ option :d, :device, String, "The device id (default: /dev/sdh)"
121
+ option :s, :size, Integer, "The size of disk (in GB)"
122
+ option :C, :create, "Create a disk definition"
123
+ option :D, :destroy, "Destroy a disk definition"
124
+ option :A, :attach, "Attach a disk"
125
+ usage "rudy [global options] disks [-C -p path -d device -s size] [-A] [-D] [disk name]"
126
+ command :disks => Rudy::Command::Disks do |obj, argv|
127
+ #capture(:stderr) do
128
+ obj.print_header
129
+ if obj.create
130
+ raise "No filesystem path specified" unless obj.path
131
+ raise "No size specified" unless obj.size
132
+ obj.create_disk
133
+ elsif obj.destroy
134
+ raise "No disk specified" if argv.empty?
135
+ exit unless you_are_sure?
136
+ obj.destroy_disk(argv.first)
137
+ elsif obj.attach
138
+ raise "No disk specified" if argv.empty?
139
+ obj.attach_disk(argv.first)
140
+ else
141
+ obj.print_disks
142
+ end
143
+ obj.print_footer
144
+ #end
145
+ end
146
+
147
+ #command :volumes => Rudy::Command::Volumes do |obj|
148
+ # obj.print_volumes
149
+ #end
150
+
151
+ option :all, "Display all instances"
152
+ option :a, :address, String, "Amazon elastic IP"
153
+ option :i, :image, String, "Amazon machine image ID (ami)"
154
+ option :v, :volume, String, "Amazon volume ID"
155
+ option :D, :destroy, "Destroy the given instance IDs. All data will be lost!"
156
+ option :S, :start, "Start an instance"
157
+ usage "rudy [global options] instances [-D] [-S -i image ID] [instance ID OR group name]"
158
+ command :instances => Rudy::Command::Instances do |obj, argv|
159
+ capture(:stderr) do
160
+ obj.print_header
161
+ if obj.destroy
162
+ exit unless you_are_sure?
163
+ obj.destroy_instances(argv.first)
164
+ elsif obj.start
165
+ exit unless you_are_sure?
166
+ obj.start_instance
167
+ else
168
+ obj.print_instances(argv.first)
169
+ end
170
+ obj.print_footer
171
+ end
172
+ end
173
+
174
+
175
+ option :e, :external, "Display only external IP address"
176
+ option :i, :internal, "Display only internal IP address"
177
+ usage "rudy myaddress [-i] [-e]"
178
+ command :myaddress do |obj|
179
+ ea = Rudy::Utils::external_ip_address
180
+ ia = Rudy::Utils::internal_ip_address
181
+ puts "%10s: %s" % ['Internal', ia] unless obj.external && !obj.internal
182
+ puts "%10s: %s" % ['External', ea] unless obj.internal && !obj.external
183
+ end
184
+
185
+ option :a, :account, String, "Your Amazon Account Number"
186
+ option :i, :image_name, String, "The name of the image"
187
+ option :b, :bucket_name, String, "The name of the bucket that will store the image"
188
+ option :C, :create, "Create an image"
189
+ usage "rudy images [-C -i image -b bucket -a account]"
190
+ command :images => Rudy::Command::Images do |obj|
191
+ if obj.create
192
+ puts "Make sure the machine is clean. I don't want archive no crud!"
193
+ exit unless you_are_sure?
194
+ obj.create_image
195
+ else
196
+ obj.print_images
197
+ end
198
+ end
199
+
200
+ command :release => Rudy::Command::Release do |obj|
201
+ obj.print_header
202
+
203
+ raise "No SCM defined. Set RUDY_SVN_BASE or RUDY_GIT_BASE." unless obj.scm
204
+
205
+ #exit unless you_are_sure?
206
+ obj.create_release
207
+
208
+ end
209
+
210
+
211
+ option :all, "Display all security groups"
212
+ option :r, :protocols, Array, "Comma-separated list of protocols. One of: tcp (default), udp, icmp"
213
+ option :p, :ports, Array, "List of comma-separated ports to authorize (default: 22,80,443)"
214
+ option :a, :addresses, Array, "List of comma-separated IP addresses to authorize (default: your external IP)"
215
+ option :C, :create, "Create a security group"
216
+ option :D, :destroy, "Destroy a security group"
217
+ option :M, :modify, "Modify a security group"
218
+ usage "rudy [global options] groups [-C] [-a IP addresses] [-p ports] [group name]"
219
+ command :groups => Rudy::Command::Groups do |obj, argv|
220
+ if obj.create
221
+ obj.create_group(argv.first)
222
+
223
+ elsif obj.modify
224
+ obj.modify_group(argv.first)
225
+
226
+ elsif obj.destroy
227
+ exit unless you_are_sure?
228
+ obj.destroy_group(argv.first)
229
+
230
+ else
231
+ obj.print_groups(argv.first)
232
+ end
233
+ end
234
+
235
+
236
+ __END__
237
+
238
+ # Create the SimpleDB domain for storing metadata. Check environment variables.
239
+ rudy init
240
+ rudy create-group -a address -i ami-1111111 -v vol-2222222
241
+
242
+ # Connect to stage-app-01 as root
243
+ rudy --user root connect
244
+
245
+ # Zone, environment, role and position are implied.
246
+ rudy disks -C -p /rilli/app -d /dev/sdh -s 100
247
+ rudy disks -D disk-us-east-1b-stage-app-01-rilli-db222
248
+
249
+ # Create an image from an existing instance
250
+ rudy -e prod images -C -b rilli-ami-us -i rilli-app-32-r3
251
+
252
+ # Create an instance
253
+ rudy -e prod instances -S -i ami-11111
254
+
255
+
256
+ rudy release
257
+
258
+
@@ -0,0 +1,525 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ #
5
+ #
6
+ module Drydock
7
+ # The base class for all command objects. There is an instance of this class
8
+ # for every command defined. Global and command-specific options are added
9
+ # as attributes to this class dynamically.
10
+ #
11
+ # i.e. "example -v date -f yaml"
12
+ #
13
+ # global_option :v, :verbose, "I want mooooore!"
14
+ # option :f, :format, String, "Long date format"
15
+ # command :date do |obj|
16
+ # puts obj.verbose #=> true
17
+ # puts obj.format #=> "yaml"
18
+ # end
19
+ #
20
+ # You can inherit from this class to create your own: EatFood < Drydock::Command.
21
+ # And then specific your class in the command definition:
22
+ #
23
+ # command :eat => EatFood do |obj|; ...; end
24
+ #
25
+ class Command
26
+ attr_reader :cmd, :alias
27
+ attr_accessor :verbose
28
+
29
+ # The default constructor sets the short name of the command
30
+ # and stores a reference to the block (if supplied).
31
+ # You don't need to override this method to add functionality
32
+ # to your custom Command classes. Define an +init+ method instead.
33
+ # It will be called just before the block is executed.
34
+ # +cmd+ is the short name of this command.
35
+ # +b+ is the block associated to this command.
36
+ def initialize(cmd, &b)
37
+ @cmd = (cmd.kind_of?(Symbol)) ? cmd : cmd.to_sym
38
+ @b = b
39
+ @verbose = 0
40
+ end
41
+
42
+ # Execute the block.
43
+ #
44
+ # Calls self.init before calling the block. Implement this method when
45
+ #
46
+ # +cmd_str+ is the short name used to evoke this command. It will equal @cmd
47
+ # unless an alias was used used to evoke this command.
48
+ # +argv+ an array of unnamed arguments. If ignore :options was declared this
49
+ # will contain the arguments exactly as they were defined on the command-line.
50
+ # +stdin+ contains the output of stdin do; ...; end otherwise it's a STDIN IO handle.
51
+ # +global_options+ a hash of the global options specified on the command-line
52
+ # +options+ a hash of the command-specific options specific on the command-line.
53
+ def call(cmd_str=nil, argv=[], stdin=[], global_options={}, options={})
54
+ @alias = cmd_str.nil? ? @cmd : cmd_str
55
+ global_options.merge(options).each_pair do |n,v|
56
+ self.send("#{n}=", v)
57
+ end
58
+
59
+ self.init if respond_to? :init
60
+
61
+ block_args = [self, argv, stdin] # TODO: review order
62
+ @b.call(*block_args[0..(@b.arity-1)]) # send only as many args as defined
63
+ end
64
+
65
+ # The name of the command
66
+ def to_s
67
+ @cmd.to_s
68
+ end
69
+ end
70
+ end
71
+
72
+ module Drydock
73
+ class UnknownCommand < RuntimeError
74
+ attr_reader :name
75
+ def initialize(name)
76
+ @name = name || :unknown
77
+ end
78
+ def message
79
+ "Unknown command: #{@name}"
80
+ end
81
+ end
82
+ class NoCommandsDefined < RuntimeError
83
+ def message
84
+ "No commands defined"
85
+ end
86
+ end
87
+ class InvalidArgument < RuntimeError
88
+ attr_accessor :args
89
+ def initialize(args)
90
+ @args = args || []
91
+ end
92
+ def message
93
+ "Unknown option: #{@args.join(", ")}"
94
+ end
95
+ end
96
+ class MissingArgument < InvalidArgument
97
+ def message
98
+ "Option requires a value: #{@args.join(", ")}"
99
+ end
100
+ end
101
+ end
102
+
103
+ # Drydock is a DSL for command-line apps.
104
+ # See bin/example for usage examples.
105
+ module Drydock
106
+ extend self
107
+
108
+ VERSION = 0.3
109
+
110
+ private
111
+ # Stolen from Sinatra!
112
+ #def delegate(*args)
113
+ # args.each do |m|
114
+ # eval(<<-end_eval, binding, "(__Drydock__)", __LINE__)
115
+ # def #{m}(*args, &b)
116
+ # Drydock.#{m}(*args, &b)
117
+ # end
118
+ # end_eval
119
+ # end
120
+ #end
121
+ #
122
+ #delegate :before, :after, :alias_command, :desc
123
+ #delegate :global_option, :global_usage, :usage, :commands, :command
124
+ #delegate :debug, :option, :stdin, :default, :ignore, :command_alias
125
+
126
+ @@debug = false
127
+ @@has_run = false
128
+ @@run = true
129
+ @@default_command = nil
130
+
131
+ public
132
+ # Enable or disable debug output.
133
+ #
134
+ # debug :on
135
+ # debug :off
136
+ #
137
+ # Calling without :on or :off will toggle the value.
138
+ #
139
+ def debug(toggle=false)
140
+ if toggle.is_a? Symbol
141
+ @@debug = true if toggle == :on
142
+ @@debug = false if toggle == :off
143
+ else
144
+ @@debug = (!@@debug)
145
+ end
146
+ end
147
+ # Returns true if debug output is enabled.
148
+ def debug?
149
+ @@debug
150
+ end
151
+
152
+ # Define a default command.
153
+ #
154
+ # default :task
155
+ #
156
+ def default(cmd)
157
+ @@default_command = canonize(cmd)
158
+ end
159
+
160
+ # Provide a description for a method
161
+ def desc(txt)
162
+ @@command_descriptions ||= []
163
+ @@command_descriptions << txt
164
+ end
165
+
166
+ # Define a block for processing STDIN before the command is called.
167
+ # The command block receives the return value of this block in a named argument:
168
+ #
169
+ # command :task do |obj, argv, stdin|; ...; end
170
+ #
171
+ # If a stdin block isn't defined, +stdin+ above will be the STDIN IO handle.
172
+ def stdin(&b)
173
+ @@stdin_block = b
174
+ end
175
+
176
+ # Define a block to be called before the command.
177
+ # This is useful for opening database connections, etc...
178
+ def before(&b)
179
+ @@before_block = b
180
+ end
181
+
182
+ # Define a block to be called after the command.
183
+ # This is useful for stopping, closing, etc... the stuff in the before block.
184
+ def after(&b)
185
+ @@after_block = b
186
+ end
187
+
188
+ # Define the default global usage banner. This is displayed
189
+ # with "script -h".
190
+ def global_usage(msg)
191
+ @@global_options ||= OpenStruct.new
192
+ global_opts_parser.banner = "USAGE: #{msg}"
193
+ end
194
+
195
+ # Define a command-specific usage banner. This is displayed
196
+ # with "script command -h"
197
+ def usage(msg)
198
+ get_current_option_parser.banner = "USAGE: #{msg}"
199
+ end
200
+
201
+ # Grab the options parser for the current command or create it if it doesn't exist.
202
+ def get_current_option_parser
203
+ @@command_opts_parser ||= []
204
+ @@command_index ||= 0
205
+ (@@command_opts_parser[@@command_index] ||= OptionParser.new)
206
+ end
207
+
208
+ # Tell the Drydock parser to ignore something.
209
+ # Drydock will currently only listen to you if you tell it to "ignore :options",
210
+ # otherwise it will ignore you!
211
+ #
212
+ # +what+ the thing to ignore. When it equals :options Drydock will not parse
213
+ # the command-specific arguments. It will pass the arguments directly to the
214
+ # Command object. This is useful when you want to parse the arguments in some a way
215
+ # that's too crazy, dangerous for Drydock to handle automatically.
216
+ def ignore(what=:nothing)
217
+ @@command_opts_parser[@@command_index] = :ignore if what == :options || what == :all
218
+ end
219
+
220
+ # Define a global option. See +option+ for more info.
221
+ def global_option(*args, &b)
222
+ args.unshift(global_opts_parser)
223
+ global_option_names << option_parser(args, &b)
224
+ end
225
+
226
+ # Define a command-specific option.
227
+ #
228
+ # +args+ is passed directly to OptionParser.on so it can contain anything
229
+ # that's valid to that method. Some examples:
230
+ # [:h, :help, "Displays this message"]
231
+ # [:m, :max, Integer, "Maximum threshold"]
232
+ # ['-l x,y,z', '--lang=x,y,z', Array, "Requested languages"]
233
+ # If a class is included, it will tell OptionParser to expect a value
234
+ # otherwise it assumes a boolean value.
235
+ #
236
+ # All calls to +option+ must come before the command they're associated
237
+ # to. Example:
238
+ #
239
+ # option :l, :longname, String, "Description" do; ...; end
240
+ # command :task do |obj|; ...; end
241
+ #
242
+ # When calling your script with a specific command-line option, the value
243
+ # is available via obj.longname inside the command block.
244
+ #
245
+ def option(*args, &b)
246
+ args.unshift(get_current_option_parser)
247
+ current_command_option_names << option_parser(args, &b)
248
+ end
249
+
250
+ # Define a command.
251
+ #
252
+ # command :task do
253
+ # ...
254
+ # end
255
+ #
256
+ # A custom command class can be specified using Hash syntax. The class
257
+ # must inherit from Drydock::Command (class CustomeClass < Drydock::Command)
258
+ #
259
+ # command :task => CustomCommand do
260
+ # ...
261
+ # end
262
+ #
263
+ def command(*cmds, &b)
264
+ @@command_index ||= 0
265
+ @@command_opts_parser ||= []
266
+ @@command_option_names ||= []
267
+ cmds.each do |cmd|
268
+ if cmd.is_a? Hash
269
+ c = cmd.values.first.new(cmd.keys.first, &b)
270
+ else
271
+ c = Drydock::Command.new(cmd, &b)
272
+ end
273
+ commands[c.cmd] = c
274
+ command_index_map[c.cmd] = @@command_index
275
+ @@command_index += 1
276
+ end
277
+
278
+ end
279
+
280
+ # Used to create an alias to a defined command.
281
+ # Here's an example:
282
+ #
283
+ # command :task do; ...; end
284
+ # alias_command :pointer, :task
285
+ #
286
+ # Either name can be used on the command-line:
287
+ #
288
+ # $ script task [options]
289
+ # $ script pointer [options]
290
+ #
291
+ # Inside of the command definition, you have access to the
292
+ # command name that was used via obj.alias.
293
+ def alias_command(aliaz, cmd)
294
+ return unless commands.has_key? cmd
295
+ commands[canonize(aliaz)] = commands[cmd]
296
+ end
297
+
298
+ # Identical to +alias_command+ with reversed arguments.
299
+ # For whatever reason I forget the order so Drydock supports both.
300
+ # Tip: the argument order matches the method name.
301
+ def command_alias(cmd, aliaz)
302
+ return unless commands.has_key? cmd
303
+ puts "#{canonize(aliaz)} to #{commands[cmd]}"
304
+ commands[canonize(aliaz)] = commands[cmd]
305
+ end
306
+
307
+ # An array of the currently defined Drydock::Command objects
308
+ def commands
309
+ @@commands ||= {}
310
+ @@commands
311
+ end
312
+
313
+ # An array of the currently defined commands names
314
+ def command_names
315
+ @@commands ||= {}
316
+ @@commands.keys.collect { |cmd| decanonize(cmd); }
317
+ end
318
+
319
+ # Returns true if automatic execution is enabled.
320
+ def run?
321
+ @@run
322
+ end
323
+
324
+ # Disable automatic execution (enabled by default)
325
+ #
326
+ # Drydock.run = false
327
+ def run=(v)
328
+ @@run = (v == true) ? true : false
329
+ end
330
+
331
+ # Return true if a command has been executed.
332
+ def has_run?
333
+ @@has_run
334
+ end
335
+
336
+ # Execute the given command.
337
+ # By default, Drydock automatically executes itself and provides handlers for known errors.
338
+ # You can override this functionality by calling +Drydock.run!+ yourself. Drydock
339
+ # will only call +run!+ once.
340
+ def run!(argv=[], stdin=STDIN)
341
+ return if has_run?
342
+ @@has_run = true
343
+ raise NoCommandsDefined.new if commands.empty?
344
+ @@global_options, cmd_name, @@command_options, argv = process_arguments(argv)
345
+
346
+ cmd_name ||= default_command
347
+
348
+ raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
349
+
350
+ stdin = (defined? @@stdin_block) ? @@stdin_block.call(stdin, []) : stdin
351
+ @@before_block.call if defined? @@before_block
352
+
353
+ call_command(cmd_name, argv, stdin)
354
+
355
+ @@after_block.call if defined? @@after_block
356
+
357
+ rescue OptionParser::InvalidOption => ex
358
+ raise Drydock::InvalidArgument.new(ex.args)
359
+ rescue OptionParser::MissingArgument => ex
360
+ raise Drydock::MissingArgument.new(ex.args)
361
+ end
362
+
363
+ private
364
+
365
+ # Executes the block associated to +cmd+
366
+ def call_command(cmd, argv=[], stdin=nil)
367
+ return unless command?(cmd)
368
+ get_command(cmd).call(cmd, argv, stdin, @@global_options || {}, @@command_options || {})
369
+ end
370
+
371
+ # Returns the Drydock::Command object with the name +cmd+
372
+ def get_command(cmd)
373
+ return unless command?(cmd)
374
+ @@commands[canonize(cmd)]
375
+ end
376
+
377
+ # Returns true if a command with the name +cmd+ has been defined.
378
+ def command?(cmd)
379
+ name = canonize(cmd)
380
+ (@@commands || {}).has_key? name
381
+ end
382
+
383
+ # Canonizes a string (+cmd+) to the symbol for command names
384
+ # '-' is replaced with '_'
385
+ def canonize(cmd)
386
+ return unless cmd
387
+ return cmd if cmd.kind_of?(Symbol)
388
+ cmd.to_s.tr('-', '_').to_sym
389
+ end
390
+
391
+ # Returns a string version of +cmd+, decanonized.
392
+ # Lowercase, '_' is replaced with '-'
393
+ def decanonize(cmd)
394
+ return unless cmd
395
+ cmd.to_s.tr('_', '-')
396
+ end
397
+
398
+ # Processes calls to option and global_option. Symbols are converted into
399
+ # OptionParser style strings (:h and :help become '-h' and '--help').
400
+ def option_parser(args=[], &b)
401
+ return if args.empty?
402
+ opts_parser = args.shift
403
+
404
+ arg_name = ''
405
+ symbol_switches = []
406
+ args.each_with_index do |arg, index|
407
+ if arg.is_a? Symbol
408
+ arg_name = arg.to_s if arg.to_s.size > arg_name.size
409
+ args[index] = (arg.to_s.length == 1) ? "-#{arg.to_s}" : "--#{arg.to_s}"
410
+ symbol_switches << args[index]
411
+ elsif arg.kind_of?(Class)
412
+ symbol_switches.each do |arg|
413
+ arg << "=S"
414
+ end
415
+ end
416
+ end
417
+
418
+ if args.size == 1
419
+ opts_parser.on(args.shift)
420
+ else
421
+ opts_parser.on(*args) do |v|
422
+ block_args = [v, opts_parser]
423
+ result = (b.nil?) ? v : b.call(*block_args[0..(b.arity-1)])
424
+ end
425
+ end
426
+
427
+ arg_name
428
+ end
429
+
430
+
431
+ # Split the +argv+ array into global args and command args and
432
+ # find the command name.
433
+ # i.e. ./script -H push -f (-H is a global arg, push is the command, -f is a command arg)
434
+ # returns [global_options, cmd, command_options, argv]
435
+ def process_arguments(argv=[])
436
+ global_options = command_options = {}
437
+ cmd = nil
438
+
439
+ global_options = global_opts_parser.getopts(argv)
440
+
441
+ cmd_name = (argv.empty?) ? @@default_command : argv.shift
442
+ raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
443
+
444
+ cmd = get_command(cmd_name)
445
+
446
+ command_parser = @@command_opts_parser[get_command_index(cmd.cmd)]
447
+ command_options = {}
448
+
449
+ # We only need to parse the options out of the arguments when
450
+ # there are args available, there is a valid parser, and
451
+ # we weren't requested to ignore the options.
452
+ if !argv.empty? && command_parser && command_parser != :ignore
453
+ command_options = command_parser.getopts(argv)
454
+ end
455
+
456
+ # Add accessors to the Drydock::Command object
457
+ # for the global and command specific options
458
+ [global_option_names, (command_option_names[get_command_index(cmd_name)] || [])].flatten.each do |n|
459
+ unless cmd.respond_to?(n)
460
+ cmd.class.send(:define_method, n) do
461
+ instance_variable_get("@#{n}")
462
+ end
463
+ end
464
+ unless cmd.respond_to?("#{n}=")
465
+ cmd.class.send(:define_method, "#{n}=") do |val|
466
+ instance_variable_set("@#{n}", val)
467
+ end
468
+ end
469
+ end
470
+
471
+ [global_options, cmd_name, command_options, argv]
472
+ end
473
+
474
+ def global_option_names
475
+ @@global_option_names ||= []
476
+ end
477
+
478
+ # Grab the current list of command-specific option names. This is a list of the
479
+ # long names.
480
+ def current_command_option_names
481
+ @@command_option_names ||= []
482
+ @@command_index ||= 0
483
+ (@@command_option_names[@@command_index] ||= [])
484
+ end
485
+
486
+ def command_index_map
487
+ @@command_index_map ||= {}
488
+ end
489
+
490
+ def get_command_index(cmd)
491
+ command_index_map[canonize(cmd)] || -1
492
+ end
493
+
494
+ def command_option_names
495
+ @@command_option_names ||= []
496
+ end
497
+
498
+ def global_opts_parser
499
+ @@global_opts_parser ||= OptionParser.new
500
+ end
501
+
502
+ def default_command
503
+ @@default_command ||= nil
504
+ end
505
+
506
+ end
507
+
508
+
509
+
510
+ trap ("SIGINT") do
511
+ puts "#{$/}Exiting..."
512
+ exit 1
513
+ end
514
+
515
+
516
+ at_exit {
517
+ begin
518
+ Drydock.run!(ARGV, STDIN) if Drydock.run? && !Drydock.has_run?
519
+ rescue => ex
520
+ STDERR.puts "ERROR: #{ex.message}"
521
+ STDERR.puts ex.backtrace if Drydock.debug?
522
+ end
523
+ }
524
+
525
+