delano-drydock 0.3.0 → 0.3.1

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