clamp 0.0.1 → 0.0.7
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/.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)
|