[DRE-commits] [ruby-clamp] 01/05: Imported Upstream version 0.6.3
Tim Potter
tpot-guest at moszumanska.debian.org
Tue Aug 5 05:37:59 UTC 2014
This is an automated email from the git hooks/post-receive script.
tpot-guest pushed a commit to branch master
in repository ruby-clamp.
commit 43337ef469dfce706a882df9f408f5e232d1a252
Author: Tim Potter <tpot at hp.com>
Date: Fri Aug 1 12:05:14 2014 +1000
Imported Upstream version 0.6.3
---
.autotest | 9 +
.gitignore | 8 +
.rspec | 1 +
.travis.yml | 6 +
CHANGES.md | 21 +
Gemfile | 9 +
LICENSE | 20 +
README.md | 389 ++++++++++++++
Rakefile | 12 +
checksums.yaml.gz | Bin 0 -> 424 bytes
clamp.gemspec | 26 +
examples/admin | 20 +
examples/flipflop | 29 +
examples/fubar | 21 +
examples/gitdown | 70 +++
examples/scoop | 17 +
examples/speak | 31 ++
lib/clamp.rb | 7 +
lib/clamp/attribute/declaration.rb | 54 ++
lib/clamp/attribute/definition.rb | 90 ++++
lib/clamp/attribute/instance.rb | 82 +++
lib/clamp/command.rb | 140 +++++
lib/clamp/errors.rb | 29 +
lib/clamp/help.rb | 100 ++++
lib/clamp/option/declaration.rb | 59 ++
lib/clamp/option/definition.rb | 94 ++++
lib/clamp/option/parsing.rb | 68 +++
lib/clamp/parameter/declaration.rb | 29 +
lib/clamp/parameter/definition.rb | 48 ++
lib/clamp/parameter/parsing.rb | 29 +
lib/clamp/subcommand/declaration.rb | 67 +++
lib/clamp/subcommand/definition.rb | 25 +
lib/clamp/subcommand/execution.rb | 35 ++
lib/clamp/subcommand/parsing.rb | 24 +
lib/clamp/truthy.rb | 9 +
lib/clamp/version.rb | 3 +
metadata.yml | 94 ++++
spec/clamp/command_group_spec.rb | 305 +++++++++++
spec/clamp/command_spec.rb | 927 ++++++++++++++++++++++++++++++++
spec/clamp/option/definition_spec.rb | 260 +++++++++
spec/clamp/option_module_spec.rb | 37 ++
spec/clamp/parameter/definition_spec.rb | 242 +++++++++
spec/spec_helper.rb | 45 ++
43 files changed, 3591 insertions(+)
diff --git a/.autotest b/.autotest
new file mode 100644
index 0000000..2a2cc1e
--- /dev/null
+++ b/.autotest
@@ -0,0 +1,9 @@
+Autotest.add_hook :initialize do |at|
+
+ at.add_exception ".git"
+
+ at.add_mapping(%r{^lib/(.*)\.rb$}, :prepend) do |_, match|
+ ["spec/unit/#{match[1]}_spec.rb"] + Dir['spec/clamp/command*_spec.rb']
+ end
+
+end
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0b36852
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*.gem
+.bundle
+.rvmrc
+.yardoc
+doc
+pkg/*
+Gemfile.lock
+
\ No newline at end of file
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..4e1e0d2
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--color
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..bcc1675
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: ruby
+rvm:
+ - 1.8.7
+ - 1.9.2
+ - 1.9.3
+ - 2.0.0
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..75deb09
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,21 @@
+# Changelog
+
+## 0.6.2 (2013-11-06)
+
+* Refactoring around multi-valued attributes.
+* Allow injection of a custom help-builder.
+
+## 0.6.1 (2013-05-07)
+
+* Signal a usage error when an environment_variable fails validation.
+* Refactor setting, defaulting and inheritance of attributes.
+
+## 0.6.0 (2013-04-28)
+
+* Introduce "banner" to describe a command (replacing "self.description=").
+* Introduce "Clamp do ... end" syntax sugar.
+* Allow parameters to be specified before a subcommand.
+* Add support for :multivalued options.
+* Multi valued options and parameters get an "#append_to_foo_list" method, rather than
+ "#foo_list=".
+* default_subcommand must be specified before any subcommands.
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..7304213
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,9 @@
+source "http://rubygems.org"
+
+gemspec
+
+group :test do
+ gem "rake"
+ gem "rspec"
+ gem "rr"
+end
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9d166ef
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2010 Mike Williams <mdub at dogbiscuit.org>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..59d0e11
--- /dev/null
+++ b/README.md
@@ -0,0 +1,389 @@
+
+Clamp [](http://travis-ci.org/mdub/clamp)
+=====
+
+"Clamp" is a minimal framework for command-line utilities.
+
+It handles boring stuff like parsing the command-line, and generating help, so you can get on with making your command actually do stuff.
+
+Not another one!
+----------------
+
+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. (Although, Clamp has a _lot_ in common with Ara T. Howard's [main.rb]. Had I been aware of that project at the time, I might not have written Clamp.)
+
+[optparse]: http://ruby-doc.org/stdlib/libdoc/optparse/rdoc/index.html
+[Thor]: http://github.com/wycats/thor
+[Clip]: http://clip.rubyforge.org/
+[main.rb]: https://github.com/ahoward/main
+
+Quick Start
+-----------
+
+A typical Clamp script looks like this:
+
+```ruby
+require 'clamp'
+
+Clamp do
+
+ option "--loud", :flag, "say it loud"
+ option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
+ Integer(s)
+ end
+
+ parameter "WORDS ...", "the thing to say", :attribute_name => :words
+
+ def execute
+ the_truth = words.join(" ")
+ the_truth.upcase! if loud?
+ iterations.times do
+ puts the_truth
+ end
+ end
+
+end
+```
+
+Internally, Clamp models a command as a Ruby class (a subclass of `Clamp::Command`), and a command execution as an instance of that class. The example above is really just syntax-sugar for:
+
+```ruby
+require 'clamp'
+
+class SpeakCommand < Clamp::Command
+
+ option "--loud", :flag, "say it loud"
+ option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
+ Integer(s)
+ end
+
+ parameter "WORDS ...", "the thing to say", :attribute_name => :words
+
+ def execute
+ the_truth = words.join(" ")
+ the_truth.upcase! if loud?
+ iterations.times do
+ puts the_truth
+ end
+ end
+
+end
+
+SpeakCommand.run
+```
+
+Class-level methods like `option` and `parameter` declare attributes, in a similar way to `attr_accessor`, and arrange for them to be populated automatically based on command-line arguments. They are also used to generate `help` documentation.
+
+There are more examples demonstrating various features of Clamp [on Github][examples].
+
+[examples]: https://github.com/mdub/clamp/tree/master/examples
+
+Declaring options
+-----------------
+
+Options are declared using the `option` method. The three required arguments are:
+
+ 1. the option switch (or switches),
+ 2. an option argument name
+ 3. a short description
+
+For example:
+
+```ruby
+option "--flavour", "FLAVOUR", "ice-cream flavour"
+```
+
+It works a little like `attr_accessor`, defining reader and writer methods on the command class. The attribute name is inferred 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.
+
+```ruby
+def execute
+ puts "You chose #{flavour}. Excellent choice!"
+end
+```
+
+If you don't like the inferred attribute name, you can override it:
+
+```ruby
+option "--type", "TYPE", "type of widget", :attribute_name => :widget_type
+ # to avoid clobbering Object#type
+```
+
+### Short/long option switches
+
+The first argument to `option` can be an array, rather than a single string, in which case all the switches are treated as aliases:
+
+```ruby
+option ["-s", "--subject"], "SUBJECT", "email subject line"
+```
+
+### Flag options
+
+Some options are just boolean flags. Pass "`:flag`" as the second parameter to tell Clamp not to expect an option argument:
+
+```ruby
+option "--verbose", :flag, "be chatty"
+```
+
+For flag options, Clamp appends "`?`" to the generated reader method; ie. you get a method called "`#verbose?`", rather than just "`#verbose`".
+
+Negatable flags are easy to generate, too:
+
+```ruby
+option "--[no-]force", :flag, "be forceful (or not)"
+```
+
+Clamp will handle both "`--force`" and "`--no-force`" options, setting the value of "`#force?`" appropriately.
+
+### Required options
+
+Although 'required option' is a an oxymoron, Clamp lets you mark an option as required, and will verify that a value is provided:
+
+```ruby
+option "--password", "PASSWORD", "the secret password", :required => true
+```
+
+Note that it makes no sense to mark a `:flag` option, or one with a `:default`, as `:required`.
+
+### Multivalued options
+
+Declaring an option "`:multivalued`" allows it to be specified multiple times on the command line.
+
+```ruby
+option "--format", "FORMAT", "output format", :multivalued => true
+```
+
+The underlying attribute becomes an Array, and the suffix "`_list`" is appended to the default attribute name. In this case, an attribute called "`format_list`" would be generated (unless you override the default by specifying an `:attribute_name`).
+
+Declaring parameters
+--------------------
+
+Positional parameters can be declared using `parameter`, specifying
+
+ 1. the parameter name, and
+ 2. a short description
+
+For example:
+
+```ruby
+parameter "SRC", "source file"
+```
+
+Like options, parameters are implemented as attributes of the command, with the default attribute name derived from the parameter name (in this case, "`src`"). By convention, parameter names are specified in uppercase, to make them obvious in usage help.
+
+### Optional parameters
+
+Wrapping a parameter name in square brackets indicates that it's optional, e.g.
+
+```ruby
+parameter "[TARGET_DIR]", "target directory"
+```
+
+### Multivalued (aka "greedy") parameters
+
+Three dots at the end of a parameter name makes it "greedy" - it will consume all remaining command-line arguments. For example:
+
+```ruby
+parameter "FILE ...", "input files", :attribute_name => :files
+```
+
+Like multivalued options, greedy parameters are backed by an Array attribute (named with a "`_list`" suffix, by default).
+
+Parsing and validation of options and parameters
+------------------------------------------------
+
+When you `#run` a command, it will first attempt to `#parse` command-line arguments, and map them onto the declared options and parameters, before invoking your `#execute` method.
+
+Clamp will verify that all required (ie. non-optional) parameters are present, and signal a error if they aren't.
+
+### Validation
+
+Both `option` and `parameter` accept an optional block. If present, the block will be
+called with the raw string argument, and is expected to validate it. The value returned by the block will be assigned to the underlying attribute, so it's also a good place to coerce the String to a different type, if appropriate.
+
+For example:
+
+```ruby
+option "--port", "PORT", "port to listen on" do |s|
+ Integer(s)
+end
+```
+
+If the block raises an ArgumentError, Clamp will catch it, and report that the value was bad:
+
+```ruby
+!!!plain
+ERROR: option '--port': invalid value for Integer: "blah"
+```
+
+For multivalued options and parameters, the validation block will be called for each value specified.
+
+More complex validation, e.g. those involving multiple options/parameters, should be performed within the `#execute` method. Use `#signal_usage_error` to tell the user what they did wrong, e.g.
+
+```ruby
+def execute
+ if port < 1024 && user != 'root'
+ signal_usage_error "port restricted for non-root users"
+ end
+ # ... carry on ...
+end
+```
+
+### Advanced option/parameter handling
+
+While Clamp provides an attribute-writer method for each declared option or parameter, you always have the option of overriding it to provide custom argument-handling logic, e.g.
+
+```ruby
+parameter "SERVER", "location of server"
+
+def server=(server)
+ @server_address, @server_port = server.split(":")
+end
+```
+
+### Default values
+
+Default values can be specified for options, and optional parameters:
+
+```ruby
+option "--flavour", "FLAVOUR", "ice-cream flavour", :default => "chocolate"
+
+parameter "[HOST]", "server host", :default => "localhost"
+```
+
+For more advanced cases, you can also specify default values by defining a method called "`default_#{attribute_name}`":
+
+```ruby
+option "--http-port", "PORT", "web-server port", :default => 9000
+
+option "--admin-port", "PORT", "admin port"
+
+def default_admin_port
+ http_port + 1
+end
+```
+
+### Environment variable support
+
+Options (and optional parameters) can also be associated with environment variables:
+
+```ruby
+option "--port", "PORT", "the port to listen on", :environment_variable => "MYAPP_PORT" do |val|
+ val.to_i
+end
+
+parameter "[HOST]", "server address", :environment_variable => "MYAPP_HOST"
+```
+
+Clamp will check the specified envariables in the absence of values supplied on the command line, before looking for a default value.
+
+Declaring Subcommands
+---------------------
+
+Subcommand support helps you wrap a number of related commands into a single script (ala tools like "`git`"). Clamp will inspect the first command-line argument (after options are parsed), and delegate to the named subcommand.
+
+Unsuprisingly, subcommands are declared using the `subcommand` method. e.g.
+
+```ruby
+Clamp do
+
+ subcommand "init", "Initialize the repository" do
+
+ def execute
+ # ...
+ end
+
+ end
+
+end
+```
+
+Clamp generates an anonymous subclass of the current class, to represent the subcommand. Alternatively, you can provide an explicit subcommand class:
+
+```ruby
+class MainCommand < Clamp::Command
+
+ subcommand "init", "Initialize the repository", InitCommand
+
+end
+
+class InitCommand < Clamp::Command
+
+ def execute
+ # ...
+ end
+
+end
+```
+
+### Default subcommand
+
+You can set a default subcommand, at the class level, as follows:
+
+```ruby
+Clamp do
+
+ self.default_subcommand = "status"
+
+ subcommand "status", "Display current status" do
+
+ def execute
+ # ...
+ end
+
+ end
+
+end
+```
+
+Then, if when no SUBCOMMAND argument is provided, the default will be selected.
+
+### Subcommand options and parameters
+
+Options are inheritable, so any options declared for a command are supported by it's sub-classes (e.g. those created using the block form of `subcommand`). Parameters, on the other hand, are not inherited - each subcommand must declare it's own parameter list.
+
+Note that, if a subcommand accepts options, they must be specified on the command-line _after_ the subcommand name.
+
+Getting help
+------------
+
+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 `parameter`.
+
+```sh
+$ speak --help
+Usage:
+ speak [OPTIONS] WORDS ...
+
+Arguments:
+ WORDS ... the thing to say
+
+Options:
+ --loud say it loud
+ -n, --iterations N say it N times (default: 1)
+ -h, --help print help
+```
+
+License
+-------
+
+Copyright (C) 2011 [Mike Williams](mailto:mdub at dogbiscuit.org)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Contributing to Clamp
+---------------------
+
+Source-code for Clamp is [on Github](https://github.com/mdub/clamp).
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..06105c4
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,12 @@
+require 'bundler'
+
+Bundler::GemHelper.install_tasks
+
+require "rspec/core/rake_task"
+
+task "default" => "spec"
+
+RSpec::Core::RakeTask.new do |t|
+ t.pattern = 'spec/**/*_spec.rb'
+ t.rspec_opts = ["--colour", "--format", "nested"]
+end
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
new file mode 100644
index 0000000..aed0152
Binary files /dev/null and b/checksums.yaml.gz differ
diff --git a/clamp.gemspec b/clamp.gemspec
new file mode 100644
index 0000000..32ead1d
--- /dev/null
+++ b/clamp.gemspec
@@ -0,0 +1,26 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "clamp/version"
+
+Gem::Specification.new do |s|
+
+ s.name = "clamp"
+ s.version = Clamp::VERSION.dup
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Mike Williams"]
+ s.email = "mdub at dogbiscuit.org"
+ s.homepage = "http://github.com/mdub/clamp"
+
+ s.license = 'MIT'
+
+ s.summary = %q{a minimal framework for command-line utilities}
+ s.description = <<EOF
+Clamp provides an object-model for command-line utilities.
+It handles parsing of command-line options, and generation of usage help.
+EOF
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.require_paths = ["lib"]
+
+end
diff --git a/examples/admin b/examples/admin
new file mode 100755
index 0000000..2adea32
--- /dev/null
+++ b/examples/admin
@@ -0,0 +1,20 @@
+#! /usr/bin/env ruby
+
+# An example of subcommands
+
+require "clamp"
+
+Clamp do
+
+ option "--timeout", "SECONDS", "connection timeout", :default => 5, :environment_variable => "MYAPP_TIMEOUT" do |x|
+ Integer(x)
+ end
+
+ parameter "HOST", "server address"
+ parameter "[PORT]", "server port", :default => 80, :environment_variable => "MYAPP_PORT"
+
+ def execute
+ puts "trying to connect to #{host} on port #{port} (waiting up to #{timeout} seconds)"
+ end
+
+end
diff --git a/examples/flipflop b/examples/flipflop
new file mode 100755
index 0000000..e9c72a5
--- /dev/null
+++ b/examples/flipflop
@@ -0,0 +1,29 @@
+#! /usr/bin/env ruby
+
+# An example of subcommands
+
+require "clamp"
+require "clamp/version"
+
+Clamp do
+
+ option ["--version", "-v"], :flag, "Show version" do
+ puts "Powered by Clamp-#{Clamp::VERSION}"
+ exit(0)
+ end
+
+ self.default_subcommand = "flip"
+
+ subcommand "flip", "flip it" do
+ def execute
+ puts "FLIPPED"
+ end
+ end
+
+ subcommand "flop", "flop it" do
+ def execute
+ puts "FLOPPED"
+ end
+ end
+
+end
diff --git a/examples/fubar b/examples/fubar
new file mode 100755
index 0000000..ae3d500
--- /dev/null
+++ b/examples/fubar
@@ -0,0 +1,21 @@
+#! /usr/bin/env ruby
+
+# An example of subcommands
+
+require "clamp"
+
+Clamp do
+
+ subcommand "foo", "Foo!" do
+
+ subcommand "bar", "Baaaa!" do
+
+ def execute
+ puts "FUBAR"
+ end
+
+ end
+
+ end
+
+end
diff --git a/examples/gitdown b/examples/gitdown
new file mode 100755
index 0000000..d8d2d54
--- /dev/null
+++ b/examples/gitdown
@@ -0,0 +1,70 @@
+#! /usr/bin/env ruby
+
+# Demonstrate how subcommands can be declared as classes
+
+require "clamp"
+
+module GitDown
+
+ class AbstractCommand < Clamp::Command
+
+ option ["-v", "--verbose"], :flag, "be verbose"
+
+ option "--version", :flag, "show version" do
+ puts "GitDown-0.0.0a"
+ exit(0)
+ end
+
+ def say(message)
+ message = message.upcase if verbose?
+ puts message
+ end
+
+ end
+
+ class CloneCommand < AbstractCommand
+
+ parameter "REPOSITORY", "repository to clone"
+ parameter "[DIR]", "working directory", :default => "."
+
+ def execute
+ say "cloning to #{dir}"
+ end
+
+ end
+
+ class PullCommand < AbstractCommand
+
+ option "--[no-]commit", :flag, "Perform the merge and commit the result."
+
+ def execute
+ say "pulling"
+ end
+
+ end
+
+ class StatusCommand < AbstractCommand
+
+ option ["-s", "--short"], :flag, "Give the output in the short-format."
+
+ def execute
+ if short?
+ say "good"
+ else
+ say "it's all good ..."
+ end
+ end
+
+ end
+
+ class MainCommand < AbstractCommand
+
+ subcommand "clone", "Clone a remote repository.", CloneCommand
+ subcommand "pull", "Fetch and merge updates.", PullCommand
+ subcommand "status", "Display status of local repository.", StatusCommand
+
+ end
+
+end
+
+GitDown::MainCommand.run
diff --git a/examples/scoop b/examples/scoop
new file mode 100644
index 0000000..7f54df1
--- /dev/null
+++ b/examples/scoop
@@ -0,0 +1,17 @@
+#! /usr/bin/env ruby
+
+# An example of multi-valued options
+
+require "clamp"
+
+Clamp do
+
+ option ["-f", "--flavour"], "FLAVOUR", "flavour",
+ :multivalued => true, :default => ['chocolate'],
+ :attribute_name => :flavours
+
+ def execute
+ puts "one #{flavours.join(' and ')} ice-cream"
+ end
+
+end
diff --git a/examples/speak b/examples/speak
new file mode 100755
index 0000000..37cdf28
--- /dev/null
+++ b/examples/speak
@@ -0,0 +1,31 @@
+#! /usr/bin/env ruby
+
+# A simple Clamp command, with options and parameters
+
+require "clamp"
+
+Clamp do
+
+ banner %{
+ Say something.
+ }
+
+ option "--loud", :flag, "say it loud"
+ option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
+ Integer(s)
+ end
+
+ parameter "WORDS ...", "the thing to say", :attribute_name => :words
+
+ def execute
+
+ the_truth = words.join(" ")
+ the_truth.upcase! if loud?
+
+ iterations.times do
+ puts the_truth
+ end
+
+ end
+
+end
diff --git a/lib/clamp.rb b/lib/clamp.rb
new file mode 100644
index 0000000..877c556
--- /dev/null
+++ b/lib/clamp.rb
@@ -0,0 +1,7 @@
+require 'clamp/version'
+
+require 'clamp/command'
+
+def Clamp(&block)
+ Class.new(Clamp::Command, &block).run
+end
diff --git a/lib/clamp/attribute/declaration.rb b/lib/clamp/attribute/declaration.rb
new file mode 100644
index 0000000..37c3af2
--- /dev/null
+++ b/lib/clamp/attribute/declaration.rb
@@ -0,0 +1,54 @@
+module Clamp
+ module Attribute
+
+ module Declaration
+
+ protected
+
+ def define_accessors_for(attribute, &block)
+ define_reader_for(attribute)
+ define_default_for(attribute)
+ if attribute.multivalued?
+ define_appender_for(attribute, &block)
+ define_multi_writer_for(attribute)
+ else
+ define_simple_writer_for(attribute, &block)
+ end
+ end
+
+ def define_reader_for(attribute)
+ define_method(attribute.read_method) do
+ attribute.of(self)._read
+ end
+ end
+
+ def define_default_for(attribute)
+ define_method(attribute.default_method) do
+ attribute.default_value
+ end
+ end
+
+ def define_simple_writer_for(attribute, &block)
+ define_method(attribute.write_method) do |value|
+ value = instance_exec(value, &block) if block
+ attribute.of(self).set(value)
+ end
+ end
+
+ def define_appender_for(attribute, &block)
+ define_method(attribute.append_method) do |value|
+ value = instance_exec(value, &block) if block
+ attribute.of(self)._append(value)
+ end
+ end
+
+ def define_multi_writer_for(attribute)
+ define_method(attribute.write_method) do |values|
+ attribute.of(self)._replace(values)
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/attribute/definition.rb b/lib/clamp/attribute/definition.rb
new file mode 100644
index 0000000..ddf8d0f
--- /dev/null
+++ b/lib/clamp/attribute/definition.rb
@@ -0,0 +1,90 @@
+require 'clamp/attribute/instance'
+
+module Clamp
+ module Attribute
+
+ class Definition
+
+ def initialize(options)
+ if options.has_key?(:attribute_name)
+ @attribute_name = options[:attribute_name].to_s
+ end
+ if options.has_key?(:default)
+ @default_value = options[:default]
+ end
+ if options.has_key?(:environment_variable)
+ @environment_variable = options[:environment_variable]
+ end
+ end
+
+ attr_reader :description, :environment_variable
+
+ def help_rhs
+ description + default_description
+ end
+
+ def help
+ [help_lhs, help_rhs]
+ end
+
+ def ivar_name
+ "@#{attribute_name}"
+ end
+
+ def read_method
+ attribute_name
+ end
+
+ def default_method
+ "default_#{read_method}"
+ end
+
+ def write_method
+ "#{attribute_name}="
+ end
+
+ def append_method
+ if multivalued?
+ "append_to_#{attribute_name}"
+ end
+ end
+
+ def multivalued?
+ @multivalued
+ end
+
+ def required?
+ @required
+ end
+
+ def attribute_name
+ @attribute_name ||= infer_attribute_name
+ end
+
+ def default_value
+ if defined?(@default_value)
+ @default_value
+ elsif multivalued?
+ []
+ end
+ end
+
+ def of(command)
+ Attribute::Instance.new(self, command)
+ end
+
+ private
+
+ def default_description
+ default_sources = [
+ ("$#{@environment_variable}" if defined?(@environment_variable)),
+ (@default_value.inspect if defined?(@default_value))
+ ].compact
+ return "" if default_sources.empty?
+ " (default: " + default_sources.join(", or ") + ")"
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/attribute/instance.rb b/lib/clamp/attribute/instance.rb
new file mode 100644
index 0000000..8f51cc3
--- /dev/null
+++ b/lib/clamp/attribute/instance.rb
@@ -0,0 +1,82 @@
+module Clamp
+ module Attribute
+
+ # Represents an option/parameter of a Clamp::Command instance.
+ #
+ class Instance
+
+ def initialize(attribute, command)
+ @attribute = attribute
+ @command = command
+ end
+
+ attr_reader :attribute, :command
+
+ def defined?
+ command.instance_variable_defined?(attribute.ivar_name)
+ end
+
+ # get value directly
+ def get
+ command.instance_variable_get(attribute.ivar_name)
+ end
+
+ # set value directly
+ def set(value)
+ command.instance_variable_set(attribute.ivar_name, value)
+ end
+
+ def default
+ command.send(attribute.default_method)
+ end
+
+ # default implementation of read_method
+ def _read
+ if self.defined?
+ get
+ else
+ default
+ end
+ end
+
+ # default implementation of append_method
+ def _append(value)
+ current_values = get || []
+ set(current_values + [value])
+ end
+
+ # default implementation of write_method for multi-valued attributes
+ def _replace(values)
+ set([])
+ Array(values).each { |value| take(value) }
+ end
+
+ def read
+ command.send(attribute.read_method)
+ end
+
+ def take(value)
+ if attribute.multivalued?
+ command.send(attribute.append_method, value)
+ else
+ command.send(attribute.write_method, value)
+ end
+ end
+
+ def default_from_environment
+ return if self.defined?
+ return if attribute.environment_variable.nil?
+ return unless ENV.has_key?(attribute.environment_variable)
+ # Set the parameter value if it's environment variable is present
+ value = ENV[attribute.environment_variable]
+ begin
+ take(value)
+ rescue ArgumentError => e
+ command.send(:signal_usage_error, "$#{attribute.environment_variable}: #{e.message}")
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/command.rb b/lib/clamp/command.rb
new file mode 100644
index 0000000..c6c50f4
--- /dev/null
+++ b/lib/clamp/command.rb
@@ -0,0 +1,140 @@
+require 'clamp/errors'
+require 'clamp/help'
+require 'clamp/option/declaration'
+require 'clamp/option/parsing'
+require 'clamp/parameter/declaration'
+require 'clamp/parameter/parsing'
+require 'clamp/subcommand/declaration'
+require 'clamp/subcommand/parsing'
+
+module Clamp
+
+ # {Command} models a shell command. Each command invocation is a new object.
+ # Command options and parameters are represented as attributes
+ # (see {Command::Declaration}).
+ #
+ # The main entry-point is {#run}, which uses {#parse} to populate attributes based
+ # on an array of command-line arguments, then calls {#execute} (which you provide)
+ # to make it go.
+ #
+ class Command
+
+ # Create a command execution.
+ #
+ # @param [String] invocation_path the path used to invoke the command
+ # @param [Hash] context additional data the command may need
+ #
+ def initialize(invocation_path, context = {}, parent_attribute_values = {})
+ @invocation_path = invocation_path
+ @context = context
+ parent_attribute_values.each do |attribute, value|
+ attribute.of(self).set(value)
+ end
+ end
+
+ # @return [String] the path used to invoke this command
+ #
+ attr_reader :invocation_path
+
+ # @return [Array<String>] unconsumed command-line arguments
+ #
+ def remaining_arguments
+ @remaining_arguments
+ end
+
+ # Parse command-line arguments.
+ #
+ # @param [Array<String>] arguments command-line arguments
+ # @return [Array<String>] unconsumed arguments
+ #
+ def parse(arguments)
+ @remaining_arguments = arguments.dup
+ parse_options
+ parse_parameters
+ parse_subcommand
+ handle_remaining_arguments
+ end
+
+ # Run the command, with the specified arguments.
+ #
+ # This calls {#parse} to process the command-line arguments,
+ # then delegates to {#execute}.
+ #
+ # @param [Array<String>] arguments command-line arguments
+ #
+ def run(arguments)
+ parse(arguments)
+ execute
+ end
+
+ # Execute the command (assuming that all options/parameters have been set).
+ #
+ # This method is designed to be overridden in sub-classes.
+ #
+ def execute
+ raise "you need to define #execute"
+ end
+
+ # @return [String] usage documentation for this command
+ #
+ def help
+ self.class.help(invocation_path)
+ end
+
+ include Clamp::Option::Parsing
+ include Clamp::Parameter::Parsing
+ include Clamp::Subcommand::Parsing
+
+ protected
+
+ attr_accessor :context
+
+ def handle_remaining_arguments
+ unless remaining_arguments.empty?
+ signal_usage_error "too many arguments"
+ end
+ end
+
+ private
+
+ def signal_usage_error(message)
+ e = UsageError.new(message, self)
+ e.set_backtrace(caller)
+ raise e
+ end
+
+ def request_help
+ raise HelpWanted, self
+ end
+
+ class << self
+
+ include Clamp::Option::Declaration
+ include Clamp::Parameter::Declaration
+ include Clamp::Subcommand::Declaration
+ include Help
+
+ # Create an instance of this command class, and run it.
+ #
+ # @param [String] invocation_path the path used to invoke the command
+ # @param [Array<String>] arguments command-line arguments
+ # @param [Hash] context additional data the command may need
+ #
+ def run(invocation_path = File.basename($0), arguments = ARGV, context = {})
+ begin
+ new(invocation_path, context).run(arguments)
+ rescue Clamp::UsageError => e
+ $stderr.puts "ERROR: #{e.message}"
+ $stderr.puts ""
+ $stderr.puts "See: '#{e.command.invocation_path} --help'"
+ exit(1)
+ rescue Clamp::HelpWanted => e
+ puts e.command.help
+ end
+ end
+
+ end
+
+ end
+
+end
diff --git a/lib/clamp/errors.rb b/lib/clamp/errors.rb
new file mode 100644
index 0000000..cf14247
--- /dev/null
+++ b/lib/clamp/errors.rb
@@ -0,0 +1,29 @@
+module Clamp
+
+ class DeclarationError < StandardError
+ end
+
+ class RuntimeError < StandardError
+
+ def initialize(message, command)
+ super(message)
+ @command = command
+ end
+
+ attr_reader :command
+
+ end
+
+ # raise to signal incorrect command usage
+ class UsageError < RuntimeError; end
+
+ # raise to request usage help
+ class HelpWanted < RuntimeError
+
+ def initialize(command)
+ super("I need help", command)
+ end
+
+ end
+
+end
diff --git a/lib/clamp/help.rb b/lib/clamp/help.rb
new file mode 100644
index 0000000..e09731a
--- /dev/null
+++ b/lib/clamp/help.rb
@@ -0,0 +1,100 @@
+require 'stringio'
+
+module Clamp
+
+ module Help
+
+ def usage(usage)
+ @declared_usage_descriptions ||= []
+ @declared_usage_descriptions << usage
+ end
+
+ attr_reader :declared_usage_descriptions
+
+ def description=(description)
+ @description = description.dup
+ if @description =~ /^\A\n*( +)/
+ indent = $1
+ @description.gsub!(/^#{indent}/, '')
+ end
+ @description.strip!
+ end
+
+ def banner(description)
+ self.description = description
+ end
+
+ attr_reader :description
+
+ def derived_usage_description
+ parts = ["[OPTIONS]"]
+ parts += parameters.map { |a| a.name }
+ parts.join(" ")
+ end
+
+ def usage_descriptions
+ declared_usage_descriptions || [derived_usage_description]
+ end
+
+ def help(invocation_path, builder = Builder.new)
+ help = builder
+ help.add_usage(invocation_path, usage_descriptions)
+ help.add_description(description)
+ if has_parameters?
+ help.add_list("Parameters", parameters)
+ end
+ if has_subcommands?
+ help.add_list("Subcommands", recognised_subcommands)
+ end
+ help.add_list("Options", recognised_options)
+ help.string
+ end
+
+ class Builder
+
+ def initialize
+ @out = StringIO.new
+ end
+
+ def string
+ @out.string
+ end
+
+ def add_usage(invocation_path, usage_descriptions)
+ puts "Usage:"
+ usage_descriptions.each do |usage|
+ puts " #{invocation_path} #{usage}".rstrip
+ end
+ end
+
+ def add_description(description)
+ if description
+ puts ""
+ puts description.gsub(/^/, " ")
+ end
+ end
+
+ DETAIL_FORMAT = " %-29s %s"
+
+ def add_list(heading, items)
+ puts "\n#{heading}:"
+ items.each do |item|
+ label, description = item.help
+ description.each_line do |line|
+ puts DETAIL_FORMAT % [label, line]
+ label = ''
+ end
+ end
+ end
+
+ private
+
+ def puts(*args)
+ @out.puts(*args)
+ end
+
+ end
+
+ end
+
+end
diff --git a/lib/clamp/option/declaration.rb b/lib/clamp/option/declaration.rb
new file mode 100644
index 0000000..a5a3a8d
--- /dev/null
+++ b/lib/clamp/option/declaration.rb
@@ -0,0 +1,59 @@
+require 'clamp/attribute/declaration'
+require 'clamp/option/definition'
+
+module Clamp
+ module Option
+
+ module Declaration
+
+ include Clamp::Attribute::Declaration
+
+ def option(switches, type, description, opts = {}, &block)
+ Option::Definition.new(switches, type, description, opts).tap do |option|
+ declared_options << option
+ block ||= option.default_conversion_block
+ define_accessors_for(option, &block)
+ end
+ end
+
+ def find_option(switch)
+ recognised_options.find { |o| o.handles?(switch) }
+ end
+
+ def declared_options
+ @declared_options ||= []
+ end
+
+ def recognised_options
+ declare_implicit_options
+ effective_options
+ end
+
+ private
+
+ def declare_implicit_options
+ return nil if defined?(@implicit_options_declared)
+ unless effective_options.find { |o| o.handles?("--help") }
+ help_switches = ["--help"]
+ help_switches.unshift("-h") unless effective_options.find { |o| o.handles?("-h") }
+ option help_switches, :flag, "print help" do
+ request_help
+ end
+ end
+ @implicit_options_declared = true
+ end
+
+ def effective_options
+ ancestors.inject([]) do |options, ancestor|
+ if ancestor.kind_of?(Clamp::Option::Declaration)
+ options + ancestor.declared_options
+ else
+ options
+ end
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/option/definition.rb b/lib/clamp/option/definition.rb
new file mode 100644
index 0000000..b0c14a1
--- /dev/null
+++ b/lib/clamp/option/definition.rb
@@ -0,0 +1,94 @@
+require 'clamp/attribute/definition'
+require 'clamp/truthy'
+
+module Clamp
+ module Option
+
+ class Definition < Attribute::Definition
+
+ def initialize(switches, type, description, options = {})
+ @switches = Array(switches)
+ @type = type
+ @description = description
+ super(options)
+ @multivalued = options[:multivalued]
+ if options.has_key?(:required)
+ @required = options[:required]
+ # Do some light validation for conflicting settings.
+ if options.has_key?(:default)
+ raise ArgumentError, "Specifying a :default value also :required doesn't make sense"
+ end
+ if type == :flag
+ raise ArgumentError, "A required flag (boolean) doesn't make sense."
+ end
+ end
+ end
+
+ attr_reader :switches, :type
+
+ def long_switch
+ switches.find { |switch| switch =~ /^--/ }
+ end
+
+ def handles?(switch)
+ recognised_switches.member?(switch)
+ end
+
+ def flag?
+ @type == :flag
+ end
+
+ def flag_value(switch)
+ !(switch =~ /^--no-(.*)/ && switches.member?("--\[no-\]#{$1}"))
+ end
+
+ def read_method
+ if flag?
+ super + "?"
+ else
+ super
+ end
+ end
+
+ def extract_value(switch, arguments)
+ if flag?
+ flag_value(switch)
+ else
+ arguments.shift
+ end
+ end
+
+ def default_conversion_block
+ if flag?
+ Clamp.method(:truthy?)
+ end
+ end
+
+ def help_lhs
+ lhs = switches.join(", ")
+ lhs += " " + type unless flag?
+ lhs
+ end
+
+ private
+
+ def recognised_switches
+ switches.map do |switch|
+ if switch =~ /^--\[no-\](.*)/
+ ["--#{$1}", "--no-#{$1}"]
+ else
+ switch
+ end
+ end.flatten
+ end
+
+ def infer_attribute_name
+ inferred_name = long_switch.sub(/^--(\[no-\])?/, '').tr('-', '_')
+ inferred_name += "_list" if multivalued?
+ inferred_name
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/option/parsing.rb b/lib/clamp/option/parsing.rb
new file mode 100644
index 0000000..9f057f1
--- /dev/null
+++ b/lib/clamp/option/parsing.rb
@@ -0,0 +1,68 @@
+module Clamp
+ module Option
+
+ module Parsing
+
+ protected
+
+ def parse_options
+
+ while remaining_arguments.first =~ /\A-/
+
+ switch = remaining_arguments.shift
+ break if switch == "--"
+
+ case switch
+ when /\A(-\w)(.+)\z/m # combined short options
+ switch = $1
+ if find_option(switch).flag?
+ remaining_arguments.unshift("-" + $2)
+ else
+ remaining_arguments.unshift($2)
+ end
+ when /\A(--[^=]+)=(.*)\z/m
+ switch = $1
+ remaining_arguments.unshift($2)
+ end
+
+ option = find_option(switch)
+ value = option.extract_value(switch, remaining_arguments)
+
+ begin
+ option.of(self).take(value)
+ rescue ArgumentError => e
+ signal_usage_error "option '#{switch}': #{e.message}"
+ end
+
+ end
+
+ # Fill in gap from environment
+ self.class.recognised_options.each do |option|
+ option.of(self).default_from_environment
+ end
+
+ # Verify that all required options are present
+ self.class.recognised_options.each do |option|
+ # If this option is required and the value is nil, there's an error.
+ if option.required? and send(option.attribute_name).nil?
+ message = "option '#{option.switches.first}'"
+ if option.environment_variable
+ message += " (or env #{option.environment_variable})"
+ end
+ message += " is required"
+ signal_usage_error message
+ end
+ end
+ end
+
+ private
+
+ def find_option(switch)
+ self.class.find_option(switch) ||
+ signal_usage_error("Unrecognised option '#{switch}'")
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/parameter/declaration.rb b/lib/clamp/parameter/declaration.rb
new file mode 100644
index 0000000..a14350e
--- /dev/null
+++ b/lib/clamp/parameter/declaration.rb
@@ -0,0 +1,29 @@
+require 'clamp/attribute/declaration'
+require 'clamp/parameter/definition'
+
+module Clamp
+ module Parameter
+
+ module Declaration
+
+ include Clamp::Attribute::Declaration
+
+ def parameters
+ @parameters ||= []
+ end
+
+ def has_parameters?
+ !parameters.empty?
+ end
+
+ def parameter(name, description, options = {}, &block)
+ Parameter::Definition.new(name, description, options).tap do |parameter|
+ parameters << parameter
+ define_accessors_for(parameter, &block)
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/parameter/definition.rb b/lib/clamp/parameter/definition.rb
new file mode 100644
index 0000000..6412546
--- /dev/null
+++ b/lib/clamp/parameter/definition.rb
@@ -0,0 +1,48 @@
+require 'clamp/attribute/definition'
+
+module Clamp
+ module Parameter
+
+ class Definition < Attribute::Definition
+
+ def initialize(name, description, options = {})
+ @name = name
+ @description = description
+ super(options)
+ @multivalued = (@name =~ ELLIPSIS_SUFFIX)
+ @required = options.fetch(:required) do
+ (@name !~ OPTIONAL)
+ end
+ end
+
+ attr_reader :name
+
+ def help_lhs
+ name
+ end
+
+ def consume(arguments)
+ raise ArgumentError, "no value provided" if required? && arguments.empty?
+ arguments.shift(multivalued? ? arguments.length : 1)
+ end
+
+ private
+
+ ELLIPSIS_SUFFIX = / \.\.\.$/
+ OPTIONAL = /^\[(.*)\]/
+
+ VALID_ATTRIBUTE_NAME = /^[a-z0-9_]+$/
+
+ def infer_attribute_name
+ inferred_name = name.downcase.tr('-', '_').sub(ELLIPSIS_SUFFIX, '').sub(OPTIONAL) { $1 }
+ unless inferred_name =~ VALID_ATTRIBUTE_NAME
+ raise "cannot infer attribute_name from #{name.inspect}"
+ end
+ inferred_name += "_list" if multivalued?
+ inferred_name
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/parameter/parsing.rb b/lib/clamp/parameter/parsing.rb
new file mode 100644
index 0000000..8a70719
--- /dev/null
+++ b/lib/clamp/parameter/parsing.rb
@@ -0,0 +1,29 @@
+module Clamp
+ module Parameter
+
+ module Parsing
+
+ protected
+
+ def parse_parameters
+
+ self.class.parameters.each do |parameter|
+ begin
+ parameter.consume(remaining_arguments).each do |value|
+ parameter.of(self).take(value)
+ end
+ rescue ArgumentError => e
+ signal_usage_error "parameter '#{parameter.name}': #{e.message}"
+ end
+ end
+
+ self.class.parameters.each do |parameter|
+ parameter.of(self).default_from_environment
+ end
+
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/subcommand/declaration.rb b/lib/clamp/subcommand/declaration.rb
new file mode 100644
index 0000000..1cb490b
--- /dev/null
+++ b/lib/clamp/subcommand/declaration.rb
@@ -0,0 +1,67 @@
+require 'clamp/errors'
+require 'clamp/subcommand/definition'
+
+module Clamp
+ module Subcommand
+
+ module Declaration
+
+ def recognised_subcommands
+ @recognised_subcommands ||= []
+ end
+
+ def subcommand(name, description, subcommand_class = self, &block)
+ unless has_subcommands?
+ @subcommand_parameter = if @default_subcommand
+ parameter "[SUBCOMMAND]", "subcommand", :attribute_name => :subcommand_name, :default => @default_subcommand
+ else
+ parameter "SUBCOMMAND", "subcommand", :attribute_name => :subcommand_name, :required => false
+ end
+ remove_method :default_subcommand_name
+ parameter "[ARG] ...", "subcommand arguments", :attribute_name => :subcommand_arguments
+ end
+ if block
+ # generate a anonymous sub-class
+ subcommand_class = Class.new(subcommand_class, &block)
+ end
+ recognised_subcommands << Subcommand::Definition.new(name, description, subcommand_class)
+ end
+
+ def has_subcommands?
+ !recognised_subcommands.empty?
+ end
+
+ def find_subcommand(name)
+ recognised_subcommands.find { |sc| sc.is_called?(name) }
+ end
+
+ def parameters_before_subcommand
+ parameters.take_while { |p| p != @subcommand_parameter }
+ end
+
+ def inheritable_attributes
+ recognised_options + parameters_before_subcommand
+ end
+
+ def default_subcommand=(name)
+ if has_subcommands?
+ raise Clamp::DeclarationError, "default_subcommand must be defined before subcommands"
+ end
+ @default_subcommand = name
+ end
+
+ def default_subcommand(*args, &block)
+ if args.empty?
+ @default_subcommand
+ else
+ $stderr.puts "WARNING: Clamp default_subcommand syntax has changed; check the README."
+ $stderr.puts " (from #{caller.first})"
+ self.default_subcommand = args.first
+ subcommand(*args, &block)
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/subcommand/definition.rb b/lib/clamp/subcommand/definition.rb
new file mode 100644
index 0000000..8b86270
--- /dev/null
+++ b/lib/clamp/subcommand/definition.rb
@@ -0,0 +1,25 @@
+module Clamp
+ module Subcommand
+
+ class Definition < Struct.new(:name, :description, :subcommand_class)
+
+ def initialize(names, description, subcommand_class)
+ @names = Array(names)
+ @description = description
+ @subcommand_class = subcommand_class
+ end
+
+ attr_reader :names, :description, :subcommand_class
+
+ def is_called?(name)
+ names.member?(name)
+ end
+
+ def help
+ [names.join(", "), description]
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/subcommand/execution.rb b/lib/clamp/subcommand/execution.rb
new file mode 100644
index 0000000..d15c56b
--- /dev/null
+++ b/lib/clamp/subcommand/execution.rb
@@ -0,0 +1,35 @@
+module Clamp
+ module Subcommand
+
+ module Execution
+
+ # override default Command behaviour
+
+ def execute
+ # delegate to subcommand
+ subcommand = instatiate_subcommand(subcommand_name)
+ subcommand.run(subcommand_arguments)
+ end
+
+ private
+
+ def instatiate_subcommand(name)
+ subcommand_class = find_subcommand_class(name)
+ parent_attribute_values = {}
+ self.class.inheritable_attributes.each do |attribute|
+ if attribute.of(self).defined?
+ parent_attribute_values[attribute] = attribute.of(self).get
+ end
+ end
+ subcommand_class.new("#{invocation_path} #{name}", context, parent_attribute_values)
+ end
+
+ def find_subcommand_class(name)
+ subcommand_def = self.class.find_subcommand(name) || signal_usage_error("No such sub-command '#{name}'")
+ subcommand_def.subcommand_class
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/subcommand/parsing.rb b/lib/clamp/subcommand/parsing.rb
new file mode 100644
index 0000000..d785a68
--- /dev/null
+++ b/lib/clamp/subcommand/parsing.rb
@@ -0,0 +1,24 @@
+require 'clamp/subcommand/execution'
+
+module Clamp
+ module Subcommand
+
+ module Parsing
+
+ protected
+
+ def parse_subcommand
+ return false unless self.class.has_subcommands?
+ self.extend(Subcommand::Execution)
+ end
+
+ private
+
+ def default_subcommand_name
+ self.class.default_subcommand || request_help
+ end
+
+ end
+
+ end
+end
diff --git a/lib/clamp/truthy.rb b/lib/clamp/truthy.rb
new file mode 100644
index 0000000..449958e
--- /dev/null
+++ b/lib/clamp/truthy.rb
@@ -0,0 +1,9 @@
+module Clamp
+
+ TRUTHY_VALUES = %w(1 yes enable on true)
+
+ def self.truthy?(arg)
+ TRUTHY_VALUES.include?(arg.to_s.downcase)
+ end
+
+end
diff --git a/lib/clamp/version.rb b/lib/clamp/version.rb
new file mode 100644
index 0000000..09b5a5d
--- /dev/null
+++ b/lib/clamp/version.rb
@@ -0,0 +1,3 @@
+module Clamp
+ VERSION = "0.6.3".freeze
+end
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..90683cd
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,94 @@
+--- !ruby/object:Gem::Specification
+name: clamp
+version: !ruby/object:Gem::Version
+ version: 0.6.3
+platform: ruby
+authors:
+- Mike Williams
+autorequire:
+bindir: bin
+cert_chain: []
+date: 2013-11-14 00:00:00.000000000 Z
+dependencies: []
+description: ! 'Clamp provides an object-model for command-line utilities.
+
+ It handles parsing of command-line options, and generation of usage help.
+
+'
+email: mdub at dogbiscuit.org
+executables: []
+extensions: []
+extra_rdoc_files: []
+files:
+- .autotest
+- .gitignore
+- .rspec
+- .travis.yml
+- CHANGES.md
+- Gemfile
+- LICENSE
+- README.md
+- Rakefile
+- clamp.gemspec
+- examples/admin
+- examples/flipflop
+- examples/fubar
+- examples/gitdown
+- examples/scoop
+- examples/speak
+- lib/clamp.rb
+- lib/clamp/attribute/declaration.rb
+- lib/clamp/attribute/definition.rb
+- lib/clamp/attribute/instance.rb
+- lib/clamp/command.rb
+- lib/clamp/errors.rb
+- lib/clamp/help.rb
+- lib/clamp/option/declaration.rb
+- lib/clamp/option/definition.rb
+- lib/clamp/option/parsing.rb
+- lib/clamp/parameter/declaration.rb
+- lib/clamp/parameter/definition.rb
+- lib/clamp/parameter/parsing.rb
+- lib/clamp/subcommand/declaration.rb
+- lib/clamp/subcommand/definition.rb
+- lib/clamp/subcommand/execution.rb
+- lib/clamp/subcommand/parsing.rb
+- lib/clamp/truthy.rb
+- lib/clamp/version.rb
+- spec/clamp/command_group_spec.rb
+- spec/clamp/command_spec.rb
+- spec/clamp/option/definition_spec.rb
+- spec/clamp/option_module_spec.rb
+- spec/clamp/parameter/definition_spec.rb
+- spec/spec_helper.rb
+homepage: http://github.com/mdub/clamp
+licenses:
+- MIT
+metadata: {}
+post_install_message:
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ! '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+required_rubygems_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ! '>='
+ - !ruby/object:Gem::Version
+ version: '0'
+requirements: []
+rubyforge_project:
+rubygems_version: 2.0.7
+signing_key:
+specification_version: 4
+summary: a minimal framework for command-line utilities
+test_files:
+- spec/clamp/command_group_spec.rb
+- spec/clamp/command_spec.rb
+- spec/clamp/option/definition_spec.rb
+- spec/clamp/option_module_spec.rb
+- spec/clamp/parameter/definition_spec.rb
+- spec/spec_helper.rb
diff --git a/spec/clamp/command_group_spec.rb b/spec/clamp/command_group_spec.rb
new file mode 100644
index 0000000..8b2a564
--- /dev/null
+++ b/spec/clamp/command_group_spec.rb
@@ -0,0 +1,305 @@
+require 'spec_helper'
+
+describe Clamp::Command do
+
+ extend CommandFactory
+ include OutputCapture
+
+ describe "with subcommands" do
+
+ given_command "flipflop" do
+
+ def execute
+ puts message
+ end
+
+ subcommand "flip", "flip it" do
+ def message
+ "FLIPPED"
+ end
+ end
+
+ subcommand "flop", "flop it\nfor extra flop" do
+ def message
+ "FLOPPED"
+ end
+ end
+
+ end
+
+ it "delegates to sub-commands" do
+
+ command.run(["flip"])
+ stdout.should =~ /FLIPPED/
+
+ command.run(["flop"])
+ stdout.should =~ /FLOPPED/
+
+ end
+
+ context "executed with no subcommand" do
+
+ it "triggers help" do
+ lambda do
+ command.run([])
+ end.should raise_error(Clamp::HelpWanted)
+ end
+
+ end
+
+ describe "#help" do
+
+ it "shows subcommand parameters in usage" do
+ command.help.should include("flipflop [OPTIONS] SUBCOMMAND [ARG] ...")
+ end
+
+ it "lists subcommands" do
+ help = command.help
+ help.should =~ /Subcommands:/
+ help.should =~ /flip +flip it/
+ help.should =~ /flop +flop it/
+ end
+
+ it "handles new lines in subcommand descriptions" do
+ command.help.should =~ /flop +flop it\n +for extra flop/
+ end
+
+ end
+
+ end
+
+ describe "with an aliased subcommand" do
+
+ given_command "blah" do
+
+ subcommand ["say", "talk"], "Say something" do
+
+ parameter "WORD ...", "stuff to say"
+
+ def execute
+ puts word_list
+ end
+
+ end
+
+ end
+
+ it "responds to both aliases" do
+
+ command.run(["say", "boo"])
+ stdout.should =~ /boo/
+
+ command.run(["talk", "jive"])
+ stdout.should =~ /jive/
+
+ end
+
+ describe "#help" do
+
+ it "lists all aliases" do
+ help = command.help
+ help.should =~ /say, talk .* Say something/
+ end
+
+ end
+
+ end
+
+ describe "with nested subcommands" do
+
+ given_command "fubar" do
+
+ subcommand "foo", "Foo!" do
+
+ subcommand "bar", "Baaaa!" do
+ def execute
+ puts "FUBAR"
+ end
+ end
+
+ end
+
+ end
+
+ it "delegates multiple levels" do
+ command.run(["foo", "bar"])
+ stdout.should =~ /FUBAR/
+ end
+
+ end
+
+ describe "with a default subcommand" do
+
+ given_command "admin" do
+
+ self.default_subcommand = "status"
+
+ subcommand "status", "Show status" do
+
+ def execute
+ puts "All good!"
+ end
+
+ end
+
+ end
+
+ context "executed with no subcommand" do
+
+ it "invokes the default subcommand" do
+ command.run([])
+ stdout.should =~ /All good/
+ end
+
+ end
+
+ end
+
+ describe "with a default subcommand, declared the old way" do
+
+ given_command "admin" do
+
+ default_subcommand "status", "Show status" do
+
+ def execute
+ puts "All good!"
+ end
+
+ end
+
+ end
+
+ context "executed with no subcommand" do
+
+ it "invokes the default subcommand" do
+ command.run([])
+ stdout.should =~ /All good/
+ end
+
+ end
+
+ end
+
+ describe "declaring a default subcommand after subcommands" do
+
+ it "is not supported" do
+
+ lambda do
+ Class.new(Clamp::Command) do
+
+ subcommand "status", "Show status" do
+
+ def execute
+ puts "All good!"
+ end
+
+ end
+
+ self.default_subcommand = "status"
+
+ end
+ end.should raise_error(/default_subcommand must be defined before subcommands/)
+
+ end
+
+ end
+
+ describe "with subcommands, declared after a parameter" do
+
+ given_command "with" do
+
+ parameter "THING", "the thing"
+
+ subcommand "spit", "spit it" do
+ def execute
+ puts "spat the #{thing}"
+ end
+ end
+
+ end
+
+ it "allows the parameter to be specified first" do
+
+ command.run(["dummy", "spit"])
+ stdout.strip.should == "spat the dummy"
+
+ end
+
+ end
+
+ describe "each subcommand" do
+
+ let(:command_class) do
+
+ speed_options = Module.new do
+ extend Clamp::Option::Declaration
+ option "--speed", "SPEED", "how fast", :default => "slowly"
+ end
+
+ Class.new(Clamp::Command) do
+
+ option "--direction", "DIR", "which way", :default => "home"
+
+ include speed_options
+
+ subcommand "move", "move in the appointed direction" do
+
+ def execute
+ motion = context[:motion] || "walking"
+ puts "#{motion} #{direction} #{speed}"
+ end
+
+ end
+
+ end
+ end
+
+ let(:command) do
+ command_class.new("go")
+ end
+
+ it "accepts options defined in superclass (specified after the subcommand)" do
+ command.run(["move", "--direction", "north"])
+ stdout.should =~ /walking north/
+ end
+
+ it "accepts options defined in superclass (specified before the subcommand)" do
+ command.run(["--direction", "north", "move"])
+ stdout.should =~ /walking north/
+ end
+
+ it "accepts options defined in included modules" do
+ command.run(["move", "--speed", "very quickly"])
+ stdout.should =~ /walking home very quickly/
+ end
+
+ it "has access to command context" do
+ command = command_class.new("go", :motion => "wandering")
+ command.run(["move"])
+ stdout.should =~ /wandering home/
+ end
+
+ end
+
+ describe "with a subcommand, with options" do
+
+ given_command 'weeheehee' do
+ option '--json', 'JSON', 'a json blob' do |option|
+ print "parsing!"
+ option
+ end
+
+ subcommand 'woohoohoo', 'like weeheehee but with more o' do
+ def execute
+ end
+ end
+ end
+
+ it "only parses options once" do
+ command.run(['--json', '{"a":"b"}', 'woohoohoo'])
+ stdout.should == 'parsing!'
+ end
+
+ end
+
+end
diff --git a/spec/clamp/command_spec.rb b/spec/clamp/command_spec.rb
new file mode 100644
index 0000000..7f8780b
--- /dev/null
+++ b/spec/clamp/command_spec.rb
@@ -0,0 +1,927 @@
+
+require 'spec_helper'
+
+describe Clamp::Command do
+
+ extend CommandFactory
+ include OutputCapture
+
+ given_command("cmd") do
+
+ def execute
+ puts "Hello, world"
+ end
+
+ end
+
+ describe "#help" do
+
+ it "describes usage" do
+ command.help.should =~ /^Usage:\n cmd.*\n/
+ end
+
+ end
+
+ describe "#run" do
+
+ before do
+ command.run([])
+ end
+
+ it "executes the #execute method" do
+ stdout.should_not be_empty
+ end
+
+ end
+
+ describe ".option" do
+
+ it "declares option argument accessors" do
+ command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
+ command.flavour.should == nil
+ command.flavour = "chocolate"
+ command.flavour.should == "chocolate"
+ end
+
+ describe "with type :flag" do
+
+ before do
+ command.class.option "--verbose", :flag, "Be heartier"
+ end
+
+ it "declares a predicate-style reader" do
+ command.should respond_to(:verbose?)
+ command.should_not respond_to(:verbose)
+ end
+
+ end
+
+ describe "with explicit :attribute_name" do
+
+ before do
+ command.class.option "--foo", "FOO", "A foo", :attribute_name => :bar
+ end
+
+ it "uses the specified attribute_name name to name accessors" do
+ command.bar = "chocolate"
+ command.bar.should == "chocolate"
+ end
+
+ it "does not attempt to create the default accessors" do
+ command.should_not respond_to(:foo)
+ command.should_not respond_to(:foo=)
+ end
+
+ end
+
+ describe "with default method" do
+
+ before do
+ command.class.option "--port", "PORT", "port"
+ command.class.class_eval do
+ def default_port
+ 4321
+ end
+ end
+ end
+
+ it "sets the specified default value" do
+ command.port.should == 4321
+ end
+
+ end
+
+ describe "with :default value" do
+
+ before do
+ command.class.option "--port", "PORT", "port to listen on", :default => 4321
+ end
+
+ it "declares default method" do
+ command.default_port.should == 4321
+ end
+
+ describe "#help" do
+
+ it "describes the default value" do
+ command.help.should include("port to listen on (default: 4321)")
+ end
+
+ end
+
+ end
+
+ describe "with :multivalued" do
+
+ before do
+ command.class.option "--flavour", "FLAVOUR", "flavour(s)", :multivalued => true, :attribute_name => :flavours
+ end
+
+ it "defaults to empty array" do
+ command.flavours.should == []
+ end
+
+ it "supports multiple values" do
+ command.parse(%w(--flavour chocolate --flavour vanilla))
+ command.flavours.should == %w(chocolate vanilla)
+ end
+
+ it "generates a single-value appender method" do
+ command.append_to_flavours("mud")
+ command.append_to_flavours("pie")
+ command.flavours.should == %w(mud pie)
+ end
+
+ it "generates a multi-value setter method" do
+ command.append_to_flavours("replaceme")
+ command.flavours = %w(mud pie)
+ command.flavours.should == %w(mud pie)
+ end
+
+ end
+
+ describe "with :environment_variable" do
+
+ before do
+ command.class.option "--port", "PORT", "port to listen on", :default => 4321, :environment_variable => "PORT" do |value|
+ value.to_i
+ end
+ end
+
+ context "when no environment variable is present" do
+
+ before do
+ ENV.delete("PORT")
+ end
+
+ it "uses the default" do
+ command.parse([])
+ command.port.should == 4321
+ end
+
+ end
+
+ context "when environment variable is present" do
+
+ before do
+ ENV["PORT"] = "12345"
+ end
+
+ it "uses the environment variable" do
+ command.parse([])
+ command.port.should == 12345
+ end
+
+ context "and a value is specified on the command-line" do
+
+ it "uses command-line value" do
+ command.parse(%w(--port 1500))
+ command.port.should == 1500
+ end
+
+ end
+
+ end
+
+ describe "#help" do
+
+ it "describes the default value and env usage" do
+ command.help.should include("port to listen on (default: $PORT, or 4321)")
+ end
+
+ end
+
+ end
+
+ describe "with :environment_variable and type :flag" do
+
+ before do
+ command.class.option "--[no-]enable", :flag, "enable?", :default => false, :environment_variable => "ENABLE"
+ end
+
+ context "when no environment variable is present" do
+
+ before do
+ ENV.delete("ENABLE")
+ end
+
+ it "uses the default" do
+ command.parse([])
+ command.enable?.should == false
+ end
+
+ end
+
+ %w(1 yes enable on true).each do |truthy_value|
+
+ context "when environment variable is #{truthy_value.inspect}" do
+
+ it "sets the flag" do
+ ENV["ENABLE"] = truthy_value
+ command.parse([])
+ command.enable?.should == true
+ end
+
+ end
+
+ end
+
+ %w(0 no disable off false).each do |falsey_value|
+
+ context "when environment variable is #{falsey_value.inspect}" do
+
+ it "clears the flag" do
+ ENV["ENABLE"] = falsey_value
+ command.parse([])
+ command.enable?.should == false
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "with :required" do
+
+ before do
+ command.class.option "--port", "PORT", "port to listen on", :required => true
+ end
+
+ context "when no value is provided" do
+
+ it "raises a UsageError" do
+ expect do
+ command.parse([])
+ end.to raise_error(Clamp::UsageError)
+ end
+
+ end
+
+ context "when a value is provided" do
+
+ it "does not raise an error" do
+ expect do
+ command.parse(["--port", "12345"])
+ end.not_to raise_error
+ end
+
+ end
+
+ end
+
+ describe "with a block" do
+
+ before do
+ command.class.option "--port", "PORT", "Port to listen on" do |port|
+ Integer(port)
+ end
+ end
+
+ it "uses the block to validate and convert the option argument" do
+ lambda do
+ command.port = "blah"
+ end.should raise_error(ArgumentError)
+ command.port = "1234"
+ command.port.should == 1234
+ end
+
+ end
+
+ end
+
+ describe "with options declared" do
+
+ before do
+ command.class.option ["-f", "--flavour"], "FLAVOUR", "Flavour of the month"
+ command.class.option ["-c", "--color"], "COLOR", "Preferred hue"
+ command.class.option ["--scoops"], "N", "Number of scoops",
+ :default => 1,
+ :environment_variable => "DEFAULT_SCOOPS" do |arg|
+ Integer(arg)
+ end
+ command.class.option ["-n", "--[no-]nuts"], :flag, "Nuts (or not)\nMay include nuts"
+ command.class.parameter "[ARG] ...", "extra arguments", :attribute_name => :arguments
+ end
+
+ describe "#parse" do
+
+ describe "with an unrecognised option" do
+
+ it "raises a UsageError" do
+ lambda do
+ command.parse(%w(--foo bar))
+ end.should raise_error(Clamp::UsageError)
+ end
+
+ end
+
+ describe "with options" do
+
+ before do
+ command.parse(%w(--flavour strawberry --nuts --color blue))
+ end
+
+ it "maps the option values onto the command object" do
+ command.flavour.should == "strawberry"
+ command.color.should == "blue"
+ command.nuts?.should == true
+ end
+
+ end
+
+ describe "with short options" do
+
+ before do
+ command.parse(%w(-f strawberry -c blue))
+ end
+
+ it "recognises short options as aliases" do
+ command.flavour.should == "strawberry"
+ command.color.should == "blue"
+ end
+
+ end
+
+ describe "with a value appended to a short option" do
+
+ before do
+ command.parse(%w(-fstrawberry))
+ end
+
+ it "works as though the value were separated" do
+ command.flavour.should == "strawberry"
+ end
+
+ end
+
+ describe "with combined short options" do
+
+ before do
+ command.parse(%w(-nf strawberry))
+ end
+
+ it "works as though the options were separate" do
+ command.flavour.should == "strawberry"
+ command.nuts?.should == true
+ end
+
+ end
+
+ describe "with option arguments attached using equals sign" do
+
+ before do
+ command.parse(%w(--flavour=strawberry --color=blue))
+ end
+
+ it "works as though the option arguments were separate" do
+ command.flavour.should == "strawberry"
+ command.color.should == "blue"
+ end
+
+ end
+
+ describe "with option-like things beyond the arguments" do
+
+ it "treats them as positional arguments" do
+ command.parse(%w(a b c --flavour strawberry))
+ command.arguments.should == %w(a b c --flavour strawberry)
+ end
+
+ end
+
+ describe "with multi-line arguments that look like options" do
+
+ before do
+ command.parse(["foo\n--flavour=strawberry", "bar\n-cblue"])
+ end
+
+ it "treats them as positional arguments" do
+ command.arguments.should == ["foo\n--flavour=strawberry", "bar\n-cblue"]
+ command.flavour.should be_nil
+ command.color.should be_nil
+ end
+
+ end
+
+ describe "with an option terminator" do
+
+ it "considers everything after the terminator to be an argument" do
+ command.parse(%w(--color blue -- --flavour strawberry))
+ command.arguments.should == %w(--flavour strawberry)
+ end
+
+ end
+
+ describe "with --flag" do
+
+ before do
+ command.parse(%w(--nuts))
+ end
+
+ it "sets the flag" do
+ command.nuts?.should be_true
+ end
+
+ end
+
+ describe "with --no-flag" do
+
+ before do
+ command.nuts = true
+ command.parse(%w(--no-nuts))
+ end
+
+ it "clears the flag" do
+ command.nuts?.should be_false
+ end
+
+ end
+
+ describe "with --help" do
+
+ it "requests help" do
+ lambda do
+ command.parse(%w(--help))
+ end.should raise_error(Clamp::HelpWanted)
+ end
+
+ end
+
+ describe "with -h" do
+
+ it "requests help" do
+ lambda do
+ command.parse(%w(-h))
+ end.should raise_error(Clamp::HelpWanted)
+ end
+
+ end
+
+ describe "when a bad option value is specified on the command-line" do
+
+ it "signals a UsageError" do
+ lambda do
+ command.parse(%w(--scoops reginald))
+ end.should raise_error(Clamp::UsageError, /^option '--scoops': invalid value for Integer/)
+ end
+
+ end
+
+ describe "when a bad option value is specified in the environment" do
+
+ it "signals a UsageError" do
+ ENV["DEFAULT_SCOOPS"] = "marjorie"
+ lambda do
+ command.parse([])
+ end.should raise_error(Clamp::UsageError, /^\$DEFAULT_SCOOPS: invalid value for Integer/)
+ end
+
+ end
+
+ end
+
+ describe "#help" do
+
+ it "indicates that there are options" do
+ command.help.should include("cmd [OPTIONS]")
+ end
+
+ it "includes option details" do
+ command.help.should =~ %r(--flavour FLAVOUR +Flavour of the month)
+ command.help.should =~ %r(--color COLOR +Preferred hue)
+ end
+
+ it "handles new lines in option descriptions" do
+ command.help.should =~ %r(--\[no-\]nuts +Nuts \(or not\)\n +May include nuts)
+ end
+
+ end
+
+ end
+
+ describe "with an explicit --help option declared" do
+
+ before do
+ command.class.option ["--help"], :flag, "help wanted"
+ end
+
+ it "does not generate implicit help option" do
+ lambda do
+ command.parse(%w(--help))
+ end.should_not raise_error
+ command.help.should be_true
+ end
+
+ it "does not recognise -h" do
+ lambda do
+ command.parse(%w(-h))
+ end.should raise_error(Clamp::UsageError)
+ end
+
+ end
+
+ describe "with an explicit -h option declared" do
+
+ before do
+ command.class.option ["-h", "--humidity"], "PERCENT", "relative humidity" do |n|
+ Integer(n)
+ end
+ end
+
+ it "does not map -h to help" do
+ command.help.should_not =~ %r( -h[, ].*help)
+ end
+
+ it "still recognises --help" do
+ lambda do
+ command.parse(%w(--help))
+ end.should raise_error(Clamp::HelpWanted)
+ end
+
+ end
+
+ describe ".parameter" do
+
+ it "declares option argument accessors" do
+ command.class.parameter "FLAVOUR", "flavour of the month"
+ command.flavour.should == nil
+ command.flavour = "chocolate"
+ command.flavour.should == "chocolate"
+ end
+
+ describe "with explicit :attribute_name" do
+
+ before do
+ command.class.parameter "FOO", "a foo", :attribute_name => :bar
+ end
+
+ it "uses the specified attribute_name name to name accessors" do
+ command.bar = "chocolate"
+ command.bar.should == "chocolate"
+ end
+
+ end
+
+ describe "with :default value" do
+
+ before do
+ command.class.parameter "[ORIENTATION]", "direction", :default => "west"
+ end
+
+ it "sets the specified default value" do
+ command.orientation.should == "west"
+ end
+
+ describe "#help" do
+
+ it "describes the default value" do
+ command.help.should include("direction (default: \"west\")")
+ end
+
+ end
+
+ end
+
+ describe "with a block" do
+
+ before do
+ command.class.parameter "PORT", "port to listen on" do |port|
+ Integer(port)
+ end
+ end
+
+ it "uses the block to validate and convert the argument" do
+ lambda do
+ command.port = "blah"
+ end.should raise_error(ArgumentError)
+ command.port = "1234"
+ command.port.should == 1234
+ end
+
+ end
+
+ describe "with ellipsis" do
+
+ before do
+ command.class.parameter "FILE ...", "files"
+ end
+
+ it "accepts multiple arguments" do
+ command.parse(%w(X Y Z))
+ command.file_list.should == %w(X Y Z)
+ end
+
+ end
+
+ describe "optional, with ellipsis" do
+
+ before do
+ command.class.parameter "[FILE] ...", "files"
+ end
+
+ it "default to an empty list" do
+ command.parse([])
+ command.default_file_list.should == []
+ command.file_list.should == []
+ end
+
+ end
+
+ describe "with :environment_variable value" do
+
+ before do
+ command.class.parameter "[FILE]", "a file", :environment_variable => "FILE",
+ :default => "/dev/null"
+ end
+
+ it "should use the default if neither flag nor env var are present" do
+ command.parse([])
+ command.file.should == "/dev/null"
+ end
+
+ it "should use the env value if present (instead of default)" do
+ ENV["FILE"] = "/etc/motd"
+ command.parse([])
+ command.file.should == ENV["FILE"]
+ end
+
+ it "should use the the flag value if present (instead of env)" do
+ ENV["FILE"] = "/etc/motd"
+ command.parse(%w(/bin/sh))
+ command.file.should == "/bin/sh"
+ end
+
+ describe "#help" do
+
+ it "describes the default value and env usage" do
+ command.help.should include(%{ (default: $FILE, or "/dev/null")})
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "with no parameters declared" do
+
+ describe "#parse" do
+
+ describe "with arguments" do
+
+ it "raises a UsageError" do
+ lambda do
+ command.parse(["crash"])
+ end.should raise_error(Clamp::UsageError, "too many arguments")
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "with parameters declared" do
+
+ before do
+ command.class.parameter "X", "x\nxx"
+ command.class.parameter "Y", "y"
+ command.class.parameter "[Z]", "z", :default => "ZZZ"
+ end
+
+ describe "#parse" do
+
+ describe "with arguments for all parameters" do
+
+ before do
+ command.parse(["crash", "bang", "wallop"])
+ end
+
+ it "maps arguments onto the command object" do
+ command.x.should == "crash"
+ command.y.should == "bang"
+ command.z.should == "wallop"
+ end
+
+ end
+
+ describe "with insufficient arguments" do
+
+ it "raises a UsageError" do
+ lambda do
+ command.parse(["crash"])
+ end.should raise_error(Clamp::UsageError, "parameter 'Y': no value provided")
+ end
+
+ end
+
+ describe "with optional argument omitted" do
+
+ it "defaults the optional argument" do
+ command.parse(["crash", "bang"])
+ command.x.should == "crash"
+ command.y.should == "bang"
+ command.z.should == "ZZZ"
+ end
+
+ end
+
+ describe "with multi-line arguments" do
+
+ it "parses them correctly" do
+ command.parse(["foo\nhi", "bar", "baz"])
+ command.x.should == "foo\nhi"
+ command.y.should == "bar"
+ command.z.should == "baz"
+ end
+
+ end
+
+ describe "with too many arguments" do
+
+ it "raises a UsageError" do
+ lambda do
+ command.parse(["crash", "bang", "wallop", "kapow"])
+ end.should raise_error(Clamp::UsageError, "too many arguments")
+ end
+
+ end
+
+ end
+
+ describe "#help" do
+
+ it "indicates that there are parameters" do
+ command.help.should include("cmd [OPTIONS] X Y [Z]")
+ end
+
+ it "includes parameter details" do
+ command.help.should =~ %r(X +x)
+ command.help.should =~ %r(Y +y)
+ command.help.should =~ %r(\[Z\] +z \(default: "ZZZ"\))
+ end
+
+ it "handles new lines in option descriptions" do
+ command.help.should =~ %r(X +x\n +xx)
+ end
+
+ end
+
+
+ end
+
+ describe "with explicit usage" do
+
+ given_command("blah") do
+
+ usage "FOO BAR ..."
+
+ end
+
+ describe "#help" do
+
+ it "includes the explicit usage" do
+ command.help.should include("blah FOO BAR ...\n")
+ end
+
+ end
+
+ end
+
+ describe "with multiple usages" do
+
+ given_command("put") do
+
+ usage "THIS HERE"
+ usage "THAT THERE"
+
+ end
+
+ describe "#help" do
+
+ it "includes both potential usages" do
+ command.help.should include("put THIS HERE\n")
+ command.help.should include("put THAT THERE\n")
+ end
+
+ end
+
+ end
+
+ describe "with a banner" do
+
+ given_command("punt") do
+
+ banner <<-EOF
+ Punt is an example command. It doesn't do much, really.
+
+ The prefix at the beginning of this description should be normalised
+ to two spaces.
+ EOF
+
+ end
+
+ describe "#help" do
+
+ it "includes the banner" do
+ command.help.should =~ /^ Punt is an example command/
+ command.help.should =~ /^ The prefix/
+ end
+
+ end
+
+ end
+
+ describe ".run" do
+
+ it "creates a new Command instance and runs it" do
+ command.class.class_eval do
+ parameter "WORD ...", "words"
+ def execute
+ print word_list.inspect
+ end
+ end
+ @xyz = %w(x y z)
+ command.class.run("cmd", @xyz)
+ stdout.should == @xyz.inspect
+ end
+
+ describe "invoked with a context hash" do
+
+ it "makes the context available within the command" do
+ command.class.class_eval do
+ def execute
+ print context[:foo]
+ end
+ end
+ command.class.run("xyz", [], :foo => "bar")
+ stdout.should == "bar"
+ end
+
+ end
+
+ describe "when there's a UsageError" do
+
+ before do
+
+ command.class.class_eval do
+ def execute
+ signal_usage_error "bad dog!"
+ end
+ end
+
+ begin
+ command.class.run("cmd", [])
+ rescue SystemExit => e
+ @system_exit = e
+ end
+
+ end
+
+ it "outputs the error message" do
+ stderr.should include "ERROR: bad dog!"
+ end
+
+ it "outputs help" do
+ stderr.should include "See: 'cmd --help'"
+ end
+
+ it "exits with a non-zero status" do
+ @system_exit.should_not be_nil
+ @system_exit.status.should == 1
+ end
+
+ end
+
+ describe "when help is requested" do
+
+ it "outputs help" do
+ command.class.run("cmd", ["--help"])
+ stdout.should include "Usage:"
+ end
+
+ end
+
+ end
+
+ describe "subclass" do
+
+ let(:command) do
+ parent_command_class = Class.new(Clamp::Command) do
+ option "--verbose", :flag, "be louder"
+ end
+ derived_command_class = Class.new(parent_command_class) do
+ option "--iterations", "N", "number of times to go around"
+ end
+ derived_command_class.new("cmd")
+ end
+
+ it "inherits options from it's superclass" do
+ command.parse(["--verbose"])
+ command.should be_verbose
+ end
+
+ end
+
+end
diff --git a/spec/clamp/option/definition_spec.rb b/spec/clamp/option/definition_spec.rb
new file mode 100644
index 0000000..6093331
--- /dev/null
+++ b/spec/clamp/option/definition_spec.rb
@@ -0,0 +1,260 @@
+require 'spec_helper'
+
+describe Clamp::Option::Definition do
+
+ describe "with String argument" do
+
+ let(:option) do
+ described_class.new("--key-file", "FILE", "SSH identity")
+ end
+
+ it "has a long_switch" do
+ option.long_switch.should == "--key-file"
+ end
+
+ it "has a type" do
+ option.type.should == "FILE"
+ end
+
+ it "has a description" do
+ option.description.should == "SSH identity"
+ end
+
+ describe "#attribute_name" do
+
+ it "is derived from the (long) switch" do
+ option.attribute_name.should == "key_file"
+ end
+
+ it "can be overridden" do
+ option = described_class.new("--key-file", "FILE", "SSH identity", :attribute_name => "ssh_identity")
+ option.attribute_name.should == "ssh_identity"
+ end
+
+ end
+
+ describe "#write_method" do
+
+ it "is derived from the attribute_name" do
+ option.write_method.should == "key_file="
+ end
+
+ end
+
+ describe "#default_value" do
+
+ it "defaults to nil" do
+ option = described_class.new("-n", "N", "iterations")
+ option.default_value.should == nil
+ end
+
+ it "can be overridden" do
+ option = described_class.new("-n", "N", "iterations", :default => 1)
+ option.default_value.should == 1
+ end
+
+ end
+
+ describe "#help" do
+
+ it "combines switch, type and description" do
+ option.help.should == ["--key-file FILE", "SSH identity"]
+ end
+
+ end
+
+ end
+
+ describe "flag" do
+
+ let(:option) do
+ described_class.new("--verbose", :flag, "Blah blah blah")
+ end
+
+ describe "#default_conversion_block" do
+
+ it "converts truthy values to true" do
+ option.default_conversion_block.call("true").should == true
+ option.default_conversion_block.call("yes").should == true
+ end
+
+ it "converts falsey values to false" do
+ option.default_conversion_block.call("false").should == false
+ option.default_conversion_block.call("no").should == false
+ end
+
+ end
+
+ describe "#help" do
+
+ it "excludes option argument" do
+ option.help.should == ["--verbose", "Blah blah blah"]
+ end
+
+ end
+
+ end
+
+ describe "negatable flag" do
+
+ let(:option) do
+ described_class.new("--[no-]force", :flag, "Force installation")
+ end
+
+ it "handles both positive and negative forms" do
+ option.handles?("--force").should be_true
+ option.handles?("--no-force").should be_true
+ end
+
+ describe "#flag_value" do
+
+ it "returns true for the positive variant" do
+ option.flag_value("--force").should be_true
+ option.flag_value("--no-force").should be_false
+ end
+
+ end
+
+ describe "#attribute_name" do
+
+ it "is derived from the (long) switch" do
+ option.attribute_name.should == "force"
+ end
+
+ end
+
+ end
+
+ describe "with both short and long switches" do
+
+ let(:option) do
+ described_class.new(["-k", "--key-file"], "FILE", "SSH identity")
+ end
+
+ it "handles both switches" do
+ option.handles?("--key-file").should be_true
+ option.handles?("-k").should be_true
+ end
+
+ describe "#help" do
+
+ it "includes both switches" do
+ option.help.should == ["-k, --key-file FILE", "SSH identity"]
+ end
+
+ end
+
+ end
+
+ describe "with an associated environment variable" do
+
+ let(:option) do
+ described_class.new("-x", "X", "mystery option", :environment_variable => "APP_X")
+ end
+
+ describe "#help" do
+
+ it "describes environment variable" do
+ option.help.should == ["-x X", "mystery option (default: $APP_X)"]
+ end
+
+ end
+
+ describe "and a default value" do
+
+ let(:option) do
+ described_class.new("-x", "X", "mystery option", :environment_variable => "APP_X", :default => "xyz")
+ end
+
+ describe "#help" do
+
+ it "describes both environment variable and default" do
+ option.help.should == ["-x X", %{mystery option (default: $APP_X, or "xyz")}]
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "multivalued" do
+
+ let(:option) do
+ described_class.new(["-H", "--header"], "HEADER", "extra header", :multivalued => true)
+ end
+
+ it "is multivalued" do
+ option.should be_multivalued
+ end
+
+ describe "#default_value" do
+
+ it "defaults to an empty Array" do
+ option.default_value.should == []
+ end
+
+ it "can be overridden" do
+ option = described_class.new("-H", "HEADER", "extra header", :multivalued => true, :default => [1,2,3])
+ option.default_value.should == [1,2,3]
+ end
+
+ end
+
+ describe "#attribute_name" do
+
+ it "gets a _list suffix" do
+ option.attribute_name.should == "header_list"
+ end
+
+ end
+
+ describe "#append_method" do
+
+ it "is derived from the attribute_name" do
+ option.append_method.should == "append_to_header_list"
+ end
+
+ end
+
+ end
+
+ describe "in subcommand" do
+
+ let(:command_class) do
+
+ Class.new(Clamp::Command) do
+ subcommand "foo", "FOO!" do
+ option "--bar", "BAR", "Bars foo."
+ end
+ end
+
+ end
+
+ describe "Command#help" do
+
+ it "includes help for each option exactly once" do
+ subcommand = command_class.send(:find_subcommand, 'foo')
+ subcommand_help = subcommand.subcommand_class.help("")
+ subcommand_help.lines.grep(/--bar BAR/).count.should == 1
+ end
+
+ end
+
+ end
+
+ describe "a required option" do
+ it "rejects :default" do
+ expect do
+ described_class.new("--key-file", "FILE", "SSH identity",
+ :required => true, :default => "hello")
+ end.to raise_error(ArgumentError)
+ end
+
+ it "rejects :flag options" do
+ expect do
+ described_class.new("--awesome", :flag, "Be awesome?", :required => true)
+ end.to raise_error(ArgumentError)
+ end
+ end
+end
diff --git a/spec/clamp/option_module_spec.rb b/spec/clamp/option_module_spec.rb
new file mode 100644
index 0000000..079947b
--- /dev/null
+++ b/spec/clamp/option_module_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Clamp::Command do
+
+ include OutputCapture
+
+ describe "with included module" do
+
+ let(:command) do
+
+ shared_options = Module.new do
+ extend Clamp::Option::Declaration
+ option "--size", "SIZE", :default => 4
+ end
+
+ command_class = Class.new(Clamp::Command) do
+
+ include shared_options
+
+ def execute
+ puts "size = #{size}"
+ end
+
+ end
+
+ command_class.new("foo")
+
+ end
+
+ it "accepts options from included module" do
+ command.run(["--size", "42"])
+ stdout.should == "size = 42\n"
+ end
+
+ end
+
+end
diff --git a/spec/clamp/parameter/definition_spec.rb b/spec/clamp/parameter/definition_spec.rb
new file mode 100644
index 0000000..f71a107
--- /dev/null
+++ b/spec/clamp/parameter/definition_spec.rb
@@ -0,0 +1,242 @@
+require 'spec_helper'
+
+describe Clamp::Parameter::Definition do
+
+ describe "normal" do
+
+ let(:parameter) do
+ described_class.new("COLOR", "hue of choice")
+ end
+
+ it "has a name" do
+ parameter.name.should == "COLOR"
+ end
+
+ it "has a description" do
+ parameter.description.should == "hue of choice"
+ end
+
+ it "is single-valued" do
+ parameter.should_not be_multivalued
+ end
+
+ describe "#attribute_name" do
+
+ it "is derived from the name" do
+ parameter.attribute_name.should == "color"
+ end
+
+ it "can be overridden" do
+ parameter = described_class.new("COLOR", "hue of choice", :attribute_name => "hue")
+ parameter.attribute_name.should == "hue"
+ end
+
+ end
+
+ describe "#consume" do
+
+ it "consumes one argument" do
+ arguments = %w(a b c)
+ parameter.consume(arguments).should == ["a"]
+ arguments.should == %w(b c)
+ end
+
+ describe "with no arguments" do
+
+ it "raises an Argument error" do
+ arguments = []
+ lambda do
+ parameter.consume(arguments)
+ end.should raise_error(ArgumentError)
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "optional (name in square brackets)" do
+
+ let(:parameter) do
+ described_class.new("[COLOR]", "hue of choice")
+ end
+
+ it "is single-valued" do
+ parameter.should_not be_multivalued
+ end
+
+ describe "#attribute_name" do
+
+ it "omits the brackets" do
+ parameter.attribute_name.should == "color"
+ end
+
+ end
+
+ describe "#consume" do
+
+ it "consumes one argument" do
+ arguments = %w(a b c)
+ parameter.consume(arguments).should == ["a"]
+ arguments.should == %w(b c)
+ end
+
+ describe "with no arguments" do
+
+ it "consumes nothing" do
+ arguments = []
+ parameter.consume(arguments).should == []
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "list (name followed by ellipsis)" do
+
+ let(:parameter) do
+ described_class.new("FILE ...", "files to process")
+ end
+
+ it "is multi-valued" do
+ parameter.should be_multivalued
+ end
+
+ describe "#attribute_name" do
+
+ it "gets a _list suffix" do
+ parameter.attribute_name.should == "file_list"
+ end
+
+ end
+
+ describe "#append_method" do
+
+ it "is derived from the attribute_name" do
+ parameter.append_method.should == "append_to_file_list"
+ end
+
+ end
+
+ describe "#consume" do
+
+ it "consumes all the remaining arguments" do
+ arguments = %w(a b c)
+ parameter.consume(arguments).should == %w(a b c)
+ arguments.should == []
+ end
+
+ describe "with no arguments" do
+
+ it "raises an Argument error" do
+ arguments = []
+ lambda do
+ parameter.consume(arguments)
+ end.should raise_error(ArgumentError)
+ end
+
+ end
+
+ end
+
+ context "with a weird parameter name, and an explicit attribute_name" do
+
+ let(:parameter) do
+ described_class.new("KEY=VALUE ...", "config-settings", :attribute_name => :config_settings)
+ end
+
+ describe "#attribute_name" do
+
+ it "is the specified one" do
+ parameter.attribute_name.should == "config_settings"
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "optional list" do
+
+ let(:parameter) do
+ described_class.new("[FILES] ...", "files to process")
+ end
+
+ it "is multi-valued" do
+ parameter.should be_multivalued
+ end
+
+ describe "#attribute_name" do
+
+ it "gets a _list suffix" do
+ parameter.attribute_name.should == "files_list"
+ end
+
+ end
+
+ describe "#default_value" do
+
+ it "is an empty list" do
+ parameter.default_value.should == []
+ end
+
+ end
+
+ describe "#help" do
+
+ it "does not include default" do
+ parameter.help_rhs.should_not include("default:")
+ end
+
+ end
+
+ describe "with specified default value" do
+
+ let(:parameter) do
+ described_class.new("[FILES] ...", "files to process", :default => %w(a b c))
+ end
+
+ describe "#default_value" do
+
+ it "is that specified" do
+ parameter.default_value.should == %w(a b c)
+ end
+
+ end
+
+ describe "#help" do
+
+ it "includes the default value" do
+ parameter.help_rhs.should include("default:")
+ end
+
+ end
+
+ describe "#consume" do
+
+ it "consumes all the remaining arguments" do
+ arguments = %w(a b c)
+ parameter.consume(arguments).should == %w(a b c)
+ arguments.should == []
+ end
+
+ describe "with no arguments" do
+
+ it "don't override defaults" do
+ arguments = []
+ parameter.consume(arguments).should == []
+ end
+
+ end
+
+ end
+
+ end
+
+ end
+
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..cc33a93
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,45 @@
+require "rspec"
+require "clamp"
+require 'stringio'
+
+RSpec.configure do |config|
+
+ config.mock_with :rr
+
+end
+
+module OutputCapture
+
+ def self.included(target)
+
+ target.before do
+ $stdout = @out = StringIO.new
+ $stderr = @err = StringIO.new
+ end
+
+ target.after do
+ $stdout = STDOUT
+ $stderr = STDERR
+ end
+
+ end
+
+ def stdout
+ @out.string
+ end
+
+ def stderr
+ @err.string
+ end
+
+end
+
+module CommandFactory
+
+ def given_command(name, &block)
+ let(:command) do
+ Class.new(Clamp::Command, &block).new(name)
+ end
+ end
+
+end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-clamp.git
More information about the Pkg-ruby-extras-commits
mailing list