[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  [![Build Status](https://secure.travis-ci.org/mdub/clamp.png?branch=master)](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