delano-drydock 0.3.0 → 0.3.1

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.
data/CHANGES.txt ADDED
@@ -0,0 +1,16 @@
1
+ DRYDOCK, CHANGES
2
+
3
+ #### 0.3 (2009-02-05) ###############################
4
+
5
+ * Added support for custom Drydock::Commands objects
6
+ * Global and command-specific options are now available as
7
+ attributes of the Drydock::Commands class instance.
8
+ * Automatic execution
9
+ * Now in a single file (lib/drydock.rb)
10
+ * Started adding tests
11
+ * Improved documentation
12
+
13
+ #### 0.2 (2008-12-27) ###############################
14
+
15
+ * Initial release
16
+ * Forked from bmizerany/frylock
data/README.rdoc CHANGED
@@ -1,56 +1,69 @@
1
- = Drydock - Easy Command line apps
1
+ = Drydock - v0.3
2
2
 
3
- Inspired by "github-gem":http://github.com/defunkt/github-gem
4
-
5
- Inspired by "bmizerany-frylock":http://github.com/bmizerany/frylock/tree
3
+ Inspired by github-gem and bmizerany-frylock.
6
4
 
7
5
  == Overview
8
6
 
9
- Drydock is a DSL for command line apps.
7
+ Drydock is a seaworthy DSL for command line apps. It is contained in a single .rb which can be copied directly into your project.
10
8
 
11
9
  == Install
12
10
 
13
- git clone git://github.com/delano/drydock.git
11
+ One of:
12
+
13
+ * gem install drydock
14
+ * copy lib/drydock.rb into your lib directory.
14
15
 
16
+ Or for GitHub fans:
17
+
18
+ * git clone git://github.com/delano/drydock.git
19
+ * gem install delano-drydock
15
20
 
16
21
  == Examples
17
22
 
18
23
  See bin/example for more.
19
24
 
20
- <pre><code>
21
- require 'rubygems'
22
- require 'drydock'
25
+ require 'rubygems'
26
+ require 'drydock'
23
27
 
24
- default :welcome
28
+ default :welcome
25
29
 
26
- before do
27
- # You can execute a block before the requests command is executed. Instance
28
- # variables defined here will be available to all commands.
29
- end
30
+ before do
31
+ # You can execute a block before the requests command is executed. Instance
32
+ # variables defined here will be available to all commands.
33
+ end
30
34
 
31
- command :welcome do
32
- # Example: ruby bin/example
35
+ command :welcome do
36
+ # Example: ruby bin/example
33
37
 
34
- puts "Meatwad: Science is a mystery to man, isn't it Frylock?"
35
- print "Frylock: At least we have some commands: "
38
+ puts "Meatwad: Science is a mystery to man, isn't it Frylock?"
39
+ print "Frylock: At least we have some commands: "
36
40
 
37
- # The commands method returns a hash of Frylock::Command objects
38
- puts commands.keys.inject([]) { |list, command| list << command.to_s }.sort.join(', ')
39
- end
40
-
41
- option :f, :found, "A boolean value. Did you find the car?"
42
- command :findcar do |options|
43
- # +options+ is a hash containing the options defined above
44
- # Example: ruby bin/example -f findcar
41
+ # The commands method returns a hash of Frylock::Command objects
42
+ puts commands.keys.inject([]) { |list, command| list << command.to_s }.sort.join(', ')
43
+ end
44
+
45
+ option :f, :found, "A boolean value. Did you find the car?"
46
+ command :findcar do |options|
47
+ # +options+ is a hash containing the options defined above
48
+ # Example: ruby bin/example -f findcar
45
49
 
46
- puts "Frylock: So, did they ever find your car?"
50
+ puts "Frylock: So, did they ever find your car?"
47
51
 
48
- # The keys to the hash are the long string from the option definition.
49
- # If only the short string is provided, those will be used instead (i.e. :f).
50
- puts (!options[:found]) ? "Carl: No" :
51
- "Carl: Oh, they found part of it, hangin' from a trestle near the turnpike."
52
- end
53
- </code></pre>
52
+ # The keys to the hash are the long string from the option definition.
53
+ # If only the short string is provided, those will be used instead (i.e. :f).
54
+ puts (!options[:found]) ? "Carl: No" :
55
+ "Carl: Oh, they found part of it, hangin' from a trestle near the turnpike."
56
+ end
57
+
58
+ == More Information
59
+
60
+ http://www.youtube.com/watch?v=m_wFEB4Oxlo
61
+
62
+ == Credits
63
+
64
+ * Delano Mandelbaum (delano@solutious.com)
65
+ * Bernie Kopell (bernie@solutious.com)
66
+
54
67
 
55
68
  == License
56
69
 
data/Rakefile ADDED
@@ -0,0 +1,72 @@
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 => :test
9
+
10
+ # SPECS ===============================================================
11
+
12
+ desc 'Run specs with unit test style output'
13
+ task :test do |t|
14
+ sh "ruby tests/*_test.rb"
15
+ end
16
+
17
+ # PACKAGE =============================================================
18
+
19
+
20
+ require File.dirname(__FILE__) + "/lib/drydock"
21
+ load "drydock.gemspec"
22
+
23
+ version = Drydock::VERSION.to_s
24
+
25
+ Drydock.run = false
26
+
27
+ Rake::GemPackageTask.new(@spec) do |p|
28
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
29
+ end
30
+
31
+ task :release => [ :rdoc, :package ]
32
+
33
+ task :install => [ :rdoc, :package ] do
34
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
35
+ end
36
+
37
+ task :uninstall => [ :clean ] do
38
+ sh %{sudo gem uninstall #{name}}
39
+ end
40
+
41
+
42
+ # Rubyforge Release / Publish Tasks ==================================
43
+
44
+ desc 'Publish website to rubyforge'
45
+ task 'publish:doc' => 'doc/index.html' do
46
+ sh 'scp -rp doc/* rubyforge.org:/var/www/gforge-projects/drydock/'
47
+ end
48
+ puts "rubyforge add_release drydock drydock #{@spec.version} pkg/drydock-#{@spec.version}.gem "
49
+ task 'publish:gem' => [:package] do |t|
50
+ sh <<-end
51
+ rubyforge add_release -o Any -a CHANGES.txt -f -n README.rdoc drydock drydock #{@spec.version} pkg/drydock-#{@spec.version}.gem &&
52
+ rubyforge add_file -o Any -a CHANGES.txt -f -n README.rdoc drydock drydock #{@spec.version} pkg/drydock-#{@spec.version}.tgz
53
+ end
54
+ end
55
+
56
+
57
+ Rake::RDocTask.new do |t|
58
+ t.rdoc_dir = 'doc'
59
+ t.title = "Drydock, A seaworthy DSL for command-line apps."
60
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
61
+ t.options << '--charset' << 'utf-8'
62
+ t.rdoc_files.include('LICENSE.txt')
63
+ t.rdoc_files.include('README.rdoc')
64
+ t.rdoc_files.include('CHANGES.txt')
65
+ t.rdoc_files.include('bin/*')
66
+ t.rdoc_files.include('lib/*.rb')
67
+ end
68
+
69
+ CLEAN.include [ 'pkg', '*.gem', '.config', 'doc' ]
70
+
71
+
72
+
data/bin/example CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- DRYDOCK_HOME = File.expand_path(File.join(File.dirname(__FILE__), '..'))
4
- $: << File.join(DRYDOCK_HOME, 'lib')
3
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), '..')), 'lib'
5
4
 
6
- require 'rubygems'
7
5
  require 'drydock'
8
6
 
9
7
  default :welcome
10
8
 
11
-
12
9
  before do
13
10
  # You can execute a block before the requests command is executed. Instance
14
11
  # variables defined here will be available to all commands.
15
12
  end
16
13
 
14
+ after do
15
+ # And this will be called after the command.
16
+ end
17
17
 
18
18
  command :welcome do
19
19
  # Example: ruby bin/example
@@ -27,15 +27,15 @@ end
27
27
 
28
28
 
29
29
  option :f, :found, "A boolean value. Did you find the car?"
30
- command :findcar do |options|
31
- # +options+ is a hash containing the options defined above
30
+ command :findcar do |obj|
31
+ # +obj+ is the Drylock::Command object instance. It contains accessors for all options.
32
32
  # Example: ruby bin/example -f findcar
33
33
 
34
34
  puts "Frylock: So, did they ever find your car?"
35
35
 
36
36
  # The keys to the hash are the long string from the option definition.
37
37
  # If only the short string is provided, those will be used instead (i.e. :f).
38
- puts (!options[:found]) ? "Carl: No" :
38
+ puts (!obj.found) ? "Carl: No" :
39
39
  "Carl: Oh, they found part of it, hangin' from a trestle near the turnpike."
40
40
  end
41
41
 
@@ -51,17 +51,28 @@ global_option :v, :verbose, "Verbosity level (i.e. -vvv is greater than -v)" do
51
51
  end
52
52
 
53
53
 
54
- usage "ruby bin/example [--seconds] [-vv] time"
55
- command :date do |options, argv, global_options|
56
- # +argv+ contains the unnamed arguments
57
- # +global_options+ contains hash of the options defined with global_options
58
-
54
+ usage "#{$0} [-s] [-vv] date"
55
+ command :date do |obj, argv|
56
+ # +argv+ is an array containing the unnamed arguments
59
57
  require 'time'
60
58
  now = Time.now
61
- puts "More verbosely, the date is now: " if (global_options[:verbose] || 0) >= 2
62
- puts (global_options[:seconds]) ? now.to_i : now.to_s
59
+ puts "(Not verbose enough. Try adding a -v.)" if (obj.verbose || 0) == 1
60
+ puts "More verbosely, the date is now: " if (obj.verbose || 0) >= 2
61
+ puts (obj.seconds) ? now.to_i : now.to_s
63
62
  end
64
63
 
64
+ usage "#{$0} rogue"
65
+ ignore :options
66
+ command :rogue do |obj, argv|
67
+ # You can use ignore :options to tell Drydock to not process the
68
+ # command-specific options.
69
+ if argv.empty?
70
+ puts "Had you supplied some arguments, I would have ignored them."
71
+ else
72
+ puts "Hi! You supplied some arguments but I ignored them."
73
+ puts "They're all still here in this array: %s" % argv.join(', ')
74
+ end
75
+ end
65
76
 
66
77
  option :c, :check, "Check response codes for each URI"
67
78
  option :d, :delim, String, "Output delimiter"
@@ -73,31 +84,31 @@ option :t, :timeout, Float, "Timeout value for HTTP request" do |v|
73
84
  end
74
85
 
75
86
  usage 'echo "http://github.com/" | ruby bin/example process -c -d " " -t 15 http://solutious.com/'
76
- command :processuris do |options, argv, global_options, stdin, cmd|
77
- # +stdin+ is either an IO object or a custom object defined with a stdin block (see below)
87
+ command :processuri do |obj, argv, stdin|
78
88
  # +cmd+ is the string used to evoke this command. Useful with alias_command (see below).
89
+ # +stdin+ is either an IO object or a custom object defined with a stdin block (see below)
79
90
 
80
91
  require 'net/http'
81
92
  require 'uri'
82
93
  require 'timeout'
83
94
 
84
95
  uris = [stdin, argv].flatten # Combine the argv and stdin arrays
85
- delim = options[:delim] || ','
86
- timeout = options[:timeout] || 5
96
+ delim = obj.delim || ','
97
+ timeout = obj.timeout || 5
87
98
  code = :notchecked # The default code when :check is false
88
99
 
89
100
  if uris.empty?
90
101
  puts "Frylock: You didn't provide any URIs. "
91
- puts "Master Shake: Ya, see #{$0} #{cmd} -h"
102
+ puts "Master Shake: Ya, see #{$0} #{obj.alias} -h"
92
103
  exit 0
93
104
  end
94
105
 
95
106
  uris.each_with_index do |uri, index|
96
- code = response_code(uri, timeout) if (options[:check])
107
+ code = response_code(uri, timeout) if (obj.check)
97
108
  puts [index+1, uri, code].join(delim)
98
109
  end
99
110
  end
100
- alias_command :process, :processuris
111
+ alias_command :checkuri, :processuri
101
112
 
102
113
 
103
114
 
@@ -139,32 +150,3 @@ def response_code(uri_str, duration=5)
139
150
  response
140
151
  end
141
152
 
142
-
143
- at_exit do
144
- # This is an example of how to call Frylock in your script.
145
- begin
146
- Drydock.run!(ARGV, STDIN)
147
-
148
- rescue Drydock::UnknownCommand => ex
149
- STDERR.puts "Frylock: I don't know what the #{ex.name} command is. #{$/}"
150
- STDERR.puts "Master Shake: I'll tell you what it is, friends... it's shut up and let me eat it."
151
-
152
- rescue Drydock::NoCommandsDefined => ex
153
- STDERR.puts "Frylock: Carl, I don't want it. And I'd appreciate it if you'd define at least one command. #{$/}"
154
- STDERR.puts "Carl: Fryman, don't be that way! This sorta thing happens every day! People just don't... you know, talk about it this loud."
155
-
156
- rescue Drydock::InvalidArgument => ex
157
- STDERR.puts "Frylock: Shake, how many arguments have you not provided a value for this year? #{$/}"
158
- STDERR.puts "Master Shake: A *lot* more than *you* have! (#{@args.join(', ')})"
159
-
160
- rescue Drydock::MissingArgument => ex
161
- STDERR.puts "Frylock: I don't know what #{ex.args.join(', ')} is. #{$/}"
162
- STDERR.puts "Master Shake: I'll tell you what it is, friends... it's shut up and let me eat it."
163
-
164
- rescue => ex
165
- STDERR.puts "Master Shake: Okay, but when we go in, watch your step. "
166
- STDERR.puts "Frylock: Why?"
167
- STDERR.puts "Meatwad: [explosion] #{ex.message}"
168
- STDERR.puts ex.backtrace
169
- end
170
- end
data/drydock.gemspec ADDED
@@ -0,0 +1,53 @@
1
+ @spec = Gem::Specification.new do |s|
2
+ s.name = %q{drydock}
3
+ s.version = "0.3.1"
4
+ s.specification_version = 1 if s.respond_to? :specification_version=
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+
7
+ s.authors = ["Delano Mandelbaum"]
8
+ s.date = %q{2008-08-17}
9
+ s.description = %q{A seaworthy DSL for writing command line apps inspired by Blake Mizerany's Frylock}
10
+ s.email = %q{delano@solutious.com}
11
+ s.files = %w(
12
+ CHANGES.txt
13
+ LICENSE.txt
14
+ README.rdoc
15
+ Rakefile
16
+ bin/example
17
+ drydock.gemspec
18
+ lib/drydock.rb
19
+ test/command_test.rb
20
+ doc
21
+ doc/classes
22
+ doc/classes/Drydock
23
+ doc/classes/Drydock/Command.html
24
+ doc/classes/Drydock/InvalidArgument.html
25
+ doc/classes/Drydock/MissingArgument.html
26
+ doc/classes/Drydock/NoCommandsDefined.html
27
+ doc/classes/Drydock/UnknownCommand.html
28
+ doc/classes/Drydock.html
29
+ doc/created.rid
30
+ doc/files
31
+ doc/files/bin
32
+ doc/files/bin/example.html
33
+ doc/files/CHANGES_txt.html
34
+ doc/files/lib
35
+ doc/files/lib/drydock_rb.html
36
+ doc/files/LICENSE_txt.html
37
+ doc/files/README_rdoc.html
38
+ doc/fr_class_index.html
39
+ doc/fr_file_index.html
40
+ doc/fr_method_index.html
41
+ doc/index.html
42
+ doc/rdoc-style.css
43
+ )
44
+ s.has_rdoc = true
45
+ s.homepage = %q{http://github.com/delano/drydock}
46
+ s.extra_rdoc_files = %w[README.rdoc LICENSE.txt CHANGES.txt]
47
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Drydock: a seaworthy DSL for command-line apps", "--main", "README.rdoc"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.1.1}
50
+ s.summary = %q{A seaworthy DSL for writing command line apps}
51
+
52
+ s.rubyforge_project = "drydock"
53
+ end
data/lib/drydock.rb CHANGED
@@ -1,133 +1,368 @@
1
1
  require 'optparse'
2
2
  require 'ostruct'
3
- require 'pp'
4
-
5
- require 'drydock/exceptions'
6
3
 
4
+ #
5
+ #
7
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
+ #
8
25
  class Command
9
- attr_reader :cmd, :index
10
- def initialize(cmd, index, &b)
11
- @cmd = (cmd.kind_of?(Symbol)) ? cmd.to_s : cmd
12
- @index = index
26
+ attr_reader :cmd, :alias
27
+ # +cmd+ is the short name of this command.
28
+ # +b+ is the block associated to this command.
29
+ def initialize(cmd, &b)
30
+ @cmd = (cmd.kind_of?(Symbol)) ? cmd : cmd.to_sym
13
31
  @b = b
14
32
  end
15
33
 
16
- def call(cmd_str, argv, stdin, global_options, options)
17
- block_args = [options, argv, global_options, stdin, cmd_str, self]
18
- @b.call(*block_args[0..(@b.arity-1)])
34
+ # Execute the block.
35
+ #
36
+ # +cmd_str+ is the short name used to evoke this command. It will equal @cmd
37
+ # unless an alias was used used to evoke this command.
38
+ # +argv+ an array of unnamed arguments. If ignore :options was declared this
39
+ # will contain the arguments exactly as they were defined on the command-line.
40
+ # +stdin+ contains the output of stdin do; ...; end otherwise it's a STDIN IO handle.
41
+ # +global_options+ a hash of the global options specified on the command-line
42
+ # +options+ a hash of the command-specific options specific on the command-line.
43
+ def call(cmd_str=nil, argv=[], stdin=[], global_options={}, options={})
44
+ @alias = cmd_str.nil? ? @cmd : cmd_str
45
+ global_options.merge(options).each_pair do |n,v|
46
+ self.send("#{n}=", v)
47
+ end
48
+ block_args = [self, argv, stdin] # TODO: review order
49
+ @b.call(*block_args[0..(@b.arity-1)]) # send only as many args as defined
19
50
  end
51
+
52
+ # The name of the command
20
53
  def to_s
21
54
  @cmd.to_s
22
55
  end
23
56
  end
24
57
  end
25
58
 
59
+ module Drydock
60
+ class UnknownCommand < RuntimeError
61
+ attr_reader :name
62
+ def initialize(name)
63
+ @name = name || :unknown
64
+ end
65
+ def message
66
+ "Unknown command: #{@name}"
67
+ end
68
+ end
69
+ class NoCommandsDefined < RuntimeError
70
+ def message
71
+ "No commands defined"
72
+ end
73
+ end
74
+ class InvalidArgument < RuntimeError
75
+ attr_accessor :args
76
+ def initialize(args)
77
+ @args = args || []
78
+ end
79
+ def message
80
+ "Unknown option: #{@args.join(", ")}"
81
+ end
82
+ end
83
+ class MissingArgument < InvalidArgument
84
+ def message
85
+ "Option requires a value: #{@args.join(", ")}"
86
+ end
87
+ end
88
+ end
89
+
90
+ # Drydock is a DSL for command-line apps.
91
+ # See bin/example for usage examples.
26
92
  module Drydock
27
93
  extend self
28
94
 
29
- FORWARDED_METHODS = %w(command before alias_command global_option global_usage usage option stdin default commands).freeze
95
+ VERSION = 0.3
96
+
97
+ private
98
+ # Stolen from Sinatra!
99
+ def delegate(*args)
100
+ args.each do |m|
101
+ eval(<<-end_eval, binding, "(__Drydock__)", __LINE__)
102
+ def #{m}(*args, &b)
103
+ Drydock.#{m}(*args, &b)
104
+ end
105
+ end_eval
106
+ end
107
+ end
108
+
109
+ delegate :before, :after, :alias_command, :commands
110
+ delegate :global_option, :global_usage, :usage, :command
111
+ delegate :debug, :option, :stdin, :default, :ignore, :command_alias
112
+
113
+ @@debug = false
114
+ @@has_run = false
115
+ @@run = true
116
+
117
+ public
118
+ # Enable or disable debug output.
119
+ #
120
+ # debug :on
121
+ # debug :off
122
+ #
123
+ # Calling without :on or :off will toggle the value.
124
+ #
125
+ def debug(toggle=false)
126
+ if toggle.is_a? Symbol
127
+ @@debug = true if toggle == :on
128
+ @@debug = false if toggle == :off
129
+ else
130
+ @@debug = (!@@debug)
131
+ end
132
+ end
133
+ # Returns true if debug output is enabled.
134
+ def debug?
135
+ @@debug
136
+ end
30
137
 
138
+ # Define a default command.
139
+ #
140
+ # default :task
141
+ #
31
142
  def default(cmd)
32
- @default_command = canonize(cmd)
143
+ @@default_command = canonize(cmd)
33
144
  end
34
145
 
146
+ # Define a block for processing STDIN before the command is called.
147
+ # The command block receives the return value of this block in a named argument:
148
+ #
149
+ # command :task do |obj, argv, stdin|; ...; end
150
+ #
151
+ # If a stdin block isn't defined, +stdin+ above will be the STDIN IO handle.
35
152
  def stdin(&b)
36
- @stdin_block = b
153
+ @@stdin_block = b
37
154
  end
155
+
156
+ # Define a block to be called before the command.
157
+ # This is useful for opening database connections, etc...
38
158
  def before(&b)
39
- @before_block = b
159
+ @@before_block = b
40
160
  end
41
161
 
42
- # global_usage
43
- # ex: usage "Usage: frylla [global options] command [command options]"
162
+ # Define a block to be called after the command.
163
+ # This is useful for stopping, closing, etc... the stuff in the before block.
164
+ def after(&b)
165
+ @@after_block = b
166
+ end
167
+
168
+ # Define the default global usage banner. This is displayed
169
+ # with "script -h".
44
170
  def global_usage(msg)
45
- @global_opts_parser ||= OptionParser.new
46
- @global_options ||= OpenStruct.new
171
+ @@global_options ||= OpenStruct.new
172
+ global_opts_parser.banner = "USAGE: #{msg}"
173
+ end
47
174
 
48
- @global_opts_parser.banner = msg
175
+ # Define a command-specific usage banner. This is displayed
176
+ # with "script command -h"
177
+ def usage(msg)
178
+ get_current_option_parser.banner = "USAGE: #{msg}"
49
179
  end
50
180
 
51
-
181
+ # Grab the options parser for the current command or create it if it doesn't exist.
182
+ def get_current_option_parser
183
+ @@command_opts_parser ||= []
184
+ @@command_index ||= 0
185
+ (@@command_opts_parser[@@command_index] ||= OptionParser.new)
186
+ end
52
187
 
53
- # process_arguments
188
+ # Tell the Drydock parser to ignore something.
189
+ # Drydock will currently only listen to you if you tell it to "ignore :options",
190
+ # otherwise it will ignore you!
191
+ #
192
+ # +what+ the thing to ignore. When it equals :options Drydock will not parse
193
+ # the command-specific arguments. It will pass the Command object the list of
194
+ # arguments. This is useful when you want to parse the arguments in some a way
195
+ # that's too crazy, dangerous for Drydock to handle automatically.
196
+ def ignore(what=:nothing)
197
+ @@command_opts_parser[@@command_index] = :ignore if what == :options || what == :all
198
+ end
199
+
200
+ # Define a global option. See +option+ for more info.
201
+ def global_option(*args, &b)
202
+ args.unshift(global_opts_parser)
203
+ global_option_names << option_parser(args, &b)
204
+ end
205
+
206
+ # Define a command-specific option.
207
+ #
208
+ # +args+ is passed directly to OptionParser.on so it can contain anything
209
+ # that's valid to that method. Some examples:
210
+ # [:h, :help, "Displays this message"]
211
+ # [:m, :max, Integer, "Maximum threshold"]
212
+ # ['-l x,y,z', '--lang=x,y,z', Array, "Requested languages"]
213
+ # If a class is included, it will tell OptionParser to expect a value
214
+ # otherwise it assumes a boolean value.
54
215
  #
55
- # Split the +argv+ array into global args and command args and
56
- # find the command name.
57
- # i.e. ./script -H push -f (-H is a global arg, push is the command, -f is a command arg)
58
- # returns [global_options, cmd, command_options, argv]
59
- def process_arguments(argv)
60
- global_options = command_options = {}
61
- cmd = nil
216
+ # All calls to +option+ must come before the command they're associated
217
+ # to. Example:
218
+ #
219
+ # option :l, :longname, String, "Description" do; ...; end
220
+ # command :task do |obj|; ...; end
221
+ #
222
+ # When calling your script with a specific command-line option, the value
223
+ # is available via obj.longname inside the command block.
224
+ #
225
+ def option(*args, &b)
226
+ args.unshift(get_current_option_parser)
227
+ current_command_option_names << option_parser(args, &b)
228
+ end
229
+
230
+ # Define a command.
231
+ #
232
+ # command :task do
233
+ # ...
234
+ # end
235
+ #
236
+ # A custom command class can be specified using Hash syntax. The class
237
+ # must inherit from Drydock::Command (class CustomeClass < Drydock::Command)
238
+ #
239
+ # command :task => CustomCommand do
240
+ # ...
241
+ # end
242
+ #
243
+ def command(*cmds, &b)
244
+ @@command_index ||= 0
245
+ @@command_opts_parser ||= []
246
+ @@command_option_names ||= []
247
+ cmds.each do |cmd|
248
+ if cmd.is_a? Hash
249
+ c = cmd.values.first.new(cmd.keys.first, &b)
250
+ else
251
+ c = Drydock::Command.new(cmd, &b)
252
+ end
253
+ commands[c.cmd] = c
254
+ command_index_map[c.cmd] = @@command_index
255
+ @@command_index += 1
256
+ end
62
257
 
63
- global_parser = @global_opts_parser
258
+ end
259
+
260
+ # Used to create an alias to a defined command.
261
+ # Here's an example:
262
+ #
263
+ # command :task do; ...; end
264
+ # alias_command :pointer, :task
265
+ #
266
+ # Either name can be used on the command-line:
267
+ #
268
+ # $ script task [options]
269
+ # $ script pointer [options]
270
+ #
271
+ # Inside of the command definition, you have access to the
272
+ # command name that was used via obj.alias.
273
+ def alias_command(aliaz, cmd)
274
+ return unless commands.has_key? cmd
275
+ @@commands[aliaz] = commands[cmd]
276
+ end
277
+ alias :command_alias :alias_command
278
+
279
+ # An array of the currently defined Drydock::Command objects
280
+ def commands
281
+ @@commands ||= {}
282
+ end
283
+
284
+ # Returns true if automatic execution is enabled.
285
+ def run?
286
+ @@run
287
+ end
288
+
289
+ # Disable automatic execution (enabled by default)
290
+ #
291
+ # Drydock.run = false
292
+ def run=(v)
293
+ @@run = (v == true) ? true : false
294
+ end
295
+
296
+ # Return true if a command has been executed.
297
+ def has_run?
298
+ @@has_run
299
+ end
300
+
301
+ # Execute the given command.
302
+ # By default, Drydock automatically executes itself and provides handlers for known errors.
303
+ # You can override this functionality by calling +Drydock.run!+ yourself. Drydock
304
+ # will only call +run!+ once.
305
+ def run!(argv=[], stdin=STDIN)
306
+ return if has_run?
307
+ @@has_run = true
308
+ raise NoCommandsDefined.new unless commands
309
+ @@global_options, cmd_name, @@command_options, argv = process_arguments(argv)
64
310
 
65
- global_options = global_parser.getopts(argv)
66
- global_options = global_options.keys.inject({}) do |hash, key|
67
- hash[key.to_sym] = global_options[key]
68
- hash
69
- end
311
+ cmd_name ||= default_command
70
312
 
71
- cmd_name = (argv.empty?) ? @default_command : argv.shift
72
313
  raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
73
314
 
74
- cmd = get_command(cmd_name)
75
- command_parser = @command_opts_parser[cmd.index]
315
+ stdin = (defined? @@stdin_block) ? @@stdin_block.call(stdin, []) : stdin
316
+ @@before_block.call if defined? @@before_block
76
317
 
77
- command_options = command_parser.getopts(argv) if (!argv.empty? && command_parser)
78
- command_options = command_options.keys.inject({}) do |hash, key|
79
- hash[key.to_sym] = command_options[key]
80
- hash
81
- end
318
+ call_command(cmd_name, argv, stdin)
82
319
 
83
- [global_options, cmd_name, command_options, argv]
320
+ @@after_block.call if defined? @@after_block
321
+
322
+ rescue OptionParser::InvalidOption => ex
323
+ raise Drydock::InvalidArgument.new(ex.args)
324
+ rescue OptionParser::MissingArgument => ex
325
+ raise Drydock::MissingArgument.new(ex.args)
84
326
  end
85
327
 
86
-
328
+ private
87
329
 
88
- def usage(msg)
89
- get_current_option_parser.banner = msg
330
+ # Executes the block associated to +cmd+
331
+ def call_command(cmd, argv=[], stdin=nil)
332
+ return unless command?(cmd)
333
+ get_command(cmd).call(cmd, argv, stdin, @@global_options || {}, @@command_options || {})
90
334
  end
91
335
 
92
- # get_current_option_parser
93
- #
94
- # Grab the options parser for the current command or create it if it doesn't exist.
95
- def get_current_option_parser
96
- @command_opts_parser ||= []
97
- @command_index ||= 0
98
- (@command_opts_parser[@command_index] ||= OptionParser.new)
99
- end
336
+ # Returns the Drydock::Command object with the name +cmd+
337
+ def get_command(cmd)
338
+ return unless command?(cmd)
339
+ @@commands[canonize(cmd)]
340
+ end
100
341
 
101
- def global_option(*args, &b)
102
- @global_opts_parser ||= OptionParser.new
103
- args.unshift(@global_opts_parser)
104
- option_parser(args, &b)
342
+ # Returns true if a command with the name +cmd+ has been defined.
343
+ def command?(cmd)
344
+ name = canonize(cmd)
345
+ (@@commands || {}).has_key? name
105
346
  end
106
347
 
107
- def option(*args, &b)
108
- args.unshift(get_current_option_parser)
109
- option_parser(args, &b)
348
+ # Canonizes a string to the symbol format for command names
349
+ def canonize(cmd)
350
+ return unless cmd
351
+ return cmd if cmd.kind_of?(Symbol)
352
+ cmd.tr('-', '_').to_sym
110
353
  end
111
354
 
112
- # option_parser
113
- #
114
355
  # Processes calls to option and global_option. Symbols are converted into
115
- # OptionParser style strings (:h and :help become '-h' and '--help'). If a
116
- # class is included, it will tell OptionParser to expect a value otherwise
117
- # it assumes a boolean value.
118
- #
119
- # +args+ is passed directly to OptionParser.on so it can contain anything
120
- # that's valid to that method. Some examples:
121
- # [:h, :help, "Displays this message"]
122
- # [:m, :max, Integer, "Maximum threshold"]
123
- # ['-l x,y,z', '--lang=x,y,z', Array, "Requested languages"]
356
+ # OptionParser style strings (:h and :help become '-h' and '--help').
124
357
  def option_parser(args=[], &b)
125
358
  return if args.empty?
126
359
  opts_parser = args.shift
127
360
 
361
+ arg_name = ''
128
362
  symbol_switches = []
129
363
  args.each_with_index do |arg, index|
130
364
  if arg.is_a? Symbol
365
+ arg_name = arg.to_s if arg.to_s.size > arg_name.size
131
366
  args[index] = (arg.to_s.length == 1) ? "-#{arg.to_s}" : "--#{arg.to_s}"
132
367
  symbol_switches << args[index]
133
368
  elsif arg.kind_of?(Class)
@@ -145,88 +380,103 @@ module Drydock
145
380
  result = (b.nil?) ? v : b.call(*block_args[0..(b.arity-1)])
146
381
  end
147
382
  end
148
- end
149
-
150
- def command(*cmds, &b)
151
- @command_index ||= 0
152
- @command_opts_parser ||= []
153
- cmds.each do |cmd|
154
- c = Command.new(cmd, @command_index, &b)
155
- (@commands ||= {})[cmd] = c
156
- end
157
383
 
158
- @command_index += 1
384
+ arg_name
159
385
  end
160
386
 
161
- def alias_command(aliaz, cmd)
162
- return unless @commands.has_key? cmd
163
- @commands[aliaz] = @commands[cmd]
164
- end
165
387
 
166
- def run!(argv, stdin=nil)
167
- raise NoCommandsDefined.new unless @commands
168
- @global_options, cmd_name, @command_options, argv = process_arguments(argv)
169
-
170
- cmd_name ||= @default_command
388
+ # Split the +argv+ array into global args and command args and
389
+ # find the command name.
390
+ # i.e. ./script -H push -f (-H is a global arg, push is the command, -f is a command arg)
391
+ # returns [global_options, cmd, command_options, argv]
392
+ def process_arguments(argv=[])
393
+ global_options = command_options = {}
394
+ cmd = nil
171
395
 
396
+ global_options = global_opts_parser.getopts(argv)
397
+
398
+ cmd_name = (argv.empty?) ? @@default_command : argv.shift
172
399
  raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
173
400
 
174
- stdin = (defined? @stdin_block) ? @stdin_block.call(stdin, []) : stdin
175
- @before_block.call if defined? @before_block
401
+ cmd = get_command(cmd_name)
176
402
 
403
+ command_parser = @@command_opts_parser[get_command_index(cmd_name)]
404
+ command_options = {}
177
405
 
178
- call_command(cmd_name, argv, stdin)
406
+ # We only need to parse the options out of the arguments when
407
+ # there are args available, there is a valid parser, and
408
+ # we weren't requested to ignore the options.
409
+ if !argv.empty? && command_parser && command_parser != :ignore
410
+ command_options = command_parser.getopts(argv)
411
+ end
179
412
 
413
+ # Add accessors to the Drydock::Command object
414
+ # for the global and command specific options
415
+ [global_option_names, (command_option_names[get_command_index(cmd_name)] || [])].flatten.each do |n|
416
+ unless cmd.respond_to?(n)
417
+ cmd.class.send(:define_method, n) do
418
+ instance_variable_get("@#{n}")
419
+ end
420
+ end
421
+ unless cmd.respond_to?("#{n}=")
422
+ cmd.class.send(:define_method, "#{n}=") do |val|
423
+ instance_variable_set("@#{n}", val)
424
+ end
425
+ end
426
+ end
180
427
 
181
- rescue OptionParser::InvalidOption => ex
182
- raise Drydock::InvalidArgument.new(ex.args)
183
- rescue OptionParser::MissingArgument => ex
184
- raise Drydock::MissingArgument.new(ex.args)
428
+ [global_options, cmd_name, command_options, argv]
185
429
  end
186
430
 
187
-
188
- def call_command(cmd_str, argv=[], stdin=nil)
189
- return unless command?(cmd_str)
190
- get_command(cmd_str).call(cmd_str, argv, stdin, @global_options, @command_options)
431
+ def global_option_names
432
+ @@global_option_names ||= []
191
433
  end
192
434
 
193
- def get_command(cmd)
194
- return unless command?(cmd)
195
- @commands[canonize(cmd)]
196
- end
435
+ # Grab the current list of command-specific option names. This is a list of the
436
+ # long names.
437
+ def current_command_option_names
438
+ @@command_option_names ||= []
439
+ @@command_index ||= 0
440
+ (@@command_option_names[@@command_index] ||= [])
441
+ end
197
442
 
198
- def commands
199
- @commands
443
+ def command_index_map
444
+ @@command_index_map ||= {}
200
445
  end
201
446
 
202
- def run
203
- @run || true
447
+ def get_command_index(cmd)
448
+ command_index_map[canonize(cmd)] || -1
204
449
  end
205
450
 
206
- def run=(v)
207
- @run = v
451
+ def command_option_names
452
+ @@command_option_names ||= []
208
453
  end
209
454
 
210
- def command?(cmd)
211
- name = canonize(cmd)
212
- (@commands || {}).has_key? name
455
+ def global_opts_parser
456
+ @@global_opts_parser ||= OptionParser.new
213
457
  end
214
- def canonize(cmd)
215
- return unless cmd
216
- return cmd if cmd.kind_of?(Symbol)
217
- cmd.tr('-', '_').to_sym
458
+
459
+ def default_command
460
+ @@default_command ||= nil
218
461
  end
219
462
 
220
463
  end
221
464
 
222
- Drydock::FORWARDED_METHODS.each do |m|
223
- eval(<<-end_eval, binding, "(Drydock)", __LINE__)
224
- def #{m}(*args, &b)
225
- Drydock.#{m}(*args, &b)
226
- end
227
- end_eval
465
+ include Drydock
466
+
467
+ trap ("SIGINT") do
468
+ puts "#{$/}Exiting..."
469
+ exit 1
228
470
  end
229
471
 
230
472
 
473
+ at_exit {
474
+ begin
475
+ Drydock.run!(ARGV, STDIN) if Drydock.run? && !Drydock.has_run?
476
+ rescue => ex
477
+ STDERR.puts "ERROR: #{ex.message}"
478
+ STDERR.puts ex.backtrace if Drydock.debug?
479
+ end
480
+ }
231
481
 
232
482
 
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + "/../lib/drydock"
2
+ require "rubygems"
3
+ require "test/spec"
4
+ require "mocha"
5
+
6
+ class Test::Unit::TestCase
7
+ def test_command_direct(cmd, argv, &b)
8
+ Drydock::Command.new(cmd, &b).call(*argv)
9
+ end
10
+ def test_command(*args, &b)
11
+ command(*args, &b)
12
+ end
13
+ end
14
+
15
+ class JohnWestSmokedOysters < Drydock::Command; end;
16
+
17
+ Drydock.run = false
18
+
19
+ context "command" do
20
+
21
+ setup do
22
+ @mock = mock()
23
+ end
24
+
25
+ specify "should know a command alias" do
26
+ @mock.expects(:called).with(:eat_alias)
27
+ test_command_direct(:eat, [:eat_alias]) { |obj,argv|
28
+ @mock.called(obj.alias)
29
+ }
30
+ end
31
+
32
+ specify "should accept a custom command class" do
33
+ @mock.expects(:called).with(JohnWestSmokedOysters)
34
+ test_command(:eat => JohnWestSmokedOysters) { |obj,argv|
35
+ @mock.called(obj.class)
36
+ }
37
+ Drydock.run!(['eat'])
38
+ end
39
+
40
+ end
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: delano-drydock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
8
- - Blake Mizerany
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
@@ -14,7 +13,7 @@ date: 2008-08-17 00:00:00 -07:00
14
13
  default_executable:
15
14
  dependencies: []
16
15
 
17
- description: Command line apps made easy
16
+ description: A seaworthy DSL for writing command line apps inspired by Blake Mizerany's Frylock
18
17
  email: delano@solutious.com
19
18
  executables: []
20
19
 
@@ -23,12 +22,39 @@ extensions: []
23
22
  extra_rdoc_files:
24
23
  - README.rdoc
25
24
  - LICENSE.txt
25
+ - CHANGES.txt
26
26
  files:
27
+ - CHANGES.txt
27
28
  - LICENSE.txt
28
29
  - README.rdoc
30
+ - Rakefile
29
31
  - bin/example
30
- - lib/drydock/exceptions.rb
32
+ - drydock.gemspec
31
33
  - lib/drydock.rb
34
+ - test/command_test.rb
35
+ - doc
36
+ - doc/classes
37
+ - doc/classes/Drydock
38
+ - doc/classes/Drydock/Command.html
39
+ - doc/classes/Drydock/InvalidArgument.html
40
+ - doc/classes/Drydock/MissingArgument.html
41
+ - doc/classes/Drydock/NoCommandsDefined.html
42
+ - doc/classes/Drydock/UnknownCommand.html
43
+ - doc/classes/Drydock.html
44
+ - doc/created.rid
45
+ - doc/files
46
+ - doc/files/bin
47
+ - doc/files/bin/example.html
48
+ - doc/files/CHANGES_txt.html
49
+ - doc/files/lib
50
+ - doc/files/lib/drydock_rb.html
51
+ - doc/files/LICENSE_txt.html
52
+ - doc/files/README_rdoc.html
53
+ - doc/fr_class_index.html
54
+ - doc/fr_file_index.html
55
+ - doc/fr_method_index.html
56
+ - doc/index.html
57
+ - doc/rdoc-style.css
32
58
  has_rdoc: true
33
59
  homepage: http://github.com/delano/drydock
34
60
  post_install_message:
@@ -36,7 +62,7 @@ rdoc_options:
36
62
  - --line-numbers
37
63
  - --inline-source
38
64
  - --title
39
- - "Drydock: Easy command-line apps"
65
+ - "Drydock: a seaworthy DSL for command-line apps"
40
66
  - --main
41
67
  - README.rdoc
42
68
  require_paths:
@@ -55,10 +81,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
81
  version:
56
82
  requirements: []
57
83
 
58
- rubyforge_project:
84
+ rubyforge_project: drydock
59
85
  rubygems_version: 1.2.0
60
86
  signing_key:
61
87
  specification_version: 1
62
- summary: Command line apps made easy
88
+ summary: A seaworthy DSL for writing command line apps
63
89
  test_files: []
64
90
 
@@ -1,24 +0,0 @@
1
- module Drylock
2
-
3
- class UnknownCommand < RuntimeError
4
- attr_reader :name
5
- def initialize(name)
6
- @name = name || :unknown
7
- end
8
- end
9
-
10
- class NoCommandsDefined < RuntimeError
11
- end
12
-
13
- class InvalidArgument < RuntimeError
14
- attr_accessor :args
15
- def initialize(args)
16
- # We grab just the name of the argument
17
- @args = args || []
18
- end
19
- end
20
-
21
- class MissingArgument < InvalidArgument
22
- end
23
-
24
- end