clamp 0.0.1 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.markdown +152 -11
- data/examples/flipflop +21 -0
- data/examples/icecream +16 -0
- data/examples/rename +0 -2
- data/examples/speak +28 -0
- data/lib/clamp/command.rb +60 -111
- data/lib/clamp/help_support.rb +69 -0
- data/lib/clamp/option.rb +7 -2
- data/lib/clamp/option_support.rb +75 -0
- data/lib/clamp/subcommand_support.rb +35 -0
- data/lib/clamp/version.rb +1 -1
- data/spec/clamp/command_group_spec.rb +93 -0
- data/spec/clamp/command_spec.rb +61 -44
- data/spec/clamp/option_spec.rb +13 -0
- data/spec/spec_helper.rb +26 -0
- metadata +12 -6
- data/Gemfile.lock +0 -30
- data/lib/clamp/argument.rb +0 -6
data/README.markdown
CHANGED
@@ -17,24 +17,165 @@ Yeah, sorry. There are a bunch of existing command-line parsing libraries out t
|
|
17
17
|
Quick Start
|
18
18
|
-----------
|
19
19
|
|
20
|
-
Clamp models a command as a Ruby class
|
20
|
+
Clamp models a command as a Ruby class; a subclass of `Clamp::Command`. They look something like this:
|
21
21
|
|
22
|
-
|
22
|
+
class SpeakCommand < Clamp::Command
|
23
23
|
|
24
|
-
|
24
|
+
option "--loud", :flag, "say it loud"
|
25
|
+
option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
|
26
|
+
Integer(s)
|
27
|
+
end
|
28
|
+
|
29
|
+
argument "WORDS ...", "the thing to say"
|
30
|
+
|
31
|
+
def execute
|
32
|
+
|
33
|
+
signal_usage_error "I have nothing to say" if arguments.empty?
|
34
|
+
the_truth = arguments.join(" ")
|
35
|
+
the_truth.upcase! if loud?
|
36
|
+
|
37
|
+
iterations.times do
|
38
|
+
puts the_truth
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
Class-level methods (like `option` and `argument`) are available to declare command-line options, and document usage.
|
46
|
+
|
47
|
+
The command can be invoked by instantiating the class, and asking it to run:
|
48
|
+
|
49
|
+
SpeakCommand.new("speak").run(["--loud", "a", "b", "c"])
|
50
|
+
|
51
|
+
but it's more typical to use the class-level "`run`" method:
|
52
|
+
|
53
|
+
SpeakCommand.run
|
54
|
+
|
55
|
+
which takes arguments from `ARGV`, and includes some handy error-handling.
|
56
|
+
|
57
|
+
Declaring options
|
58
|
+
-----------------
|
59
|
+
|
60
|
+
Options are declared using the [`option`](../Clamp/Command.option) method. The three required arguments are:
|
61
|
+
|
62
|
+
1. the option switch (or switches),
|
63
|
+
2. a short description of the option argument type, and
|
64
|
+
3. a description of the option itself
|
65
|
+
|
66
|
+
For example:
|
67
|
+
|
68
|
+
option "--flavour", "FLAVOUR", "ice-cream flavour"
|
69
|
+
|
70
|
+
It works a little like `attr_accessor`, defining reader and writer methods on the command class. The attribute name is derived from the switch (in this case, "`flavour`"). When you pass options to your command, Clamp will populate the attributes, which are then available for use in your `#execute` method.
|
71
|
+
|
72
|
+
def execute
|
73
|
+
puts "You chose #{flavour}. Excellent choice!"
|
74
|
+
end
|
75
|
+
|
76
|
+
If you don't like the inferred attribute name, you can override it:
|
77
|
+
|
78
|
+
option "--type", "TYPE", "type of widget", :attribute_name => :widget_type
|
79
|
+
# to avoid clobbering Object#type
|
80
|
+
|
81
|
+
### Short/long option switches
|
82
|
+
|
83
|
+
The first argument to `option` can be an array, rather than a single string, in which case all the switches are treated as aliases:
|
84
|
+
|
85
|
+
option ["-s", "--subject"], "SUBJECT", "email subject line"
|
86
|
+
|
87
|
+
### Flag options
|
88
|
+
|
89
|
+
Some options are just boolean flags. Pass "`:flag`" as the second parameter to tell Clamp not to expect an option argument:
|
90
|
+
|
91
|
+
option "--verbose", :flag, "be chatty"
|
92
|
+
|
93
|
+
For flag options, Clamp appends "`?`" to the generated reader method; ie. you get a method called "`verbose?`", rather than just "`verbose`".
|
94
|
+
|
95
|
+
Negatable flags are easy to generate, too:
|
96
|
+
|
97
|
+
option "--[no-]force", :flag, "be forceful (or not)"
|
98
|
+
|
99
|
+
Clamp will handle both "`--force`" and "`--no-force`" options, setting the value of "`#force?`" appropriately.
|
100
|
+
|
101
|
+
### Validation and conversion of option arguments
|
102
|
+
|
103
|
+
If a block is passed to `option`, it will be called with the raw string option argument, and is expected to coerce that String to the correct type, e.g.
|
104
|
+
|
105
|
+
option "--port", "PORT", "port to listen on" do |s|
|
106
|
+
Integer(s)
|
107
|
+
end
|
108
|
+
|
109
|
+
If the block raises an ArgumentError, Clamp will catch it, and report that the option value was bad:
|
110
|
+
|
111
|
+
!!!plain
|
112
|
+
ERROR: option '--port': invalid value for Integer: "blah"
|
113
|
+
|
114
|
+
Declaring arguments
|
115
|
+
-------------------
|
116
|
+
|
117
|
+
The `argument` method is used to declare command arguments:
|
118
|
+
|
119
|
+
argument "FILE ...", "source files"
|
120
|
+
argument "DIR", "target directory"
|
121
|
+
|
122
|
+
Use of `argument` is entirely for documentation purposes. Whether or not you declare and describe your expected arguments, the actual arguments that remain after option parsing will be available as `arguments` when your `#execute` method is called.
|
123
|
+
|
124
|
+
Sub-commands
|
125
|
+
------------
|
126
|
+
|
127
|
+
The `subcommand` method declares sub-commands:
|
128
|
+
|
129
|
+
class MainCommand < Clamp::Command
|
130
|
+
|
131
|
+
subcommand "init", "Initialize the repository" do
|
132
|
+
|
133
|
+
def execute
|
134
|
+
# ...
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
Clamp generates an anonymous sub-class of the current class, to represent the sub-command. Additional options may be declared within subcommand blocks, but all options declared on the parent class are also accepted.
|
142
|
+
|
143
|
+
Alternatively, you can provide an explicit sub-command class, rather than a block:
|
25
144
|
|
26
|
-
|
27
|
-
|
145
|
+
class MainCommand < Clamp::Command
|
146
|
+
|
147
|
+
subcommand "init", "Initialize the repository", InitCommand
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
class InitCommand < Clamp::Command
|
152
|
+
|
28
153
|
def execute
|
29
|
-
#
|
154
|
+
# ...
|
30
155
|
end
|
31
|
-
|
156
|
+
|
32
157
|
end
|
33
158
|
|
34
|
-
|
159
|
+
When a command has sub-commands, Clamp will attempt to delegate based on the first command-line argument, before options are parsed. Remaining arguments will be passed on to the sub-command.
|
160
|
+
|
161
|
+
Getting help
|
162
|
+
------------
|
163
|
+
|
164
|
+
All Clamp commands support a "`--help`" option, which outputs brief usage documentation, based on those seemingly useless extra parameters that you had to pass to `option` and `argument`.
|
165
|
+
|
166
|
+
$ speak --help
|
167
|
+
Usage:
|
168
|
+
speak [OPTIONS] WORDS ...
|
169
|
+
|
170
|
+
Arguments:
|
171
|
+
WORDS ... the thing to say
|
35
172
|
|
36
|
-
|
173
|
+
Options:
|
174
|
+
--loud say it loud
|
175
|
+
-n, --iterations N say it N times
|
176
|
+
--help print help
|
37
177
|
|
38
|
-
|
178
|
+
Contributing to Clamp
|
179
|
+
---------------------
|
39
180
|
|
40
|
-
|
181
|
+
Source-code for Clamp is [on Github](https://github.com/mdub/clamp).
|
data/examples/flipflop
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require "clamp"
|
4
|
+
|
5
|
+
class FlipFlop < Clamp::Command
|
6
|
+
|
7
|
+
subcommand "flip", "flip it" do
|
8
|
+
def execute
|
9
|
+
puts "FLIPPED"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
subcommand "flop", "flop it" do
|
14
|
+
def execute
|
15
|
+
puts "FLOPPED"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
FlipFlop.run
|
data/examples/icecream
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require "clamp"
|
4
|
+
|
5
|
+
class Icecream < Clamp::Command
|
6
|
+
|
7
|
+
option "--flavour", "FLAVOUR", "ice-cream flavour"
|
8
|
+
|
9
|
+
def execute
|
10
|
+
signal_usage_error "what flavour?" unless flavour
|
11
|
+
puts "You chose #{flavour}. Excellent choice!"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
Icecream.run
|
data/examples/rename
CHANGED
data/examples/speak
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require "clamp"
|
4
|
+
|
5
|
+
class SpeakCommand < Clamp::Command
|
6
|
+
|
7
|
+
option "--loud", :flag, "say it loud"
|
8
|
+
option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
|
9
|
+
Integer(s)
|
10
|
+
end
|
11
|
+
|
12
|
+
argument "WORDS ...", "the thing to say"
|
13
|
+
|
14
|
+
def execute
|
15
|
+
|
16
|
+
signal_usage_error "I have nothing to say" if arguments.empty?
|
17
|
+
the_truth = arguments.join(" ")
|
18
|
+
the_truth.upcase! if loud?
|
19
|
+
|
20
|
+
iterations.times do
|
21
|
+
puts the_truth
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
SpeakCommand.run
|
data/lib/clamp/command.rb
CHANGED
@@ -1,17 +1,22 @@
|
|
1
|
-
require 'clamp/
|
2
|
-
require 'clamp/
|
1
|
+
require 'clamp/help_support'
|
2
|
+
require 'clamp/option_support'
|
3
|
+
require 'clamp/subcommand_support'
|
3
4
|
|
4
5
|
module Clamp
|
5
|
-
|
6
|
+
|
6
7
|
class Command
|
7
|
-
|
8
|
-
def initialize(name)
|
8
|
+
|
9
|
+
def initialize(name, context = {})
|
9
10
|
@name = name
|
11
|
+
@context = context
|
10
12
|
end
|
11
|
-
|
13
|
+
|
12
14
|
attr_reader :name
|
13
15
|
attr_reader :arguments
|
14
16
|
|
17
|
+
attr_accessor :context
|
18
|
+
attr_accessor :parent_command
|
19
|
+
|
15
20
|
def parse(arguments)
|
16
21
|
while arguments.first =~ /^-/
|
17
22
|
case (switch = arguments.shift)
|
@@ -31,166 +36,110 @@ module Clamp
|
|
31
36
|
rescue ArgumentError => e
|
32
37
|
signal_usage_error "option '#{switch}': #{e.message}"
|
33
38
|
end
|
34
|
-
|
39
|
+
|
35
40
|
end
|
36
41
|
end
|
37
42
|
@arguments = arguments
|
38
43
|
end
|
39
|
-
|
44
|
+
|
45
|
+
# default implementation
|
40
46
|
def execute
|
41
|
-
|
47
|
+
if self.class.has_subcommands?
|
48
|
+
execute_subcommand
|
49
|
+
else
|
50
|
+
raise "you need to define #execute"
|
51
|
+
end
|
42
52
|
end
|
43
|
-
|
53
|
+
|
44
54
|
def run(arguments)
|
45
55
|
parse(arguments)
|
46
56
|
execute
|
47
57
|
end
|
48
58
|
|
49
59
|
def help
|
50
|
-
self.class.help
|
60
|
+
self.class.help(name)
|
51
61
|
end
|
52
|
-
|
62
|
+
|
53
63
|
private
|
54
64
|
|
65
|
+
def execute_subcommand
|
66
|
+
signal_usage_error "no subcommand specified" if arguments.empty?
|
67
|
+
subcommand_name = arguments.shift
|
68
|
+
subcommand_class = find_subcommand_class(subcommand_name)
|
69
|
+
subcommand = subcommand_class.new("#{name} #{subcommand_name}", context)
|
70
|
+
subcommand.parent_command = self
|
71
|
+
subcommand.run(arguments)
|
72
|
+
end
|
73
|
+
|
55
74
|
def find_option(switch)
|
56
75
|
self.class.find_option(switch) ||
|
57
76
|
signal_usage_error("Unrecognised option '#{switch}'")
|
58
77
|
end
|
59
78
|
|
79
|
+
def find_subcommand(name)
|
80
|
+
self.class.find_subcommand(name) ||
|
81
|
+
signal_usage_error("No such sub-command '#{name}'")
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_subcommand_class(name)
|
85
|
+
subcommand = find_subcommand(name)
|
86
|
+
subcommand.subcommand_class if subcommand
|
87
|
+
end
|
88
|
+
|
60
89
|
def signal_usage_error(message)
|
61
90
|
e = UsageError.new(message, self)
|
62
91
|
e.set_backtrace(caller)
|
63
92
|
raise e
|
64
93
|
end
|
65
|
-
|
66
|
-
class << self
|
67
|
-
|
68
|
-
def options
|
69
|
-
@options ||= []
|
70
|
-
end
|
71
|
-
|
72
|
-
def option(switches, argument_type, description, opts = {}, &block)
|
73
|
-
option = Clamp::Option.new(switches, argument_type, description, opts)
|
74
|
-
self.options << option
|
75
|
-
declare_option_reader(option)
|
76
|
-
declare_option_writer(option, &block)
|
77
|
-
end
|
78
|
-
|
79
|
-
def help_option(switches = ["-h", "--help"])
|
80
|
-
option(switches, :flag, "print help", :attribute_name => :help_requested) do
|
81
|
-
raise Clamp::HelpWanted.new(self)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def has_options?
|
86
|
-
!options.empty?
|
87
|
-
end
|
88
|
-
|
89
|
-
def find_option(switch)
|
90
|
-
options.find { |o| o.handles?(switch) }
|
91
|
-
end
|
92
94
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
end
|
95
|
+
def help_requested=(value)
|
96
|
+
raise Clamp::HelpWanted.new(self)
|
97
|
+
end
|
97
98
|
|
98
|
-
|
99
|
-
@arguments ||= []
|
100
|
-
end
|
101
|
-
|
102
|
-
def argument(name, description)
|
103
|
-
arguments << Argument.new(name, description)
|
104
|
-
end
|
99
|
+
class << self
|
105
100
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
def help
|
113
|
-
help = StringIO.new
|
114
|
-
help.puts "Usage:"
|
115
|
-
usages = @usages || [derived_usage]
|
116
|
-
usages.each_with_index do |usage, i|
|
117
|
-
help.puts " __COMMAND__ #{usage}".rstrip
|
118
|
-
end
|
119
|
-
detail_format = " %-29s %s"
|
120
|
-
unless arguments.empty?
|
121
|
-
help.puts "\nArguments:"
|
122
|
-
arguments.each do |argument|
|
123
|
-
help.puts detail_format % [argument.name, argument.description]
|
124
|
-
end
|
125
|
-
end
|
126
|
-
unless options.empty?
|
127
|
-
help.puts "\nOptions:"
|
128
|
-
options.each do |option|
|
129
|
-
help.puts detail_format % option.help
|
130
|
-
end
|
131
|
-
end
|
132
|
-
help.string
|
133
|
-
end
|
134
|
-
|
135
|
-
def run(name = $0, args = ARGV)
|
101
|
+
include OptionSupport
|
102
|
+
include SubcommandSupport
|
103
|
+
include HelpSupport
|
104
|
+
|
105
|
+
def run(name = $0, args = ARGV, context = {})
|
136
106
|
begin
|
137
|
-
new(name).run(args)
|
107
|
+
new(name, context).run(args)
|
138
108
|
rescue Clamp::UsageError => e
|
139
109
|
$stderr.puts "ERROR: #{e.message}"
|
140
110
|
$stderr.puts ""
|
141
|
-
$stderr.puts
|
111
|
+
$stderr.puts "See: '#{name} --help'"
|
142
112
|
exit(1)
|
143
113
|
rescue Clamp::HelpWanted => e
|
144
114
|
puts e.command.help
|
145
115
|
end
|
146
116
|
end
|
147
117
|
|
148
|
-
private
|
149
|
-
|
150
|
-
def declare_option_reader(option)
|
151
|
-
reader_name = option.attribute_name
|
152
|
-
reader_name += "?" if option.flag?
|
153
|
-
class_eval <<-RUBY
|
154
|
-
def #{reader_name}
|
155
|
-
@#{option.attribute_name}
|
156
|
-
end
|
157
|
-
RUBY
|
158
|
-
end
|
159
|
-
|
160
|
-
def declare_option_writer(option, &block)
|
161
|
-
define_method("#{option.attribute_name}=") do |value|
|
162
|
-
if block
|
163
|
-
value = instance_exec(value, &block)
|
164
|
-
end
|
165
|
-
instance_variable_set("@#{option.attribute_name}", value)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
118
|
end
|
170
|
-
|
119
|
+
|
171
120
|
end
|
172
|
-
|
121
|
+
|
173
122
|
class Error < StandardError
|
174
|
-
|
123
|
+
|
175
124
|
def initialize(message, command)
|
176
125
|
super(message)
|
177
126
|
@command = command
|
178
127
|
end
|
179
128
|
|
180
129
|
attr_reader :command
|
181
|
-
|
130
|
+
|
182
131
|
end
|
183
132
|
|
184
133
|
# raise to signal incorrect command usage
|
185
134
|
class UsageError < Error; end
|
186
|
-
|
135
|
+
|
187
136
|
# raise to request usage help
|
188
137
|
class HelpWanted < Error
|
189
|
-
|
138
|
+
|
190
139
|
def initialize(command)
|
191
140
|
super("I need help", command)
|
192
141
|
end
|
193
|
-
|
142
|
+
|
194
143
|
end
|
195
|
-
|
144
|
+
|
196
145
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Argument < Struct.new(:name, :description)
|
4
|
+
|
5
|
+
def help
|
6
|
+
[name, description]
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
module HelpSupport
|
12
|
+
|
13
|
+
def declared_arguments
|
14
|
+
@declared_arguments ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def argument(name, description)
|
18
|
+
declared_arguments << Argument.new(name, description)
|
19
|
+
end
|
20
|
+
|
21
|
+
def usage(usage)
|
22
|
+
@declared_usage_descriptions ||= []
|
23
|
+
@declared_usage_descriptions << usage
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :declared_usage_descriptions
|
27
|
+
|
28
|
+
def derived_usage_description
|
29
|
+
parts = declared_arguments.map { |a| a.name }
|
30
|
+
parts.unshift("SUBCOMMAND") if has_subcommands?
|
31
|
+
parts.unshift("[OPTIONS]") if has_options?
|
32
|
+
parts.join(" ")
|
33
|
+
end
|
34
|
+
|
35
|
+
def usage_descriptions
|
36
|
+
declared_usage_descriptions || [derived_usage_description]
|
37
|
+
end
|
38
|
+
|
39
|
+
def help(command_name)
|
40
|
+
help = StringIO.new
|
41
|
+
help.puts "Usage:"
|
42
|
+
usage_descriptions.each_with_index do |usage, i|
|
43
|
+
help.puts " #{command_name} #{usage}".rstrip
|
44
|
+
end
|
45
|
+
detail_format = " %-29s %s"
|
46
|
+
unless declared_arguments.empty?
|
47
|
+
help.puts "\nArguments:"
|
48
|
+
declared_arguments.each do |argument|
|
49
|
+
help.puts detail_format % argument.help
|
50
|
+
end
|
51
|
+
end
|
52
|
+
unless recognised_subcommands.empty?
|
53
|
+
help.puts "\nSubcommands:"
|
54
|
+
recognised_subcommands.each do |subcommand|
|
55
|
+
help.puts detail_format % subcommand.help
|
56
|
+
end
|
57
|
+
end
|
58
|
+
if has_options?
|
59
|
+
help.puts "\nOptions:"
|
60
|
+
recognised_options.each do |option|
|
61
|
+
help.puts detail_format % option.help
|
62
|
+
end
|
63
|
+
end
|
64
|
+
help.string
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/clamp/option.rb
CHANGED
@@ -6,10 +6,15 @@ module Clamp
|
|
6
6
|
@switches = Array(switches)
|
7
7
|
@argument_type = argument_type
|
8
8
|
@description = description
|
9
|
-
|
9
|
+
if options.has_key?(:attribute_name)
|
10
|
+
@attribute_name = options[:attribute_name].to_s
|
11
|
+
end
|
12
|
+
if options.has_key?(:default)
|
13
|
+
@default_value = options[:default]
|
14
|
+
end
|
10
15
|
end
|
11
16
|
|
12
|
-
attr_reader :switches, :argument_type, :description
|
17
|
+
attr_reader :switches, :argument_type, :description, :default_value
|
13
18
|
|
14
19
|
def attribute_name
|
15
20
|
@attribute_name ||= long_switch.sub(/^--(\[no-\])?/, '').tr('-', '_')
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'clamp/option'
|
2
|
+
|
3
|
+
module Clamp
|
4
|
+
|
5
|
+
module OptionSupport
|
6
|
+
|
7
|
+
def option(switches, argument_type, description, opts = {}, &block)
|
8
|
+
option = Clamp::Option.new(switches, argument_type, description, opts)
|
9
|
+
declare_option(option, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_options?
|
13
|
+
!declared_options.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def declared_options
|
17
|
+
my_declared_options + inherited_declared_options
|
18
|
+
end
|
19
|
+
|
20
|
+
def recognised_options
|
21
|
+
declared_options + standard_options
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_option(switch)
|
25
|
+
recognised_options.find { |o| o.handles?(switch) }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def my_declared_options
|
31
|
+
@my_declared_options ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def declare_option(option, &block)
|
35
|
+
my_declared_options << option
|
36
|
+
declare_option_reader(option)
|
37
|
+
declare_option_writer(option, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def inherited_declared_options
|
41
|
+
if superclass.respond_to?(:declared_options)
|
42
|
+
superclass.declared_options
|
43
|
+
else
|
44
|
+
[]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
HELP_OPTION = Clamp::Option.new("--help", :flag, "print help", :attribute_name => :help_requested)
|
49
|
+
|
50
|
+
def standard_options
|
51
|
+
[HELP_OPTION]
|
52
|
+
end
|
53
|
+
|
54
|
+
def declare_option_reader(option)
|
55
|
+
reader_name = option.attribute_name
|
56
|
+
reader_name += "?" if option.flag?
|
57
|
+
define_method(reader_name) do
|
58
|
+
value = instance_variable_get("@#{option.attribute_name}")
|
59
|
+
value = option.default_value if value.nil?
|
60
|
+
value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def declare_option_writer(option, &block)
|
65
|
+
define_method("#{option.attribute_name}=") do |value|
|
66
|
+
if block
|
67
|
+
value = instance_exec(value, &block)
|
68
|
+
end
|
69
|
+
instance_variable_set("@#{option.attribute_name}", value)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Subcommand < Struct.new(:name, :description, :subcommand_class)
|
4
|
+
|
5
|
+
def help
|
6
|
+
[name, description]
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
module SubcommandSupport
|
12
|
+
|
13
|
+
def recognised_subcommands
|
14
|
+
@recognised_subcommands ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def subcommand(name, description, subcommand_class = self, &block)
|
18
|
+
if block
|
19
|
+
# generate a anonymous sub-class
|
20
|
+
subcommand_class = Class.new(subcommand_class, &block)
|
21
|
+
end
|
22
|
+
recognised_subcommands << Subcommand.new(name, description, subcommand_class)
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_subcommands?
|
26
|
+
!recognised_subcommands.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_subcommand(name)
|
30
|
+
recognised_subcommands.find { |sc| sc.name == name }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
data/lib/clamp/version.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
describe Clamp::Command do
|
5
|
+
|
6
|
+
include OutputCapture
|
7
|
+
|
8
|
+
def self.given_command(name, &block)
|
9
|
+
before do
|
10
|
+
@command = Class.new(Clamp::Command, &block).new(name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "with subcommands" do
|
15
|
+
|
16
|
+
given_command "flipflop" do
|
17
|
+
|
18
|
+
subcommand "flip", "flip it" do
|
19
|
+
def execute
|
20
|
+
puts "FLIPPED"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
subcommand "flop", "flop it" do
|
25
|
+
def execute
|
26
|
+
puts "FLOPPED"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
it "delegates to sub-commands" do
|
33
|
+
|
34
|
+
@command.run(["flip"])
|
35
|
+
stdout.should =~ /FLIPPED/
|
36
|
+
|
37
|
+
@command.run(["flop"])
|
38
|
+
stdout.should =~ /FLOPPED/
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#help" do
|
43
|
+
|
44
|
+
it "lists subcommands" do
|
45
|
+
@help = @command.help
|
46
|
+
@help.should =~ /Subcommands:/
|
47
|
+
@help.should =~ /flip +flip it/
|
48
|
+
@help.should =~ /flop +flop it/
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "each subcommand" do
|
56
|
+
|
57
|
+
before do
|
58
|
+
|
59
|
+
@command_class = Class.new(Clamp::Command) do
|
60
|
+
|
61
|
+
option "--direction", "DIR", "which way"
|
62
|
+
|
63
|
+
subcommand "walk", "step carefully in the appointed direction" do
|
64
|
+
|
65
|
+
def execute
|
66
|
+
if direction
|
67
|
+
puts "walking #{direction}"
|
68
|
+
else
|
69
|
+
puts "wandering #{context[:default_direction]} by default"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
@command = @command_class.new("go", :default_direction => "south")
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
it "accepts parents options (specified after the subcommand)" do
|
82
|
+
@command.run(["walk", "--direction", "north"])
|
83
|
+
stdout.should =~ /walking north/
|
84
|
+
end
|
85
|
+
|
86
|
+
it "has access to command context" do
|
87
|
+
@command.run(["walk"])
|
88
|
+
stdout.should =~ /wandering south by default/
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
data/spec/clamp/command_spec.rb
CHANGED
@@ -3,23 +3,7 @@ require 'stringio'
|
|
3
3
|
|
4
4
|
describe Clamp::Command do
|
5
5
|
|
6
|
-
|
7
|
-
$stdout = @out = StringIO.new
|
8
|
-
$stderr = @err = StringIO.new
|
9
|
-
end
|
10
|
-
|
11
|
-
after do
|
12
|
-
$stdout = STDOUT
|
13
|
-
$stderr = STDERR
|
14
|
-
end
|
15
|
-
|
16
|
-
def stdout
|
17
|
-
@out.string
|
18
|
-
end
|
19
|
-
|
20
|
-
def stderr
|
21
|
-
@err.string
|
22
|
-
end
|
6
|
+
include OutputCapture
|
23
7
|
|
24
8
|
def self.given_command(name, &block)
|
25
9
|
before do
|
@@ -81,34 +65,43 @@ describe Clamp::Command do
|
|
81
65
|
|
82
66
|
describe ".option" do
|
83
67
|
|
84
|
-
before do
|
85
|
-
@command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
|
86
|
-
end
|
87
|
-
|
88
68
|
it "declares option argument accessors" do
|
69
|
+
@command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
|
89
70
|
@command.flavour.should == nil
|
90
71
|
@command.flavour = "chocolate"
|
91
72
|
@command.flavour.should == "chocolate"
|
92
73
|
end
|
93
74
|
|
94
|
-
|
75
|
+
describe "with explicit :attribute_name" do
|
95
76
|
|
96
|
-
|
77
|
+
before do
|
78
|
+
@command.class.option "--foo", "FOO", "A foo", :attribute_name => :bar
|
79
|
+
end
|
97
80
|
|
98
|
-
|
99
|
-
|
100
|
-
|
81
|
+
it "uses the specified attribute_name name to name accessors" do
|
82
|
+
@command.bar = "chocolate"
|
83
|
+
@command.bar.should == "chocolate"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "does not attempt to create the default accessors" do
|
87
|
+
@command.should_not respond_to(:foo)
|
88
|
+
@command.should_not respond_to(:foo=)
|
89
|
+
end
|
101
90
|
|
102
|
-
it "uses the specified attribute_name name to name accessors" do
|
103
|
-
@command.bar = "chocolate"
|
104
|
-
@command.bar.should == "chocolate"
|
105
91
|
end
|
106
92
|
|
107
|
-
|
108
|
-
|
109
|
-
|
93
|
+
describe "with :default value" do
|
94
|
+
|
95
|
+
given_command("cmd") do
|
96
|
+
option "--nodes", "N", "number of nodes", :default => 2
|
97
|
+
end
|
98
|
+
|
99
|
+
it "sets the specified default value" do
|
100
|
+
@command.nodes.should == 2
|
101
|
+
end
|
102
|
+
|
110
103
|
end
|
111
|
-
|
104
|
+
|
112
105
|
end
|
113
106
|
|
114
107
|
describe "with options declared" do
|
@@ -329,6 +322,20 @@ describe Clamp::Command do
|
|
329
322
|
stdout.should == @xyz.inspect
|
330
323
|
end
|
331
324
|
|
325
|
+
describe "invoked with a context hash" do
|
326
|
+
|
327
|
+
it "makes the context available within the command" do
|
328
|
+
@command.class.class_eval do
|
329
|
+
def execute
|
330
|
+
print context[:foo]
|
331
|
+
end
|
332
|
+
end
|
333
|
+
@command.class.run("xyz", [], :foo => "bar")
|
334
|
+
stdout.should == "bar"
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
338
|
+
|
332
339
|
describe "when there's a UsageError" do
|
333
340
|
|
334
341
|
before do
|
@@ -352,7 +359,7 @@ describe Clamp::Command do
|
|
352
359
|
end
|
353
360
|
|
354
361
|
it "outputs help" do
|
355
|
-
stderr.should include "
|
362
|
+
stderr.should include "See: 'cmd --help'"
|
356
363
|
end
|
357
364
|
|
358
365
|
it "exits with a non-zero status" do
|
@@ -364,17 +371,8 @@ describe Clamp::Command do
|
|
364
371
|
|
365
372
|
describe "when help is requested" do
|
366
373
|
|
367
|
-
before do
|
368
|
-
|
369
|
-
@command.class.class_eval do
|
370
|
-
help_option "--help"
|
371
|
-
end
|
372
|
-
|
373
|
-
@command.class.run("cmd", ["--help"])
|
374
|
-
|
375
|
-
end
|
376
|
-
|
377
374
|
it "outputs help" do
|
375
|
+
@command.class.run("cmd", ["--help"])
|
378
376
|
stdout.should include "Usage:"
|
379
377
|
end
|
380
378
|
|
@@ -382,4 +380,23 @@ describe Clamp::Command do
|
|
382
380
|
|
383
381
|
end
|
384
382
|
|
383
|
+
describe "subclass" do
|
384
|
+
|
385
|
+
before do
|
386
|
+
@parent_command_class = Class.new(Clamp::Command) do
|
387
|
+
option "--verbose", :flag, "be louder"
|
388
|
+
end
|
389
|
+
@derived_command_class = Class.new(@parent_command_class) do
|
390
|
+
option "--iterations", "N", "number of times to go around"
|
391
|
+
end
|
392
|
+
@command = @derived_command_class.new("cmd")
|
393
|
+
end
|
394
|
+
|
395
|
+
it "inherits options from it's superclass" do
|
396
|
+
@command.parse(["--verbose"])
|
397
|
+
@command.should be_verbose
|
398
|
+
end
|
399
|
+
|
400
|
+
end
|
401
|
+
|
385
402
|
end
|
data/spec/clamp/option_spec.rb
CHANGED
@@ -33,6 +33,19 @@ describe Clamp::Option do
|
|
33
33
|
|
34
34
|
end
|
35
35
|
|
36
|
+
describe "#default_value" do
|
37
|
+
|
38
|
+
it "defaults to nil" do
|
39
|
+
@option.default_value.should == nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "can be overridden" do
|
43
|
+
@option = Clamp::Option.new("-n", "N", "iterations", :default => 1)
|
44
|
+
@option.default_value.should == 1
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
36
49
|
describe "#help" do
|
37
50
|
|
38
51
|
it "combines switch, argument_type and description" do
|
data/spec/spec_helper.rb
CHANGED
@@ -6,3 +6,29 @@ Rspec.configure do |config|
|
|
6
6
|
config.mock_with :rr
|
7
7
|
|
8
8
|
end
|
9
|
+
|
10
|
+
module OutputCapture
|
11
|
+
|
12
|
+
def self.included(target)
|
13
|
+
|
14
|
+
target.before do
|
15
|
+
$stdout = @out = StringIO.new
|
16
|
+
$stderr = @err = StringIO.new
|
17
|
+
end
|
18
|
+
|
19
|
+
target.after do
|
20
|
+
$stdout = STDOUT
|
21
|
+
$stderr = STDERR
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def stdout
|
27
|
+
@out.string
|
28
|
+
end
|
29
|
+
|
30
|
+
def stderr
|
31
|
+
@err.string
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clamp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 7
|
10
|
+
version: 0.0.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Mike Williams
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-11-
|
18
|
+
date: 2010-11-08 00:00:00 +11:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -33,16 +33,21 @@ extra_rdoc_files: []
|
|
33
33
|
files:
|
34
34
|
- .gitignore
|
35
35
|
- Gemfile
|
36
|
-
- Gemfile.lock
|
37
36
|
- README.markdown
|
38
37
|
- Rakefile
|
39
38
|
- clamp.gemspec
|
39
|
+
- examples/flipflop
|
40
|
+
- examples/icecream
|
40
41
|
- examples/rename
|
42
|
+
- examples/speak
|
41
43
|
- lib/clamp.rb
|
42
|
-
- lib/clamp/argument.rb
|
43
44
|
- lib/clamp/command.rb
|
45
|
+
- lib/clamp/help_support.rb
|
44
46
|
- lib/clamp/option.rb
|
47
|
+
- lib/clamp/option_support.rb
|
48
|
+
- lib/clamp/subcommand_support.rb
|
45
49
|
- lib/clamp/version.rb
|
50
|
+
- spec/clamp/command_group_spec.rb
|
46
51
|
- spec/clamp/command_spec.rb
|
47
52
|
- spec/clamp/option_spec.rb
|
48
53
|
- spec/spec_helper.rb
|
@@ -81,6 +86,7 @@ signing_key:
|
|
81
86
|
specification_version: 3
|
82
87
|
summary: a minimal framework for command-line utilities
|
83
88
|
test_files:
|
89
|
+
- spec/clamp/command_group_spec.rb
|
84
90
|
- spec/clamp/command_spec.rb
|
85
91
|
- spec/clamp/option_spec.rb
|
86
92
|
- spec/spec_helper.rb
|
data/Gemfile.lock
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
clamp (0.0.1)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: http://rubygems.org/
|
8
|
-
specs:
|
9
|
-
diff-lcs (1.1.2)
|
10
|
-
rake (0.8.7)
|
11
|
-
rr (1.0.0)
|
12
|
-
rspec (2.0.1)
|
13
|
-
rspec-core (~> 2.0.1)
|
14
|
-
rspec-expectations (~> 2.0.1)
|
15
|
-
rspec-mocks (~> 2.0.1)
|
16
|
-
rspec-core (2.0.1)
|
17
|
-
rspec-expectations (2.0.1)
|
18
|
-
diff-lcs (>= 1.1.2)
|
19
|
-
rspec-mocks (2.0.1)
|
20
|
-
rspec-core (~> 2.0.1)
|
21
|
-
rspec-expectations (~> 2.0.1)
|
22
|
-
|
23
|
-
PLATFORMS
|
24
|
-
ruby
|
25
|
-
|
26
|
-
DEPENDENCIES
|
27
|
-
clamp!
|
28
|
-
rake
|
29
|
-
rr (~> 1.0.0)
|
30
|
-
rspec (~> 2.0.1)
|