rudy 0.2

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.
@@ -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
+