clamp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +30 -0
- data/README.markdown +40 -0
- data/Rakefile +12 -0
- data/clamp.gemspec +24 -0
- data/examples/rename +35 -0
- data/lib/clamp.rb +3 -0
- data/lib/clamp/argument.rb +6 -0
- data/lib/clamp/command.rb +196 -0
- data/lib/clamp/option.rb +54 -0
- data/lib/clamp/version.rb +3 -0
- data/spec/clamp/command_spec.rb +385 -0
- data/spec/clamp/option_spec.rb +113 -0
- data/spec/spec_helper.rb +8 -0
- metadata +86 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
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)
|
data/README.markdown
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Clamp
|
2
|
+
=====
|
3
|
+
|
4
|
+
"Clamp" is a minimal framework for command-line utilities.
|
5
|
+
|
6
|
+
It handles boring stuff like parsing the command-line, and generating help, so you can get on with making your command actually do stuff.
|
7
|
+
|
8
|
+
Not another one!
|
9
|
+
----------------
|
10
|
+
|
11
|
+
Yeah, sorry. There are a bunch of existing command-line parsing libraries out there, and Clamp draws inspiration from a variety of sources, including [Thor], [optparse], and [Clip]. In the end, though, I wanted a slightly rounder wheel.
|
12
|
+
|
13
|
+
[optparse]: http://ruby-doc.org/stdlib/libdoc/optparse/rdoc/index.html
|
14
|
+
[Thor]: http://github.com/wycats/thor
|
15
|
+
[Clip]: http://clip.rubyforge.org/
|
16
|
+
|
17
|
+
Quick Start
|
18
|
+
-----------
|
19
|
+
|
20
|
+
Clamp models a command as a Ruby class, and command invocations as instances of that class.
|
21
|
+
|
22
|
+
"Command classes" are subclasses of `Clamp::Command`. They look like this:
|
23
|
+
|
24
|
+
class InstallCommand < Clamp::Command
|
25
|
+
|
26
|
+
option "--force", :flag, ""
|
27
|
+
|
28
|
+
def execute
|
29
|
+
# do something
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
Class-level methods are available to declare command-line options, and document usage.
|
35
|
+
|
36
|
+
Clamp commands are invoked like so:
|
37
|
+
|
38
|
+
InstallCommand.run
|
39
|
+
|
40
|
+
This will instantiate a new `InstallCommand`, handle command-line args, and finally call the `#execute` method to do the real work.
|
data/Rakefile
ADDED
data/clamp.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "clamp/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
|
7
|
+
s.name = "clamp"
|
8
|
+
s.version = Clamp::VERSION.dup
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Mike Williams"]
|
11
|
+
s.email = "mdub@dogbiscuit.org"
|
12
|
+
s.homepage = "http://github.com/mdub/clamp"
|
13
|
+
|
14
|
+
s.summary = %q{a minimal framework for command-line utilities}
|
15
|
+
s.description = <<EOF
|
16
|
+
Clamp makes provides an object-model for command-line utilities.
|
17
|
+
It handles parsing of command-line options, and generation of usage help.
|
18
|
+
EOF
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
|
24
|
+
end
|
data/examples/rename
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require "clamp"
|
4
|
+
|
5
|
+
class Rename < Clamp::Command
|
6
|
+
|
7
|
+
usage "[OPTIONS] TRANSFORM FILE ..."
|
8
|
+
|
9
|
+
argument "TRANSFORM", "a Ruby expression"
|
10
|
+
argument "FILE", "a file to rename"
|
11
|
+
|
12
|
+
option ["-v", "--verbose"], :flag, "be verbose"
|
13
|
+
|
14
|
+
option ["-n", "--times"], "TIMES", "repetitions" do |n|
|
15
|
+
n = Integer(n)
|
16
|
+
raise ArgumentError, "too big" if n > 9
|
17
|
+
n
|
18
|
+
end
|
19
|
+
|
20
|
+
help_option
|
21
|
+
|
22
|
+
def initialize(name)
|
23
|
+
super
|
24
|
+
@times = 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute
|
28
|
+
@times.times do
|
29
|
+
puts "Blah blah blah" if verbose?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
Rename.run
|
data/lib/clamp.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'clamp/argument'
|
2
|
+
require 'clamp/option'
|
3
|
+
|
4
|
+
module Clamp
|
5
|
+
|
6
|
+
class Command
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
attr_reader :arguments
|
14
|
+
|
15
|
+
def parse(arguments)
|
16
|
+
while arguments.first =~ /^-/
|
17
|
+
case (switch = arguments.shift)
|
18
|
+
|
19
|
+
when /\A--\z/
|
20
|
+
break
|
21
|
+
|
22
|
+
else
|
23
|
+
option = find_option(switch)
|
24
|
+
value = if option.flag?
|
25
|
+
option.flag_value(switch)
|
26
|
+
else
|
27
|
+
arguments.shift
|
28
|
+
end
|
29
|
+
begin
|
30
|
+
send("#{option.attribute_name}=", value)
|
31
|
+
rescue ArgumentError => e
|
32
|
+
signal_usage_error "option '#{switch}': #{e.message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
@arguments = arguments
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute
|
41
|
+
raise "you need to define #execute"
|
42
|
+
end
|
43
|
+
|
44
|
+
def run(arguments)
|
45
|
+
parse(arguments)
|
46
|
+
execute
|
47
|
+
end
|
48
|
+
|
49
|
+
def help
|
50
|
+
self.class.help.gsub("__COMMAND__", name)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def find_option(switch)
|
56
|
+
self.class.find_option(switch) ||
|
57
|
+
signal_usage_error("Unrecognised option '#{switch}'")
|
58
|
+
end
|
59
|
+
|
60
|
+
def signal_usage_error(message)
|
61
|
+
e = UsageError.new(message, self)
|
62
|
+
e.set_backtrace(caller)
|
63
|
+
raise e
|
64
|
+
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
|
+
|
93
|
+
def usage(usage)
|
94
|
+
@usages ||= []
|
95
|
+
@usages << usage
|
96
|
+
end
|
97
|
+
|
98
|
+
def arguments
|
99
|
+
@arguments ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
def argument(name, description)
|
103
|
+
arguments << Argument.new(name, description)
|
104
|
+
end
|
105
|
+
|
106
|
+
def derived_usage
|
107
|
+
parts = arguments.map { |a| a.name }
|
108
|
+
parts.unshift("[OPTIONS]") if has_options?
|
109
|
+
parts.join(" ")
|
110
|
+
end
|
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)
|
136
|
+
begin
|
137
|
+
new(name).run(args)
|
138
|
+
rescue Clamp::UsageError => e
|
139
|
+
$stderr.puts "ERROR: #{e.message}"
|
140
|
+
$stderr.puts ""
|
141
|
+
$stderr.puts e.command.help
|
142
|
+
exit(1)
|
143
|
+
rescue Clamp::HelpWanted => e
|
144
|
+
puts e.command.help
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
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
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
class Error < StandardError
|
174
|
+
|
175
|
+
def initialize(message, command)
|
176
|
+
super(message)
|
177
|
+
@command = command
|
178
|
+
end
|
179
|
+
|
180
|
+
attr_reader :command
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
# raise to signal incorrect command usage
|
185
|
+
class UsageError < Error; end
|
186
|
+
|
187
|
+
# raise to request usage help
|
188
|
+
class HelpWanted < Error
|
189
|
+
|
190
|
+
def initialize(command)
|
191
|
+
super("I need help", command)
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
data/lib/clamp/option.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Option
|
4
|
+
|
5
|
+
def initialize(switches, argument_type, description, options = {})
|
6
|
+
@switches = Array(switches)
|
7
|
+
@argument_type = argument_type
|
8
|
+
@description = description
|
9
|
+
@attribute_name = options[:attribute_name].to_s if options.has_key?(:attribute_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :switches, :argument_type, :description
|
13
|
+
|
14
|
+
def attribute_name
|
15
|
+
@attribute_name ||= long_switch.sub(/^--(\[no-\])?/, '').tr('-', '_')
|
16
|
+
end
|
17
|
+
|
18
|
+
def long_switch
|
19
|
+
switches.find { |switch| switch =~ /^--/ }
|
20
|
+
end
|
21
|
+
|
22
|
+
def handles?(switch)
|
23
|
+
recognised_switches.member?(switch)
|
24
|
+
end
|
25
|
+
|
26
|
+
def flag?
|
27
|
+
@argument_type == :flag
|
28
|
+
end
|
29
|
+
|
30
|
+
def flag_value(switch)
|
31
|
+
!(switch =~ /^--no-(.*)/ && switches.member?("--\[no-\]#{$1}"))
|
32
|
+
end
|
33
|
+
|
34
|
+
def help
|
35
|
+
lhs = switches.join(", ")
|
36
|
+
lhs += " " + argument_type unless flag?
|
37
|
+
[lhs, description]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def recognised_switches
|
43
|
+
switches.map do |switch|
|
44
|
+
if switch =~ /^--\[no-\](.*)/
|
45
|
+
["--#{$1}", "--no-#{$1}"]
|
46
|
+
else
|
47
|
+
switch
|
48
|
+
end
|
49
|
+
end.flatten
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,385 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
describe Clamp::Command do
|
5
|
+
|
6
|
+
before do
|
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
|
23
|
+
|
24
|
+
def self.given_command(name, &block)
|
25
|
+
before do
|
26
|
+
@command = Class.new(Clamp::Command, &block).new(name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
given_command("cmd") do
|
31
|
+
|
32
|
+
def execute
|
33
|
+
print arguments.inspect
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#help" do
|
39
|
+
|
40
|
+
it "describes usage" do
|
41
|
+
@command.help.should include("Usage:\n cmd\n")
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#parse" do
|
47
|
+
|
48
|
+
it "sets arguments" do
|
49
|
+
@command.parse(%w(a b c))
|
50
|
+
@command.arguments.should == %w(a b c)
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "with an unrecognised option" do
|
54
|
+
|
55
|
+
it "raises a UsageError" do
|
56
|
+
lambda do
|
57
|
+
@command.parse(%w(--foo bar))
|
58
|
+
end.should raise_error(Clamp::UsageError)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#run" do
|
66
|
+
|
67
|
+
before do
|
68
|
+
@abc = %w(a b c)
|
69
|
+
@command.run(@abc)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "executes the #execute method" do
|
73
|
+
stdout.should_not be_empty
|
74
|
+
end
|
75
|
+
|
76
|
+
it "provides access to the argument list" do
|
77
|
+
stdout.should == @abc.inspect
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
describe ".option" do
|
83
|
+
|
84
|
+
before do
|
85
|
+
@command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "declares option argument accessors" do
|
89
|
+
@command.flavour.should == nil
|
90
|
+
@command.flavour = "chocolate"
|
91
|
+
@command.flavour.should == "chocolate"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
describe ".option", "with explicit :attribute_name" do
|
97
|
+
|
98
|
+
before do
|
99
|
+
@command.class.option "--foo", "FOO", "A foo", :attribute_name => :bar
|
100
|
+
end
|
101
|
+
|
102
|
+
it "uses the specified attribute_name name to name accessors" do
|
103
|
+
@command.bar = "chocolate"
|
104
|
+
@command.bar.should == "chocolate"
|
105
|
+
end
|
106
|
+
|
107
|
+
it "does not attempt to create the default accessors" do
|
108
|
+
@command.should_not respond_to(:foo)
|
109
|
+
@command.should_not respond_to(:foo=)
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "with options declared" do
|
115
|
+
|
116
|
+
before do
|
117
|
+
@command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
|
118
|
+
@command.class.option "--color", "COLOR", "Preferred hue"
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#parse" do
|
122
|
+
|
123
|
+
describe "with options" do
|
124
|
+
|
125
|
+
before do
|
126
|
+
@command.parse(%w(--flavour strawberry --color blue a b c))
|
127
|
+
end
|
128
|
+
|
129
|
+
it "extracts the option values" do
|
130
|
+
@command.flavour.should == "strawberry"
|
131
|
+
@command.color.should == "blue"
|
132
|
+
end
|
133
|
+
|
134
|
+
it "retains unconsumed arguments" do
|
135
|
+
@command.arguments.should == %w(a b c)
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "with option-like things beyond the arguments" do
|
141
|
+
|
142
|
+
it "treats them as positional arguments" do
|
143
|
+
@command.parse(%w(a b c --flavour strawberry))
|
144
|
+
@command.arguments.should == %w(a b c --flavour strawberry)
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "with an option terminator" do
|
150
|
+
|
151
|
+
it "considers everything after the terminator to be an argument" do
|
152
|
+
@command.parse(%w(--color blue -- --flavour strawberry))
|
153
|
+
@command.arguments.should == %w(--flavour strawberry)
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "#help" do
|
161
|
+
|
162
|
+
it "indicates that there are options" do
|
163
|
+
@command.help.should include("cmd [OPTIONS]")
|
164
|
+
end
|
165
|
+
|
166
|
+
it "includes option details" do
|
167
|
+
@command.help.should =~ %r(--flavour FLAVOUR +Flavour of the month)
|
168
|
+
@command.help.should =~ %r(--color COLOR +Preferred hue)
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "with a flag option declared" do
|
176
|
+
|
177
|
+
before do
|
178
|
+
@command.class.option "--verbose", :flag, "Be heartier"
|
179
|
+
end
|
180
|
+
|
181
|
+
it "declares a predicate-style reader" do
|
182
|
+
@command.should respond_to(:verbose?)
|
183
|
+
@command.should_not respond_to(:verbose)
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "#parse" do
|
187
|
+
|
188
|
+
describe "with option" do
|
189
|
+
|
190
|
+
before do
|
191
|
+
@command.parse(%w(--verbose foo))
|
192
|
+
end
|
193
|
+
|
194
|
+
it "sets the flag" do
|
195
|
+
@command.should be_verbose
|
196
|
+
end
|
197
|
+
|
198
|
+
it "does not consume an argument" do
|
199
|
+
@command.arguments.should == %w(foo)
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "with a negatable flag option declared" do
|
209
|
+
|
210
|
+
before do
|
211
|
+
@command.class.option "--[no-]sync", :flag, "Synchronise"
|
212
|
+
end
|
213
|
+
|
214
|
+
describe "#parse" do
|
215
|
+
|
216
|
+
describe "with --flag" do
|
217
|
+
|
218
|
+
before do
|
219
|
+
@command.parse(%w(--sync))
|
220
|
+
end
|
221
|
+
|
222
|
+
it "sets the flag" do
|
223
|
+
@command.sync?.should be_true
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
describe "with --no-flag" do
|
229
|
+
|
230
|
+
before do
|
231
|
+
@command.sync = true
|
232
|
+
@command.parse(%w(--no-sync))
|
233
|
+
end
|
234
|
+
|
235
|
+
it "clears the flag" do
|
236
|
+
@command.sync?.should be_false
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
describe ".option, with a block" do
|
246
|
+
|
247
|
+
before do
|
248
|
+
@command.class.option "--port", "PORT", "Port to listen on" do |port|
|
249
|
+
Integer(port)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
it "uses the block to validate and convert the option argument" do
|
254
|
+
lambda do
|
255
|
+
@command.port = "blah"
|
256
|
+
end.should raise_error(ArgumentError)
|
257
|
+
@command.port = "1234"
|
258
|
+
@command.port.should == 1234
|
259
|
+
end
|
260
|
+
|
261
|
+
describe "#parse" do
|
262
|
+
|
263
|
+
describe "with a valid option argument" do
|
264
|
+
|
265
|
+
it "stores the converted value" do
|
266
|
+
@command.parse(%w(--port 4321))
|
267
|
+
@command.port.should == 4321
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
describe "with an invalid option argument" do
|
273
|
+
|
274
|
+
it "raises a UsageError" do
|
275
|
+
lambda do
|
276
|
+
@command.parse(%w(--port blah))
|
277
|
+
end.should raise_error(Clamp::UsageError, /^option '--port': invalid value/)
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
|
286
|
+
describe "with explicit usage" do
|
287
|
+
|
288
|
+
given_command("blah") do
|
289
|
+
|
290
|
+
usage "FOO BAR ..."
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "#help" do
|
295
|
+
|
296
|
+
it "includes the explicit usage" do
|
297
|
+
@command.help.should include("blah FOO BAR ...\n")
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
describe "with multiple usages" do
|
305
|
+
|
306
|
+
given_command("put") do
|
307
|
+
|
308
|
+
usage "THIS HERE"
|
309
|
+
usage "THAT THERE"
|
310
|
+
|
311
|
+
end
|
312
|
+
|
313
|
+
describe "#help" do
|
314
|
+
|
315
|
+
it "includes both potential usages" do
|
316
|
+
@command.help.should include("put THIS HERE\n")
|
317
|
+
@command.help.should include("put THAT THERE\n")
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
|
324
|
+
describe ".run" do
|
325
|
+
|
326
|
+
it "creates a new Command instance and runs it" do
|
327
|
+
@xyz = %w(x y z)
|
328
|
+
@command.class.run("cmd", @xyz)
|
329
|
+
stdout.should == @xyz.inspect
|
330
|
+
end
|
331
|
+
|
332
|
+
describe "when there's a UsageError" do
|
333
|
+
|
334
|
+
before do
|
335
|
+
|
336
|
+
@command.class.class_eval do
|
337
|
+
def execute
|
338
|
+
signal_usage_error "bad dog!"
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
begin
|
343
|
+
@command.class.run("cmd", [])
|
344
|
+
rescue SystemExit => e
|
345
|
+
@system_exit = e
|
346
|
+
end
|
347
|
+
|
348
|
+
end
|
349
|
+
|
350
|
+
it "outputs the error message" do
|
351
|
+
stderr.should include "ERROR: bad dog!"
|
352
|
+
end
|
353
|
+
|
354
|
+
it "outputs help" do
|
355
|
+
stderr.should include "Usage:"
|
356
|
+
end
|
357
|
+
|
358
|
+
it "exits with a non-zero status" do
|
359
|
+
@system_exit.should_not be_nil
|
360
|
+
@system_exit.status.should == 1
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|
364
|
+
|
365
|
+
describe "when help is requested" do
|
366
|
+
|
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
|
+
it "outputs help" do
|
378
|
+
stdout.should include "Usage:"
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
|
385
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Clamp::Option do
|
4
|
+
|
5
|
+
describe "with String argument" do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@option = Clamp::Option.new("--key-file", "FILE", "SSH identity")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "has a long_switch" do
|
12
|
+
@option.long_switch.should == "--key-file"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has an argument_type" do
|
16
|
+
@option.argument_type.should == "FILE"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "has a description" do
|
20
|
+
@option.description.should == "SSH identity"
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#attribute_name" do
|
24
|
+
|
25
|
+
it "is derived from the (long) switch" do
|
26
|
+
@option.attribute_name.should == "key_file"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can be overridden" do
|
30
|
+
@option = Clamp::Option.new("--key-file", "FILE", "SSH identity", :attribute_name => "ssh_identity")
|
31
|
+
@option.attribute_name.should == "ssh_identity"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#help" do
|
37
|
+
|
38
|
+
it "combines switch, argument_type and description" do
|
39
|
+
@option.help.should == ["--key-file FILE", "SSH identity"]
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "flag" do
|
47
|
+
|
48
|
+
before do
|
49
|
+
@option = Clamp::Option.new("--verbose", :flag, "Blah blah blah")
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#help" do
|
53
|
+
|
54
|
+
it "excludes option argument" do
|
55
|
+
@option.help.should == ["--verbose", "Blah blah blah"]
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "negatable flag" do
|
63
|
+
|
64
|
+
before do
|
65
|
+
@option = Clamp::Option.new("--[no-]force", :flag, "Force installation")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "handles both positive and negative forms" do
|
69
|
+
@option.handles?("--force").should be_true
|
70
|
+
@option.handles?("--no-force").should be_true
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#flag_value" do
|
74
|
+
|
75
|
+
it "returns true for the positive variant" do
|
76
|
+
@option.flag_value("--force").should be_true
|
77
|
+
@option.flag_value("--no-force").should be_false
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#attribute_name" do
|
83
|
+
|
84
|
+
it "is derived from the (long) switch" do
|
85
|
+
@option.attribute_name.should == "force"
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "with both short and long switches" do
|
93
|
+
|
94
|
+
before do
|
95
|
+
@option = Clamp::Option.new(["-k", "--key-file"], "FILE", "SSH identity")
|
96
|
+
end
|
97
|
+
|
98
|
+
it "handles both switches" do
|
99
|
+
@option.handles?("--key-file").should be_true
|
100
|
+
@option.handles?("-k").should be_true
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#help" do
|
104
|
+
|
105
|
+
it "includes both switches" do
|
106
|
+
@option.help.should == ["-k, --key-file FILE", "SSH identity"]
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clamp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Mike Williams
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-11-02 00:00:00 +11:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: |
|
23
|
+
Clamp makes provides an object-model for command-line utilities.
|
24
|
+
It handles parsing of command-line options, and generation of usage help.
|
25
|
+
|
26
|
+
email: mdub@dogbiscuit.org
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- README.markdown
|
38
|
+
- Rakefile
|
39
|
+
- clamp.gemspec
|
40
|
+
- examples/rename
|
41
|
+
- lib/clamp.rb
|
42
|
+
- lib/clamp/argument.rb
|
43
|
+
- lib/clamp/command.rb
|
44
|
+
- lib/clamp/option.rb
|
45
|
+
- lib/clamp/version.rb
|
46
|
+
- spec/clamp/command_spec.rb
|
47
|
+
- spec/clamp/option_spec.rb
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/mdub/clamp
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.3.7
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: a minimal framework for command-line utilities
|
83
|
+
test_files:
|
84
|
+
- spec/clamp/command_spec.rb
|
85
|
+
- spec/clamp/option_spec.rb
|
86
|
+
- spec/spec_helper.rb
|