New Upstream Snapshot - ruby-clamp

Ready changes

Summary

Merged new upstream version: 1.3.2+git20220917.1.2bea305 (was: 1.1.1).

Resulting package

Built on 2022-12-30T03:59 (took 14m51s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-clamp

Lintian Result

Diff

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..b372c22
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+max_line_length = 120
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 0b36852..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-*.gem
-.bundle
-.rvmrc
-.yardoc
-doc
-pkg/*
-Gemfile.lock
- 
\ No newline at end of file
diff --git a/.rspec b/.rspec
index 4e1e0d2..023c417 100644
--- a/.rspec
+++ b/.rspec
@@ -1 +1,2 @@
 --color
+--warnings
diff --git a/.rubocop.yml b/.rubocop.yml
index aa0fe6b..80e7751 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,17 +1,42 @@
-Eval:
-  Exclude:
-    - "Rakefile"
+require:
+  - rubocop-performance
+  - rubocop-rake
+  - rubocop-rspec
+
+AllCops:
+  TargetRubyVersion: 2.5
+  NewCops: enable
+
+Layout/LineLength:
+  Max: 120
+
+Layout/EmptyLinesAroundBlockBody:
+  Enabled: false
+
+Layout/EmptyLinesAroundClassBody:
+  EnforcedStyle: empty_lines
+
+Layout/EmptyLinesAroundModuleBody:
+  Enabled: false
 
 Metrics/AbcSize:
   Enabled: false
 
-Metrics/LineLength:
-  Max: 120
+Metrics/BlockLength:
+  Exclude:
+    - "spec/**/*"
 
 Metrics/MethodLength:
   Max: 30
 
-Style/AccessorMethodName:
+Naming/AccessorMethodName:
+  Enabled: false
+
+Naming/FileName:
+  Exclude:
+    - "bin/*"
+
+Naming/PredicateName:
   Enabled: false
 
 Style/ClassAndModuleChildren:
@@ -22,35 +47,19 @@ Style/ClassAndModuleChildren:
 Style/Documentation:
   Exclude:
     - "lib/**/version.rb"
+    - "examples/*"
     - "spec/**/*"
 
-Style/EmptyLinesAroundBlockBody:
-  Enabled: false
-
-Style/EmptyLinesAroundClassBody:
-  EnforcedStyle: empty_lines
-
-Style/EmptyLinesAroundModuleBody:
-  Enabled: false
-
 Style/Encoding:
-  EnforcedStyle: when_needed
   Enabled: true
 
-Style/FileName:
-  Exclude:
-    - "bin/*"
-
-Style/HashSyntax:
-  EnforcedStyle: hash_rockets
-
 Style/Lambda:
   Enabled: false
 
 Style/NumericLiterals:
   Enabled: false
 
-Style/PredicateName:
+Style/StderrPuts:
   Enabled: false
 
 Style/StringLiterals:
@@ -58,3 +67,6 @@ Style/StringLiterals:
 
 Style/WordArray:
   Enabled: false
+
+RSpec/NestedGroups:
+  Enabled: false
diff --git a/.travis.yml b/.travis.yml
index 83aca4a..bb7cb35 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,6 @@
 language: ruby
 rvm:
-  - 2.1.8
-  - 2.2.4
-  - 2.3.1
-before_install:
-  - gem update --system
-  - gem install bundler
+  - 2.5
+  - 2.6
+  - 2.7
+  - 3.0
diff --git a/CHANGES.md b/CHANGES.md
index 828a7da..6feedb0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,29 @@
 # Changelog
 
+## 1.3.2 (2020-08-20)
+
+* Fix Ruby warnings.
+
+## 1.3.1 (2019-07-11)
+
+* Choose a sensible column width in generated help, based on content.
+* Fix issue#99: extraneous parameter names in subcommand help.
+
+## 1.3.0 (2018-06-17)
+
+* Add `.execute` DSL method.
+* Append '(required)' to the description of required options.
+* Fix issue#75: don't generate `default_XXX` method unless a default is specified.
+* Fix issue#90: allow required options to be provided after subcommands.
+
+## 1.2.0 (2018-02-12)
+
+* Add option to `Clamp.allow_options_after_parameters`.
+
+## 1.1.2 (2017-02-12)
+
+* Improve usage help for commands with both parameters and subcommands.
+
 ## 1.1.1 (2016-10-19)
 
 * Rename `.declare_attribute` back to `.define_accessors_for`.
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..23f528b
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+*	@mdub
diff --git a/Gemfile b/Gemfile
index 52237b2..2f01e4f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,15 +1,21 @@
+# frozen_string_literal: true
+
 source "http://rubygems.org"
 
 gemspec
 
 group :development do
-  gem "guard-rspec", "~> 4.6.5", :require => false
-  gem "listen", "~> 3.0.2"
-  gem "rake", "~> 10.4"
-  gem "rubocop", "~> 0.43.0", :require => false
+  gem "guard-rspec", "~> 4.7", require: false
+  gem "highline"
+  gem "listen", "~> 3.0"
+  gem "pry-byebug", "~> 3.9"
+  gem "rake", "~> 13.0"
+  gem "rubocop", "~> 1.22.3", require: false
+  gem "rubocop-performance", "~> 1.11", require: false
+  gem "rubocop-rake", "~> 0.6.0", require: false
+  gem "rubocop-rspec", "~> 2.6.0", require: false
 end
 
 group :test do
-  gem "rspec", "~> 3.5.0"
-  gem "rr", "~> 1.2.0"
+  gem "rspec", "~> 3.7"
 end
diff --git a/Guardfile b/Guardfile
index 3534307..67a4072 100644
--- a/Guardfile
+++ b/Guardfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # A sample Guardfile
 # More info at https://github.com/guard/guard#readme
 
@@ -5,7 +7,7 @@
 # directories %w(app lib config test spec features) \
 #  .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
 
-## Note: if you are using the `directories` clause above and you are not
+## NOTE: if you are using the `directories` clause above and you are not
 ## watching the project directory ('.'), then you will want to move
 ## the Guardfile to a watched dir and symlink it back, e.g.
 #
@@ -15,7 +17,7 @@
 #
 # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
 
-# Note: The cmd option is now required due to the increasing number of ways
+# NOTE: The cmd option is now required due to the increasing number of ways
 #       rspec may be run, below are examples of the most common uses.
 #  * bundler: 'bundle exec rspec'
 #  * bundler binstubs: 'bin/rspec'
@@ -24,7 +26,7 @@
 #  * zeus: 'zeus rspec' (requires the server to be started separately)
 #  * 'just' rspec: 'rspec'
 
-guard :rspec, :cmd => "bundle exec rspec" do
+guard :rspec, cmd: "bundle exec rspec" do
   require "guard/rspec/dsl"
   dsl = Guard::RSpec::Dsl.new(self)
 
diff --git a/README.md b/README.md
index 474ec81..1f7c05f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-Clamp
-=====
+# Clamp
 
 [![Gem Version](https://badge.fury.io/rb/clamp.png)](http://badge.fury.io/rb/clamp)
 [![Build Status](https://secure.travis-ci.org/mdub/clamp.png?branch=master)](http://travis-ci.org/mdub/clamp)
@@ -8,8 +7,7 @@ Clamp
 
 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!
-----------------
+## 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.)
 
@@ -18,8 +16,7 @@ Yeah, sorry.  There are a bunch of existing command-line parsing libraries out t
 [Clip]: http://clip.rubyforge.org/
 [main.rb]: https://github.com/ahoward/main
 
-Quick Start
------------
+## Quick Start
 
 A typical Clamp script looks like this:
 
@@ -29,11 +26,11 @@ require 'clamp'
 Clamp do
 
   option "--loud", :flag, "say it loud"
-  option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
+  option ["-n", "--iterations"], "N", "say it N times", default: 1 do |s|
     Integer(s)
   end
 
-  parameter "WORDS ...", "the thing to say", :attribute_name => :words
+  parameter "WORDS ...", "the thing to say", attribute_name: :words
 
   def execute
     the_truth = words.join(" ")
@@ -54,11 +51,11 @@ require 'clamp'
 class SpeakCommand < Clamp::Command
 
   option "--loud", :flag, "say it loud"
-  option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
+  option ["-n", "--iterations"], "N", "say it N times", default: 1 do |s|
     Integer(s)
   end
 
-  parameter "WORDS ...", "the thing to say", :attribute_name => :words
+  parameter "WORDS ...", "the thing to say", attribute_name: :words
 
   def execute
     the_truth = words.join(" ")
@@ -79,8 +76,7 @@ There are more examples demonstrating various features of Clamp [on Github][exam
 
 [examples]: https://github.com/mdub/clamp/tree/master/examples
 
-Declaring options
------------------
+## Declaring options
 
 Options are declared using the `option` method.  The three required arguments are:
 
@@ -105,7 +101,7 @@ 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
+option "--type", "TYPE", "type of widget", attribute_name: :widget_type
                                            # to avoid clobbering Object#type
 ```
 
@@ -140,7 +136,7 @@ Clamp will handle both "`--force`" and "`--no-force`" options, setting the value
 Although "required option" is 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
+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`.
@@ -150,7 +146,7 @@ Note that it makes no sense to mark a `:flag` option, or one with a `:default`,
 Declaring an option "`:multivalued`" allows it to be specified multiple times on the command line.
 
 ```ruby
-option "--format", "FORMAT", "output format", :multivalued => true
+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`).
@@ -160,12 +156,21 @@ The underlying attribute becomes an Array, and the suffix "`_list`" is appended
 Declaring an option "`:hidden`" will cause it to be hidden from `--help` output.
 
 ```ruby
-option "--some-option", "VALUE", "Just a little option", :hidden => true
+option "--some-option", "VALUE", "Just a little option", hidden: true
 ```
 
+### Version option
 
-Declaring parameters
---------------------
+A common idiom is to have an option `--version` that outputs the command version and doesn't run any subcommands.  This can be achieved by:
+
+```ruby
+option "--version", :flag, "Show version" do
+  puts MyGem::VERSION
+  exit(0)
+end
+```
+
+## Declaring parameters
 
 Positional parameters can be declared using `parameter`, specifying
 
@@ -193,13 +198,12 @@ parameter "[TARGET_DIR]", "target directory"
 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
+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
-------------------------------------------------
+## 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.
 
@@ -255,15 +259,15 @@ end
 Default values can be specified for options, and optional parameters:
 
 ```ruby
-option "--flavour", "FLAVOUR", "ice-cream flavour", :default => "chocolate"
+option "--flavour", "FLAVOUR", "ice-cream flavour", default: "chocolate"
 
-parameter "[HOST]", "server host", :default => "localhost"
+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 "--http-port", "PORT", "web-server port", default:  9000
 
 option "--admin-port", "PORT", "admin port"
 
@@ -277,17 +281,30 @@ end
 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|
+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"
+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
----------------------
+### Allowing options after parameters
+
+By default, Clamp only recognises options _before_ positional parameters.
+
+Some other option-parsing libraries - notably [GNU `getopt(3)`](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) - allow option and parameter arguments to appear in any order on the command-line, e.g.
+
+    foobar --foo=bar something --fnord=snuffle another-thing
+
+If you want Clamp to allow options and parameters to be "interspersed" in this way, set:
+
+```ruby
+Clamp.allow_options_after_parameters = true
+```
+
+## 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.
 
@@ -378,8 +395,7 @@ Clamp do
 end
 ```
 
-Getting help
-------------
+## 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`.
 
@@ -397,27 +413,27 @@ Options:
     -h, --help                    print help
 ```
 
-Localization
-------------
+## Localization
 
 Clamp comes with support for overriding strings with custom translations. You can use localization library of your choice and override the strings at startup.
 
 Example usage:
+
 ```ruby
 require 'gettext'
 
 Clamp.messages = {
-  :too_many_arguments =>       _("too many arguments"),
-  :option_required =>          _("option '%<option>s' is required"),
-  :option_or_env_required =>   _("option '%<option>s' (or env %<env>s) is required"),
-  :option_argument_error =>    _("option '%<switch>s': %<message>s")
+  too_many_arguments:        _("too many arguments"),
+  option_required:           _("option '%<option>s' is required"),
+  option_or_env_required:    _("option '%<option>s' (or env %<env>s) is required"),
+  option_argument_error:     _("option '%<switch>s': %<message>s")
   # ...
 }
 ```
+
 See [messages.rb](https://github.com/mdub/clamp/blob/master/lib/clamp/messages.rb) for full list of available messages.
 
-License
--------
+## License
 
 Copyright (C) 2011 [Mike Williams](mailto:mdub@dogbiscuit.org)
 
@@ -438,7 +454,6 @@ 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
----------------------
+## Contributing to Clamp
 
 Source-code for Clamp is [on Github](https://github.com/mdub/clamp).
diff --git a/Rakefile b/Rakefile
index 079fbc0..fec582a 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,12 +1,18 @@
+# frozen_string_literal: true
+
 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", "documentation"]
 end
+
+require "rubocop/rake_task"
+
+RuboCop::RakeTask.new
+
+task "default" => ["spec", "rubocop"]
diff --git a/clamp.gemspec b/clamp.gemspec
index 2a4e677..bb59525 100644
--- a/clamp.gemspec
+++ b/clamp.gemspec
@@ -1,4 +1,6 @@
-$LOAD_PATH.push File.expand_path("../lib", __FILE__)
+# frozen_string_literal: true
+
+$LOAD_PATH.push File.expand_path("lib", __dir__)
 require "clamp/version"
 
 Gem::Specification.new do |s|
@@ -8,18 +10,19 @@ Gem::Specification.new do |s|
   s.platform      = Gem::Platform::RUBY
   s.authors       = ["Mike Williams"]
   s.email         = "mdub@dogbiscuit.org"
-  s.homepage      = "http://github.com/mdub/clamp"
+  s.homepage      = "https://github.com/mdub/clamp"
 
   s.license       = "MIT"
 
   s.summary       = "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.description   = <<-TEXT.gsub(/^\s+/, "")
+    Clamp provides an object-model for command-line utilities.
+    It handles parsing of command-line options, and generation of usage help.
+  TEXT
 
   s.files         = `git ls-files`.split("\n")
   s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
   s.require_paths = ["lib"]
 
+  s.required_ruby_version = ">= 2.5", "< 4"
 end
diff --git a/debian/changelog b/debian/changelog
index 17b517a..26c6d24 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-ruby-clamp (1.1.1-2) UNRELEASED; urgency=medium
+ruby-clamp (1.3.2+git20220917.1.2bea305-1) UNRELEASED; urgency=medium
 
   [ Utkarsh Gupta ]
   * Add salsa-ci.yml
@@ -15,8 +15,9 @@ ruby-clamp (1.1.1-2) UNRELEASED; urgency=medium
   * Use canonical URL in Vcs-Git.
   * Apply multi-arch hints.
     + ruby-clamp: Add :any qualifier for ruby dependency.
+  * New upstream snapshot.
 
- -- Utkarsh Gupta <guptautkarsh2102@gmail.com>  Tue, 13 Aug 2019 03:45:14 +0530
+ -- Utkarsh Gupta <guptautkarsh2102@gmail.com>  Fri, 30 Dec 2022 03:47:05 -0000
 
 ruby-clamp (1.1.1-1) unstable; urgency=low
 
diff --git a/examples/admin b/examples/admin
index 2adea32..4237d0a 100755
--- a/examples/admin
+++ b/examples/admin
@@ -1,17 +1,18 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
-# An example of subcommands
+# An example of options and parameters
 
 require "clamp"
 
 Clamp do
 
-  option "--timeout", "SECONDS", "connection timeout", :default => 5, :environment_variable => "MYAPP_TIMEOUT" do |x|
+  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"
+  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)"
diff --git a/examples/defaulted b/examples/defaulted
index 7c66bcb..938fd0e 100755
--- a/examples/defaulted
+++ b/examples/defaulted
@@ -1,4 +1,5 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
 # An example of default values and methods
 
@@ -8,10 +9,10 @@ require "highline"
 Clamp do
 
   option ["-U", "--user"], "USER", "user name",
-         :environment_variable => "THE_USER",
-         :default => "bob"
+         environment_variable: "THE_USER",
+         default: "bob"
   option ["-P", "--password"], "PASSWORD", "password",
-         :environment_variable => "THE_PASSWORD"
+         environment_variable: "THE_PASSWORD"
 
   def execute
     puts "User: #{user}, Password: #{password}"
diff --git a/examples/flipflop b/examples/flipflop
index e9c72a5..109ca92 100755
--- a/examples/flipflop
+++ b/examples/flipflop
@@ -1,6 +1,7 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
-# An example of subcommands
+# An example of subcommands (including a default subcommand)
 
 require "clamp"
 require "clamp/version"
diff --git a/examples/fubar b/examples/fubar
index 681d1c4..953bf43 100755
--- a/examples/fubar
+++ b/examples/fubar
@@ -1,6 +1,7 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
-# An example of subcommands
+# An example of nested subcommands
 
 require "clamp"
 
diff --git a/examples/gitdown b/examples/gitdown
index d8d2d54..2530b2c 100755
--- a/examples/gitdown
+++ b/examples/gitdown
@@ -1,4 +1,5 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
 # Demonstrate how subcommands can be declared as classes
 
@@ -25,7 +26,7 @@ module GitDown
   class CloneCommand < AbstractCommand
 
     parameter "REPOSITORY", "repository to clone"
-    parameter "[DIR]", "working directory", :default => "."
+    parameter "[DIR]", "working directory", default: "."
 
     def execute
       say "cloning to #{dir}"
diff --git a/examples/scoop b/examples/scoop
old mode 100644
new mode 100755
index 4861587..6505c55
--- a/examples/scoop
+++ b/examples/scoop
@@ -1,4 +1,5 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
 # An example of multi-valued options
 
@@ -7,8 +8,8 @@ require "clamp"
 Clamp do
 
   option ["-f", "--flavour"], "FLAVOUR", "flavour",
-         :multivalued => true, :default => ["chocolate"],
-         :attribute_name => :flavours
+         multivalued: true, default: ["chocolate"],
+         attribute_name: :flavours
 
   def execute
     puts "one #{flavours.join(' and ')} ice-cream"
diff --git a/examples/speak b/examples/speak
index aa8d7b3..9890207 100755
--- a/examples/speak
+++ b/examples/speak
@@ -1,4 +1,5 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
 # A simple Clamp command, with options and parameters
 
@@ -11,11 +12,11 @@ Clamp do
   )
 
   option "--loud", :flag, "say it loud"
-  option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
+  option ["-n", "--iterations"], "N", "say it N times", default: 1 do |s|
     Integer(s)
   end
 
-  parameter "WORDS ...", "the thing to say", :attribute_name => :words
+  parameter "WORDS ...", "the thing to say", attribute_name: :words
 
   def execute
     the_truth = words.join(" ")
diff --git a/examples/subcommand_missing b/examples/subcommand_missing
index 25d4264..aef78a6 100755
--- a/examples/subcommand_missing
+++ b/examples/subcommand_missing
@@ -1,4 +1,7 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
+
+# Demonstrate subcommand_missing
 
 require "clamp"
 
diff --git a/examples/word b/examples/word
new file mode 100755
index 0000000..ea60d6a
--- /dev/null
+++ b/examples/word
@@ -0,0 +1,28 @@
+#! /usr/bin/env ruby
+# frozen_string_literal: true
+
+# Demonstrate "restful" style; parameters precede subcommands
+
+require "clamp"
+
+Clamp do
+
+  banner %(
+    Word ops.
+  )
+
+  parameter "WORD", "the word in question"
+
+  subcommand "say", "Say it" do
+    def execute
+      puts word
+    end
+  end
+
+  subcommand "shout", "Say it loud" do
+    def execute
+      puts word.upcase
+    end
+  end
+
+end
diff --git a/lib/clamp.rb b/lib/clamp.rb
index ef6d3b1..c35fd6a 100644
--- a/lib/clamp.rb
+++ b/lib/clamp.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
 require "clamp/version"
 
 require "clamp/command"
 
-def Clamp(&block)
+def Clamp(&block) # rubocop:disable Naming/MethodName
   Class.new(Clamp::Command, &block).run
 end
diff --git a/lib/clamp/attribute/declaration.rb b/lib/clamp/attribute/declaration.rb
index c146c9d..ce0ab0d 100644
--- a/lib/clamp/attribute/declaration.rb
+++ b/lib/clamp/attribute/declaration.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
 module Clamp
   module Attribute
 
+    # Methods to generate attribute accessors.
+    #
     module Declaration
 
       protected
@@ -25,6 +29,8 @@ module Clamp
       end
 
       def define_default_for(attribute)
+        return false if attribute.default_value.nil?
+
         define_method(attribute.default_method) do
           attribute.default_value
         end
diff --git a/lib/clamp/attribute/definition.rb b/lib/clamp/attribute/definition.rb
index de90048..b56d435 100644
--- a/lib/clamp/attribute/definition.rb
+++ b/lib/clamp/attribute/definition.rb
@@ -1,25 +1,28 @@
+# frozen_string_literal: true
+
 require "clamp/attribute/instance"
 
 module Clamp
   module Attribute
 
+    # Represents an attribute of a Clamp::Command class.
+    #
     class Definition
 
       def initialize(options)
-        if options.key?(:attribute_name)
-          @attribute_name = options[:attribute_name].to_s
-        end
+        @attribute_name = options[:attribute_name].to_s if options.key?(:attribute_name)
         @default_value = options[:default] if options.key?(:default)
-        if options.key?(:environment_variable)
-          @environment_variable = options[:environment_variable]
-        end
+        @environment_variable = options[:environment_variable] if options.key?(:environment_variable)
         @hidden = options[:hidden] if options.key?(:hidden)
       end
 
       attr_reader :description, :environment_variable
 
       def help_rhs
-        description + default_description
+        rhs = description
+        comments = [required_indicator, default_description].compact
+        rhs += " (#{comments.join(', ')})" unless comments.empty?
+        rhs
       end
 
       def help
@@ -51,11 +54,11 @@ module Clamp
       end
 
       def required?
-        @required
+        @required ||= false
       end
 
       def hidden?
-        @hidden
+        @hidden ||= false
       end
 
       def attribute_name
@@ -74,6 +77,17 @@ module Clamp
         Attribute::Instance.new(self, command)
       end
 
+      def option_missing_message
+        if environment_variable
+          Clamp.message(:option_or_env_required,
+                        option: switches.first,
+                        env: environment_variable)
+        else
+          Clamp.message(:option_required,
+                        option: switches.first)
+        end
+      end
+
       private
 
       def default_description
@@ -81,8 +95,10 @@ module Clamp
           ("$#{@environment_variable}" if defined?(@environment_variable)),
           (@default_value.inspect if defined?(@default_value))
         ].compact
-        return "" if default_sources.empty?
-        " (default: " + default_sources.join(", or ") + ")"
+
+        return nil if default_sources.empty?
+
+        "#{Clamp.message(:default)}: " + default_sources.join(", #{Clamp.message(:or)} ")
       end
 
     end
diff --git a/lib/clamp/attribute/instance.rb b/lib/clamp/attribute/instance.rb
index 7669d16..f831f0b 100644
--- a/lib/clamp/attribute/instance.rb
+++ b/lib/clamp/attribute/instance.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
 module Clamp
   module Attribute
 
-    # Represents an option/parameter of a Clamp::Command instance.
+    # Represents an attribute of a Clamp::Command instance.
     #
     class Instance
 
@@ -27,7 +29,7 @@ module Clamp
       end
 
       def default
-        command.send(attribute.default_method)
+        command.send(attribute.default_method) if command.respond_to?(attribute.default_method, true)
       end
 
       # default implementation of read_method
@@ -60,19 +62,40 @@ module Clamp
         end
       end
 
+      def signal_usage_error(*args)
+        command.send(:signal_usage_error, *args)
+      end
+
       def default_from_environment
         return if self.defined?
         return if attribute.environment_variable.nil?
         return unless ENV.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, Clamp.message(:env_argument_error, :env => attribute.environment_variable, :message => e.message))
+          signal_usage_error Clamp.message(:env_argument_error, env: attribute.environment_variable, message: e.message)
         end
       end
 
+      def unset?
+        if attribute.multivalued?
+          read.empty?
+        else
+          read.nil?
+        end
+      end
+
+      def missing?
+        attribute.required? && unset?
+      end
+
+      def verify_not_missing
+        signal_usage_error attribute.option_missing_message if missing?
+      end
+
     end
 
   end
diff --git a/lib/clamp/command.rb b/lib/clamp/command.rb
index 7c558fb..ff5d68e 100644
--- a/lib/clamp/command.rb
+++ b/lib/clamp/command.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require "clamp/messages"
 require "clamp/errors"
 require "clamp/help"
@@ -48,6 +50,7 @@ module Clamp
       parse_options
       parse_parameters
       parse_subcommand
+      verify_required_options_are_set
       handle_remaining_arguments
     end
 
@@ -81,7 +84,7 @@ module Clamp
     #
     # @ param [String] name subcommand_name
     def subcommand_missing(name)
-      signal_usage_error(Clamp.message(:no_such_subcommand, :name => name))
+      signal_usage_error(Clamp.message(:no_such_subcommand, name: name))
     end
 
     include Clamp::Option::Parsing
@@ -122,6 +125,11 @@ module Clamp
       include Clamp::Subcommand::Declaration
       include Help
 
+      # An alternative to "def execute"
+      def execute(&block)
+        define_method(:execute, &block)
+      end
+
       # Create an instance of this command class, and run it.
       #
       # @param [String] invocation_path the path used to invoke the command
diff --git a/lib/clamp/errors.rb b/lib/clamp/errors.rb
index 9c5ccbb..8b3d257 100644
--- a/lib/clamp/errors.rb
+++ b/lib/clamp/errors.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
 module Clamp
 
+  # raised to indicate invalid option/parameter declaration
   class DeclarationError < StandardError
   end
 
+  # abstract command runtime error
   class RuntimeError < StandardError
 
     def initialize(message, command)
@@ -14,10 +18,10 @@ module Clamp
 
   end
 
-  # raise to signal incorrect command usage
+  # raised to signal incorrect command usage
   class UsageError < RuntimeError; end
 
-  # raise to request usage help
+  # raised to request usage help
   class HelpWanted < RuntimeError
 
     def initialize(command)
@@ -26,7 +30,7 @@ module Clamp
 
   end
 
-  # raise to signal error during execution
+  # raised to signal error during execution
   class ExecutionError < RuntimeError
 
     def initialize(message, command, status = 1)
diff --git a/lib/clamp/help.rb b/lib/clamp/help.rb
index 775d4b1..b729435 100644
--- a/lib/clamp/help.rb
+++ b/lib/clamp/help.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
 require "stringio"
 require "clamp/messages"
 
 module Clamp
 
+  # Command help generation.
+  #
   module Help
 
     def usage(usage)
@@ -10,7 +14,7 @@ module Clamp
       @declared_usage_descriptions << usage
     end
 
-    attr_reader :declared_usage_descriptions
+    attr_reader :declared_usage_descriptions, :description
 
     def description=(description)
       @description = description.dup
@@ -25,8 +29,6 @@ module Clamp
       self.description = description
     end
 
-    attr_reader :description
-
     def derived_usage_description
       parts = ["[OPTIONS]"]
       parts += parameters.map(&:name)
@@ -41,47 +43,67 @@ module Clamp
       help = builder
       help.add_usage(invocation_path, usage_descriptions)
       help.add_description(description)
-      if has_parameters?
-        help.add_list(Clamp.message(:parameters_heading), parameters)
-      end
-      if has_subcommands?
-        help.add_list(Clamp.message(:subcommands_heading), recognised_subcommands)
-      end
+      help.add_list(Clamp.message(:parameters_heading), parameters) if has_parameters?
+      help.add_list(Clamp.message(:subcommands_heading), recognised_subcommands) if has_subcommands?
       help.add_list(Clamp.message(:options_heading), recognised_options)
       help.string
     end
 
+    # A builder for auto-generated help.
+    #
     class Builder
 
       def initialize
-        @out = StringIO.new
+        @lines = []
       end
 
       def string
-        @out.string
+        left_column_width = lines.grep(Array).map(&:first).map(&:size).max
+        StringIO.new.tap do |out|
+          lines.each do |line|
+            case line
+            when Array
+              line[0] = line[0].ljust(left_column_width)
+              line.unshift("")
+              out.puts(line.join("    "))
+            else
+              out.puts(line)
+            end
+          end
+        end.string
+      end
+
+      def line(text = "")
+        @lines << text
+      end
+
+      def row(lhs, rhs)
+        @lines << [lhs, rhs]
       end
 
       def add_usage(invocation_path, usage_descriptions)
-        puts Clamp.message(:usage_heading) + ":"
+        line "#{Clamp.message(:usage_heading)}:"
         usage_descriptions.each do |usage|
-          puts "    #{invocation_path} #{usage}".rstrip
+          line "    #{invocation_path} #{usage}".rstrip
         end
       end
 
       def add_description(description)
         return unless description
-        puts ""
-        puts description.gsub(/^/, "  ")
+
+        line
+        line description.gsub(/^/, "  ")
       end
 
-      DETAIL_FORMAT = "    %-29s %s".freeze
+      DETAIL_FORMAT = "    %-29s %s"
 
       def add_list(heading, items)
-        puts "\n#{heading}:"
+        line
+        line "#{heading}:"
         items.reject { |i| i.respond_to?(:hidden?) && i.hidden? }.each do |item|
           label, description = item.help
           description.each_line do |line|
-            puts format(DETAIL_FORMAT, label, line)
+            row(label, line)
             label = ""
           end
         end
@@ -89,9 +111,7 @@ module Clamp
 
       private
 
-      def puts(*args)
-        @out.puts(*args)
-      end
+      attr_accessor :lines
 
     end
 
diff --git a/lib/clamp/messages.rb b/lib/clamp/messages.rb
index 74be72d..9641907 100644
--- a/lib/clamp/messages.rb
+++ b/lib/clamp/messages.rb
@@ -1,5 +1,9 @@
-module Clamp
+# frozen_string_literal: true
 
+module Clamp # :nodoc:
+
+  # Message lookup, to allow localization.
+  #
   module Messages
 
     def messages=(new_messages)
@@ -7,7 +11,10 @@ module Clamp
     end
 
     def message(key, options = {})
-      format(messages.fetch(key), options)
+      string = messages.fetch(key)
+      return string if options.empty?
+
+      format string, options
     end
 
     def clear_messages!
@@ -17,19 +24,22 @@ module Clamp
     private
 
     DEFAULTS = {
-      :too_many_arguments => "too many arguments",
-      :option_required => "option '%<option>s' is required",
-      :option_or_env_required => "option '%<option>s' (or env %<env>s) is required",
-      :option_argument_error => "option '%<switch>s': %<message>s",
-      :parameter_argument_error => "parameter '%<param>s': %<message>s",
-      :env_argument_error => "$%<env>s: %<message>s",
-      :unrecognised_option => "Unrecognised option '%<switch>s'",
-      :no_such_subcommand => "No such sub-command '%<name>s'",
-      :no_value_provided => "no value provided",
-      :usage_heading => "Usage",
-      :parameters_heading => "Parameters",
-      :subcommands_heading => "Subcommands",
-      :options_heading => "Options"
+      too_many_arguments: "too many arguments",
+      option_required: "option '%<option>s' is required",
+      option_or_env_required: "option '%<option>s' (or env %<env>s) is required",
+      option_argument_error: "option '%<switch>s': %<message>s",
+      parameter_argument_error: "parameter '%<param>s': %<message>s",
+      env_argument_error: "$%<env>s: %<message>s",
+      unrecognised_option: "Unrecognised option '%<switch>s'",
+      no_such_subcommand: "No such sub-command '%<name>s'",
+      no_value_provided: "no value provided",
+      default: "default",
+      or: "or",
+      required: "required",
+      usage_heading: "Usage",
+      parameters_heading: "Parameters",
+      subcommands_heading: "Subcommands",
+      options_heading: "Options"
     }.freeze
 
     def messages
diff --git a/lib/clamp/option/declaration.rb b/lib/clamp/option/declaration.rb
index 3b3833a..180aece 100644
--- a/lib/clamp/option/declaration.rb
+++ b/lib/clamp/option/declaration.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
 require "clamp/attribute/declaration"
 require "clamp/option/definition"
 
 module Clamp
   module Option
 
+    # Option declaration methods.
+    #
     module Declaration
 
       include Clamp::Attribute::Declaration
@@ -25,7 +29,7 @@ module Clamp
       end
 
       def recognised_options
-        unless @implicit_options_declared
+        unless @implicit_options_declared ||= false
           declare_implicit_help_option
           @implicit_options_declared = true
         end
@@ -36,6 +40,7 @@ module Clamp
 
       def declare_implicit_help_option
         return false if 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
@@ -51,6 +56,7 @@ module Clamp
 
       def options_declared_on(ancestor)
         return [] unless ancestor.is_a?(Clamp::Option::Declaration)
+
         ancestor.declared_options
       end
 
diff --git a/lib/clamp/option/definition.rb b/lib/clamp/option/definition.rb
index 75c3fda..1c2a749 100644
--- a/lib/clamp/option/definition.rb
+++ b/lib/clamp/option/definition.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
 require "clamp/attribute/definition"
 require "clamp/truthy"
 
 module Clamp
   module Option
 
+    # Represents an option of a Clamp::Command class.
+    #
     class Definition < Attribute::Definition
 
       def initialize(switches, type, description, options = {})
@@ -12,7 +16,9 @@ module Clamp
         @description = description
         super(options)
         @multivalued = options[:multivalued]
+
         return unless options.key?(:required)
+
         @required = options[:required]
         # Do some light validation for conflicting settings.
         raise ArgumentError, "Specifying a :default value with :required doesn't make sense" if options.key?(:default)
@@ -39,7 +45,7 @@ module Clamp
 
       def read_method
         if flag?
-          super + "?"
+          "#{super}?"
         else
           super
         end
@@ -59,7 +65,7 @@ module Clamp
 
       def help_lhs
         lhs = switches.join(", ")
-        lhs += " " + type unless flag?
+        lhs += " #{type}" unless flag?
         lhs
       end
 
@@ -76,14 +82,17 @@ module Clamp
       end
 
       def infer_attribute_name
-        unless long_switch
-          raise Clamp::DeclarationError, "You must specify either a long-switch or an :attribute_value"
-        end
+        raise Clamp::DeclarationError, "You must specify either a long-switch or an :attribute_value" unless long_switch
+
         inferred_name = long_switch.sub(/^--(\[no-\])?/, "").tr("-", "_")
         inferred_name += "_list" if multivalued?
         inferred_name
       end
 
+      def required_indicator
+        Clamp.message(:required) if required?
+      end
+
     end
 
   end
diff --git a/lib/clamp/option/parsing.rb b/lib/clamp/option/parsing.rb
index b854824..3b21fb0 100644
--- a/lib/clamp/option/parsing.rb
+++ b/lib/clamp/option/parsing.rb
@@ -1,6 +1,17 @@
-module Clamp
+# frozen_string_literal: true
+
+module Clamp # :nodoc:
+
+  class << self
+
+    attr_accessor :allow_options_after_parameters
+
+  end
+
   module Option
 
+    # Option parsing methods.
+    #
     module Parsing
 
       protected
@@ -8,40 +19,56 @@ module Clamp
       def parse_options
         set_options_from_command_line
         default_options_from_environment
-        verify_required_options_are_set
       end
 
       private
 
       def set_options_from_command_line
-        while remaining_arguments.first && remaining_arguments.first.start_with?("-")
+        argument_buffer = []
+        argument_buffer_limit = self.class.parameter_buffer_limit
+        until remaining_arguments.empty?
+          unless remaining_arguments.first.start_with?("-")
+            break unless argument_buffer.size < argument_buffer_limit
+
+            argument_buffer << remaining_arguments.shift
+
+            next
+          end
 
           switch = remaining_arguments.shift
+
           break if switch == "--"
 
-          case switch
-          when /\A(-\w)(.+)\z/m # combined short options
-            switch = Regexp.last_match(1)
-            if find_option(switch).flag?
-              remaining_arguments.unshift("-" + Regexp.last_match(2))
-            else
-              remaining_arguments.unshift(Regexp.last_match(2))
-            end
-          when /\A(--[^=]+)=(.*)\z/m
-            switch = Regexp.last_match(1)
-            remaining_arguments.unshift(Regexp.last_match(2))
-          end
+          handle_switch(switch)
+        end
+        remaining_arguments.unshift(*argument_buffer)
+      end
 
-          option = find_option(switch)
-          value = option.extract_value(switch, remaining_arguments)
+      def handle_switch(switch)
+        switch = split_trailing_switches(switch)
+        option = find_option(switch)
+        value = option.extract_value(switch, remaining_arguments)
+        begin
+          option.of(self).take(value)
+        rescue ArgumentError => e
+          signal_usage_error Clamp.message(:option_argument_error, switch: switch, message: e.message)
+        end
+      end
 
-          begin
-            option.of(self).take(value)
-          rescue ArgumentError => e
-            signal_usage_error Clamp.message(:option_argument_error, :switch => switch, :message => e.message)
+      def split_trailing_switches(switch)
+        case switch
+        when /\A(-\w)(.+)\z/m # combined short options
+          switch = Regexp.last_match(1)
+          if find_option(switch).flag?
+            remaining_arguments.unshift("-#{Regexp.last_match(2)}")
+          else
+            remaining_arguments.unshift(Regexp.last_match(2))
           end
-
+        when /\A(--[^=]+)=(.*)\z/m
+          switch = Regexp.last_match(1)
+          remaining_arguments.unshift(Regexp.last_match(2))
         end
+        switch
       end
 
       def default_options_from_environment
@@ -52,26 +79,17 @@ module Clamp
 
       def verify_required_options_are_set
         self.class.recognised_options.each do |option|
-          # If this option is required and the value is nil, there's an error.
-          next unless option.required? && send(option.attribute_name).nil?
-          if option.environment_variable
-            message = Clamp.message(:option_or_env_required,
-                                    :option => option.switches.first,
-                                    :env => option.environment_variable)
-          else
-            message = Clamp.message(:option_required,
-                                    :option => option.switches.first)
-          end
-          signal_usage_error message
+          option.of(self).verify_not_missing
         end
       end
 
       def find_option(switch)
         self.class.find_option(switch) ||
-          signal_usage_error(Clamp.message(:unrecognised_option, :switch => switch))
+          signal_usage_error(Clamp.message(:unrecognised_option, switch: switch))
       end
 
     end
 
   end
+
 end
diff --git a/lib/clamp/parameter/declaration.rb b/lib/clamp/parameter/declaration.rb
index 3ad8ede..9c95881 100644
--- a/lib/clamp/parameter/declaration.rb
+++ b/lib/clamp/parameter/declaration.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
 require "clamp/attribute/declaration"
 require "clamp/parameter/definition"
 
 module Clamp
   module Parameter
 
+    # Parameter declaration methods.
+    #
     module Declaration
 
       include Clamp::Attribute::Declaration
@@ -23,16 +27,23 @@ module Clamp
         end
       end
 
-      protected
-
       def inheritable_parameters
         superclass_inheritable_parameters + parameters.select(&:inheritable?)
       end
 
+      def parameter_buffer_limit
+        return 0 unless Clamp.allow_options_after_parameters
+
+        return Float::INFINITY if inheritable_parameters.any?(&:multivalued?)
+
+        inheritable_parameters.size
+      end
+
       private
 
       def superclass_inheritable_parameters
         return [] unless superclass.respond_to?(:inheritable_parameters, true)
+
         superclass.inheritable_parameters
       end
 
diff --git a/lib/clamp/parameter/definition.rb b/lib/clamp/parameter/definition.rb
index e48b41d..16837f0 100644
--- a/lib/clamp/parameter/definition.rb
+++ b/lib/clamp/parameter/definition.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
 require "clamp/attribute/definition"
 
 module Clamp
   module Parameter
 
+    # Represents an parameter of a Clamp::Command class.
+    #
     class Definition < Attribute::Definition
 
       def initialize(name, description, options = {})
@@ -28,25 +32,30 @@ module Clamp
 
       def consume(arguments)
         raise ArgumentError, Clamp.message(:no_value_provided) if required? && arguments.empty?
+
         arguments.shift(multivalued? ? arguments.length : 1)
       end
 
       private
 
-      ELLIPSIS_SUFFIX = / \.\.\.$/
-      OPTIONAL = /^\[(.*)\]/
+      ELLIPSIS_SUFFIX = / \.\.\.$/.freeze
+      OPTIONAL = /^\[(.*)\]/.freeze
 
-      VALID_ATTRIBUTE_NAME = /^[a-z0-9_]+$/
+      VALID_ATTRIBUTE_NAME = /^[a-z0-9_]+$/.freeze
 
       def infer_attribute_name
         inferred_name = name.downcase.tr("-", "_").sub(ELLIPSIS_SUFFIX, "").sub(OPTIONAL) { Regexp.last_match(1) }
-        unless inferred_name =~ VALID_ATTRIBUTE_NAME
-          raise "cannot infer attribute_name from #{name.inspect}"
-        end
+
+        raise "cannot infer attribute_name from #{name.inspect}" unless inferred_name.match? VALID_ATTRIBUTE_NAME
+
         inferred_name += "_list" if multivalued?
         inferred_name
       end
 
+      def required_indicator
+        # implied by LHS
+      end
+
     end
 
   end
diff --git a/lib/clamp/parameter/parsing.rb b/lib/clamp/parameter/parsing.rb
index e36540f..842745b 100644
--- a/lib/clamp/parameter/parsing.rb
+++ b/lib/clamp/parameter/parsing.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
 module Clamp
   module Parameter
 
+    # Parameter parsing methods.
+    #
     module Parsing
 
       protected
@@ -14,13 +18,11 @@ module Clamp
 
       def set_parameters_from_command_line
         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 Clamp.message(:parameter_argument_error, :param => parameter.name, :message => e.message)
+          parameter.consume(remaining_arguments).each do |value|
+            parameter.of(self).take(value)
           end
+        rescue ArgumentError => e
+          signal_usage_error Clamp.message(:parameter_argument_error, param: parameter.name, message: e.message)
         end
       end
 
diff --git a/lib/clamp/subcommand/declaration.rb b/lib/clamp/subcommand/declaration.rb
index 01f6306..2965840 100644
--- a/lib/clamp/subcommand/declaration.rb
+++ b/lib/clamp/subcommand/declaration.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
 require "clamp/errors"
 require "clamp/subcommand/definition"
 
 module Clamp
   module Subcommand
 
+    # Subcommand declaration methods.
+    #
     module Declaration
 
       def recognised_subcommands
@@ -27,8 +31,9 @@ module Clamp
       def find_subcommand_class(*names)
         names.inject(self) do |command_class, name|
           return nil unless command_class
+
           subcommand = command_class.find_subcommand(name)
-          subcommand.subcommand_class if subcommand
+          subcommand&.subcommand_class
         end
       end
 
@@ -37,18 +42,17 @@ module Clamp
       end
 
       def default_subcommand=(name)
-        if has_subcommands?
-          raise Clamp::DeclarationError, "default_subcommand must be defined before subcommands"
-        end
+        raise Clamp::DeclarationError, "default_subcommand must be defined before subcommands" if has_subcommands?
+
         @default_subcommand = name
       end
 
       def default_subcommand(*args, &block)
         if args.empty?
-          @default_subcommand
+          @default_subcommand ||= false
         else
           $stderr.puts "WARNING: Clamp default_subcommand syntax has changed; check the README."
-          $stderr.puts "  (from #{caller.first})"
+          $stderr.puts "  (from #{caller(1..1).first})"
           self.default_subcommand = args.first
           subcommand(*args, &block)
         end
@@ -57,21 +61,21 @@ module Clamp
       private
 
       def declare_subcommand_parameters
-        if @default_subcommand
+        if default_subcommand
           parameter "[SUBCOMMAND]", "subcommand",
-                    :attribute_name => :subcommand_name,
-                    :default => @default_subcommand,
-                    :inheritable => false
+                    attribute_name: :subcommand_name,
+                    default: default_subcommand,
+                    inheritable: false
         else
           parameter "SUBCOMMAND", "subcommand",
-                    :attribute_name => :subcommand_name,
-                    :required => false,
-                    :inheritable => false
+                    attribute_name: :subcommand_name,
+                    required: false,
+                    inheritable: false
         end
-        remove_method :default_subcommand_name
+        remove_method :default_subcommand_name if method_defined?(:default_subcommand_name)
         parameter "[ARG] ...", "subcommand arguments",
-                  :attribute_name => :subcommand_arguments,
-                  :inheritable => false
+                  attribute_name: :subcommand_arguments,
+                  inheritable: false
       end
 
     end
diff --git a/lib/clamp/subcommand/definition.rb b/lib/clamp/subcommand/definition.rb
index 2aa1415..30c3ee6 100644
--- a/lib/clamp/subcommand/definition.rb
+++ b/lib/clamp/subcommand/definition.rb
@@ -1,16 +1,15 @@
+# frozen_string_literal: true
+
 module Clamp
   module Subcommand
 
-    Definition = Struct.new(:name, :description, :subcommand_class) do
+    Definition = Struct.new(:names, :description, :subcommand_class) do
 
       def initialize(names, description, subcommand_class)
-        @names = Array(names)
-        @description = description
-        @subcommand_class = subcommand_class
+        names = Array(names)
+        super
       end
 
-      attr_reader :names, :description, :subcommand_class
-
       def is_called?(name)
         names.member?(name)
       end
diff --git a/lib/clamp/subcommand/execution.rb b/lib/clamp/subcommand/execution.rb
index 10541e5..6308787 100644
--- a/lib/clamp/subcommand/execution.rb
+++ b/lib/clamp/subcommand/execution.rb
@@ -1,6 +1,13 @@
+# frozen_string_literal: true
+
 module Clamp
   module Subcommand
 
+    # Support for subcommand execution.
+    #
+    # This module is mixed into command instances that have subcommands, overriding
+    # default behaviour in {Clamp::Command}.
+    #
     module Execution
 
       # override default Command behaviour
@@ -15,20 +22,31 @@ module Clamp
 
       def instantiate_subcommand(name)
         subcommand_class = find_subcommand_class(name)
-        subcommand = subcommand_class.new("#{invocation_path} #{name}", context)
+        subcommand = subcommand_class.new(invocation_path_for(name), context)
         self.class.inheritable_attributes.each do |attribute|
           next unless attribute.of(self).defined?
+
           attribute.of(subcommand).set(attribute.of(self).get)
         end
         subcommand
       end
 
+      def invocation_path_for(name)
+        param_names = self.class.parameters.select(&:inheritable?).map(&:name)
+        [invocation_path, *param_names, name].join(" ")
+      end
+
       def find_subcommand_class(name)
         subcommand_def = self.class.find_subcommand(name)
         return subcommand_def.subcommand_class if subcommand_def
+
         subcommand_missing(name)
       end
 
+      def verify_required_options_are_set
+        # not required
+      end
+
     end
 
   end
diff --git a/lib/clamp/subcommand/parsing.rb b/lib/clamp/subcommand/parsing.rb
index 4415956..fca5670 100644
--- a/lib/clamp/subcommand/parsing.rb
+++ b/lib/clamp/subcommand/parsing.rb
@@ -1,14 +1,19 @@
+# frozen_string_literal: true
+
 require "clamp/subcommand/execution"
 
 module Clamp
   module Subcommand
 
+    # Subcommand parsing methods.
+    #
     module Parsing
 
       protected
 
       def parse_subcommand
         return false unless self.class.has_subcommands?
+
         extend(Subcommand::Execution)
       end
 
diff --git a/lib/clamp/truthy.rb b/lib/clamp/truthy.rb
index ac6b07c..23671ac 100644
--- a/lib/clamp/truthy.rb
+++ b/lib/clamp/truthy.rb
@@ -1,6 +1,8 @@
-module Clamp
+# frozen_string_literal: true
 
-  TRUTHY_VALUES = %w(1 yes enable on true).freeze
+module Clamp # :nodoc:
+
+  TRUTHY_VALUES = %w[1 yes enable on true].freeze
 
   def self.truthy?(arg)
     TRUTHY_VALUES.include?(arg.to_s.downcase)
diff --git a/lib/clamp/version.rb b/lib/clamp/version.rb
index a2c7f18..4ac3ae1 100644
--- a/lib/clamp/version.rb
+++ b/lib/clamp/version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Clamp
-  VERSION = "1.1.1".freeze
+  VERSION = "1.3.2"
 end
diff --git a/spec/clamp/command_group_spec.rb b/spec/clamp/command_group_spec.rb
index 73f6d16..00ca3c5 100644
--- a/spec/clamp/command_group_spec.rb
+++ b/spec/clamp/command_group_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require "spec_helper"
 
 describe Clamp::Command do
@@ -27,17 +29,27 @@ describe Clamp::Command do
 
     end
 
-    it "delegates to sub-commands" do
+    describe "flip command" do
+      before do
+        command.run(["flip"])
+      end
 
-      command.run(["flip"])
-      expect(stdout).to match(/FLIPPED/)
+      it "delegates to sub-commands" do
+        expect(stdout).to match(/FLIPPED/)
+      end
+    end
 
-      command.run(["flop"])
-      expect(stdout).to match(/FLOPPED/)
+    describe "flop command" do
+      before do
+        command.run(["flop"])
+      end
 
+      it "delegates to sub-commands" do
+        expect(stdout).to match(/FLOPPED/)
+      end
     end
 
-    context "executed with no subcommand" do
+    context "when executed with no subcommand" do
 
       it "triggers help" do
         expect do
@@ -54,10 +66,7 @@ describe Clamp::Command do
       end
 
       it "lists subcommands" do
-        help = command.help
-        expect(help).to match(/Subcommands:/)
-        expect(help).to match(/flip +flip it/)
-        expect(help).to match(/flop +flop it/)
+        expect(command.help).to match(/Subcommands:\n +flip +flip it\n +flop +flop it/)
       end
 
       it "handles new lines in subcommand descriptions" do
@@ -93,13 +102,27 @@ describe Clamp::Command do
 
     end
 
-    it "responds to both aliases" do
+    describe "the first alias" do
+
+      before do
+        command.run(["say", "boo"])
+      end
+
+      it "responds to it" do
+        expect(stdout).to match(/boo/)
+      end
+
+    end
+
+    describe "the second alias" do
 
-      command.run(["say", "boo"])
-      expect(stdout).to match(/boo/)
+      before do
+        command.run(["talk", "jive"])
+      end
 
-      command.run(["talk", "jive"])
-      expect(stdout).to match(/jive/)
+      it "responds to it" do
+        expect(stdout).to match(/jive/)
+      end
 
     end
 
@@ -122,8 +145,7 @@ describe Clamp::Command do
 
         subcommand "bar", "Baaaa!" do
 
-          def self.this_is_bar
-          end
+          def self.this_is_bar; end
 
           def execute
             puts "FUBAR"
@@ -166,7 +188,7 @@ describe Clamp::Command do
 
     end
 
-    context "executed with no subcommand" do
+    context "when executed with no subcommand" do
 
       it "invokes the default subcommand" do
         command.run([])
@@ -191,7 +213,7 @@ describe Clamp::Command do
 
     end
 
-    context "executed with no subcommand" do
+    context "when executed with no subcommand" do
 
       it "invokes the default subcommand" do
         command.run([])
@@ -202,24 +224,26 @@ describe Clamp::Command do
 
   end
 
-  context "declaring a default subcommand after subcommands" do
+  context "when declaring a default subcommand after subcommands" do
 
-    it "is not supported" do
+    let(:command) do
+      Class.new(Clamp::Command) do
 
-      expect do
-        Class.new(Clamp::Command) do
+        subcommand "status", "Show status" do
 
-          subcommand "status", "Show status" do
+          def execute
+            puts "All good!"
+          end
 
-            def execute
-              puts "All good!"
-            end
+        end
 
-          end
+      end
+    end
 
-          self.default_subcommand = "status"
+    it "is not supported" do
 
-        end
+      expect do
+        command.default_subcommand = "status"
       end.to raise_error(/default_subcommand must be defined before subcommands/)
 
     end
@@ -250,12 +274,18 @@ describe Clamp::Command do
 
     it "allows the parameter to be specified first" do
       command.run(["dummy", "spit"])
-      expect(stdout.strip).to eql "spat the dummy"
+      expect(stdout.strip).to eq "spat the dummy"
     end
 
     it "passes the parameter down the stack" do
       command.run(["money", "say", "loud"])
-      expect(stdout.strip).to eql "MONEY"
+      expect(stdout.strip).to eq "MONEY"
+    end
+
+    it "shows parameter in usage help" do
+      command.run(["stuff", "say", "loud", "--help"])
+    rescue Clamp::HelpWanted => e
+      expect(e.command.invocation_path).to eq "with THING say loud"
     end
 
   end
@@ -266,12 +296,12 @@ describe Clamp::Command do
 
       speed_options = Module.new do
         extend Clamp::Option::Declaration
-        option "--speed", "SPEED", "how fast", :default => "slowly"
+        option "--speed", "SPEED", "how fast", default: "slowly"
       end
 
       Class.new(Clamp::Command) do
 
-        option "--direction", "DIR", "which way", :default => "home"
+        option "--direction", "DIR", "which way", default: "home"
 
         include speed_options
 
@@ -307,7 +337,7 @@ describe Clamp::Command do
     end
 
     it "has access to command context" do
-      command = command_class.new("go", :motion => "wandering")
+      command = command_class.new("go", motion: "wandering")
       command.run(["move"])
       expect(stdout).to match(/wandering home/)
     end
@@ -323,14 +353,13 @@ describe Clamp::Command do
       end
 
       subcommand "woohoohoo", "like weeheehee but with more o" do
-        def execute
-        end
+        def execute; end
       end
     end
 
     it "only parses options once" do
       command.run(["--json", '{"a":"b"}', "woohoohoo"])
-      expect(stdout).to eql "parsing!"
+      expect(stdout).to eq "parsing!"
     end
 
   end
@@ -362,8 +391,7 @@ describe Clamp::Command do
           end
         end
 
-        def execute
-        end
+        def execute; end
       end
     end
 
@@ -371,23 +399,39 @@ describe Clamp::Command do
       command_class.new("foo")
     end
 
-    it "should signal no such subcommand usage error" do
-      expect { command.run(["foo"]) }.to raise_error(Clamp::UsageError) do |exception|
-        expect(exception.message).to eq "No such sub-command 'foo'"
-      end
+    it "signals no such subcommand usage error" do
+      expect { command.run(["foo"]) }.to raise_error(Clamp::UsageError, "No such sub-command 'foo'")
     end
 
-    it "should execute the subcommand missing method" do
+    it "executes the subcommand missing method" do
       command.extend subcommand_missing
-      expect { command.run(["foo"]) }.to raise_error(SystemExit)
-      expect(stderr).to match(/there is no such thing/)
+      expect { command.run(["foo"]) }.to raise_error(SystemExit, /there is no such thing/)
     end
 
-    it "should use the subcommand class returned from subcommand_missing" do
+    it "uses the subcommand class returned from subcommand_missing" do
       command.extend subcommand_missing_with_return
       command.run(["foo"])
       expect(stdout).to match(/known subcommand/)
     end
+
+  end
+
+  context "with a subcommand and required options" do
+
+    given_command "movements" do
+      option "--direction", "N|S|E|W", "bearing", required: true
+      subcommand "hop", "Hop" do
+        def execute
+          puts "Hopping #{direction}"
+        end
+      end
+    end
+
+    it "allows options after the subcommand" do
+      command.run(%w[hop --direction south])
+      expect(stdout).to eq "Hopping south\n"
+    end
+
   end
 
 end
diff --git a/spec/clamp/option_module_spec.rb b/spec/clamp/command_option_module_spec.rb
similarity index 82%
rename from spec/clamp/option_module_spec.rb
rename to spec/clamp/command_option_module_spec.rb
index e932ad3..9614e37 100644
--- a/spec/clamp/option_module_spec.rb
+++ b/spec/clamp/command_option_module_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require "spec_helper"
 
 describe Clamp::Command do
@@ -10,7 +12,7 @@ describe Clamp::Command do
 
       shared_options = Module.new do
         extend Clamp::Option::Declaration
-        option "--size", "SIZE", :default => 4
+        option "--size", "SIZE", default: 4
       end
 
       command_class = Class.new(Clamp::Command) do
@@ -29,7 +31,7 @@ describe Clamp::Command do
 
     it "accepts options from included module" do
       command.run(["--size", "42"])
-      expect(stdout).to eql "size = 42\n"
+      expect(stdout).to eq "size = 42\n"
     end
 
   end
diff --git a/spec/clamp/command_option_reordering_spec.rb b/spec/clamp/command_option_reordering_spec.rb
new file mode 100644
index 0000000..8ce8559
--- /dev/null
+++ b/spec/clamp/command_option_reordering_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe Clamp::Command do
+
+  extend CommandFactory
+  include OutputCapture
+
+  context "with allow_options_after_parameters enabled" do
+
+    before do
+      Clamp.allow_options_after_parameters = true
+    end
+
+    after do
+      Clamp.allow_options_after_parameters = false
+    end
+
+    given_command("cmd") do
+
+      option ["-v", "--verbose"], :flag, "Be noisy"
+
+      subcommand "say", "Say something" do
+
+        option "--loud", :flag, "say it loud"
+
+        parameter "WORDS ...", "the thing to say", attribute_name: :words
+
+        def execute
+          message = words.join(" ")
+          message = message.upcase if loud?
+          message *= 3 if verbose?
+          $stdout.puts message
+        end
+
+      end
+
+    end
+
+    it "still works" do
+      command.run(%w[say foo])
+      expect(stdout).to eq "foo\n"
+    end
+
+    it "honours options after positional arguments" do
+      command.run(%w[say blah --verbose])
+      expect(stdout).to eq "blahblahblah\n"
+    end
+
+    it "honours options declared on subcommands" do
+      command.run(%w[say --loud blah])
+      expect(stdout).to eq "BLAH\n"
+    end
+
+  end
+
+end
diff --git a/spec/clamp/command_spec.rb b/spec/clamp/command_spec.rb
index 602aa29..5dd4c94 100644
--- a/spec/clamp/command_spec.rb
+++ b/spec/clamp/command_spec.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
 
 require "spec_helper"
 
@@ -30,18 +31,35 @@ describe Clamp::Command do
     end
 
     it "executes the #execute method" do
-      expect(stdout).to_not be_empty
+      expect(stdout).not_to be_empty
     end
 
   end
 
   describe ".option" do
 
-    it "declares option argument accessors" do
-      command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
-      expect(command.flavour).to eql nil
-      command.flavour = "chocolate"
-      expect(command.flavour).to eql "chocolate"
+    context "when regular" do
+
+      before do
+        command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
+      end
+
+      context "when not set" do
+        it "equals nil" do
+          expect(command.flavour).to be_nil
+        end
+      end
+
+      context "when set" do
+        before do
+          command.flavour = "chocolate"
+        end
+
+        it "equals the value" do
+          expect(command.flavour).to eq "chocolate"
+        end
+      end
+
     end
 
     context "with type :flag" do
@@ -50,9 +68,20 @@ describe Clamp::Command do
         command.class.option "--verbose", :flag, "Be heartier"
       end
 
-      it "declares a predicate-style reader" do
-        expect(command).to respond_to(:verbose?)
-        expect(command).to_not respond_to(:verbose)
+      describe "predicate-style reader" do
+
+        it "exists" do
+          expect(command).to respond_to(:verbose?)
+        end
+
+      end
+
+      describe "regular reader" do
+
+        it "does not exist" do
+          expect(command).not_to respond_to(:verbose)
+        end
+
       end
 
     end
@@ -60,17 +89,24 @@ describe Clamp::Command do
     context "with explicit :attribute_name" do
 
       before do
-        command.class.option "--foo", "FOO", "A foo", :attribute_name => :bar
+        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"
-        expect(command.bar).to eql "chocolate"
+        expect(command.bar).to eq "chocolate"
+      end
+
+      describe "default reader" do
+        it "does not exist" do
+          expect(command).not_to respond_to(:foo)
+        end
       end
 
-      it "does not attempt to create the default accessors" do
-        expect(command).to_not respond_to(:foo)
-        expect(command).to_not respond_to(:foo=)
+      describe "default writer" do
+        it "does not exist" do
+          expect(command).not_to respond_to(:foo=)
+        end
       end
 
     end
@@ -87,7 +123,7 @@ describe Clamp::Command do
       end
 
       it "sets the specified default value" do
-        expect(command.port).to eql 4321
+        expect(command.port).to eq 4321
       end
 
     end
@@ -95,11 +131,11 @@ describe Clamp::Command do
     context "with :default value" do
 
       before do
-        command.class.option "--port", "PORT", "port to listen on", :default => 4321
+        command.class.option "--port", "PORT", "port to listen on", default: 4321
       end
 
       it "declares default method" do
-        expect(command.default_port).to eql 4321
+        expect(command.default_port).to eq 4321
       end
 
       describe "#help" do
@@ -112,31 +148,49 @@ describe Clamp::Command do
 
     end
 
+    context "without :default value" do
+
+      before do
+        command.class.option "--port", "PORT", "port to listen on"
+      end
+
+      it "does not declare default method" do
+        expect(command).not_to respond_to(:default_port)
+      end
+
+    end
+
     context "with :multivalued" do
 
       before do
-        command.class.option "--flavour", "FLAVOUR", "flavour(s)", :multivalued => true, :attribute_name => :flavours
+        command.class.option "--flavour", "FLAVOUR", "flavour(s)", multivalued: true, attribute_name: :flavours
       end
 
       it "defaults to empty array" do
-        expect(command.flavours).to eql []
+        expect(command.flavours).to be_empty
       end
 
       it "supports multiple values" do
-        command.parse(%w(--flavour chocolate --flavour vanilla))
-        expect(command.flavours).to eql %w(chocolate vanilla)
+        command.parse(%w[--flavour chocolate --flavour vanilla])
+        expect(command.flavours).to eq %w[chocolate vanilla]
       end
 
       it "generates a single-value appender method" do
         command.append_to_flavours("mud")
         command.append_to_flavours("pie")
-        expect(command.flavours).to eql %w(mud pie)
+        expect(command.flavours).to eq %w[mud pie]
       end
 
       it "generates a multi-value setter method" do
         command.append_to_flavours("replaceme")
-        command.flavours = %w(mud pie)
-        expect(command.flavours).to eql %w(mud pie)
+        command.flavours = %w[mud pie]
+        expect(command.flavours).to eq %w[mud pie]
+      end
+
+      it "does not require a value" do
+        expect do
+          command.parse([])
+        end.not_to raise_error
       end
 
     end
@@ -148,8 +202,8 @@ describe Clamp::Command do
 
       before do
         command.class.option "--port", "PORT", "port to listen on",
-                             :default => 4321,
-                             :environment_variable => "PORT",
+                             default: 4321,
+                             environment_variable: "PORT",
                              &:to_i
         set_env("PORT", environment_value)
         command.parse(args)
@@ -158,7 +212,7 @@ describe Clamp::Command do
       context "when no environment variable is present" do
 
         it "uses the default" do
-          expect(command.port).to eql 4321
+          expect(command.port).to eq 4321
         end
 
       end
@@ -168,15 +222,15 @@ describe Clamp::Command do
         let(:environment_value) { "12345" }
 
         it "uses the environment variable" do
-          expect(command.port).to eql 12_345
+          expect(command.port).to eq 12_345
         end
 
-        context "and a value is specified on the command-line" do
+        context "when a value is specified on the command-line" do
 
-          let(:args) { %w(--port 1500) }
+          let(:args) { %w[--port 1500] }
 
           it "uses command-line value" do
-            expect(command.port).to eql 1500
+            expect(command.port).to eq 1500
           end
 
         end
@@ -198,7 +252,7 @@ describe Clamp::Command do
       let(:environment_value) { nil }
 
       before do
-        command.class.option "--[no-]enable", :flag, "enable?", :default => false, :environment_variable => "ENABLE"
+        command.class.option "--[no-]enable", :flag, "enable?", default: false, environment_variable: "ENABLE"
         set_env("ENABLE", environment_value)
         command.parse([])
       end
@@ -206,33 +260,33 @@ describe Clamp::Command do
       context "when no environment variable is present" do
 
         it "uses the default" do
-          expect(command.enable?).to eql false
+          expect(command.enable?).to be false
         end
 
       end
 
-      %w(1 yes enable on true).each do |truthy_value|
+      %w[1 yes enable on true].each do |truthy_value|
 
         context "when environment variable is #{truthy_value.inspect}" do
 
           let(:environment_value) { truthy_value }
 
           it "sets the flag" do
-            expect(command.enable?).to eql true
+            expect(command.enable?).to be true
           end
 
         end
 
       end
 
-      %w(0 no disable off false).each do |falsey_value|
+      %w[0 no disable off false].each do |falsey_value|
 
         context "when environment variable is #{falsey_value.inspect}" do
 
           let(:environment_value) { falsey_value }
 
           it "clears the flag" do
-            expect(command.enable?).to eql false
+            expect(command.enable?).to be false
           end
 
         end
@@ -244,7 +298,43 @@ describe Clamp::Command do
     context "with :required" do
 
       before do
-        command.class.option "--port", "PORT", "port to listen on", :required => true
+        command.class.option "--port", "PORT", "port to listen on", required: true
+      end
+
+      describe "#help" do
+
+        it "marks it as required" do
+          expect(command.help).to include("port to listen on (required)")
+        end
+
+      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
+
+    context "with :required and :multivalued" do
+
+      before do
+        command.class.option "--port", "PORT", "port to listen on", required: true, multivalued: true
       end
 
       context "when no value is provided" do
@@ -277,12 +367,23 @@ describe Clamp::Command do
         end
       end
 
-      it "uses the block to validate and convert the option argument" do
-        expect do
-          command.port = "blah"
-        end.to raise_error(ArgumentError)
-        command.port = "1234"
-        expect(command.port).to eql 1234
+      context "when value is incorrect" do
+
+        it "raises an error" do
+          expect do
+            command.port = "blah"
+          end.to raise_error(ArgumentError)
+        end
+
+      end
+
+      context "when value is convertible" do
+
+        it "uses the block to validate and convert the option argument" do
+          command.port = "1234"
+          expect(command.port).to eq 1234
+        end
+
       end
 
     end
@@ -295,12 +396,12 @@ describe Clamp::Command 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|
+                           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
+      command.class.parameter "[ARG] ...", "extra arguments", attribute_name: :arguments
     end
 
     describe "#parse" do
@@ -309,7 +410,7 @@ describe Clamp::Command do
 
         it "raises a UsageError" do
           expect do
-            command.parse(%w(--foo bar))
+            command.parse(%w[--foo bar])
           end.to raise_error(Clamp::UsageError)
         end
 
@@ -318,13 +419,25 @@ describe Clamp::Command do
       context "with options" do
 
         before do
-          command.parse(%w(--flavour strawberry --nuts --color blue))
+          command.parse(%w[--flavour strawberry --nuts --color blue])
         end
 
-        it "maps the option values onto the command object" do
-          expect(command.flavour).to eql "strawberry"
-          expect(command.color).to eql "blue"
-          expect(command.nuts?).to eql true
+        describe "flavour" do
+          it "maps the option value onto the command object" do
+            expect(command.flavour).to eq "strawberry"
+          end
+        end
+
+        describe "color" do
+          it "maps the option value onto the command object" do
+            expect(command.color).to eq "blue"
+          end
+        end
+
+        describe "nuts?" do
+          it "maps the option value onto the command object" do
+            expect(command.nuts?).to be true
+          end
         end
 
       end
@@ -332,12 +445,19 @@ describe Clamp::Command do
       context "with short options" do
 
         before do
-          command.parse(%w(-f strawberry -c blue))
+          command.parse(%w[-f strawberry -c blue])
         end
 
-        it "recognises short options as aliases" do
-          expect(command.flavour).to eql "strawberry"
-          expect(command.color).to eql "blue"
+        describe "flavour" do
+          it "recognizes short options as aliases" do
+            expect(command.flavour).to eq "strawberry"
+          end
+        end
+
+        describe "color" do
+          it "recognizes short options as aliases" do
+            expect(command.color).to eq "blue"
+          end
         end
 
       end
@@ -345,11 +465,11 @@ describe Clamp::Command do
       context "with a value appended to a short option" do
 
         before do
-          command.parse(%w(-fstrawberry))
+          command.parse(%w[-fstrawberry])
         end
 
         it "works as though the value were separated" do
-          expect(command.flavour).to eql "strawberry"
+          expect(command.flavour).to eq "strawberry"
         end
 
       end
@@ -357,12 +477,19 @@ describe Clamp::Command do
       context "with combined short options" do
 
         before do
-          command.parse(%w(-nf strawberry))
+          command.parse(%w[-nf strawberry])
         end
 
-        it "works as though the options were separate" do
-          expect(command.flavour).to eql "strawberry"
-          expect(command.nuts?).to eql true
+        describe "flavour" do
+          it "works as though the options were separate" do
+            expect(command.flavour).to eq "strawberry"
+          end
+        end
+
+        describe "nuts?" do
+          it "works as though the options were separate" do
+            expect(command.nuts?).to be true
+          end
         end
 
       end
@@ -370,12 +497,39 @@ describe Clamp::Command do
       context "with option arguments attached using equals sign" do
 
         before do
-          command.parse(%w(--flavour=strawberry --color=blue))
+          command.parse(%w[--flavour=strawberry --color=blue])
         end
 
-        it "works as though the option arguments were separate" do
-          expect(command.flavour).to eql "strawberry"
-          expect(command.color).to eql "blue"
+        describe "flavour" do
+          it "maps the option value onto the command object" do
+            expect(command.flavour).to eq "strawberry"
+          end
+        end
+
+        describe "color" do
+          it "maps the option value onto the command object" do
+            expect(command.color).to eq "blue"
+          end
+        end
+
+      end
+
+      context "with option arguments that look like options" do
+
+        before do
+          command.parse(%w[--flavour=-dashing- --scoops -1])
+        end
+
+        describe "flavour" do
+          it "sets the options" do
+            expect(command.flavour).to eq("-dashing-")
+          end
+        end
+
+        describe "scoops" do
+          it "sets the options" do
+            expect(command.scoops).to eq(-1)
+          end
         end
 
       end
@@ -383,8 +537,8 @@ describe Clamp::Command do
       context "with option-like things beyond the arguments" do
 
         it "treats them as positional arguments" do
-          command.parse(%w(a b c --flavour strawberry))
-          expect(command.arguments).to eql %w(a b c --flavour strawberry)
+          command.parse(%w[a b c --flavour strawberry])
+          expect(command.arguments).to eq %w[a b c --flavour strawberry]
         end
 
       end
@@ -395,10 +549,22 @@ describe Clamp::Command do
           command.parse(["foo\n--flavour=strawberry", "bar\n-cblue"])
         end
 
-        it "treats them as positional arguments" do
-          expect(command.arguments).to eql ["foo\n--flavour=strawberry", "bar\n-cblue"]
-          expect(command.flavour).to be_nil
-          expect(command.color).to be_nil
+        describe "arguments" do
+          it "treats them as positional arguments" do
+            expect(command.arguments).to eq ["foo\n--flavour=strawberry", "bar\n-cblue"]
+          end
+        end
+
+        describe "flavour" do
+          it "treats them as positional arguments" do
+            expect(command.flavour).to be_nil
+          end
+        end
+
+        describe "color" do
+          it "treats them as positional arguments" do
+            expect(command.color).to be_nil
+          end
         end
 
       end
@@ -406,8 +572,8 @@ describe Clamp::Command do
       context "with an option terminator" do
 
         it "considers everything after the terminator to be an argument" do
-          command.parse(%w(--color blue -- --flavour strawberry))
-          expect(command.arguments).to eql %w(--flavour strawberry)
+          command.parse(%w[--color blue -- --flavour strawberry])
+          expect(command.arguments).to eq %w[--flavour strawberry]
         end
 
       end
@@ -415,7 +581,7 @@ describe Clamp::Command do
       context "with --flag" do
 
         before do
-          command.parse(%w(--nuts))
+          command.parse(%w[--nuts])
         end
 
         it "sets the flag" do
@@ -428,7 +594,7 @@ describe Clamp::Command do
 
         before do
           command.nuts = true
-          command.parse(%w(--no-nuts))
+          command.parse(%w[--no-nuts])
         end
 
         it "clears the flag" do
@@ -441,7 +607,7 @@ describe Clamp::Command do
 
         it "requests help" do
           expect do
-            command.parse(%w(--help))
+            command.parse(%w[--help])
           end.to raise_error(Clamp::HelpWanted)
         end
 
@@ -451,7 +617,7 @@ describe Clamp::Command do
 
         it "requests help" do
           expect do
-            command.parse(%w(-h))
+            command.parse(%w[-h])
           end.to raise_error(Clamp::HelpWanted)
         end
 
@@ -461,7 +627,7 @@ describe Clamp::Command do
 
         it "signals a UsageError" do
           expect do
-            command.parse(%w(--scoops reginald))
+            command.parse(%w[--scoops reginald])
           end.to raise_error(Clamp::UsageError, /^option '--scoops': invalid value for Integer/)
         end
 
@@ -487,8 +653,10 @@ describe Clamp::Command do
       end
 
       it "includes option details" do
-        expect(command.help).to match(/--flavour FLAVOUR +Flavour of the month/)
-        expect(command.help).to match(/--color COLOR +Preferred hue/)
+        flavour_help = "-f, --flavour FLAVOUR +Flavour of the month"
+        color_help = "-c, --color COLOR +Preferred hue"
+
+        expect(command.help).to match(/#{flavour_help}\n +#{color_help}/)
       end
 
       it "handles new lines in option descriptions" do
@@ -505,16 +673,27 @@ describe Clamp::Command do
       command.class.option ["--help"], :flag, "help wanted"
     end
 
-    it "does not generate implicit help option" do
-      expect do
-        command.parse(%w(--help))
-      end.to_not raise_error
-      expect(command.help?).to be true
+    describe "parsing" do
+      it "does not generate implicit help option" do
+        expect do
+          command.parse(%w[--help])
+        end.not_to raise_error
+      end
+    end
+
+    describe "help?" do
+      before do
+        command.parse(%w[--help])
+      end
+
+      it "generates custom help option" do
+        expect(command.help?).to be true
+      end
     end
 
     it "does not recognise -h" do
       expect do
-        command.parse(%w(-h))
+        command.parse(%w[-h])
       end.to raise_error(Clamp::UsageError)
     end
 
@@ -529,12 +708,12 @@ describe Clamp::Command do
     end
 
     it "does not map -h to help" do
-      expect(command.help).to_not match(/-h[, ].*help/)
+      expect(command.help).not_to match(/-h[, ].*help/)
     end
 
-    it "still recognises --help" do
+    it "still recognizes --help" do
       expect do
-        command.parse(%w(--help))
+        command.parse(%w[--help])
       end.to raise_error(Clamp::HelpWanted)
     end
 
@@ -542,22 +721,39 @@ describe Clamp::Command do
 
   describe ".parameter" do
 
-    it "declares option argument accessors" do
-      command.class.parameter "FLAVOUR", "flavour of the month"
-      expect(command.flavour).to eql nil
-      command.flavour = "chocolate"
-      expect(command.flavour).to eql "chocolate"
+    context "when regular" do
+
+      before do
+        command.class.parameter "FLAVOUR", "flavour of the month"
+      end
+
+      context "when not set" do
+        it "equals nil" do
+          expect(command.flavour).to be_nil
+        end
+      end
+
+      context "when set" do
+        before do
+          command.flavour = "chocolate"
+        end
+
+        it "equals the value" do
+          expect(command.flavour).to eq "chocolate"
+        end
+      end
+
     end
 
     context "with explicit :attribute_name" do
 
       before do
-        command.class.parameter "FOO", "a foo", :attribute_name => :bar
+        command.class.parameter "FOO", "a foo", attribute_name: :bar
       end
 
       it "uses the specified attribute_name name to name accessors" do
         command.bar = "chocolate"
-        expect(command.bar).to eql "chocolate"
+        expect(command.bar).to eq "chocolate"
       end
 
     end
@@ -565,11 +761,11 @@ describe Clamp::Command do
     context "with :default value" do
 
       before do
-        command.class.parameter "[ORIENTATION]", "direction", :default => "west"
+        command.class.parameter "[ORIENTATION]", "direction", default: "west"
       end
 
       it "sets the specified default value" do
-        expect(command.orientation).to eql "west"
+        expect(command.orientation).to eq "west"
       end
 
       describe "#help" do
@@ -590,12 +786,23 @@ describe Clamp::Command do
         end
       end
 
-      it "uses the block to validate and convert the argument" do
-        expect do
-          command.port = "blah"
-        end.to raise_error(ArgumentError)
-        command.port = "1234"
-        expect(command.port).to eql 1234
+      context "when value is incorrect" do
+
+        it "raises an error" do
+          expect do
+            command.port = "blah"
+          end.to raise_error(ArgumentError)
+        end
+
+      end
+
+      context "when value is convertible" do
+
+        it "uses the block to validate and convert the argument" do
+          command.port = "1234"
+          expect(command.port).to eq 1234
+        end
+
       end
 
     end
@@ -607,51 +814,67 @@ describe Clamp::Command do
       end
 
       it "accepts multiple arguments" do
-        command.parse(%w(X Y Z))
-        expect(command.file_list).to eql %w(X Y Z)
+        command.parse(%w[X Y Z])
+        expect(command.file_list).to eq %w[X Y Z]
       end
 
     end
 
-    context "optional, with ellipsis" do
+    context "when optional, with ellipsis" do
 
       before do
         command.class.parameter "[FILE] ...", "files"
-      end
 
-      it "defaults to an empty list" do
         command.parse([])
-        expect(command.default_file_list).to eql []
-        expect(command.file_list).to eql []
       end
 
-      it "is mutable" do
-        command.parse([])
-        command.file_list << "treasure"
-        expect(command.file_list).to eql ["treasure"]
+      describe "default_file_list" do
+
+        it "defaults to an empty list" do
+          expect(command.default_file_list).to be_empty
+        end
+
       end
 
-    end
+      describe "file_list" do
 
-    context "with :environment_variable" do
+        it "defaults to an empty list" do
+          expect(command.file_list).to be_empty
+        end
 
-      before do
-        command.class.parameter "[FILE]", "a file", :environment_variable => "FILE",
-                                                    :default => "/dev/null"
       end
 
-      let(:args) { [] }
-      let(:environment_value) { nil }
+      describe "mutation" do
+
+        before do
+          command.file_list << "treasure"
+        end
+
+        it "is mutable" do
+          expect(command.file_list).to eq ["treasure"]
+        end
+
+      end
+
+    end
+
+    context "with :environment_variable" do
 
       before do
+        command.class.parameter "[FILE]", "a file", environment_variable: "FILE",
+                                                    default: "/dev/null"
+
         set_env("FILE", environment_value)
         command.parse(args)
       end
 
+      let(:args) { [] }
+      let(:environment_value) { nil }
+
       context "when neither argument nor environment variable are present" do
 
         it "uses the default" do
-          expect(command.file).to eql "/dev/null"
+          expect(command.file).to eq "/dev/null"
         end
 
       end
@@ -663,7 +886,7 @@ describe Clamp::Command do
         describe "and no argument is provided" do
 
           it "uses the environment variable" do
-            expect(command.file).to eql "/etc/motd"
+            expect(command.file).to eq "/etc/motd"
           end
 
         end
@@ -673,7 +896,7 @@ describe Clamp::Command do
           let(:args) { ["/dev/null"] }
 
           it "uses the argument" do
-            expect(command.file).to eql "/dev/null"
+            expect(command.file).to eq "/dev/null"
           end
 
         end
@@ -715,7 +938,7 @@ describe Clamp::Command do
     before do
       command.class.parameter "X", "x\nxx"
       command.class.parameter "Y", "y"
-      command.class.parameter "[Z]", "z", :default => "ZZZ"
+      command.class.parameter "[Z]", "z", default: "ZZZ"
     end
 
     describe "#parse" do
@@ -726,10 +949,22 @@ describe Clamp::Command do
           command.parse(["crash", "bang", "wallop"])
         end
 
-        it "maps arguments onto the command object" do
-          expect(command.x).to eql "crash"
-          expect(command.y).to eql "bang"
-          expect(command.z).to eql "wallop"
+        describe "x" do
+          it "maps arguments onto the command object" do
+            expect(command.x).to eq "crash"
+          end
+        end
+
+        describe "y" do
+          it "maps arguments onto the command object" do
+            expect(command.y).to eq "bang"
+          end
+        end
+
+        describe "z" do
+          it "maps arguments onto the command object" do
+            expect(command.z).to eq "wallop"
+          end
         end
 
       end
@@ -746,22 +981,52 @@ describe Clamp::Command do
 
       context "with optional argument omitted" do
 
-        it "defaults the optional argument" do
+        before do
           command.parse(["crash", "bang"])
-          expect(command.x).to eql "crash"
-          expect(command.y).to eql "bang"
-          expect(command.z).to eql "ZZZ"
+        end
+
+        describe "x" do
+          it "defaults the optional argument" do
+            expect(command.x).to eq "crash"
+          end
+        end
+
+        describe "y" do
+          it "defaults the optional argument" do
+            expect(command.y).to eq "bang"
+          end
+        end
+
+        describe "z" do
+          it "defaults the optional argument" do
+            expect(command.z).to eq "ZZZ"
+          end
         end
 
       end
 
       context "with multi-line arguments" do
 
-        it "parses them correctly" do
+        before do
           command.parse(["foo\nhi", "bar", "baz"])
-          expect(command.x).to eql "foo\nhi"
-          expect(command.y).to eql "bar"
-          expect(command.z).to eql "baz"
+        end
+
+        describe "x" do
+          it "parses them correctly" do
+            expect(command.x).to eq "foo\nhi"
+          end
+        end
+
+        describe "y" do
+          it "parses them correctly" do
+            expect(command.y).to eq "bar"
+          end
+        end
+
+        describe "z" do
+          it "parses them correctly" do
+            expect(command.z).to eq "baz"
+          end
         end
 
       end
@@ -785,9 +1050,7 @@ describe Clamp::Command do
       end
 
       it "includes parameter details" do
-        expect(command.help).to match(/X +x/)
-        expect(command.help).to match(/Y +y/)
-        expect(command.help).to match(/\[Z\] +z \(default: "ZZZ"\)/)
+        expect(command.help).to match(/X +x\n[\s\S]* +Y +y\n[\s\S]* +\[Z\] +z \(default: "ZZZ"\)/)
       end
 
       it "handles new lines in option descriptions" do
@@ -798,6 +1061,26 @@ describe Clamp::Command do
 
   end
 
+  describe ".execute" do
+
+    before do
+
+      command_class.class_eval do
+        execute do
+          puts "using execute DSL"
+        end
+      end
+
+      command.run([])
+
+    end
+
+    it "provides an alternative way to declare execute method" do
+      expect(stdout).to eq("using execute DSL\n")
+    end
+
+  end
+
   context "with explicit usage" do
 
     given_command("blah") do
@@ -828,8 +1111,7 @@ describe Clamp::Command do
     describe "#help" do
 
       it "includes both potential usages" do
-        expect(command.help).to include("put THIS HERE\n")
-        expect(command.help).to include("put THAT THERE\n")
+        expect(command.help).to match(/put THIS HERE\n +put THAT THERE/)
       end
 
     end
@@ -840,20 +1122,19 @@ describe Clamp::Command do
 
     given_command("punt") do
 
-      banner <<-EOF
+      banner <<-TEXT
         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
+      TEXT
 
     end
 
     describe "#help" do
 
       it "includes the banner" do
-        expect(command.help).to match(/^  Punt is an example command/)
-        expect(command.help).to match(/^  The prefix/)
+        expect(command.help).to match(/^  Punt is an example command.+\n^  \n^  The prefix/)
       end
 
     end
@@ -862,28 +1143,42 @@ describe Clamp::Command do
 
   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
+    context "when regular" do
+
+      before do
+
+        command.class.class_eval do
+          parameter "WORD ...", "words"
+          def execute
+            print word_list.inspect
+          end
         end
+
+      end
+
+      it "creates a new Command instance and runs it" do
+        xyz = %w[x y z]
+        command.class.run("cmd", xyz)
+        expect(stdout).to eq xyz.inspect
       end
-      @xyz = %w(x y z)
-      command.class.run("cmd", @xyz)
-      expect(stdout).to eql @xyz.inspect
+
     end
 
-    context "invoked with a context hash" do
+    context "when invoked with a context hash" do
+
+      before 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")
-        expect(stdout).to eql "bar"
+
+      end
+
+      it "makes the context available within the command" do
+        command.class.run("xyz", [], foo: "bar")
+        expect(stdout).to eq "bar"
       end
 
     end
@@ -894,24 +1189,19 @@ describe Clamp::Command do
 
         command.class.class_eval do
           def execute
-            signal_error "Oh crap!", :status => 456
+            signal_error "Oh crap!", status: 456
           end
         end
 
-        begin
-          command.class.run("cmd", [])
-        rescue SystemExit => e
-          @system_exit = e
-        end
-
-      end
-
-      it "outputs the error message" do
-        expect(stderr).to include "ERROR: Oh crap!"
       end
 
-      it "exits with the specified status" do
-        expect(@system_exit.status).to eql 456
+      it "outputs the error message and exits with the specified status" do
+        expect { command.class.run("cmd", []) }
+          .to raise_error(
+            an_instance_of(SystemExit).and(having_attributes(status: 456))
+          ).and output(<<~TEXT).to_stderr
+            ERROR: Oh crap!
+          TEXT
       end
 
     end
@@ -934,17 +1224,15 @@ describe Clamp::Command do
 
       end
 
-      it "outputs the error message" do
-        expect(stderr).to include "ERROR: bad dog!"
-      end
-
-      it "outputs help" do
-        expect(stderr).to include "See: 'cmd --help'"
-      end
+      it "outputs the error message and help" do
+        expect { command.class.run("cmd", []) }
+          .to raise_error(
+            an_instance_of(SystemExit).and(having_attributes(status: 1))
+          ).and output(<<~TEXT).to_stderr
+            ERROR: bad dog!
 
-      it "exits with a non-zero status" do
-        expect(@system_exit).to_not be_nil
-        expect(@system_exit.status).to eql 1
+            See: 'cmd --help'
+          TEXT
       end
 
     end
diff --git a/spec/clamp/help/builder_spec.rb b/spec/clamp/help/builder_spec.rb
new file mode 100644
index 0000000..a87ad37
--- /dev/null
+++ b/spec/clamp/help/builder_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe Clamp::Help::Builder do
+
+  subject(:builder) { described_class.new }
+
+  def output
+    builder.string
+  end
+
+  describe "#line" do
+
+    it "adds a line of text" do
+      builder.line("blah")
+      expect(output).to eq("blah\n")
+    end
+
+  end
+
+  describe "#row" do
+
+    it "adds two strings separated by spaces" do
+      builder.row("LHS", "RHS")
+      expect(output).to eq("    LHS    RHS\n")
+    end
+
+  end
+
+  context "with multiple rows" do
+
+    before do
+
+      builder.row("foo", "bar")
+      builder.row("flibble", "blurk")
+      builder.row("x", "y")
+
+    end
+
+    let(:expected_output) do
+      [
+        "    foo        bar\n",
+        "    flibble    blurk\n",
+        "    x          y\n"
+      ]
+    end
+
+    it "arranges them in two columns" do
+      expect(output.lines).to eq expected_output
+    end
+
+  end
+
+  context "with a mixture of lines and rows" do
+
+    before do
+
+      builder.line("ABCDEFGHIJKLMNOP")
+      builder.row("flibble", "blurk")
+      builder.line("Another section heading")
+      builder.row("x", "y")
+
+    end
+
+    let(:expected_output) do
+      [
+        "ABCDEFGHIJKLMNOP\n",
+        "    flibble    blurk\n",
+        "Another section heading\n",
+        "    x          y\n"
+      ]
+    end
+
+    it "still arranges them in two columns" do
+      expect(output.lines).to eq expected_output
+    end
+
+  end
+
+end
diff --git a/spec/clamp/messages_spec.rb b/spec/clamp/messages_spec.rb
index a711251..ab857e8 100644
--- a/spec/clamp/messages_spec.rb
+++ b/spec/clamp/messages_spec.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
 
 require "spec_helper"
 
@@ -6,8 +7,8 @@ describe Clamp::Messages do
   describe "message" do
     before do
       Clamp.messages = {
-        :too_many_arguments => "Way too many!",
-        :custom_message => "Say %<what>s to %<whom>s"
+        too_many_arguments: "Way too many!",
+        custom_message: "Say %<what>s to %<whom>s"
       }
     end
 
@@ -16,28 +17,33 @@ describe Clamp::Messages do
     end
 
     it "allows setting custom messages" do
-      expect(Clamp.message(:too_many_arguments)).to eql "Way too many!"
+      expect(Clamp.message(:too_many_arguments)).to eq "Way too many!"
     end
 
     it "fallbacks to a default message" do
-      expect(Clamp.message(:no_value_provided)).to eql "no value provided"
+      expect(Clamp.message(:no_value_provided)).to eq "no value provided"
     end
 
     it "formats the message" do
-      expect(Clamp.message(:custom_message, :what => "hello", :whom => "Clamp")).to eql "Say hello to Clamp"
+      expect(Clamp.message(:custom_message, what: "hello", whom: "Clamp")).to eq "Say hello to Clamp"
     end
   end
 
   describe "clear_messages!" do
-    it "clears messages to the defualt state" do
-      default_msg = Clamp.message(:too_many_arguments).clone
+    let!(:default_msg) { Clamp.message(:too_many_arguments).clone }
+
+    before do
 
       Clamp.messages = {
-        :too_many_arguments => "Way too many!"
+        too_many_arguments: "Way too many!"
       }
+
       Clamp.clear_messages!
 
-      expect(Clamp.message(:too_many_arguments)).to eql default_msg
+    end
+
+    it "clears messages to the defualt state" do
+      expect(Clamp.message(:too_many_arguments)).to eq default_msg
     end
   end
 
diff --git a/spec/clamp/option/definition_spec.rb b/spec/clamp/option/definition_spec.rb
index ce1e218..c5f7c9b 100644
--- a/spec/clamp/option/definition_spec.rb
+++ b/spec/clamp/option/definition_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require "spec_helper"
 
 describe Clamp::Option::Definition do
@@ -9,26 +11,26 @@ describe Clamp::Option::Definition do
     end
 
     it "has a long_switch" do
-      expect(option.long_switch).to eql "--key-file"
+      expect(option.long_switch).to eq "--key-file"
     end
 
     it "has a type" do
-      expect(option.type).to eql "FILE"
+      expect(option.type).to eq "FILE"
     end
 
     it "has a description" do
-      expect(option.description).to eql "SSH identity"
+      expect(option.description).to eq "SSH identity"
     end
 
     describe "#attribute_name" do
 
       it "is derived from the (long) switch" do
-        expect(option.attribute_name).to eql "key_file"
+        expect(option.attribute_name).to eq "key_file"
       end
 
       it "can be overridden" do
-        option = described_class.new("--key-file", "FILE", "SSH identity", :attribute_name => "ssh_identity")
-        expect(option.attribute_name).to eql "ssh_identity"
+        option = described_class.new("--key-file", "FILE", "SSH identity", attribute_name: "ssh_identity")
+        expect(option.attribute_name).to eq "ssh_identity"
       end
 
     end
@@ -36,7 +38,7 @@ describe Clamp::Option::Definition do
     describe "#write_method" do
 
       it "is derived from the attribute_name" do
-        expect(option.write_method).to eql "key_file="
+        expect(option.write_method).to eq "key_file="
       end
 
     end
@@ -45,12 +47,12 @@ describe Clamp::Option::Definition do
 
       it "defaults to nil" do
         option = described_class.new("-n", "N", "iterations")
-        expect(option.default_value).to eql nil
+        expect(option.default_value).to be_nil
       end
 
       it "can be overridden" do
-        option = described_class.new("-n", "N", "iterations", :default => 1)
-        expect(option.default_value).to eql 1
+        option = described_class.new("-n", "N", "iterations", default: 1)
+        expect(option.default_value).to eq 1
       end
 
     end
@@ -58,14 +60,14 @@ describe Clamp::Option::Definition do
     describe "#help" do
 
       it "combines switch, type and description" do
-        expect(option.help).to eql ["--key-file FILE", "SSH identity"]
+        expect(option.help).to eq ["--key-file FILE", "SSH identity"]
       end
 
     end
 
   end
 
-  context "flag" do
+  context "when flag" do
 
     let(:option) do
       described_class.new("--verbose", :flag, "Blah blah blah")
@@ -73,14 +75,28 @@ describe Clamp::Option::Definition do
 
     describe "#default_conversion_block" do
 
-      it "converts truthy values to true" do
-        expect(option.default_conversion_block.call("true")).to eql true
-        expect(option.default_conversion_block.call("yes")).to eql true
+      context "with 'true' value" do
+        it "converts to true" do
+          expect(option.default_conversion_block.call("true")).to be true
+        end
+      end
+
+      context "with 'yes' value" do
+        it "converts to true" do
+          expect(option.default_conversion_block.call("yes")).to be true
+        end
       end
 
-      it "converts falsey values to false" do
-        expect(option.default_conversion_block.call("false")).to eql false
-        expect(option.default_conversion_block.call("no")).to eql false
+      context "with 'false' value" do
+        it "converts to false" do
+          expect(option.default_conversion_block.call("false")).to be false
+        end
+      end
+
+      context "with 'no' value" do
+        it "converts to false" do
+          expect(option.default_conversion_block.call("no")).to be false
+        end
       end
 
     end
@@ -88,29 +104,43 @@ describe Clamp::Option::Definition do
     describe "#help" do
 
       it "excludes option argument" do
-        expect(option.help).to eql ["--verbose", "Blah blah blah"]
+        expect(option.help).to eq ["--verbose", "Blah blah blah"]
       end
 
     end
 
   end
 
-  context "negatable flag" do
+  context "when negatable flag" do
 
     let(:option) do
       described_class.new("--[no-]force", :flag, "Force installation")
     end
 
-    it "handles both positive and negative forms" do
-      expect(option.handles?("--force")).to be true
-      expect(option.handles?("--no-force")).to be true
+    describe "positive form" do
+      it "handles this" do
+        expect(option.handles?("--force")).to be true
+      end
+    end
+
+    describe "negative form" do
+      it "handles this" do
+        expect(option.handles?("--no-force")).to be true
+      end
     end
 
     describe "#flag_value" do
 
-      it "returns true for the positive variant" do
-        expect(option.flag_value("--force")).to be true
-        expect(option.flag_value("--no-force")).to be false
+      describe "positive variant" do
+        it "returns true" do
+          expect(option.flag_value("--force")).to be true
+        end
+      end
+
+      describe "negative variant" do
+        it "returns false" do
+          expect(option.flag_value("--no-force")).to be false
+        end
       end
 
     end
@@ -118,7 +148,7 @@ describe Clamp::Option::Definition do
     describe "#attribute_name" do
 
       it "is derived from the (long) switch" do
-        expect(option.attribute_name).to eql "force"
+        expect(option.attribute_name).to eq "force"
       end
 
     end
@@ -131,15 +161,22 @@ describe Clamp::Option::Definition do
       described_class.new(["-k", "--key-file"], "FILE", "SSH identity")
     end
 
-    it "handles both switches" do
-      expect(option.handles?("--key-file")).to be true
-      expect(option.handles?("-k")).to be true
+    describe "long switch" do
+      it "handles this" do
+        expect(option.handles?("--key-file")).to be true
+      end
+    end
+
+    describe "short switch" do
+      it "handles this" do
+        expect(option.handles?("-k")).to be true
+      end
     end
 
     describe "#help" do
 
       it "includes both switches" do
-        expect(option.help).to eql ["-k, --key-file FILE", "SSH identity"]
+        expect(option.help).to eq ["-k, --key-file FILE", "SSH identity"]
       end
 
     end
@@ -149,27 +186,43 @@ describe Clamp::Option::Definition do
   context "with an associated environment variable" do
 
     let(:option) do
-      described_class.new("-x", "X", "mystery option", :environment_variable => "APP_X")
+      described_class.new("-x", "X", "mystery option", environment_variable: "APP_X")
     end
 
     describe "#help" do
 
       it "describes environment variable" do
-        expect(option.help).to eql ["-x X", "mystery option (default: $APP_X)"]
+        expect(option.help).to eq ["-x X", "mystery option (default: $APP_X)"]
       end
 
     end
 
-    context "and a default value" do
+    context "with a default value" do
 
       let(:option) do
-        described_class.new("-x", "X", "mystery option", :environment_variable => "APP_X", :default => "xyz")
+        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
-          expect(option.help).to eql ["-x X", %{mystery option (default: $APP_X, or "xyz")}]
+          expect(option.help).to eq ["-x X", %{mystery option (default: $APP_X, or "xyz")}]
+        end
+
+      end
+
+    end
+
+    context "when it is required" do
+
+      let(:option) do
+        described_class.new("-x", "X", "mystery option", environment_variable: "APP_X", required: true)
+      end
+
+      describe "#help" do
+
+        it "describes the environment variable as the default" do
+          expect(option.help).to eql ["-x X", %{mystery option (required, default: $APP_X)}]
         end
 
       end
@@ -178,10 +231,10 @@ describe Clamp::Option::Definition do
 
   end
 
-  context "multivalued" do
+  context "when multivalued" do
 
     let(:option) do
-      described_class.new(["-H", "--header"], "HEADER", "extra header", :multivalued => true)
+      described_class.new(["-H", "--header"], "HEADER", "extra header", multivalued: true)
     end
 
     it "is multivalued" do
@@ -191,12 +244,12 @@ describe Clamp::Option::Definition do
     describe "#default_value" do
 
       it "defaults to an empty Array" do
-        expect(option.default_value).to eql []
+        expect(option.default_value).to be_empty
       end
 
       it "can be overridden" do
-        option = described_class.new("-H", "HEADER", "extra header", :multivalued => true, :default => [1, 2, 3])
-        expect(option.default_value).to eql [1, 2, 3]
+        option = described_class.new("-H", "HEADER", "extra header", multivalued: true, default: [1, 2, 3])
+        expect(option.default_value).to eq [1, 2, 3]
       end
 
     end
@@ -204,7 +257,7 @@ describe Clamp::Option::Definition do
     describe "#attribute_name" do
 
       it "gets a _list suffix" do
-        expect(option.attribute_name).to eql "header_list"
+        expect(option.attribute_name).to eq "header_list"
       end
 
     end
@@ -212,7 +265,7 @@ describe Clamp::Option::Definition do
     describe "#append_method" do
 
       it "is derived from the attribute_name" do
-        expect(option.append_method).to eql "append_to_header_list"
+        expect(option.append_method).to eq "append_to_header_list"
       end
 
     end
@@ -236,7 +289,7 @@ describe Clamp::Option::Definition do
       it "includes help for each option exactly once" do
         subcommand = command_class.send(:find_subcommand, "foo")
         subcommand_help = subcommand.subcommand_class.help("")
-        expect(subcommand_help.lines.grep(/--bar BAR/).count).to eql 1
+        expect(subcommand_help.lines.grep(/--bar BAR/).count).to eq 1
       end
 
     end
@@ -247,19 +300,20 @@ describe Clamp::Option::Definition do
     it "rejects :default" do
       expect do
         described_class.new("--key-file", "FILE", "SSH identity",
-                            :required => true, :default => "hello")
+                            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)
+        described_class.new("--awesome", :flag, "Be awesome?", required: true)
       end.to raise_error(ArgumentError)
     end
   end
 
   describe "a hidden option" do
-    let(:option) { described_class.new("--unseen", :flag, "Something", :hidden => true) }
+    let(:option) { described_class.new("--unseen", :flag, "Something", hidden: true) }
+
     it "is hidden" do
       expect(option).to be_hidden
     end
@@ -268,7 +322,7 @@ describe Clamp::Option::Definition do
   describe "a hidden option in a command" do
     let(:command_class) do
       Class.new(Clamp::Command) do
-        option "--unseen", :flag, "Something", :hidden => true
+        option "--unseen", :flag, "Something", hidden: true
 
         def execute
           # this space intentionally left blank
@@ -277,13 +331,13 @@ describe Clamp::Option::Definition do
     end
 
     it "is not shown in the help" do
-      expect(command_class.help("foo")).not_to match /^ +--unseen +Something$/
+      expect(command_class.help("foo")).not_to match(/^ +--unseen +Something$/)
     end
 
     it "sets the expected accessor" do
       command = command_class.new("foo")
       command.run(["--unseen"])
-      expect(command.unseen?).to be_truthy
+      expect(command).to be_unseen
     end
   end
 end
diff --git a/spec/clamp/parameter/definition_spec.rb b/spec/clamp/parameter/definition_spec.rb
index fd5f8ea..b3b262d 100644
--- a/spec/clamp/parameter/definition_spec.rb
+++ b/spec/clamp/parameter/definition_spec.rb
@@ -1,53 +1,71 @@
+# frozen_string_literal: true
+
 require "spec_helper"
 
 describe Clamp::Parameter::Definition do
 
-  context "normal" do
+  context "when regular" do
 
     let(:parameter) do
       described_class.new("COLOR", "hue of choice")
     end
 
     it "has a name" do
-      expect(parameter.name).to eql "COLOR"
+      expect(parameter.name).to eq "COLOR"
     end
 
     it "has a description" do
-      expect(parameter.description).to eql "hue of choice"
+      expect(parameter.description).to eq "hue of choice"
     end
 
     it "is single-valued" do
-      expect(parameter).to_not be_multivalued
+      expect(parameter).not_to be_multivalued
     end
 
     describe "#attribute_name" do
 
       it "is derived from the name" do
-        expect(parameter.attribute_name).to eql "color"
+        expect(parameter.attribute_name).to eq "color"
       end
 
       it "can be overridden" do
-        parameter = described_class.new("COLOR", "hue of choice", :attribute_name => "hue")
-        expect(parameter.attribute_name).to eql "hue"
+        parameter = described_class.new("COLOR", "hue of choice", attribute_name: "hue")
+        expect(parameter.attribute_name).to eq "hue"
       end
 
     end
 
     describe "#consume" do
 
-      it "consumes one argument" do
-        arguments = %w(a b c)
-        expect(parameter.consume(arguments)).to eql ["a"]
-        expect(arguments).to eql %w(b c)
+      subject(:consume) { parameter.consume(arguments) }
+
+      context "with arguments" do
+        let(:arguments) { %w[a b c] }
+
+        it "returns one consumed argument" do
+          expect(consume).to eq ["a"]
+        end
+
+        describe "arguments after consume" do
+
+          before do
+            consume
+          end
+
+          it "has only non-consumed" do
+            expect(arguments).to eq %w[b c]
+          end
+
+        end
+
       end
 
-      describe "with no arguments" do
+      context "without arguments" do
+
+        let(:arguments) { [] }
 
         it "raises an Argument error" do
-          arguments = []
-          expect do
-            parameter.consume(arguments)
-          end.to raise_error(ArgumentError)
+          expect { consume }.to raise_error(ArgumentError)
         end
 
       end
@@ -56,37 +74,55 @@ describe Clamp::Parameter::Definition do
 
   end
 
-  context "optional (name in square brackets)" do
+  context "when optional (name in square brackets)" do
 
     let(:parameter) do
       described_class.new("[COLOR]", "hue of choice")
     end
 
     it "is single-valued" do
-      expect(parameter).to_not be_multivalued
+      expect(parameter).not_to be_multivalued
     end
 
     describe "#attribute_name" do
 
       it "omits the brackets" do
-        expect(parameter.attribute_name).to eql "color"
+        expect(parameter.attribute_name).to eq "color"
       end
 
     end
 
     describe "#consume" do
 
-      it "consumes one argument" do
-        arguments = %w(a b c)
-        expect(parameter.consume(arguments)).to eql ["a"]
-        expect(arguments).to eql %w(b c)
+      subject(:consume) { parameter.consume(arguments) }
+
+      context "with arguments" do
+
+        let(:arguments) { %w[a b c] }
+
+        it "returns one consumed argument" do
+          expect(consume).to eq ["a"]
+        end
+
+        describe "arguments after consume" do
+
+          before do
+            consume
+          end
+
+          it "has only non-consumed" do
+            expect(arguments).to eq %w[b c]
+          end
+
+        end
       end
 
-      describe "with no arguments" do
+      context "without arguments" do
+
+        let(:arguments) { [] }
 
         it "consumes nothing" do
-          arguments = []
-          expect(parameter.consume(arguments)).to eql []
+          expect(consume).to be_empty
         end
 
       end
@@ -95,7 +131,7 @@ describe Clamp::Parameter::Definition do
 
   end
 
-  context "list (name followed by ellipsis)" do
+  context "when list (name followed by ellipsis)" do
 
     let(:parameter) do
       described_class.new("FILE ...", "files to process")
@@ -108,7 +144,7 @@ describe Clamp::Parameter::Definition do
     describe "#attribute_name" do
 
       it "gets a _list suffix" do
-        expect(parameter.attribute_name).to eql "file_list"
+        expect(parameter.attribute_name).to eq "file_list"
       end
 
     end
@@ -116,26 +152,43 @@ describe Clamp::Parameter::Definition do
     describe "#append_method" do
 
       it "is derived from the attribute_name" do
-        expect(parameter.append_method).to eql "append_to_file_list"
+        expect(parameter.append_method).to eq "append_to_file_list"
       end
 
     end
 
     describe "#consume" do
 
-      it "consumes all the remaining arguments" do
-        arguments = %w(a b c)
-        expect(parameter.consume(arguments)).to eql %w(a b c)
-        expect(arguments).to eql []
+      subject(:consume) { parameter.consume(arguments) }
+
+      context "with arguments" do
+
+        let(:arguments) { %w[a b c] }
+
+        it "returns all the consumed arguments" do
+          expect(consume).to eq %w[a b c]
+        end
+
+        describe "arguments after consume" do
+
+          before do
+            consume
+          end
+
+          it "empty" do
+            expect(arguments).to be_empty
+          end
+
+        end
+
       end
 
-      describe "with no arguments" do
+      describe "without arguments" do
+
+        let(:arguments) { [] }
 
         it "raises an Argument error" do
-          arguments = []
-          expect do
-            parameter.consume(arguments)
-          end.to raise_error(ArgumentError)
+          expect { consume }.to raise_error(ArgumentError)
         end
 
       end
@@ -145,13 +198,13 @@ describe Clamp::Parameter::Definition do
     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)
+        described_class.new("KEY=VALUE ...", "config-settings", attribute_name: :config_settings)
       end
 
       describe "#attribute_name" do
 
         it "is the specified one" do
-          expect(parameter.attribute_name).to eql "config_settings"
+          expect(parameter.attribute_name).to eq "config_settings"
         end
 
       end
@@ -160,7 +213,7 @@ describe Clamp::Parameter::Definition do
 
   end
 
-  context "optional list" do
+  context "when optional list" do
 
     let(:parameter) do
       described_class.new("[FILES] ...", "files to process")
@@ -173,7 +226,7 @@ describe Clamp::Parameter::Definition do
     describe "#attribute_name" do
 
       it "gets a _list suffix" do
-        expect(parameter.attribute_name).to eql "files_list"
+        expect(parameter.attribute_name).to eq "files_list"
       end
 
     end
@@ -181,7 +234,7 @@ describe Clamp::Parameter::Definition do
     describe "#default_value" do
 
       it "is an empty list" do
-        expect(parameter.default_value).to eql []
+        expect(parameter.default_value).to be_empty
       end
 
     end
@@ -189,7 +242,7 @@ describe Clamp::Parameter::Definition do
     describe "#help" do
 
       it "does not include default" do
-        expect(parameter.help_rhs).to_not include("default:")
+        expect(parameter.help_rhs).not_to include("default:")
       end
 
     end
@@ -197,13 +250,13 @@ describe Clamp::Parameter::Definition do
     context "with specified default value" do
 
       let(:parameter) do
-        described_class.new("[FILES] ...", "files to process", :default => %w(a b c))
+        described_class.new("[FILES] ...", "files to process", default: %w[a b c])
       end
 
       describe "#default_value" do
 
         it "is that specified" do
-          expect(parameter.default_value).to eql %w(a b c)
+          expect(parameter.default_value).to eq %w[a b c]
         end
 
       end
@@ -218,17 +271,36 @@ describe Clamp::Parameter::Definition do
 
       describe "#consume" do
 
-        it "consumes all the remaining arguments" do
-          arguments = %w(a b c)
-          expect(parameter.consume(arguments)).to eql %w(a b c)
-          expect(arguments).to eql []
+        subject(:consume) { parameter.consume(arguments) }
+
+        context "with arguments" do
+
+          let(:arguments) { %w[a b c] }
+
+          it "returns all the consumed arguments" do
+            expect(consume).to eq %w[a b c]
+          end
+
+          describe "arguments after consume" do
+
+            before do
+              consume
+            end
+
+            it "empty" do
+              expect(arguments).to be_empty
+            end
+
+          end
+
         end
 
-        context "with no arguments" do
+        context "without arguments" do
+
+          let(:arguments) { [] }
 
           it "don't override defaults" do
-            arguments = []
-            expect(parameter.consume(arguments)).to eql []
+            expect(consume).to be_empty
           end
 
         end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index e3a3a3b..b19672b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,10 +1,17 @@
+# frozen_string_literal: true
+
 require "rspec"
 require "clamp"
 require "stringio"
+require "pry-byebug"
 
 RSpec.configure do |config|
 
-  config.mock_with :rr
+  config.around do |example|
+    example.run
+  rescue SystemExit => e
+    raise "Unexpected exit with status #{e.status}"
+  end
 
 end
 

More details

Full run details