[DRE-commits] [ruby-slop] 01/05: Imported Upstream version 4.2.0

Youhei SASAKI uwabami-guest at moszumanska.debian.org
Wed Jul 29 03:44:19 UTC 2015


This is an automated email from the git hooks/post-receive script.

uwabami-guest pushed a commit to branch master
in repository ruby-slop.

commit 325b8569b680859d5aa57db34feca52ab3927edf
Author: Youhei SASAKI <uwabami at gfd-dennou.org>
Date:   Wed Jul 29 12:33:56 2015 +0900

    Imported Upstream version 4.2.0
---
 .travis.yml           |   6 +-
 CHANGELOG.md          |  36 +++
 CHANGES.md            | 309 ----------------------
 LICENSE               |   2 +-
 README.md             | 378 ++++++++++++++++++---------
 checksums.yaml.gz     | Bin 269 -> 0 bytes
 lib/slop.rb           | 698 +++-----------------------------------------------
 lib/slop/commands.rb  | 196 --------------
 lib/slop/error.rb     |  35 +++
 lib/slop/option.rb    | 283 +++++++-------------
 lib/slop/options.rb   | 141 ++++++++++
 lib/slop/parser.rb    | 120 +++++++++
 lib/slop/result.rb    |  90 +++++++
 lib/slop/types.rb     |  96 +++++++
 metadata.yml          |  53 ++--
 slop.gemspec          |   9 +-
 test/commands_test.rb |  26 --
 test/error_test.rb    |  45 ++++
 test/helper.rb        |  12 -
 test/option_test.rb   | 155 ++---------
 test/options_test.rb  |  85 ++++++
 test/parser_test.rb   |  70 +++++
 test/result_test.rb   | 107 ++++++++
 test/slop_test.rb     | 518 -------------------------------------
 test/test_helper.rb   |   6 +
 test/types_test.rb    | 120 +++++++++
 26 files changed, 1400 insertions(+), 2196 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 6fba0ce..c470913 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
 rvm:
-  - 1.9.3
+  - 2.0.0
   - 2.1
-  - ruby-head
-  - jruby-19mode
+  - 2.2
+  - rbx-2
 notifications:
   email:
     on_success: change
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f7b36a5
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,36 @@
+Changelog
+=========
+
+v4.2.0 (2015-04-18)
+-------------------
+
+Features:
+  * Support for Regexp option type #167 (Laurent Arnoud)
+  * Support prefixed `--no-` for explicitly setting boolean options
+    to `false` #168
+  * Better handling of flags with multiple words #169 (Tim Rogers)
+
+v4.1.0 (2015-04-18)
+-------------------
+
+Features:
+  * Support for FloatOption #156 (Rick Hull)
+  * Support for `limit` config to ArrayOption.
+  * Support for `tail` config to add options to the bottom of
+    the help text.
+  * Add explicit setter (#[]=) to Result class. #162
+  * Implement flag gettings for UnknownOption and MissingArgument
+    error classes. #165 (sigurdsvela)
+
+Minor enhancements:
+  * Reset parser every time `parse` is called.
+
+Bug fixes:
+  * Remove "--" from unprocessed arguments #157 (David Rodríguez).
+
+v4.0.0 (2014-12-27)
+-------------------
+
+Features:
+  * Rebuilt from the ground up. See the v3 changelog for all existing
+    changes: https://github.com/leejarvis/slop/blob/v3/CHANGES.md
diff --git a/CHANGES.md b/CHANGES.md
deleted file mode 100644
index c500beb..0000000
--- a/CHANGES.md
+++ /dev/null
@@ -1,309 +0,0 @@
-3.6.0 (2014-06-18)
-------------------
-
-* Add example of rest arguments usage in the readme file #139
-* Default values on options are printed in the help message #134
-
-3.5.0 (2014-03-12)
-------------------
-
-* Add support for `as: Regexp` #132
-
-3.4.7 (2013-11-14)
-------------------
-
-* Ensure trash is cleared on every parse so you can parse multiple
-  times with the same instance (#130)
-
-3.4.5 (2013-05-14)
-------------------
-
-* Allow specifying long options starting with numbers (#110, Peter Zotov)
-* Ensure short-options still consume trailing arguments, ie `-abc foo`
-  should assign `foo` to the option `c` if it expects an argument (#114).
-
-3.4.4 (2013-03-12)
-------------------
-
-* Disable the run callback when the help option is used and `-h`
-  or `--help` is passed. #106
-* Ensure default `--help` option exits by default (#107, Autumn Perrault).
-
-3.4.3 (2013-01-14)
-------------------
-
-* Ensure `parse!` removes commands and their options.
-
-3.4.2 (2013-01-14)
-------------------
-
-* Expose the Hash commands as public API.
-* Deprecated `Slop.optspec`.
-* Ensure help output prints to stdout, not stderr.
-
-3.4.1 (2013-01-13)
-------------------
-
-* Ensure options replace any existing duplicates
-* Command config options now inherit config options from top level Slop.
-* Command help output now adds command in usage string.
-
-3.4.0 (2013-01-12)
-------------------
-
-* Implement new command system (#95)
-* Deprecate Slop::Commands
-* Ensure 'no-foo' options are not inverted when parsing '--no-foo' (#86)
-* Code refactoring and simplification (Kenichi Kamiya, #84, #85)
-
-3.3.3 (2012-08-29)
-------------------
-
-* Ensure autocreate arguments are not created as options (#77)
-* Ensure options are not swallowed when using short options with argument
-  included (#74)
-
-3.3.2 (2012-06-26)
-------------------
-
-* Ensure multiple options are not executed unless they exist (#70)
-
-3.3.1 (2012-05-31)
-------------------
-
-* Stop multiple switches from trashing arguments (Conrad Irwin, #66)
-
-3.3.0 (2012-05-30)
-------------------
-
-* Fix `:as => :count` when using multiple switches.
-* Ensure range typecast allows negative range values.
-* Ignore nil objects send to #parse instead of choking.
-
-3.2.0 (2012-05-15)
-------------------
-
-* Ensure boolean options appear correctly in `to_hash` output. (#59)
-
-3.1.1 (2012-04-24)
-------------------
-
-* Ensure separators before any options are still being processed (#62)
-
-3.1.0 (2012-04-23)
-------------------
-
-* Allow options to be fetched via underscores instead of dashes
-  (as a fallback) (Eric Anderson, #51)
-* Added `Slop#strict?` method.
-* Added strict checks for Integer/Float type casting. (Amon Sha)
-* Ensure separators are not replacing existing separators (#61)
-
-3.0.4 (2012-01-31)
-------------------
-
-* Ensure `option=argument` syntax does not consume following arguments (#55).
-
-3.0.3 (2012-01-30)
-------------------
-
-* Ensure options passed after option terminator do not raise an exception
-  (#54, Amon Sha)
-
-3.0.2 (2012-01-27)
-------------------
-
-* Ensure `--option=value` is being evaluated before multiple switches (#52)
-
-3.0.1 (2012-01-27)
-------------------
-
-* Ensure tests run green on 1.8.7
-* Ensure `:argument => :optional` works with `:option=` format.
-* Ruby 1.8.7 compat fix (dont chain Enumerable methods!) (Eric Anderson)
-
-3.0.0 (2012-01-24)
-------------------
-
-* value_to_range returns an x..x range if the value looks like an integer.
-* Lots of code refactoring
-* Use TomDoc documentation
-* Added `Slop::Commands` and removed existing command system
-* Configuration options altered:
-	* `:optional` has been renamed to `:optional_argument`
-	* Added `:required` for mandatory options
-	* `:argument` now accepts an `:optional` symbol as well as boolean value
-* Removed Slop instance methods:
-	* description=, description
-	* summary=, summary
-	* command
-	* on_empty
-	* on_noopts
-	* execute
-	* to_struct
-* Added Slop instance methods:
-	* separator
-	* fetch_option
-	* add_callback
-
-2.4.3 (2012-01-16)
-------------------
-
-* Allow the `:as` option to accept an object responding to :call for
-  custom type conversions (#45)
-* Ensure negative integers are not parsed as possible options (#46)
-
-2.4.2 (2011-12-18)
-------------------
-
-* Fix checking of required options (Dominik Honnef)
-
-2.4.1 (2011-12-08)
-------------------
-
-* Ensure optional arguments are returned correctly
-
-2.4.0 (2011-11-26)
-------------------
-
-* Avoid `define_method` for checking an options presence (and caching it) #37
-* Ensure the short option allows an appended `=` for accepting arguments
-* Implement `respond_to?`
-
-2.3.1 (2011-11-11)
-------------------
-
-* Return `nil` for any options using casting which don't expect arguments (#33)
-* Fix parenthesis warning on 1.8.7 (@shevegen)
-* Ensure long argument is a string before attempting to use `#[]` method on it
-
-2.3.0 (2011-11-04)
-------------------
-
-* Allow flags to have suffixed `=` char for options which accept an argument
-
-2.2.0 (2011-11-02)
-------------------
-
-* Support `bup.options` style optspec parsing
-    * http://apenwarr.ca/log/?m=201111
-
-* Allow `:as` to accept a `count` value (Conrad Irwin):
-
-    `on :v, :verbose, :as => :count # -vv; opts[:verbose] #=> 2`
-
-2.1.0 (2011-08-03)
-------------------
-
-* Added `Slop#missing` for returning a list of missing options parsed
-* Allow `Slop#present?` to accept multiple arguments
-* Added `:all_accept_arguments` to Slop configuration options, this saves
-  having to specify that every option takes an argument
-* Added `Slop#to_struct` for building new classes from options
-
-2.0.0 (2011-07-07)
-------------------
-
-* Deprecations:
-  * Removed `Slop::Options#to_hash` continue using `Slop#to_hash` directly.
-    This method also now returns symbols by default instead of strings. If
-    you want strings use `opts.to_hash(false)`
-  * `:multiple_switches` is now enabled by default, to parse `fbar` as the
-    option `f` with value `bar` you must disable `:multiple_switches`
-  * Removed `Slop::Options#to_help` and merged its contents into `Slop#help`
-  * Removed `lib/slop/options.rb` and merged `Slop::Options` into slop.rb
-  * Removed `lib/slop/option.rb` and merged `Slop::Option` into slop.rb
-  * These changes make Slop much easier to vendor in libraries
-* `Slop::Option` now inherits from `Struct.new`
-* Added Slop::Error subclassing from StandardError which all exception
-  classes should inherit from
-* Added Slop::MissingOptionError and `:required` option to Slop::Option.
-  This exception is raised when a mandatory option is not used
-
-1.9.1 (2011-06-16)
-------------------
-
-* Ensure optional items with no arguments still return true when searching
-  for presence
-
-1.9.0 (2011-06-15)
-------------------
-
-* Add command completion and support for an error message when ambiguous
-  commands are used
-* Add command aliases
-* Fix: Ensure parsed elements are removed from original arguments when using
-  `:multiple_switches`
-* Ensure anything after `--` is parsed as an argument and not option even
-  if prefixed with `/--?/`
-* Performance improvements when making many calls to `Slop#option?` for
-  checking an options presence (Rob Gleeson)
-* Ensure `execute` passes command arguments to the block
-* Support for summary and description (Denis Defreyne)
-
-1.8.0 (2011-06-12)
-------------------
-
-* Added `execute` method to Slop for commands. This block will be invoked
-  when a specific command is used. The Slop object will be yielded to the
-  block
-* Allow passing a class name to `on` to be used as an `:as` option. ie:
-  `on :people, 'Some people', Array`
-* Get smart with parsing options optparse style: `on '--name NAME'` and
-  `on 'password [OPTIONAL]'`
-* Feature: `:arguments` setting to enable argument passing for all options
-
-1.7.0 (2011-06-06)
-------------------
-
-* Feature: Autocreate (auto create options at parse time, making assumptions)
-* Feature: When parsing options as arrays, push multiple arguments into a
-  single array
-
-1.6.1 (2011-06-01)
-------------------
-
-* Fix tests and using a temporary Array for ARGV, fixes RubyGems Test issues
-* General cleanup of code
-
-1.6.0 (2011-05-18)
-------------------
-
-* Add `:ignore_case` to Slop options for case insensitive option matching
-* Add `:on_noopts` for triggering an event when the arguments contain no
-  options
-* Add `:unless` to Slop::Option for omitting execution of the Options block
-  when this object exists in the Array of items passed to Slop.new
-* Bugfix: Do not parse negative integers as options. A valid option must
-  start with an alphabet character
-* Bugfix: Allow a Range to accept a negative Integer at either end
-
-1.5.5 (2011-05-03)
-------------------
-
-* Bugfix: only attempt to extract options prefixed with `-`
-
-1.5.4 (2011-05-01)
-------------------
-
-* Bugfix: `parse!` should not remove items with the same value as items used
-  in option arguments. Fixes #22 (Utkarsh Kukreti)
-
-1.5.3 (2011-04-22)
-------------------
-
-* Bugfix: Use integers when fetching array indexes, not strings
-
-1.5.2 (2011-04-17)
-------------------
-
-* Bugfix: Ensure `ARGV` is empty when using the `on_empty` event
-
-1.5.0 (2011-04-15)
-------------------
-
-* Add `Slop#get` as alias to `Slop#[]`
-* Add `Slop#present?` as alias for `Slop#<option>?`
-* Add `Option#count` for monitoring how many times an option is called
-* Add `:io` for using a custom IO object when using the `:help` option
-* Numerous performance tweaks
diff --git a/LICENSE b/LICENSE
index ab47fd7..ba9a3a9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012 Lee Jarvis
+Copyright (c) Lee Jarvis
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
diff --git a/README.md b/README.md
index 334c34b..77045a4 100644
--- a/README.md
+++ b/README.md
@@ -2,201 +2,335 @@ Slop
 ====
 
 Slop is a simple option parser with an easy to remember syntax and friendly API.
-API Documentation is available [here](http://leejarvis.github.com/rdoc/slop/).
 
-[![Build Status](https://travis-ci.org/leejarvis/slop.png?branch=master)](http://travis-ci.org/leejarvis/slop)
+Version 4 of Slop is aimed at Ruby 2.0 or later. Please use
+[Version 3](https://github.com/leejarvis/slop/tree/v3) for Ruby 1.9 support.
+
+[![Build Status](https://travis-ci.org/leejarvis/slop.svg?branch=master)](http://travis-ci.org/leejarvis/slop)
+
+Installation
+------------
+
+    gem install slop
 
 Usage
 -----
 
 ```ruby
-opts = Slop.parse do
-  banner 'Usage: foo.rb [options]'
-
-  on 'name=', 'Your name'
-  on 'p', 'password', 'An optional password', argument: :optional
-  on 'v', 'verbose', 'Enable verbose mode'
+opts = Slop.parse do |o|
+  o.string '-h', '--host', 'a hostname'
+  o.integer '--port', 'custom port', default: 80
+  o.bool '-v', '--verbose', 'enable verbose mode'
+  o.bool '-q', '--quiet', 'suppress output (quiet mode)'
+  o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host'
+  o.on '--version', 'print the version' do
+    puts Slop::VERSION
+    exit
+  end
 end
 
-# if ARGV is `--name Lee -v`
-opts.verbose?  #=> true
-opts.password? #=> false
-opts[:name]    #=> 'lee'
-opts.to_hash   #=> {:name=>"Lee", :password=>nil, :verbose=>true}
-```
+ARGV #=> -v --host 192.168.0.1 --check-ssl-certificate
 
-Installation
-------------
+opts[:host]                 #=> 192.168.0.1
+opts.verbose?               #=> true
+opts.quiet?                 #=> false
+opts.check_ssl_certificate? #=> true
 
-    gem install slop
+opts.to_hash  #=> { host: "192.168.0.1", port: 80, verbose: true, quiet: false, check_ssl_certificate: true }
+```
 
-Printing Help
--------------
+Option types
+------------
 
-Slop attempts to build a good looking help string to print to your users. You
-can see this by calling `opts.help` or simply `puts opts`.
+Built in Option types are as follows:
 
-Configuration Options
----------------------
+```ruby
+o.string  #=> Slop::StringOption, expects an argument
+o.bool    #=> Slop::BoolOption, no argument, aliased to BooleanOption
+o.integer #=> Slop::IntegerOption, expects an argument, aliased to IntOption
+o.float   #=> Slop::FloatOption, expects an argument
+o.array   #=> Slop::ArrayOption, expects an argument
+o.regexp  #=> Slop::RegexpOption, expects an argument
+o.null    #=> Slop::NullOption, no argument and ignored from `to_hash`
+o.on      #=> alias for o.null
+```
 
-All of these options can be sent to `Slop.new` or `Slop.parse` in Hash form.
+You can see all built in types in `slop/types.rb`. Suggestions or pull requests
+for more types are welcome.
 
-| Option | Description | Default/Example |
-| ------ | ----------- | --------------- |
-| strict | Enable strict mode. Slop will raise an `InvalidOptionError` for unkown options. | `false` |
-| help   | Automatically add the `--help` option. | `false` |
-| banner | Set the help banner text. | `nil` |
-| ignore_case | When enabled, `-A` will look for the `-a` option if `-A` does not exist. | `false` |
-| autocreate | Autocreate options on the fly. | `false` |
-| arguments | Force all options to expect arguments. | `false` |
-| optional_arguments | Force all options to accept optional arguments. | `false` |
-| multiple_switches | When disabled, Slop will parse `-abc` as the option `a` with the argument `bc` rather than 3 separate options. | `true` |
+Advanced Usage
+--------------
 
-Lists
------
+This example is really just to describe how the underlying API works.
+It's not necessarily the best way to do it.
 
 ```ruby
-opts = Slop.parse do
-  on :list=, as: Array
-end
-# ruby run.rb --list one,two
-opts[:list] #=> ["one", "two"]
-# ruby run.rb --list one,two --list three
-opts[:list] #=> ["one", "two", "three"]
+opts = Slop::Options.new
+opts.banner = "usage: connect [options] ..."
+opts.separator ""
+opts.separator "Connection options:"
+opts.string "-H", "--hostname", "a hostname"
+opts.int "-p", "--port", "a port", default: 80
+opts.separator ""
+opts.separator "Extra options:"
+opts.array "--files", "a list of files to import"
+opts.bool "-v", "--verbose", "enable verbose mode", default: true
+
+parser = Slop::Parser.new(opts)
+result = parser.parse(["--hostname", "192.168.0.1", "--no-verbose"])
+
+result.to_hash #=> { hostname: "192.168.0.1", port: 80,
+                 #     files: [], verbose: false }
+
+puts opts # prints out help
 ```
 
-You can also specify a delimiter and limit.
+Arguments
+---------
+
+It's common to want to retrieve an array of arguments that were not processed
+by the parser (i.e options or consumed arguments). You can do that with the
+`Result#arguments` method:
 
 ```ruby
-opts = Slop.parse do
-  on :list=, as: Array, delimiter: ':', limit: 2
+args = %w(connect --host google.com GET)
+opts = Slop.parse args do |o|
+  o.string '--host'
 end
-# ruby run.rb --list one:two:three
-opts[:list] #=> ["one", "two:three"]
+
+p opts.arguments #=> ["connect", "GET"] # also aliased to `args`
 ```
 
-Ranges
+Arrays
 ------
 
+Slop has a built in `ArrayOption` for handling array values:
+
 ```ruby
-opts = Slop.parse do
-  on :range=, as: Range
+opts = Slop.parse do |o|
+  # the delimiter defaults to ','
+  o.array '--files', 'a list of files', delimiter: ','
 end
-# ruby run.rb --range 1..10
-opts[:range] #=> 1..10
-# ruby run.rb --range 1...10
-opts[:range] #=> 1...10
-# ruby run.rb --range 1-10
-opts[:range] #=> 1..10
-# ruby run.rb --range 1,10
-opts[:range] #=> 1..10
+
+# both of these will return o[:files] as ["foo.txt", "bar.rb"]:
+# --files foo.txt,bar.rb
+# --files foo.txt --files bar.rb
 ```
 
-Autocreate
-----------
+Custom option types
+-------------------
 
-Slop has an 'autocreate' feature. This feature is intended to create
-options on the fly, without having to specify them yourself. In some case,
-using this code could be all you need in your application:
+Slop uses option type classes for every new option added. They default to the
+`NullOption`. When you type `o.array` Slop looks for an option called
+`Slop::ArrayOption`. This class must contain at least 1 method, `call`. This
+method is executed at parse time, and the return value of this method is
+used for the option value. We can use this to build custom option types:
 
 ```ruby
-# ruby run.rb --foo bar --baz --name lee
-opts = Slop.parse(autocreate: true)
-opts.to_hash #=> {:foo=>"bar", :baz=>true, :name=>"lee"}
-opts.fetch_option(:name).expects_argument? #=> true
-```
+module Slop
+  class PathOption < Option
+    def call(value)
+      Pathname.new(value)
+    end
+  end
+end
 
-Commands
---------
+opts = Slop.parse %w(--path ~/) do |o|
+  o.path '--path', 'a custom path name'
+end
 
-Slop supports git style sub-commands, like so:
+p opts[:path] #=> #<Pathname:~/>
+```
+
+Custom options can also implement a `finish` method. This method by default
+does nothing, but it's executed once *all* options have been parsed. This
+allows us to go back and mutate state without having to rely on options
+being parsed in a particular order. Here's an example:
 
 ```ruby
-opts = Slop.parse do
-  on '-v', 'Print the version' do
-    puts "Version 1.0"
+module Slop
+  class FilesOption < ArrayOption
+    def finish(opts)
+      if opts.expand?
+        self.value = value.map { |f| File.expand_path(f) }
+      end
+    end
   end
+end
 
-  command 'add' do
-    on :v, :verbose, 'Enable verbose mode'
-    on :name=, 'Your name'
+opts = Slop.parse %w(--files foo.txt,bar.rb -e) do |o|
+  o.files '--files', 'an array of files'
+  o.bool '-e', '--expand', 'if used, list of files will be expanded'
+end
 
-    run do |opts, args|
-      puts "You ran 'add' with options #{opts.to_hash} and args: #{args.inspect}"
-    end
-  end
+p opts[:files] #=> ["/full/path/foo.txt", "/full/path/bar.rb"]
+```
+
+Errors
+------
+
+Slop will raise errors for the following:
+
+* An option used without an argument when it expects one: `Slop::MissingArgument`
+* An option used that Slop doesn't know about: `Slop::UnknownOption`
+
+These errors inherit from `Slop::Error`, so you can rescue them all.
+Alternatively you can suppress these errors with the `suppress_errors` config
+option:
+
+```ruby
+opts = Slop.parse suppress_errors: true do
+  o.string '-name'
 end
 
-# ruby run.rb -v
-#=> Version 1.0
-# ruby run.rb add -v foo --name Lee
-#=> You ran 'add' with options {:verbose=>true,:name=>"Lee"} and args ["foo"]
-opts.to_hash(true) # Pass true to tell Slop to merge sub-command option values.
-# => { :v => nil, :add => { :v => true, :name => "Lee" } }
+# or per option:
+
+opts = Slop.parse do
+  o.string '-host', suppress_errors: true
+  o.int '-port'
+end
 ```
 
-Remaining arguments
--------------------
+Printing help
+-------------
 
-The *parse!*  method will remove any options and option arguments from the original Array:
+The return value of `Slop.parse` is a `Slop::Result` which provides a nice
+help string to display your options. Just `puts opts` or call `opts.to_s`:
 
 ```ruby
-# restarguments.rb
-opts = Slop.parse! do
-  on :foo
+opts = Slop.parse do |o|
+  o.string '-h', '--host', 'hostname'
+  o.int '-p', '--port', 'port (default: 80)', default: 80
+  o.string '--username'
+  o.separator ''
+  o.separator 'other options:'
+  o.bool '--quiet', 'suppress output'
+  o.on '-v', '--version' do
+    puts "1.1.1"
+  end
 end
+
+puts opts
 ```
 
-Example:
+Output:
 
 ```
-ruby restarguments.rb --foo bar
+% ruby run.rb
+usage: run.rb [options]
+    -h, --host     hostname
+    -p, --port     port (default: 80)
+    --username
+
+other options:
+    --quiet        suppress output
+    -v, --version
 ```
 
-```
-opts.to_hash = { :foo => true }
+This method takes an optional `prefix` value, which defaults to `" " * 4`:
 
-ARGV #=> ["bar"]
+```
+puts opts.to_s(prefix: "  ")
 ```
 
-Woah woah, why you hating on OptionParser?
-------------------------------------------
+It'll deal with aligning your descriptions according to the longest option
+flag.
 
-I'm not, honestly! I love OptionParser. I really do, it's a fantastic library.
-So why did I build Slop? Well, I find myself using OptionParser to simply
-gather a bunch of key/value options, usually you would do something like this:
+Here's an example of adding your own help option:
 
 ```ruby
-require 'optparse'
+o.on '--help' do
+  puts o
+  exit
+end
+```
 
-things = {}
+Commands
+--------
 
-opt = OptionParser.new do |opt|
-  opt.on('-n', '--name NAME', 'Your name') do |name|
-    things[:name] = name
-  end
+As of version 4, Slop does not have built in support for git-style subcommands.
+You can use version 3 of Slop (see `v3` branch). I also expect there to be some
+external libraries released soon that wrap around Slop to provide support for
+this feature. I'll update this document when that happens.
+
+Upgrading from version 3
+------------------------
+
+Slop v4 is completely non-backwards compatible. The code has been rewritten
+from the group up. If you're already using version 3 you have *have* to update
+your code to use version 4. Here's an overview of the more fundamental changes:
+
+#### No more `instance_eval`
+
+Before:
 
-  opt.on('-a', '--age AGE', 'Your age') do |age|
-    things[:age] = age.to_i
+```ruby
+Slop.parse do
+  on 'v', 'version' do
+    puts VERSION
   end
+end
+```
 
-  # you get the point
+After:
+
+```ruby
+Slop.parse do |o|
+  o.on '-v', '--version' do
+    puts VERSION
+  end
 end
+```
+
+#### No more `as` for option types
+
+Instead, the type is declared in the method call. Before:
 
-opt.parse
-things #=> { :name => 'lee', :age => 105 }
+```ruby
+on 'port=', as: Integer
 ```
 
-Which is all great and stuff, but it can lead to some repetition. The same
-thing in Slop:
+After:
 
 ```ruby
-require 'slop'
+o.int '--port' # or integer
+```
 
-opts = Slop.parse do
-  on :n, :name=, 'Your name'
-  on :a, :age=, 'Your age', as: Integer
+See the custom types section of the document.
+
+#### No more trailing `=`
+
+Instead, the "does this option expect an argument" question is answered by
+the option type (i.e `on` and `bool` options do not expect arguments, all
+others do. They handle type conversion, too.
+
+#### Hyphens are required
+
+This was a hard decision to make, but you must provide prefixed hyphens when
+declaring your flags. This makes the underlying code much nicer and much less
+ambiguous, which leads to less error prone code. It also means you can easily
+support single hyphen prefix for a long flag, i.e `-hostname` which you
+could not do before. It also provides a hidden feature, which is infinity flag
+aliases: `o.string '-f', '-x', '--foo', '--bar', 'this is insane'`
+
+#### Strict is now on by default
+
+v3 had a `strict` option. v4 has no such option, and to suppress errors you can
+instead provide the `suppress_errors: true` option to Slop.
+
+#### No more parse!
+
+Where v3 has both `Slop.parse` and `Slop.parse!`, v4 only has `parse`. The
+former was used to decide whether Slop should or should not mutate the
+original args (usually ARGV). This is almost never what you want, and it
+can lead to confusion. Instead, `Slop::Result` provides an `arguments`
+methods:
+
+```ruby
+opts = Slop.parse do |o|
+  o.string '--hostname', '...'
 end
 
-opts.to_hash #=> { :name => 'lee', :age => 105 }
+# ARGV is "hello --hostname foo bar"
+p opts.arguments #=> ["hello", "bar"]
 ```
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
deleted file mode 100644
index 76c7002..0000000
Binary files a/checksums.yaml.gz and /dev/null differ
diff --git a/lib/slop.rb b/lib/slop.rb
index 6943176..47a0b9e 100644
--- a/lib/slop.rb
+++ b/lib/slop.rb
@@ -1,687 +1,55 @@
 require 'slop/option'
-require 'slop/commands'
+require 'slop/options'
+require 'slop/parser'
+require 'slop/result'
+require 'slop/types'
+require 'slop/error'
 
-class Slop
-  include Enumerable
+module Slop
+  VERSION = '4.2.0'
 
-  VERSION = '3.6.0'
-
-  # The main Error class, all Exception classes inherit from this class.
-  class Error < StandardError; end
-
-  # Raised when an option argument is expected but none are given.
-  class MissingArgumentError < Error; end
-
-  # Raised when an option is expected/required but not present.
-  class MissingOptionError < Error; end
-
-  # Raised when an argument does not match its intended match constraint.
-  class InvalidArgumentError < Error; end
-
-  # Raised when an invalid option is found and the strict flag is enabled.
-  class InvalidOptionError < Error; end
-
-  # Raised when an invalid command is found and the strict flag is enabled.
-  class InvalidCommandError < Error; end
-
-  # Returns a default Hash of configuration options this Slop instance uses.
-  DEFAULT_OPTIONS = {
-    :strict => false,
-    :help => false,
-    :banner => nil,
-    :ignore_case => false,
-    :autocreate => false,
-    :arguments => false,
-    :optional_arguments => false,
-    :multiple_switches => true,
-    :longest_flag => 0
-  }
-
-  class << self
-
-    # items  - The Array of items to extract options from (default: ARGV).
-    # config - The Hash of configuration options to send to Slop.new().
-    # block  - An optional block used to add options.
-    #
-    # Examples:
-    #
-    #   Slop.parse(ARGV, :help => true) do
-    #     on '-n', '--name', 'Your username', :argument => true
-    #   end
-    #
-    # Returns a new instance of Slop.
-    def parse(items = ARGV, config = {}, &block)
-      parse! items.dup, config, &block
-    end
-
-    # items  - The Array of items to extract options from (default: ARGV).
-    # config - The Hash of configuration options to send to Slop.new().
-    # block  - An optional block used to add options.
-    #
-    # Returns a new instance of Slop.
-    def parse!(items = ARGV, config = {}, &block)
-      config, items = items, ARGV if items.is_a?(Hash) && config.empty?
-      slop = new config, &block
-      slop.parse! items
-      slop
-    end
-
-    # Build a Slop object from a option specification.
-    #
-    # This allows you to design your options via a simple String rather
-    # than programatically. Do note though that with this method, you're
-    # unable to pass any advanced options to the on() method when creating
-    # options.
-    #
-    # string - The optspec String
-    # config - A Hash of configuration options to pass to Slop.new
-    #
-    # Examples:
-    #
-    #   opts = Slop.optspec(<<-SPEC)
-    #   ruby foo.rb [options]
-    #   ---
-    #   n,name=     Your name
-    #   a,age=      Your age
-    #   A,auth      Sign in with auth
-    #   p,passcode= Your secret pass code
-    #   SPEC
-    #
-    #   opts.fetch_option(:name).description #=> "Your name"
-    #
-    # Returns a new instance of Slop.
-    def optspec(string, config = {})
-      warn "[DEPRECATED] `Slop.optspec` is deprecated and will be removed in version 4"
-      config[:banner], optspec = string.split(/^--+$/, 2) if string[/^--+$/]
-      lines = optspec.split("\n").reject(&:empty?)
-      opts  = Slop.new(config)
-
-      lines.each do |line|
-        opt, description = line.split(' ', 2)
-        short, long = opt.split(',').map { |s| s.sub(/\A--?/, '') }
-        opt = opts.on(short, long, description)
-
-        if long && long.end_with?('=')
-          long.sub!(/\=$/, '')
-          opt.config[:argument] = true
-        end
-      end
-
-      opts
-    end
-
-  end
-
-  # The Hash of configuration options for this Slop instance.
-  attr_reader :config
-
-  # The Array of Slop::Option objects tied to this Slop instance.
-  attr_reader :options
-
-  # The Hash of sub-commands for this Slop instance.
-  attr_reader :commands
-
-  # Create a new instance of Slop and optionally build options via a block.
-  #
-  # config - A Hash of configuration options.
-  # block  - An optional block used to specify options.
-  def initialize(config = {}, &block)
-    @config = DEFAULT_OPTIONS.merge(config)
-    @options = []
-    @commands = {}
-    @trash = []
-    @triggered_options = []
-    @unknown_options = []
-    @callbacks = {}
-    @separators = {}
-    @runner = nil
-    @command = config.delete(:command)
-
-    if block_given?
-      block.arity == 1 ? yield(self) : instance_eval(&block)
-    end
-
-    if config[:help]
-      on('-h', '--help', 'Display this help message.', :tail => true) do
-        puts help
-        exit
-      end
-    end
-  end
-
-  # Is strict mode enabled?
-  #
-  # Returns true if strict mode is enabled, false otherwise.
-  def strict?
-    config[:strict]
-  end
-
-  # Set the banner.
-  #
-  # banner - The String to set the banner.
-  def banner=(banner)
-    config[:banner] = banner
-  end
-
-  # Get or set the banner.
-  #
-  # banner - The String to set the banner.
-  #
-  # Returns the banner String.
-  def banner(banner = nil)
-    config[:banner] = banner if banner
-    config[:banner]
-  end
-
-  # Set the description (used for commands).
-  #
-  # desc - The String to set the description.
-  def description=(desc)
-    config[:description] = desc
-  end
-
-  # Get or set the description (used for commands).
-  #
-  # desc - The String to set the description.
-  #
-  # Returns the description String.
-  def description(desc = nil)
-    config[:description] = desc if desc
-    config[:description]
-  end
-
-  # Add a new command.
-  #
-  # command - The Symbol or String used to identify this command.
-  # options - A Hash of configuration options (see Slop::new)
-  #
-  # Returns a new instance of Slop mapped to this command.
-  def command(command, options = {}, &block)
-    options = @config.merge(options)
-    @commands[command.to_s] = Slop.new(options.merge(:command => command.to_s), &block)
-  end
-
-  # Parse a list of items, executing and gathering options along the way.
-  #
-  # items - The Array of items to extract options from (default: ARGV).
-  # block - An optional block which when used will yield non options.
-  #
-  # Returns an Array of original items.
-  def parse(items = ARGV, &block)
-    parse! items.dup, &block
-    items
-  end
-
-  # Parse a list of items, executing and gathering options along the way.
-  # unlike parse() this method will remove any options and option arguments
-  # from the original Array.
-  #
-  # items - The Array of items to extract options from (default: ARGV).
-  # block - An optional block which when used will yield non options.
-  #
-  # Returns an Array of original items with options removed.
-  def parse!(items = ARGV, &block)
-    if items.empty? && @callbacks[:empty]
-      @callbacks[:empty].each { |cb| cb.call(self) }
-      return items
-    end
-
-    # reset the trash so it doesn't carry over if you parse multiple
-    # times with the same instance
-    @trash.clear
-
-    if cmd = @commands[items[0]]
-      items.shift
-      return cmd.parse! items
-    end
-
-    items.each_with_index do |item, index|
-      @trash << index && break if item == '--'
-      autocreate(items, index) if config[:autocreate]
-      process_item(items, index, &block) unless @trash.include?(index)
-    end
-    items.reject!.with_index { |item, index| @trash.include?(index) }
-
-    missing_options = options.select { |opt| opt.required? && opt.count < 1 }
-    if missing_options.any?
-      raise MissingOptionError,
-      "Missing required option(s): #{missing_options.map(&:key).join(', ')}"
-    end
-
-    if @unknown_options.any?
-      raise InvalidOptionError, "Unknown options #{@unknown_options.join(', ')}"
-    end
-
-    if @triggered_options.empty? && @callbacks[:no_options]
-      @callbacks[:no_options].each { |cb| cb.call(self) }
-    end
-
-    if @runner.respond_to?(:call)
-      @runner.call(self, items) unless config[:help] and present?(:help)
-    end
-
-    items
-  end
-
-  # Add an Option.
-  #
-  # objects - An Array with an optional Hash as the last element.
-  #
-  # Examples:
-  #
-  #   on '-u', '--username=', 'Your username'
-  #   on :v, :verbose, 'Enable verbose mode'
-  #
-  # Returns the created instance of Slop::Option.
-  def on(*objects, &block)
-    option = build_option(objects, &block)
-    original = options.find do |o|
-      o.long and o.long == option.long or o.short and o.short == option.short
-    end
-    options.delete(original) if original
-    options << option
-    option
-  end
-  alias option on
-  alias opt on
-
-  # Fetch an options argument value.
-  #
-  # key - The Symbol or String option short or long flag.
-  #
-  # Returns the Object value for this option, or nil.
-  def [](key)
-    option = fetch_option(key)
-    option.value if option
-  end
-  alias get []
-
-  # Returns a new Hash with option flags as keys and option values as values.
-  #
-  # include_commands - If true, merge options from all sub-commands.
-  def to_hash(include_commands = false)
-    hash = Hash[options.map { |opt| [opt.key.to_sym, opt.value] }]
-    if include_commands
-      @commands.each { |cmd, opts| hash.merge!(cmd.to_sym => opts.to_hash) }
-    end
-    hash
-  end
-  alias to_h to_hash
-
-  # Enumerable interface. Yields each Slop::Option.
-  def each(&block)
-    options.each(&block)
-  end
-
-  # Specify code to be executed when these options are parsed.
-  #
-  # callable - An object responding to a call method.
-  #
-  # yields - The instance of Slop parsing these options
-  #          An Array of unparsed arguments
+  # Parse an array of options (defaults to ARGV). Accepts an
+  # optional hash of configuration options and block.
   #
   # Example:
   #
-  #   Slop.parse do
-  #     on :v, :verbose
-  #
-  #     run do |opts, args|
-  #       puts "Arguments: #{args.inspect}" if opts.verbose?
-  #     end
+  #   opts = Slop.parse(["-host", "localhost"]) do |o|
+  #     o.string '-host', 'a hostname', default: '0.0.0.0'
   #   end
-  def run(callable = nil, &block)
-    @runner = callable || block
-    unless @runner.respond_to?(:call)
-      raise ArgumentError, "You must specify a callable object or a block to #run"
-    end
-  end
-
-  # Check for an options presence.
-  #
-  # Examples:
+  #   opts.to_hash #=> { host: 'localhost' }
   #
-  #   opts.parse %w( --foo )
-  #   opts.present?(:foo) #=> true
-  #   opts.present?(:bar) #=> false
-  #
-  # Returns true if all of the keys are present in the parsed arguments.
-  def present?(*keys)
-    keys.all? { |key| (opt = fetch_option(key)) && opt.count > 0 }
-  end
-
-  # Override this method so we can check if an option? method exists.
-  #
-  # Returns true if this option key exists in our list of options.
-  def respond_to_missing?(method_name, include_private = false)
-    options.any? { |o| o.key == method_name.to_s.chop } || super
-  end
-
-  # Fetch a list of options which were missing from the parsed list.
-  #
-  # Examples:
-  #
-  #   opts = Slop.new do
-  #     on :n, :name=
-  #     on :p, :password=
-  #   end
-  #
-  #   opts.parse %w[ --name Lee ]
-  #   opts.missing #=> ['password']
-  #
-  # Returns an Array of Strings representing missing options.
-  def missing
-    (options - @triggered_options).map(&:key)
-  end
-
-  # Fetch a Slop::Option object.
-  #
-  # key - The Symbol or String option key.
-  #
-  # Examples:
-  #
-  #   opts.on(:foo, 'Something fooey', :argument => :optional)
-  #   opt = opts.fetch_option(:foo)
-  #   opt.class #=> Slop::Option
-  #   opt.accepts_optional_argument? #=> true
-  #
-  # Returns an Option or nil if none were found.
-  def fetch_option(key)
-    options.find { |option| [option.long, option.short].include?(clean(key)) }
-  end
-
-  # Fetch a Slop object associated with this command.
-  #
-  # command - The String or Symbol name of the command.
-  #
-  # Examples:
-  #
-  #   opts.command :foo do
-  #     on :v, :verbose, 'Enable verbose mode'
-  #   end
-  #
-  #   # ruby run.rb foo -v
-  #   opts.fetch_command(:foo).verbose? #=> true
-  def fetch_command(command)
-    @commands[command.to_s]
-  end
-
-  # Add a callback.
-  #
-  # label - The Symbol identifier to attach this callback.
-  #
-  # Returns nothing.
-  def add_callback(label, &block)
-    (@callbacks[label] ||= []) << block
-  end
-
-  # Add string separators between options.
-  #
-  # text - The String text to print.
-  def separator(text)
-    if @separators[options.size]
-      @separators[options.size] << "\n#{text}"
-    else
-      @separators[options.size] = text
-    end
-  end
-
-  # Print a handy Slop help string.
-  #
-  # Returns the banner followed by available option help strings.
-  def to_s
-    heads  = options.reject(&:tail?)
-    tails  = (options - heads)
-    opts = (heads + tails).select(&:help).map(&:to_s)
-    optstr = opts.each_with_index.map { |o, i|
-      (str = @separators[i + 1]) ? [o, str].join("\n") : o
-    }.join("\n")
-
-    if @commands.any?
-      optstr << "\n" if !optstr.empty?
-      optstr << "\nAvailable commands:\n\n"
-      optstr << commands_to_help
-      optstr << "\n\nSee `<command> --help` for more information on a specific command."
-    end
-
-    banner = config[:banner]
-    if banner.nil?
-      banner = "Usage: #{File.basename($0, '.*')}"
-      banner << " #{@command}" if @command
-      banner << " [command]" if @commands.any?
-      banner << " [options]"
-    end
-    if banner
-      "#{banner}\n#{@separators[0] ? "#{@separators[0]}\n" : ''}#{optstr}"
-    else
-      optstr
-    end
-  end
-  alias help to_s
-
-  private
-
-  # Convenience method for present?(:option).
-  #
-  # Examples:
-  #
-  #   opts.parse %( --verbose )
-  #   opts.verbose? #=> true
-  #   opts.other?   #=> false
-  #
-  # Returns true if this option is present. If this method does not end
-  # with a ? character it will instead call super().
-  def method_missing(method, *args, &block)
-    meth = method.to_s
-    if meth.end_with?('?')
-      meth.chop!
-      present?(meth) || present?(meth.gsub('_', '-'))
-    else
-      super
-    end
-  end
-
-  # Process a list item, figure out if it's an option, execute any
-  # callbacks, assign any option arguments, and do some sanity checks.
-  #
-  # items - The Array of items to process.
-  # index - The current Integer index of the item we want to process.
-  # block - An optional block which when passed will yield non options.
-  #
-  # Returns nothing.
-  def process_item(items, index, &block)
-    return unless item = items[index]
-    option, argument = extract_option(item) if item.start_with?('-')
-
-    if option
-      option.count += 1 unless item.start_with?('--no-')
-      option.count += 1 if option.key[0, 3] == "no-"
-      @trash << index
-      @triggered_options << option
-
-      if option.expects_argument?
-        argument ||= items.at(index + 1)
-
-        if !argument || argument =~ /\A--?[a-zA-Z][a-zA-Z0-9_-]*\z/
-          raise MissingArgumentError, "#{option.key} expects an argument"
-        end
-
-        execute_option(option, argument, index, item)
-      elsif option.accepts_optional_argument?
-        argument ||= items.at(index + 1)
-
-        if argument && argument =~ /\A([^\-?]|-\d)+/
-          execute_option(option, argument, index, item)
-        else
-          option.call(nil)
-        end
-      elsif config[:multiple_switches] && argument
-        execute_multiple_switches(option, argument, items, index)
-      else
-        option.value = option.count > 0
-        option.call(nil)
-      end
-    else
-      @unknown_options << item if strict? && item =~ /\A--?/
-      block.call(item) if block && !@trash.include?(index)
-    end
-  end
-
-  # Execute an option, firing off callbacks and assigning arguments.
-  #
-  # option   - The Slop::Option object found by #process_item.
-  # argument - The argument Object to assign to this option.
-  # index    - The current Integer index of the object we're processing.
-  # item     - The optional String item we're processing.
-  #
-  # Returns nothing.
-  def execute_option(option, argument, index, item = nil)
-    if !option
-      if config[:multiple_switches] && strict?
-        raise InvalidOptionError, "Unknown option -#{item}"
-      end
-      return
-    end
-
-    if argument
-      unless item && item.end_with?("=#{argument}")
-        @trash << index + 1 unless option.argument_in_value
-      end
-      option.value = argument
-    else
-      option.value = option.count > 0
-    end
-
-    if option.match? && !argument.match(option.config[:match])
-      raise InvalidArgumentError, "#{argument} is an invalid argument"
-    end
-
-    option.call(option.value)
-  end
-
-  # Execute a `-abc` type option where a, b and c are all options. This
-  # method is only executed if the multiple_switches argument is true.
-  #
-  # option   - The first Option object.
-  # argument - The argument to this option. (Split into multiple Options).
-  # items    - The Array of items currently being parsed.
-  # index    - The index of the current item being processed.
-  #
-  # Returns nothing.
-  def execute_multiple_switches(option, argument, items, index)
-    execute_option(option, nil, index)
-    flags = argument.split('')
-    flags.each do |key|
-      if opt = fetch_option(key)
-        opt.count += 1
-        if (opt.expects_argument? || opt.accepts_optional_argument?) &&
-            (flags[-1] == opt.key) && (val = items[index+1])
-          execute_option(opt, val, index, key)
-        else
-          execute_option(opt, nil, index, key)
-        end
-      else
-        raise InvalidOptionError, "Unknown option -#{key}" if strict?
-      end
-    end
+  # Returns a Slop::Result.
+  def self.parse(items = ARGV, **config, &block)
+    Options.new(config, &block).parse(items)
   end
 
-  # Extract an option from a flag.
-  #
-  # flag - The flag key used to extract an option.
-  #
-  # Returns an Array of [option, argument].
-  def extract_option(flag)
-    option = fetch_option(flag)
-    option ||= fetch_option(flag.downcase) if config[:ignore_case]
-    option ||= fetch_option(flag.gsub(/([^-])-/, '\1_'))
-
-    unless option
-      case flag
-      when /\A--?([^=]+)=(.+)\z/, /\A-([a-zA-Z])(.+)\z/, /\A--no-(.+)\z/
-        option, argument = fetch_option($1), ($2 || false)
-        option.argument_in_value = true if option
-      end
-    end
-
-    [option, argument]
-  end
-
-  # Autocreate an option on the fly. See the :autocreate Slop config option.
+  # Example:
   #
-  # items - The Array of items we're parsing.
-  # index - The current Integer index for the item we're processing.
+  #   Slop.option_defined?(:string) #=> true
+  #   Slop.option_defined?(:omg)    #=> false
   #
-  # Returns nothing.
-  def autocreate(items, index)
-    flag = items[index]
-    if !fetch_option(flag) && !@trash.include?(index)
-      option = build_option(Array(flag))
-      argument = items[index + 1]
-      option.config[:argument] = (argument && argument !~ /\A--?/)
-      option.config[:autocreated] = true
-      options << option
-    end
+  # Returns true if an option is defined.
+  def self.option_defined?(name)
+    const_defined?(string_to_option(name.to_s))
   end
 
-  # Build an option from a list of objects.
-  #
-  # objects - An Array of objects used to build this option.
+  # Example:
   #
-  # Returns a new instance of Slop::Option.
-  def build_option(objects, &block)
-    config = {}
-    config[:argument] = true if @config[:arguments]
-    config[:optional_argument] = true if @config[:optional_arguments]
-
-    if objects.last.is_a?(Hash)
-      config.merge!(objects.pop)
-    end
-
-    short = extract_short_flag(objects, config)
-    long  = extract_long_flag(objects, config)
-    desc  = objects.shift if objects[0].respond_to?(:to_str)
-
-    Option.new(self, short, long, desc, config, &block)
-  end
-
-  def extract_short_flag(objects, config)
-    flag = objects[0].to_s
-    if flag =~ /\A-?\w=?\z/
-      config[:argument] ||= flag.end_with?('=')
-      objects.shift
-      flag.delete('-=')
-    end
-  end
-
-  # Extract the long flag from an item.
+  #   Slop.string_to_option("string")     #=> "StringOption"
+  #   Slop.string_to_option("some_thing") #=> "SomeThingOption"
   #
-  # objects - The Array of objects passed from #build_option.
-  # config  - The Hash of configuration options built in #build_option.
-  def extract_long_flag(objects, config)
-    flag = objects.first.to_s
-    if flag =~ /\A(?:--?)?[a-zA-Z0-9][a-zA-Z0-9_.-]+\=?\??\z/
-      config[:argument] ||= true if flag.end_with?('=')
-      config[:optional_argument] = true if flag.end_with?('=?')
-      objects.shift
-      clean(flag).sub(/\=\??\z/, '')
-    end
+  # Returns a camel-cased class looking string with Option suffix.
+  def self.string_to_option(s)
+    s.gsub(/(?:^|_)([a-z])/) { $1.capitalize } + "Option"
   end
 
-  # Remove any leading -- characters from a string.
+  # Example:
   #
-  # object - The Object we want to cast to a String and clean.
+  #   Slop.string_to_option_class("string") #=> Slop::StringOption
+  #   Slop.string_to_option_class("foo")    #=> uninitialized constant FooOption
   #
-  # Returns the newly cleaned String with leading -- characters removed.
-  def clean(object)
-    object.to_s.sub(/\A--?/, '')
+  # Returns the full qualified option class. Uses `#string_to_option`.
+  def self.string_to_option_class(s)
+    const_get(string_to_option(s))
   end
-
-  def commands_to_help
-    padding = 0
-    @commands.each { |c, _| padding = c.size if c.size > padding }
-    @commands.map do |cmd, opts|
-      "  #{cmd}#{' ' * (padding - cmd.size)}   #{opts.description}"
-    end.join("\n")
-  end
-
 end
diff --git a/lib/slop/commands.rb b/lib/slop/commands.rb
deleted file mode 100644
index cebb560..0000000
--- a/lib/slop/commands.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-class Slop
-  class Commands
-    include Enumerable
-
-    attr_reader :config, :commands, :arguments
-    attr_writer :banner
-
-    # Create a new instance of Slop::Commands and optionally build
-    # Slop instances via a block. Any configuration options used in
-    # this method will be the default configuration options sent to
-    # each Slop object created.
-    #
-    # config - An optional configuration Hash.
-    # block  - Optional block used to define commands.
-    #
-    # Examples:
-    #
-    #   commands = Slop::Commands.new do
-    #     on :new do
-    #       on '-o', '--outdir=', 'The output directory'
-    #       on '-v', '--verbose', 'Enable verbose mode'
-    #     end
-    #
-    #     on :generate do
-    #       on '--assets', 'Generate assets', :default => true
-    #     end
-    #
-    #     global do
-    #       on '-D', '--debug', 'Enable debug mode', :default => false
-    #     end
-    #   end
-    #
-    #   commands[:new].class #=> Slop
-    #   commands.parse
-    #
-    def initialize(config = {}, &block)
-      @config = config
-      @commands = {}
-      @banner = nil
-      @triggered_command = nil
-
-      warn "[DEPRECATED] Slop::Commands is deprecated and will be removed in "\
-        "Slop version 4. Check out http://leejarvis.github.io/slop/#commands for "\
-        "a new implementation of commands."
-
-      if block_given?
-        block.arity == 1 ? yield(self) : instance_eval(&block)
-      end
-    end
-
-    # Optionally set the banner for this command help output.
-    #
-    # banner - The String text to set the banner.
-    #
-    # Returns the String banner if one is set.
-    def banner(banner = nil)
-      @banner = banner if banner
-      @banner
-    end
-
-    # Add a Slop instance for a specific command.
-    #
-    # command - A String or Symbol key used to identify this command.
-    # config  - A Hash of configuration options to pass to Slop.
-    # block   - An optional block used to pass options to Slop.
-    #
-    # Returns the newly created Slop instance mapped to command.
-    def on(command, config = {}, &block)
-      commands[command.to_s] = Slop.new(@config.merge(config), &block)
-    end
-
-    # Add a Slop instance used when no other commands exist.
-    #
-    # config - A Hash of configuration options to pass to Slop.
-    # block  - An optional block used to pass options to Slop.
-    #
-    # Returns the newly created Slop instance mapped to default.
-    def default(config = {}, &block)
-      on('default', config, &block)
-    end
-
-    # Add a global Slop instance.
-    #
-    # config - A Hash of configuration options to pass to Slop.
-    # block  - An optional block used to pass options to Slop.
-    #
-    # Returns the newly created Slop instance mapped to global.
-    def global(config = {}, &block)
-      on('global', config, &block)
-    end
-
-    # Fetch the instance of Slop tied to a command.
-    #
-    # key - The String or Symbol key used to locate this command.
-    #
-    # Returns the Slop instance if this key is found, nil otherwise.
-    def [](key)
-      commands[key.to_s]
-    end
-    alias get []
-
-    # Check for a command presence.
-    #
-    # Examples:
-    #
-    #   cmds.parse %w( foo )
-    #   cmds.present?(:foo) #=> true
-    #   cmds.present?(:bar) #=> false
-    #
-    # Returns true if the given key is present in the parsed arguments.
-    def present?(key)
-      key.to_s == @triggered_command
-    end
-
-    # Enumerable interface.
-    def each(&block)
-      @commands.each(&block)
-    end
-
-    # Parse a list of items.
-    #
-    # items - The Array of items to parse.
-    #
-    # Returns the original Array of items.
-    def parse(items = ARGV)
-      parse! items.dup
-      items
-    end
-
-    # Parse a list of items, removing any options or option arguments found.
-    #
-    # items - The Array of items to parse.
-    #
-    # Returns the original Array of items with options removed.
-    def parse!(items = ARGV)
-      if opts = commands[items[0].to_s]
-        @triggered_command = items.shift
-        execute_arguments! items
-        opts.parse! items
-        execute_global_opts! items
-      else
-        if opts = commands['default']
-          opts.parse! items
-        else
-          if config[:strict] && items[0]
-            raise InvalidCommandError, "Unknown command `#{items[0]}`"
-          end
-        end
-        execute_global_opts! items
-      end
-      items
-    end
-
-    # Returns a nested Hash with Slop options and values. See Slop#to_hash.
-    def to_hash
-      Hash[commands.map { |k, v| [k.to_sym, v.to_hash] }]
-    end
-
-    # Returns the help String.
-    def to_s
-      defaults = commands.delete('default')
-      globals = commands.delete('global')
-      helps = commands.reject { |_, v| v.options.none? }
-      if globals && globals.options.any?
-        helps.merge!('Global options' => globals.to_s)
-      end
-      if defaults && defaults.options.any?
-        helps.merge!('Other options' => defaults.to_s)
-      end
-      banner = @banner ? "#{@banner}\n" : ""
-      banner + helps.map { |key, opts| "  #{key}\n#{opts}" }.join("\n\n")
-    end
-    alias help to_s
-
-    # Returns the inspection String.
-    def inspect
-      "#<Slop::Commands #{config.inspect} #{commands.values.map(&:inspect)}>"
-    end
-
-    private
-
-    # Returns nothing.
-    def execute_arguments!(items)
-      @arguments = items.take_while { |arg| !arg.start_with?('-') }
-      items.shift @arguments.size
-    end
-
-    # Returns nothing.
-    def execute_global_opts!(items)
-      if global_opts = commands['global']
-        global_opts.parse! items
-      end
-    end
-
-  end
-end
diff --git a/lib/slop/error.rb b/lib/slop/error.rb
new file mode 100644
index 0000000..b02de3b
--- /dev/null
+++ b/lib/slop/error.rb
@@ -0,0 +1,35 @@
+module Slop
+  # Base error class.
+  class Error < StandardError
+  end
+
+  # Raised when calling `call` on Slop::Option (this
+  # method must be overriden in subclasses)
+  class NotImplementedError < Error
+  end
+
+  # Raised when an option that expects an argument is
+  # executed without one. Suppress with the `suppress_errors`
+  # config option.
+  class MissingArgument < Error
+    attr_reader :flags
+
+    # Get all the flags that matches
+    # the option with the missing argument
+    def initialize(msg, flags)
+      super(msg)
+      @flags = flags
+    end
+  end
+
+  # Raised when an unknown option is parsed. Suppress
+  # with the `suppress_errors` config option.
+  class UnknownOption < Error
+    attr_reader :flag
+
+    def initialize(msg, flag)
+      super(msg)
+      @flag = flag
+    end
+  end
+end
diff --git a/lib/slop/option.rb b/lib/slop/option.rb
index f2f6a19..2164f1d 100644
--- a/lib/slop/option.rb
+++ b/lib/slop/option.rb
@@ -1,214 +1,129 @@
-class Slop
+module Slop
   class Option
-
-    # The default Hash of configuration options this class uses.
-    DEFAULT_OPTIONS = {
-      :argument => false,
-      :optional_argument => false,
-      :tail => false,
-      :default => nil,
-      :callback => nil,
-      :delimiter => ',',
-      :limit => 0,
-      :match => nil,
-      :optional => true,
-      :required => false,
-      :as => String,
-      :autocreated => false
+    DEFAULT_CONFIG = {
+      help: true,
+      tail: false,
     }
 
-    attr_reader :short, :long, :description, :config, :types
-    attr_accessor :count, :argument_in_value
-
-    # Incapsulate internal option information, mainly used to store
-    # option specific configuration data, most of the meat of this
-    # class is found in the #value method.
-    #
-    # slop        - The instance of Slop tied to this Option.
-    # short       - The String or Symbol short flag.
-    # long        - The String or Symbol long flag.
-    # description - The String description text.
-    # config      - A Hash of configuration options.
-    # block       - An optional block used as a callback.
-    def initialize(slop, short, long, description, config = {}, &block)
-      @slop = slop
-      @short = short
-      @long = long
-      @description = description
-      @config = DEFAULT_OPTIONS.merge(config)
-      @count = 0
-      @callback = block_given? ? block : config[:callback]
+    # An Array of flags this option matches.
+    attr_reader :flags
+
+    # A custom description used for the help text.
+    attr_reader :desc
+
+    # A Hash of configuration options.
+    attr_reader :config
+
+    # An Integer count for the total times this option
+    # has been executed.
+    attr_reader :count
+
+    # A custom proc that yields the option value when
+    # it's executed.
+    attr_reader :block
+
+    # The end value for this option.
+    attr_writer :value
+
+    def initialize(flags, desc, **config, &block)
+      @flags  = flags
+      @desc   = desc
+      @config = DEFAULT_CONFIG.merge(config)
+      @block  = block
+      reset
+    end
+
+    # Reset the option count and value. Used when calling .reset
+    # on the Parser.
+    def reset
       @value = nil
+      @count = 0
+    end
 
-      @types = {
-        :string  => proc { |v| v.to_s },
-        :symbol  => proc { |v| v.to_sym },
-        :integer => proc { |v| value_to_integer(v) },
-        :float   => proc { |v| value_to_float(v) },
-        :range   => proc { |v| value_to_range(v) },
-        :regexp  => proc { |v| Regexp.new(v) },
-        :count   => proc { |v| @count }
-      }
-
-      if long && long.size > @slop.config[:longest_flag]
-        @slop.config[:longest_flag] = long.size
-      end
+    # Since `call()` can be used/overriden in subclasses, this
+    # method is used to do general tasks like increment count. This
+    # ensures you don't *have* to call `super` when overriding `call()`.
+    # It's used in the Parser.
+    def ensure_call(value)
+      @count += 1
 
-      @config.each_key do |key|
-        predicate = :"#{key}?"
-        unless self.class.method_defined? predicate
-          self.class.__send__(:define_method, predicate) { !!@config[key] }
-        end
+      if value.nil? && expects_argument? && !suppress_errors?
+        raise Slop::MissingArgument.new("missing argument for #{flag}", flags)
       end
-    end
 
-    # Returns true if this option expects an argument.
-    def expects_argument?
-      config[:argument] && config[:argument] != :optional
+      @value = call(value)
+      block.call(@value) if block.respond_to?(:call)
     end
 
-    # Returns true if this option accepts an optional argument.
-    def accepts_optional_argument?
-      config[:optional_argument] || config[:argument] == :optional
+    # This method is called immediately when an option is found.
+    # Override it in sub-classes.
+    def call(_value)
+      raise NotImplementedError,
+        "you must override the `call' method for option #{self.class}"
     end
 
-    # Returns the String flag of this option. Preferring the long flag.
-    def key
-      long || short
-    end
-
-    # Call this options callback if one exists, and it responds to call().
-    #
-    # Returns nothing.
-    def call(*objects)
-      @callback.call(*objects) if @callback.respond_to?(:call)
-    end
-
-    # Set the new argument value for this option.
-    #
-    # We use this setter method to handle concatenating lists. That is,
-    # when an array type is specified and used more than once, values from
-    # both options will be grouped together and flattened into a single array.
-    def value=(new_value)
-      if config[:as].to_s.downcase == 'array'
-        @value ||= []
-
-        if new_value.respond_to?(:split)
-          @value.concat new_value.split(config[:delimiter], config[:limit])
-        end
-      else
-        @value = new_value
-      end
+    # By default this method does nothing. It's called when all options
+    # have been parsed and allows you to mutate the `@value` attribute
+    # according to other options.
+    def finish(_result)
     end
 
-    # Fetch the argument value for this option.
-    #
-    # Returns the Object once any type conversions have taken place.
-    def value
-      value = @value.nil? ? config[:default] : @value
+    # Override this if this option type does not expect an argument
+    # (i.e a boolean option type).
+    def expects_argument?
+      true
+    end
 
-      if [true, false, nil].include?(value) && config[:as].to_s != 'count'
-        return value
-      end
+    # Override this if you want to ignore the return value for an option
+    # (i.e so Result#to_hash does not include it).
+    def null?
+      false
+    end
 
-      type = config[:as]
-      if type.respond_to?(:call)
-        type.call(value)
-      else
-        if callable = types[type.to_s.downcase.to_sym]
-          callable.call(value)
-        else
-          value
-        end
-      end
+    # Returns the value for this option. Falls back to the default (or nil).
+    def value
+      @value || default_value
     end
 
-    # Returns the help String for this option.
-    def to_s
-      return config[:help] if config[:help].respond_to?(:to_str)
+    # Returns the default value for this option (default is nil).
+    def default_value
+      config[:default]
+    end
 
-      out = "    #{short ? "-#{short}, " : ' ' * 4}"
+    # Returns true if we should ignore errors that cause exceptions to be raised.
+    def suppress_errors?
+      config[:suppress_errors]
+    end
 
-      if long
-        out << "--#{long}"
-        size = long.size
-        diff = @slop.config[:longest_flag] - size
-        out << (' ' * (diff + 6))
-      else
-        out << (' ' * (@slop.config[:longest_flag] + 8))
-      end
+    # Returns all flags joined by a comma. Used by the help string.
+    def flag
+      flags.join(", ")
+    end
 
-      if config[:default]
-        default = config[:default]
-        "#{out}#{description} (default: #{default})"
-      else
-        "#{out}#{description}"
-      end
+    # Returns the last key as a symbol. Used in Options.to_hash.
+    def key
+      (config[:key] || flags.last.sub(/\A--?/, '')).tr("-", "_").to_sym
     end
-    alias help to_s
-
-    # Returns the String inspection text.
-    def inspect
-      "#<Slop::Option [-#{short} | --#{long}" +
-      "#{'=' if expects_argument?}#{'=?' if accepts_optional_argument?}]" +
-      " (#{description}) #{config.inspect}"
-    end
-
-    private
-
-    # Convert an object to an Integer if possible.
-    #
-    # value - The Object we want to convert to an integer.
-    #
-    # Returns the Integer value if possible to convert, else a zero.
-    def value_to_integer(value)
-      if @slop.strict?
-        begin
-          Integer(value.to_s, 10)
-        rescue ArgumentError
-          raise InvalidArgumentError, "#{value} could not be coerced into Integer"
-        end
-      else
-        value.to_s.to_i
-      end
+
+    # Returns true if this option should be displayed in help text.
+    def help?
+      config[:help]
     end
 
-    # Convert an object to a Float if possible.
-    #
-    # value - The Object we want to convert to a float.
-    #
-    # Returns the Float value if possible to convert, else a zero.
-    def value_to_float(value)
-      if @slop.strict?
-        begin
-          Float(value.to_s)
-        rescue ArgumentError
-          raise InvalidArgumentError, "#{value} could not be coerced into Float"
-        end
-      else
-        value.to_s.to_f
-      end
+    # Returns true if this option should be added to the tail of the help text.
+    def tail?
+      config[:tail]
     end
 
-    # Convert an object to a Range if possible.
-    #
-    # value - The Object we want to convert to a range.
-    #
-    # Returns the Range value if one could be found, else the original object.
-    def value_to_range(value)
-      case value.to_s
-      when /\A(\-?\d+)\z/
-        Range.new($1.to_i, $1.to_i)
-      when /\A(-?\d+?)(\.\.\.?|-|,)(-?\d+)\z/
-        Range.new($1.to_i, $3.to_i, $2 == '...')
-      else
-        if @slop.strict?
-          raise InvalidArgumentError, "#{value} could not be coerced into Range"
-        else
-          value
-        end
-      end
+    # Returns 1 if this option should be added to the tail of the help text.
+    # Used for sorting.
+    def tail
+      tail? ? 1 : -1
     end
 
+    # Returns the help text for this option (flags and description).
+    def to_s(offset: 0)
+      "%-#{offset}s  %s" % [flag, desc]
+    end
   end
 end
diff --git a/lib/slop/options.rb b/lib/slop/options.rb
new file mode 100644
index 0000000..ed99c64
--- /dev/null
+++ b/lib/slop/options.rb
@@ -0,0 +1,141 @@
+module Slop
+  class Options
+    include Enumerable
+
+    DEFAULT_CONFIG = {
+      suppress_errors: false,
+      type:            "null",
+      banner:          true,
+    }
+
+    # The Array of Option instances we've created.
+    attr_reader :options
+
+    # An Array of separators used for the help text.
+    attr_reader :separators
+
+    # Our Parser instance.
+    attr_reader :parser
+
+    # A Hash of configuration options.
+    attr_reader :config
+
+    # The String banner prefixed to the help string.
+    attr_accessor :banner
+
+    def initialize(**config)
+      @options    = []
+      @separators = []
+      @banner     = "usage: #{$0} [options]"
+      @config     = DEFAULT_CONFIG.merge(config)
+      @parser     = Parser.new(self, @config)
+
+      yield self if block_given?
+    end
+
+    # Add a new option. This method is an alias for adding a NullOption
+    # (i.e an option with an ignored return value).
+    #
+    # Example:
+    #
+    #   opts = Slop.parse do |o|
+    #     o.on '--version' do
+    #       puts Slop::VERSION
+    #     end
+    #   end
+    #
+    #   opts.to_hash #=> {}
+    #
+    # Returns the newly created Option subclass.
+    def on(*flags, **config, &block)
+      desc   = flags.pop unless flags.last.start_with?('-')
+      config = self.config.merge(config)
+      klass  = Slop.string_to_option_class(config[:type].to_s)
+      option = klass.new(flags, desc, config, &block)
+
+      add_option option
+    end
+
+    # Add a separator between options. Used when displaying
+    # the help text.
+    def separator(string)
+      if separators[options.size]
+        separators.last << "\n#{string}"
+      else
+        separators[options.size] = string
+      end
+    end
+
+    # Sugar to avoid `options.parser.parse(x)`.
+    def parse(strings)
+      parser.parse(strings)
+    end
+
+    # Implements the Enumerable interface.
+    def each(&block)
+      options.each(&block)
+    end
+
+    # Handle custom option types. Will fall back to raising an
+    # exception if an option is not defined.
+    def method_missing(name, *args, **config, &block)
+      if respond_to_missing?(name)
+        config[:type] = name
+        on(*args, config, &block)
+      else
+        super
+      end
+    end
+
+    def respond_to_missing?(name, include_private = false)
+      Slop.option_defined?(name) || super
+    end
+
+    # Return a copy of our options Array.
+    def to_a
+      options.dup
+    end
+
+    # Returns the help text for this options. Used by Result#to_s.
+    def to_s(prefix: " " * 4)
+      str = config[:banner] ? "#{banner}\n" : ""
+      len = longest_flag_length
+
+      options.select(&:help?).sort_by(&:tail).each_with_index do |opt, i|
+        # use the index to fetch an associated separator
+        if sep = separators[i]
+          str << "#{sep}\n"
+        end
+
+        str << "#{prefix}#{opt.to_s(offset: len)}\n"
+      end
+
+      str
+    end
+
+    private
+
+    def longest_flag_length
+      (o = longest_option) && o.flag.length || 0
+    end
+
+    def longest_option
+      options.max { |a, b| a.flag.length <=> b.flag.length }
+    end
+
+    def add_option(option)
+      options.each do |o|
+        flags = o.flags & option.flags
+
+        # Raise an error if we found an existing option with the same
+        # flags. I can't immediately see a use case for this..
+        if flags.any?
+          raise ArgumentError, "duplicate flags: #{flags}"
+        end
+      end
+
+      options << option
+      option
+    end
+  end
+end
diff --git a/lib/slop/parser.rb b/lib/slop/parser.rb
new file mode 100644
index 0000000..1bfa134
--- /dev/null
+++ b/lib/slop/parser.rb
@@ -0,0 +1,120 @@
+module Slop
+  class Parser
+
+    # Our Options instance.
+    attr_reader :options
+
+    # A Hash of configuration options.
+    attr_reader :config
+
+    # Returns an Array of String arguments that were not parsed.
+    attr_reader :arguments
+
+    def initialize(options, **config)
+      @options = options
+      @config  = config
+      reset
+    end
+
+    # Reset the parser, useful to use the same instance to parse a second
+    # time without duplicating state.
+    def reset
+      @arguments = []
+      @options.each(&:reset)
+      self
+    end
+
+    # Traverse `strings` and process options one by one. Anything after
+    # `--` is ignored. If a flag includes a equals (=) it will be split
+    # so that `flag, argument = s.split('=')`.
+    #
+    # The `call` method will be executed immediately for each option found.
+    # Once all options have been executed, any found options will have
+    # the `finish` method called on them.
+    #
+    # Returns a Slop::Result.
+    def parse(strings)
+      reset # reset before every parse
+
+      pairs = strings.each_cons(2).to_a
+      # this ensures we still support the last string being a flag,
+      # otherwise it'll only be used as an argument.
+      pairs << [strings.last, nil]
+
+      @arguments = strings.dup
+
+      pairs.each do |flag, arg|
+        break if !flag
+
+        # ignore everything after '--', flag or not
+        if flag == '--'
+          arguments.delete(flag)
+          break
+        end
+
+        # support `foo=bar`
+        if flag.include?("=")
+          flag, arg = flag.split("=")
+        end
+
+        if opt = try_process(flag, arg)
+          # since the option was parsed, we remove it from our
+          # arguments (plus the arg if necessary)
+          arguments.delete(flag)
+          arguments.delete(arg) if opt.expects_argument?
+        end
+      end
+
+      Result.new(self).tap do |result|
+        used_options.each { |o| o.finish(result) }
+      end
+    end
+
+    # Returns an Array of Option instances that were used.
+    def used_options
+      options.select { |o| o.count > 0 }
+    end
+
+    # Returns an Array of Option instances that were not used.
+    def unused_options
+      options.to_a - used_options
+    end
+
+    private
+
+    # We've found an option, process and return it
+    def process(option, arg)
+      option.ensure_call(arg)
+      option
+    end
+
+    # Try and find an option to process
+    def try_process(flag, arg)
+      if option = matching_option(flag)
+        process(option, arg)
+      elsif flag.start_with?("--no-") && option = matching_option(flag.sub("no-", ""))
+        process(option, false)
+      elsif flag =~ /\A-[^-]/ && flag.size > 2
+        # try and process as a set of grouped short flags. drop(1) removes
+        # the prefixed -, then we add them back to each flag separately.
+        flags = flag.split("").drop(1).map { |f| "-#{f}" }
+        last  = flags.pop
+
+        flags.each { |f| try_process(f, nil) }
+        try_process(last, arg) # send the argument to the last flag
+      else
+        if flag.start_with?("-") && !suppress_errors?
+          raise UnknownOption.new("unknown option `#{flag}'", "#{flag}")
+        end
+      end
+    end
+
+    def suppress_errors?
+      config[:suppress_errors]
+    end
+
+    def matching_option(flag)
+      options.find { |o| o.flags.include?(flag) }
+    end
+  end
+end
diff --git a/lib/slop/result.rb b/lib/slop/result.rb
new file mode 100644
index 0000000..6f56248
--- /dev/null
+++ b/lib/slop/result.rb
@@ -0,0 +1,90 @@
+module Slop
+  # This class encapsulates a Parser and Options pair. The idea is that
+  # the Options class shouldn't have to deal with what happens when options
+  # are parsed, and the Parser shouldn't have to deal with the state of
+  # options once parsing is complete. This keeps the API really simple; A
+  # Parser parses, Options handles options, and this class handles the
+  # result of those actions. This class contains the important most used
+  # methods.
+  class Result
+    attr_reader :parser, :options
+
+    def initialize(parser)
+      @parser  = parser
+      @options = parser.options
+    end
+
+    # Returns an options value, nil if the option does not exist.
+    def [](flag)
+      (o = option(flag)) && o.value
+    end
+    alias get []
+
+    # Set the value for an option. Raises an ArgumentError if the option
+    # does not exist.
+    def []=(flag, value)
+      if o = option(flag)
+        o.value = value
+      else
+        raise ArgumentError, "no option with flag `#{flag}'"
+      end
+    end
+    alias set []=
+
+    # Returns an Option if it exists. Ignores any prefixed hyphens.
+    def option(flag)
+      cleaned = -> (f) { f.to_s.sub(/\A--?/, '').tr('_', '-') }
+      options.find do |o|
+        o.flags.any? { |f| cleaned.(f) == cleaned.(flag) }
+      end
+    end
+
+    def method_missing(name, *args, &block)
+      if respond_to_missing?(name)
+        (o = option(name.to_s.chomp("?"))) && used_options.include?(o)
+      else
+        super
+      end
+    end
+
+    def respond_to_missing?(name, include_private = false)
+      name.to_s.end_with?("?") || super
+    end
+
+    # Returns an Array of Option instances that were used.
+    def used_options
+      parser.used_options
+    end
+
+    # Returns an Array of Option instances that were not used.
+    def unused_options
+      parser.unused_options
+    end
+
+    # Example:
+    #
+    #   opts = Slop.parse do |o|
+    #     o.string '--host'
+    #     o.int '-p'
+    #   end
+    #
+    #   # ruby run.rb connect --host 123 helo
+    #   opts.arguments #=> ["connect", "helo"]
+    #
+    # Returns an Array of String arguments that were not parsed.
+    def arguments
+      parser.arguments
+    end
+    alias args arguments
+
+    # Returns a hash with option key => value.
+    def to_hash
+      Hash[options.reject(&:null?).map { |o| [o.key, o.value] }]
+    end
+    alias to_h to_hash
+
+    def to_s(**opts)
+      options.to_s(**opts)
+    end
+  end
+end
diff --git a/lib/slop/types.rb b/lib/slop/types.rb
new file mode 100644
index 0000000..672aef3
--- /dev/null
+++ b/lib/slop/types.rb
@@ -0,0 +1,96 @@
+module Slop
+  # Cast the option argument to a String.
+  class StringOption < Option
+    def call(value)
+      value.to_s
+    end
+  end
+
+  # Cast the option argument to true or false.
+  # Override default_value to default to false instead of nil.
+  # This option type does not expect an argument. However, the API
+  # supports value being passed. This is to ensure it can capture
+  # an explicit false value
+  class BoolOption < Option
+    attr_accessor :explicit_value
+
+    def call(value)
+      self.explicit_value = value
+      true
+    end
+
+    def value
+      if force_false?
+        false
+      else
+        super
+      end
+    end
+
+    def force_false?
+      explicit_value == false
+    end
+
+    def default_value
+      config[:default] || false
+    end
+
+    def expects_argument?
+      false
+    end
+  end
+  BooleanOption = BoolOption
+
+  # Cast the option argument to an Integer.
+  class IntegerOption < Option
+    def call(value)
+      value =~ /\A\d+\z/ && value.to_i
+    end
+  end
+  IntOption = IntegerOption
+
+  # Cast the option argument to a Float.
+  class FloatOption < Option
+    def call(value)
+      # TODO: scientific notation, etc.
+      value =~ /\A\d*\.*\d+\z/ && value.to_f
+    end
+  end
+
+  # Collect multiple items into a single Array. Support
+  # arguments separated by commas or multiple occurences.
+  class ArrayOption < Option
+    def call(value)
+      @value ||= []
+      @value.concat value.split(delimiter, limit)
+    end
+
+    def default_value
+      config[:default] || []
+    end
+
+    def delimiter
+      config[:delimiter] || ","
+    end
+
+    def limit
+      config[:limit] || 0
+    end
+  end
+
+  # Cast the option argument to a Regexp.
+  class RegexpOption < Option
+    def call(value)
+      Regexp.new(value)
+    end
+  end
+
+  # An option that discards the return value, inherits from Bool
+  # since it does not expect an argument.
+  class NullOption < BoolOption
+    def null?
+      true
+    end
+  end
+
+end
diff --git a/metadata.yml b/metadata.yml
index d5af654..d8c596b 100644
--- a/metadata.yml
+++ b/metadata.yml
@@ -1,64 +1,71 @@
 --- !ruby/object:Gem::Specification
 name: slop
 version: !ruby/object:Gem::Version
-  version: 3.6.0
+  version: 4.2.0
 platform: ruby
 authors:
 - Lee Jarvis
 autorequire: 
 bindir: bin
 cert_chain: []
-date: 2014-07-18 00:00:00.000000000 Z
+date: 2015-06-18 00:00:00.000000000 Z
 dependencies:
 - !ruby/object:Gem::Dependency
   name: rake
   requirement: !ruby/object:Gem::Requirement
     requirements:
-    - - '>='
+    - - ">="
       - !ruby/object:Gem::Version
         version: '0'
   type: :development
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
     requirements:
-    - - '>='
+    - - ">="
       - !ruby/object:Gem::Version
         version: '0'
 - !ruby/object:Gem::Dependency
   name: minitest
   requirement: !ruby/object:Gem::Requirement
     requirements:
-    - - ~>
+    - - "~>"
       - !ruby/object:Gem::Version
         version: 5.0.0
   type: :development
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
     requirements:
-    - - ~>
+    - - "~>"
       - !ruby/object:Gem::Version
         version: 5.0.0
-description: A simple DSL for gathering options and parsing the command line
+description: A DSL for gathering options and parsing command line flags
 email: ljjarvis at gmail.com
 executables: []
 extensions: []
 extra_rdoc_files: []
 files:
-- .gitignore
-- .travis.yml
-- CHANGES.md
+- ".gitignore"
+- ".travis.yml"
+- CHANGELOG.md
 - Gemfile
 - LICENSE
 - README.md
 - Rakefile
 - lib/slop.rb
-- lib/slop/commands.rb
+- lib/slop/error.rb
 - lib/slop/option.rb
+- lib/slop/options.rb
+- lib/slop/parser.rb
+- lib/slop/result.rb
+- lib/slop/types.rb
 - slop.gemspec
-- test/commands_test.rb
-- test/helper.rb
+- test/error_test.rb
 - test/option_test.rb
-- test/slop_test.rb
+- test/options_test.rb
+- test/parser_test.rb
+- test/result_test.rb
+- test/test_helper.rb
+- test/types_test.rb
 homepage: http://github.com/leejarvis/slop
 licenses:
 - MIT
@@ -69,22 +76,26 @@ require_paths:
 - lib
 required_ruby_version: !ruby/object:Gem::Requirement
   requirements:
-  - - '>='
+  - - ">="
     - !ruby/object:Gem::Version
-      version: 1.8.7
+      version: 2.0.0
 required_rubygems_version: !ruby/object:Gem::Requirement
   requirements:
-  - - '>='
+  - - ">="
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
 rubyforge_project: 
-rubygems_version: 2.0.14
+rubygems_version: 2.4.5
 signing_key: 
 specification_version: 4
 summary: Simple Lightweight Option Parsing
 test_files:
-- test/commands_test.rb
-- test/helper.rb
+- test/error_test.rb
 - test/option_test.rb
-- test/slop_test.rb
+- test/options_test.rb
+- test/parser_test.rb
+- test/result_test.rb
+- test/test_helper.rb
+- test/types_test.rb
+has_rdoc: 
diff --git a/slop.gemspec b/slop.gemspec
index d535e60..b94a1f2 100644
--- a/slop.gemspec
+++ b/slop.gemspec
@@ -1,8 +1,11 @@
+$:.unshift './lib'
+require 'slop'
+
 Gem::Specification.new do |s|
   s.name        = 'slop'
-  s.version     = '3.6.0'
+  s.version     = Slop::VERSION
   s.summary     = 'Simple Lightweight Option Parsing'
-  s.description = 'A simple DSL for gathering options and parsing the command line'
+  s.description = 'A DSL for gathering options and parsing command line flags'
   s.author      = 'Lee Jarvis'
   s.email       = 'ljjarvis at gmail.com'
   s.homepage    = 'http://github.com/leejarvis/slop'
@@ -10,7 +13,7 @@ Gem::Specification.new do |s|
   s.test_files  = `git ls-files -- test/*`.split("\n")
   s.license     = 'MIT'
 
-  s.required_ruby_version = '>= 1.8.7'
+  s.required_ruby_version = '>= 2.0.0'
 
   s.add_development_dependency 'rake'
   s.add_development_dependency 'minitest', '~> 5.0.0'
diff --git a/test/commands_test.rb b/test/commands_test.rb
deleted file mode 100644
index 8f0e84a..0000000
--- a/test/commands_test.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'helper'
-
-class CommandsTest < TestCase
-
-  def setup
-    @opts = Slop.new do |o|
-      o.on :v, :version
-      o.command :add do |add|
-        add.on :v, 'verbose mode'
-      end
-    end
-  end
-
-  test "parse! removes the command AND its options" do
-    items = %w'add -v'
-    @opts.parse! items
-    assert_equal [], items
-  end
-
-  test "parse does not remove the command or its options" do
-    items = %w'add -v'
-    @opts.parse items
-    assert_equal ['add', '-v'], items
-  end
-
-end
diff --git a/test/error_test.rb b/test/error_test.rb
new file mode 100644
index 0000000..848e2d5
--- /dev/null
+++ b/test/error_test.rb
@@ -0,0 +1,45 @@
+require 'test_helper'
+
+# All raised errors tested here
+
+describe Slop::MissingArgument do
+  it "raises when an argument is missing" do
+    opts = Slop::Options.new
+    opts.string "-n", "--name"
+    assert_raises(Slop::MissingArgument) { opts.parse %w(--name) }
+
+    #Assert returns the argument question
+    begin
+      opts.parse %w(--name)
+    rescue Slop::MissingArgument => e
+      assert_equal(e.flags, ["-n", "--name"])
+    end
+  end
+
+  it "does not raise when errors are suppressed" do
+    opts = Slop::Options.new(suppress_errors: true)
+    opts.string "-n", "--name"
+    opts.parse %w(--name)
+  end
+end
+
+describe Slop::UnknownOption do
+  it "raises when an option is unknown" do
+    opts = Slop::Options.new
+    opts.string "-n", "--name"
+    assert_raises(Slop::UnknownOption) { opts.parse %w(--foo) }
+
+    #Assert returns the unknown option in question
+    begin
+      opts.parse %w(--foo)
+    rescue Slop::UnknownOption => e
+      assert_equal(e.flag, "--foo")
+    end
+  end
+
+  it "does not raise when errors are suppressed" do
+    opts = Slop::Options.new(suppress_errors: true)
+    opts.string "-n", "--name"
+    opts.parse %w(--foo)
+  end
+end
diff --git a/test/helper.rb b/test/helper.rb
deleted file mode 100644
index e6ec32f..0000000
--- a/test/helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-$VERBOSE = true
-
-require 'slop'
-
-require 'minitest/autorun'
-require 'stringio'
-
-class TestCase < Minitest::Test
-  def self.test(name, &block)
-    define_method("test_#{name.gsub(/\W/, '_')}", &block) if block
-  end
-end
\ No newline at end of file
diff --git a/test/option_test.rb b/test/option_test.rb
index cec73c9..78fd55e 100644
--- a/test/option_test.rb
+++ b/test/option_test.rb
@@ -1,145 +1,28 @@
-require 'helper'
+require 'test_helper'
 
-class OptionTest < TestCase
-  def option(*args, &block)
-    Slop.new.on(*args, &block)
+describe Slop::Option do
+  def option(*args)
+    Slop::Option.new(*args)
   end
 
-  def option_with_argument(*args, &block)
-    options = args.shift
-    slop = Slop.new
-    option = slop.opt(*args)
-    slop.parse(options)
-    slop.options.find {|opt| opt.key == option.key }
+  describe "#flag" do
+    it "returns the flags joined by a comma" do
+      assert_equal "-f, --bar", option(%w(-f --bar), nil).flag
+      assert_equal "--bar", option(%w(--bar), nil).flag
+    end
   end
 
-  def option_value(*args, &block)
-    option_with_argument(*args, &block).value
-  end
-
-  test "expects_argument?" do
-    assert option(:f=).expects_argument?
-    assert option(:foo=).expects_argument?
-    assert option(:foo, :argument => true).expects_argument?
-  end
-
-  test "accepts_optional_argument?" do
-    refute option(:f=).accepts_optional_argument?
-    assert option(:f=, :argument => :optional).accepts_optional_argument?
-    assert option(:f, :optional_argument => true).accepts_optional_argument?
-  end
-
-  test "key" do
-    assert_equal 'foo', option(:foo).key
-    assert_equal 'foo', option(:f, :foo).key
-    assert_equal 'f', option(:f).key
-  end
-
-  test "call" do
-    foo = nil
-    option(:f, :callback => proc { foo = "bar" }).call
-    assert_equal "bar", foo
-    option(:f) { foo = "baz" }.call
-    assert_equal "baz", foo
-    option(:f) { |o| assert_equal 1, o }.call(1)
-  end
-
-  # type casting
-
-  test "proc/custom type cast" do
-    assert_equal 1, option_value(%w'-f 1', :f=, :as => proc {|x| x.to_i })
-    assert_equal "oof", option_value(%w'-f foo', :f=, :as => proc {|x| x.reverse })
-  end
-
-  test "integer type cast" do
-    opts = Slop.new
-    opts.on :f=, :as => Integer
-    opts.parse %w'-f 1'
-    assert_equal 1, opts[:f]
-
-    opts = Slop.new(:strict => true) { on :r=, :as => Integer }
-    assert_raises(Slop::InvalidArgumentError) { opts.parse %w/-r abc/ }
-  end
-
-  test "float type cast" do
-    opts = Slop.new(:strict => true) { on :r=, :as => Float }
-    assert_raises(Slop::InvalidArgumentError) { opts.parse %w/-r abc/ }
-  end
-
-  test "symbol type cast" do
-    assert_equal :foo, option_value(%w'-f foo', :f=, :as => Symbol)
-  end
+  describe "#key" do
+    it "uses the last flag and strips trailing hyphens" do
+      assert_equal :foo, option(%w(-f --foo), nil).key
+    end
 
-  test "range type cast" do
-    assert_equal((1..10), option_value(%w/-r 1..10/, :r=, :as => Range))
-    assert_equal((1..10), option_value(%w/-r 1-10/, :r=, :as => Range))
-    assert_equal((1..10), option_value(%w/-r 1,10/, :r=, :as => Range))
-    assert_equal((1...10), option_value(%w/-r 1...10/, :r=, :as => Range))
-    assert_equal((-1..10), option_value(%w/-r -1..10/, :r=, :as => Range))
-    assert_equal((1..-10), option_value(%w/-r 1..-10/, :r=, :as => Range))
-    assert_equal((1..1), option_value(%w/-r 1/, :r=, :as => Range))
-    assert_equal((-1..10), option_value(%w/-r -1..10/, :r, :as => Range, :optional_argument => true))
-
-    opts = Slop.new(:strict => true) { on :r=, :as => Range }
-    assert_raises(Slop::InvalidArgumentError) { opts.parse %w/-r abc/ }
-  end
-
-  test "array type cast" do
-    assert_equal %w/lee john bill/, option_value(%w/-p lee,john,bill/, :p=, :as => Array)
-    assert_equal %w/lee john bill jeff jill/, option_value(%w/-p lee,john,bill -p jeff,jill/, :p=, :as => Array)
-    assert_equal %w/lee john bill/, option_value(%w/-p lee:john:bill/, :p=, :as => Array, :delimiter => ':')
-    assert_equal %w/lee john,bill/, option_value(%w/-p lee,john,bill/, :p=, :as => Array, :limit => 2)
-    assert_equal %w/lee john:bill/, option_value(%w/-p lee:john:bill/, :p=, :as => Array, :limit => 2, :delimiter => ':')
-  end
-
-  test "regexp type cast" do
-    assert_equal Regexp.new("foo"), option_value(%w/-p foo/, :p=, :as => Regexp)
-  end
-
-  test "adding custom types" do
-    opts = Slop.new
-    opt = opts.on :f=, :as => :reverse
-    opt.types[:reverse] = proc { |v| v.reverse }
-    opts.parse %w'-f bar'
-    assert_equal 'rab', opt.value
-  end
-
-  test "count type" do
-    assert_equal 3, option_value(%w/-c -c -c/, :c, :as => :count)
-    assert_equal 0, option_value(%w/-a -b -z/, :c, :as => :count)
-    assert_equal 3, option_value(%w/-vvv/, :v, :as => :count)
-  end
-
-  # end type casting tests
-
-  test "using a default value as fallback" do
-    opts = Slop.new
-    opts.on :f, :argument => :optional, :default => 'foo'
-    opts.parse %w'-f'
-    assert_equal 'foo', opts[:f]
-  end
-
-  test "printing options" do
-    slop = Slop.new
-    slop.opt :n, :name=, 'Your name'
-    slop.opt :age=, 'Your age'
-    slop.opt :V, 'Display the version'
-
-    assert_equal "    -n, --name      Your name", slop.fetch_option(:name).to_s
-    assert_equal "        --age       Your age", slop.fetch_option(:age).to_s
-    assert_equal "    -V,             Display the version", slop.fetch_option(:V).help
-  end
-
-  test "printing options that have defaults" do
-    opts = Slop.new
-    opts.on :n, :name=, 'Your name', :default => 'Lee'
-
-    assert_equal "    -n, --name      Your name (default: Lee)", opts.fetch_option(:name).to_s
-  end
+    it "converts dashes to underscores to make multi-word options symbol-friendly" do
+      assert_equal :foo_bar, option(%w(-f --foo-bar), nil).key
+    end
 
-  test "overwriting the help text" do
-    slop = Slop.new
-    slop.on :foo, :help => '    -f, --foo  SOMETHING FOOEY'
-    assert_equal '    -f, --foo  SOMETHING FOOEY', slop.fetch_option(:foo).help
+    it "can be overridden" do
+      assert_equal :bar, option(%w(-f --foo), nil, key: "bar").key
+    end
   end
 end
diff --git a/test/options_test.rb b/test/options_test.rb
new file mode 100644
index 0000000..65461db
--- /dev/null
+++ b/test/options_test.rb
@@ -0,0 +1,85 @@
+require 'test_helper'
+
+describe Slop::Options do
+  before do
+    @options = Slop::Options.new
+  end
+
+  describe "#on" do
+    it "defaults to null type" do
+      assert_kind_of Slop::NullOption, @options.on("--foo")
+    end
+
+    it "accepts custom types" do
+      module Slop; class FooOption < Option; end; end
+      assert_kind_of Slop::FooOption, @options.on("--foo", type: :foo)
+    end
+
+    it "adds multiple flags" do
+      option = @options.on("-f", "-F", "--foo")
+      assert_equal %w(-f -F --foo), option.flags
+    end
+
+    it "accepts a trailing description" do
+      option = @options.on("--foo", "fooey")
+      assert_equal "fooey", option.desc
+    end
+
+    it "adds the option" do
+      option = @options.on("--foo")
+      assert_equal [option], @options.to_a
+    end
+
+    it "raises an error when a duplicate flag is used" do
+      @options.on("--foo")
+      assert_raises(ArgumentError) { @options.on("--foo") }
+    end
+  end
+
+  describe "#method_missing" do
+    it "uses the method name as an option type" do
+      option = @options.string("--name")
+      assert_kind_of Slop::StringOption, option
+    end
+
+    it "raises if a type doesn't exist" do
+      assert_raises(NoMethodError) { @options.unknown }
+    end
+  end
+
+  describe "#respond_to?" do
+    it "handles custom types" do
+      module Slop; class BarOption < Option; end; end
+      assert @options.respond_to?(:bar)
+    end
+  end
+
+  describe "#to_s" do
+    it "is prefixed with the banner" do
+      assert_match(/^usage/, @options.to_s)
+    end
+
+    it "aligns option strings" do
+      @options.on "-f", "--foo", "fooey"
+      @options.on "-s", "short"
+      assert_match(/^    -f, --foo  fooey/, @options.to_s)
+      assert_match(/^    -s         short/, @options.to_s)
+    end
+
+    it "can use a custom prefix" do
+      @options.on "-f", "--foo"
+      assert_match(/^ -f, --foo/, @options.to_s(prefix: " "))
+    end
+
+    it "ignores options with help: false" do
+      @options.on "-x", "something", help: false
+      refute_match(/something/, @options.to_s)
+    end
+
+    it "adds 'tail' options to the bottom of the help text" do
+      @options.on "-h", "--help", tail: true
+      @options.on "-f", "--foo"
+      assert_match(/^    -h, --help/, @options.to_s.lines.last)
+    end
+  end
+end
diff --git a/test/parser_test.rb b/test/parser_test.rb
new file mode 100644
index 0000000..85ba7e0
--- /dev/null
+++ b/test/parser_test.rb
@@ -0,0 +1,70 @@
+require 'test_helper'
+
+describe Slop::Parser do
+  before do
+    @options = Slop::Options.new
+    @verbose = @options.bool "-v", "--verbose"
+    @name    = @options.string "-n", "--name"
+    @unused  = @options.string "--unused"
+    @parser  = Slop::Parser.new(@options)
+    @result  = @parser.parse %w(foo -v --name lee argument)
+  end
+
+  it "ignores everything after --" do
+    @parser.parse %w(-v -- --name lee)
+    assert_equal [@verbose], @parser.used_options
+  end
+
+  it "parses flag=argument" do
+    @options.integer "-p", "--port"
+    @result.parser.parse %w(--name=bob -p=123)
+    assert_equal "bob", @result[:name]
+    assert_equal 123, @result[:port]
+  end
+
+  describe "parsing grouped short flags" do
+    before do
+      @options.bool "-q", "--quiet"
+    end
+
+    it "parses boolean flags" do
+      @result.parser.parse %w(-qv)
+      assert_equal true, @result.quiet?
+      assert_equal true, @result.verbose?
+    end
+
+    it "sends the argument to the last flag" do
+      @result.parser.parse %w(-qvn foo)
+      assert_equal "foo", @result[:name]
+    end
+
+    it "doesn't screw up single hyphen long options" do
+      @options.string "-host"
+      @result.parser.parse %w(-host localhost)
+      assert_equal "localhost", @result[:host]
+    end
+  end
+
+  describe "#used_options" do
+    it "returns all options that were parsed" do
+      assert_equal [@verbose, @name], @parser.used_options
+    end
+  end
+
+  describe "#unused_options" do
+    it "returns all options that were not parsed" do
+      assert_equal [@unused], @parser.unused_options
+    end
+  end
+
+  describe "#arguments" do
+    it "returns all unparsed arguments" do
+      assert_equal %w(foo argument), @parser.arguments
+    end
+
+    it "does not return --" do
+      @parser.parse %w(-v -- --name lee)
+      assert_equal %w(--name lee), @parser.arguments
+    end
+  end
+end
diff --git a/test/result_test.rb b/test/result_test.rb
new file mode 100644
index 0000000..0efdfff
--- /dev/null
+++ b/test/result_test.rb
@@ -0,0 +1,107 @@
+require 'test_helper'
+
+module Slop
+  class ReverseEverythingOption < BoolOption
+    def finish(result)
+      result.used_options.grep(Slop::StringOption).each do |opt|
+        opt.value = opt.value.reverse
+      end
+    end
+  end
+end
+
+describe Slop::Result do
+  before do
+    @options     = Slop::Options.new
+    @verbose     = @options.bool "-v", "--verbose"
+    @name        = @options.string "--name"
+    @unused      = @options.string "--unused"
+    @long_option = @options.string "--long-option"
+    @result      = @options.parse %w(foo -v --name lee --long-option bar argument)
+  end
+
+  it "increments option count" do
+    # test this here so it's more "full stack"
+    assert_equal 1, @verbose.count
+    assert_equal 1, @long_option.count
+    @result.parser.parse %w(-v --verbose)
+    assert_equal 2, @verbose.count
+  end
+
+  it "handles default values" do
+    @options.string("--foo", default: "bar")
+    @result.parser.parse %w()
+    assert_equal "bar", @result[:foo]
+  end
+
+  it "handles custom finishing" do
+    @options.string "--foo"
+    @options.reverse_everything "-r"
+    @result.parser.parse %w(-r --name lee --foo bar)
+    assert_equal %w(eel rab), @result.to_hash.values_at(:name, :foo)
+  end
+
+  it "yields arguments to option blocks" do
+    output = nil
+    @options.string("--foo") { |v| output = v }
+    @result.parser.parse %w(--foo bar)
+    assert_equal output, "bar"
+  end
+
+  describe "#[]" do
+    it "returns an options value" do
+      assert_equal "lee", @result["name"]
+      assert_equal "lee", @result[:name]
+      assert_equal "lee", @result["--name"]
+      assert_equal "bar", @result["long_option"]
+      assert_equal "bar", @result[:long_option]
+      assert_equal "bar", @result["--long-option"]
+    end
+  end
+
+  describe "#[]=" do
+    it "sets an options value" do
+      assert_equal "lee", @result["name"]
+      @result["name"] = "bob"
+      assert_equal "bob", @result[:name]
+    end
+
+    it "raises if an option isn't found" do
+      assert_raises ArgumentError do
+        @result["zomg"] = "something"
+      end
+    end
+  end
+
+  describe "#method_missing" do
+    it "checks if options have been used" do
+      assert_equal true, @result.verbose?
+      assert_equal false, @result.unused?
+      assert_equal true, @result.long_option?
+    end
+  end
+
+  describe "#option" do
+    it "returns an option by flag" do
+      assert_equal @verbose, @result.option("--verbose")
+      assert_equal @verbose, @result.option("-v")
+      assert_equal @long_option, @result.option("--long-option")
+    end
+
+    it "ignores prefixed hyphens" do
+      assert_equal @verbose, @result.option("verbose")
+      assert_equal @verbose, @result.option("-v")
+    end
+
+    it "returns nil if nothing is found" do
+      assert_equal nil, @result.option("foo")
+    end
+  end
+
+  describe "#to_hash" do
+    it "returns option keys and values" do
+      assert_equal({ verbose: true, name: "lee", unused: nil, long_option: "bar" },
+                   @result.to_hash)
+    end
+  end
+end
diff --git a/test/slop_test.rb b/test/slop_test.rb
deleted file mode 100644
index 3b630f5..0000000
--- a/test/slop_test.rb
+++ /dev/null
@@ -1,518 +0,0 @@
-require 'helper'
-
-class SlopTest < TestCase
-
-  def build_option(*args)
-    opt = Slop.new.send(:build_option, args)
-    config = opt.config.reject { |k, v| v == Slop::Option::DEFAULT_OPTIONS[k] }
-    [opt.short, opt.long, opt.description, config]
-  end
-
-  def temp_argv(items)
-    old_argv = ARGV.clone
-    ARGV.replace items
-    yield
-  ensure
-    ARGV.replace old_argv
-  end
-
-  def temp_stdout
-    $stdout = StringIO.new
-    yield $stdout.string
-  ensure
-    $stdout = STDOUT
-  end
-
-  test "includes Enumerable" do
-    assert_includes Slop.included_modules, Enumerable
-  end
-
-  test "enumerates Slop::Option objects in #each" do
-    Slop.new { on :f; on :b; }.each { |o| assert_kind_of Slop::Option, o }
-  end
-
-  test "build_option" do
-    assert_equal ['f', nil, nil, {}], build_option(:f)
-    assert_equal [nil, 'foo', nil, {}], build_option(:foo)
-    assert_equal ['f', nil, 'Some description', {}], build_option(:f, 'Some description')
-    assert_equal ['f', 'foo', nil, {}], build_option(:f, :foo)
-    assert_equal [nil, '1.8', 'Use v. 1.8', {}], build_option('--1.8', 'Use v. 1.8')
-
-    # with arguments
-    assert_equal ['f', nil, nil, {:argument=>true}], build_option('f=')
-    assert_equal [nil, 'foo', nil, {:argument=>true}], build_option('foo=')
-    assert_equal [nil, 'foo', nil, {:optional_argument=>true}], build_option('foo=?')
-  end
-
-  test "parsing option=value" do
-    slop = Slop.new { on :foo= }
-    slop.parse %w' --foo=bar '
-    assert_equal 'bar', slop[:foo]
-
-    slop = Slop.new(:multiple_switches => false) { on :f=; on :b= }
-    slop.parse %w' -fabc -bdef '
-    assert_equal 'abc', slop[:f]
-    assert_equal 'def', slop[:b]
-  end
-
-  test "fetch_option" do
-    slop = Slop.new
-    opt1 = slop.on :f, :foo
-    opt2 = slop.on :bar
-
-    assert_equal opt1, slop.fetch_option(:foo)
-    assert_equal opt1, slop.fetch_option(:f)
-    assert_equal opt2, slop.fetch_option(:bar)
-    assert_equal opt2, slop.fetch_option('--bar')
-    assert_nil slop.fetch_option(:baz)
-  end
-
-  test "default all options to take arguments" do
-    slop = Slop.new(:arguments => true)
-    opt1 = slop.on :foo
-    opt2 = slop.on :bar, :argument => false
-
-    assert opt1.expects_argument?
-    refute opt2.expects_argument?
-  end
-
-  test "extract_option" do
-    slop = Slop.new
-    extract = proc { |flag| slop.send(:extract_option, flag) }
-    slop.on :opt=
-
-    assert_kind_of Array, extract['--foo']
-    assert_equal 'bar', extract['--foo=bar'][1]
-    assert_equal 'bar', extract['-f=bar'][1]
-    assert_nil extract['--foo'][0]
-    assert_kind_of Slop::Option, extract['--opt'][0]
-    assert_equal false, extract['--no-opt'][1]
-  end
-
-  test "non-options yielded to parse()" do
-    foo = nil
-    slop = Slop.new
-    slop.parse ['foo'] do |x| foo = x end
-    assert_equal 'foo', foo
-  end
-
-  test "::parse returns a Slop object" do
-    assert_kind_of Slop, Slop.parse([])
-  end
-
-  test "parse" do
-    slop = Slop.new
-    assert_equal ['foo'], slop.parse(%w'foo')
-    assert_equal ['foo'], slop.parse!(%w'foo')
-  end
-
-  test "parse!" do
-    slop = Slop.new { on :foo= }
-    assert_equal [], slop.parse!(%w'--foo bar')
-    slop = Slop.new {  on :baz }
-    assert_equal ['etc'], slop.parse!(%w'--baz etc')
-  end
-
-  test "new() accepts a hash of configuration options" do
-    slop = Slop.new(:foo => :bar)
-    assert_equal :bar, slop.config[:foo]
-  end
-
-  test "defaulting to ARGV" do
-    temp_argv(%w/--name lee/) do
-      opts = Slop.parse { on :name= }
-      assert_equal 'lee', opts[:name]
-    end
-  end
-
-  test "automatically adding the help option" do
-    slop = Slop.new :help => true
-    refute_empty slop.options
-    assert_equal 'Display this help message.', slop.options.first.description
-  end
-
-  test "default help exits" do
-    temp_stdout do
-      slop = Slop.new :help => true
-      assert_raises SystemExit do
-        slop.parse %w/--help/
-      end
-    end
-  end
-
-  test ":arguments and :optional_arguments config options" do
-    slop = Slop.new(:arguments => true) { on :foo }
-    assert slop.fetch_option(:foo).expects_argument?
-
-    slop = Slop.new(:optional_arguments => true) { on :foo }
-    assert slop.fetch_option(:foo).accepts_optional_argument?
-  end
-
-  test "yielding non-options when a block is passed to parse()" do
-    items = []
-    opts = Slop.new { on :name= }
-    opts.parse(%w/--name lee a b c/) { |v| items << v }
-    assert_equal ['a', 'b', 'c'], items
-  end
-
-  test "on empty callback" do
-    opts = Slop.new
-    foo = nil
-    opts.add_callback(:empty) { foo = "bar" }
-    opts.parse []
-    assert_equal "bar", foo
-  end
-
-  test "on no_options callback" do
-    opts = Slop.new
-    foo = nil
-    opts.add_callback(:no_options) { foo = "bar" }
-    opts.parse %w( --foo --bar etc hello )
-    assert_equal "bar", foo
-  end
-
-  test "to_hash()" do
-    opts = Slop.new { on :foo=; on :bar; on :baz; on :zip }
-    opts.parse(%w'--foo hello --no-bar --baz')
-    assert_equal({ :foo => 'hello', :bar => false, :baz => true, :zip => nil }, opts.to_hash)
-  end
-
-  test "missing() returning all missing option keys" do
-    opts = Slop.new { on :foo; on :bar }
-    opts.parse %w'--foo'
-    assert_equal ['bar'], opts.missing
-  end
-
-  test "autocreating options" do
-    opts = Slop.new :autocreate => true
-    opts.parse %w[ --foo bar --baz ]
-    assert opts.fetch_option(:foo).expects_argument?
-    assert opts.fetch_option(:foo).autocreated?
-    assert_equal 'bar', opts.fetch_option(:foo).value
-    refute opts.fetch_option(:baz).expects_argument?
-    assert_equal nil, opts.fetch_option(:bar)
-
-    opts = Slop.new :autocreate => true do
-      on :f, :foo=
-    end
-    opts.parse %w[ --foo bar --baz stuff ]
-    assert_equal 'bar', opts[:foo]
-    assert_equal 'stuff', opts[:baz]
-  end
-
-  test "option terminator" do
-    opts = Slop.new { on :foo= }
-    items = %w' foo -- --foo bar '
-    opts.parse! items
-    assert_equal %w' foo --foo bar ', items
-  end
-
-  test "raising an InvalidArgumentError when the argument doesn't match" do
-    opts = Slop.new { on :foo=, :match => /^[a-z]+$/ }
-    assert_raises(Slop::InvalidArgumentError) { opts.parse %w' --foo b4r '}
-  end
-
-  test "raising a MissingArgumentError when the option expects an argument" do
-    opts = Slop.new { on :foo= }
-    assert_raises(Slop::MissingArgumentError) { opts.parse %w' --foo '}
-  end
-
-  test "raising a MissingOptionError when a required option is missing" do
-    opts = Slop.new { on :foo, :required => true }
-    assert_raises(Slop::MissingOptionError) { opts.parse %w'' }
-  end
-
-  test "raising InvalidOptionError when strict mode is enabled and an unknown option appears" do
-    opts = Slop.new :strict => true
-    assert_raises(Slop::InvalidOptionError) { opts.parse %w'--foo' }
-    assert_raises(Slop::InvalidOptionError) { opts.parse %w'-fabc' }
-  end
-
-  test "raising InvalidOptionError for multiple short options" do
-    opts = Slop.new :strict => true
-    opts.on :L
-    assert_raises(Slop::InvalidOptionError) { opts.parse %w'-Ly' }
-
-    # but not with no strict mode!
-    opts = Slop.new
-    opts.on :L
-    assert opts.parse %w'-Ly'
-  end
-
-  test "multiple_switches is enabled by default" do
-    opts = Slop.new { on :f; on :b }
-    opts.parse %w[ -fb ]
-    assert opts.present?(:f)
-    assert opts.present?(:b)
-  end
-
-  test "multiple_switches disabled" do
-    opts = Slop.new(:multiple_switches => false) { on :f= }
-    opts.parse %w[ -fabc123 ]
-    assert_equal 'abc123', opts[:f]
-  end
-
-  test "muiltiple_switches should not trash arguments" do
-    opts = Slop.new{ on :f; on :b }
-    args = opts.parse!(%w'-fb foo')
-    assert_equal %w'foo', args
-  end
-
-  test "multiple options should still accept trailing arguments" do
-    opts = Slop.new { on :a; on :b= }
-    opts.parse %w'-ab foo'
-    assert_equal 'foo', opts[:b]
-  end
-
-  test "setting/getting the banner" do
-    opts = Slop.new :banner => 'foo'
-    assert_equal 'foo', opts.banner
-
-    opts = Slop.new
-    opts.banner 'foo'
-    assert_equal 'foo', opts.banner
-
-    opts = Slop.new
-    opts.banner = 'foo'
-    assert_equal 'foo', opts.banner
-  end
-
-  test "get/[] fetching an options argument value" do
-    opts = Slop.new { on :foo=; on :bar; on :baz }
-    opts.parse %w' --foo hello --bar '
-    assert_equal 'hello', opts[:foo]
-    assert_equal true, opts[:bar]
-    assert_nil opts[:baz]
-  end
-
-  test "checking for an options presence" do
-    opts = Slop.new { on :foo; on :bar }
-    opts.parse %w' --foo '
-    assert opts.present?(:foo)
-    refute opts.present?(:bar)
-  end
-
-  test "ignoring case" do
-    opts = Slop.new { on :foo }
-    opts.parse %w' --FOO bar '
-    assert_nil opts[:foo]
-
-    opts = Slop.new(:ignore_case => true) { on :foo= }
-    opts.parse %w' --FOO bar '
-    assert_equal 'bar', opts[:foo]
-  end
-
-  test "supporting dash" do
-    opts = Slop.new { on :foo_bar= }
-    opts.parse %w' --foo-bar baz '
-    assert_equal 'baz', opts[:foo_bar]
-    assert opts.foo_bar?
-  end
-
-  test "supporting underscore" do
-    opts = Slop.new { on :foo_bar= }
-    opts.parse %w' --foo_bar baz '
-    assert_equal 'baz', opts[:foo_bar]
-    assert opts.foo_bar?
-  end
-
-  # test "parsing an optspec and building options" do
-  #   optspec = <<-SPEC
-  #   ruby foo.rb [options]
-  #   --
-  #   v,verbose  enable verbose mode
-  #   q,quiet   enable quiet mode
-  #   n,name=    set your name
-  #   p,pass=?   set your password
-  #   SPEC
-  #   opts = Slop.optspec(optspec.gsub(/^\s+/, ''))
-  #   opts.parse %w[ --verbose --name Lee ]
-
-  #   assert_equal 'Lee', opts[:name]
-  #   assert opts.present?(:verbose)
-  #   assert_equal 'enable quiet mode', opts.fetch_option(:quiet).description
-  #   assert opts.fetch_option(:pass).accepts_optional_argument?
-  # end
-
-  test "ensure negative integers are not processed as options" do
-    items = %w(-1)
-    Slop.parse!(items)
-    assert_equal %w(-1), items
-  end
-
-  test "separators" do
-    opts = Slop.new(:banner => false) do
-      on :foo
-      separator "hello"
-      separator "world"
-      on :bar
-    end
-    assert_equal "        --foo      \nhello\nworld\n        --bar      ", opts.help
-
-    opts = Slop.new do
-      banner "foo"
-      separator "bar"
-    end
-    assert_equal "foo\nbar\n", opts.help
-  end
-
-  test "printing help with :help => true" do
-    temp_stdout do |string|
-      opts = Slop.new(:help => true, :banner => false)
-      assert_raises SystemExit do
-        opts.parse %w( --help )
-      end
-      assert_equal "    -h, --help      Display this help message.\n", string
-    end
-
-    temp_stdout do |string|
-      opts = Slop.new(:help => true)
-      assert_raises SystemExit do
-        opts.parse %w( --help )
-      end
-      assert_equal "Usage: rake_test_loader [options]\n    -h, --help      Display this help message.\n", string
-    end
-  end
-
-  test "fallback to substituting - for _ when using <option>?" do
-    opts = Slop.new do
-      on 'foo-bar'
-    end
-    opts.parse %w( --foo-bar )
-    assert opts.foo_bar?
-  end
-
-  test "option=value syntax does NOT consume following argument" do
-    opts = Slop.new { on :foo=; on 'bar=?' }
-    args = %w' --foo=bar baz --bar=zing hello '
-    opts.parse!(args)
-    assert_equal %w' baz hello ', args
-  end
-
-  test "context and return value of constructor block" do
-    peep = nil
-    ret = Slop.new { peep = self }
-    assert_same ret, peep
-    assert !equal?(peep)
-
-    peep = nil
-    ret = Slop.new { |a| peep = self }
-    assert !peep.equal?(ret)
-    assert_same peep, self
-
-    peep = nil
-    ret = Slop.new { |a, b| peep = self }
-    assert_same ret, peep
-    assert !equal?(peep)
-
-    peep = nil
-    ret = Slop.new { |a, *rest| peep = self }
-    assert_same ret, peep
-    assert !equal?(peep)
-
-    peep = nil
-    ret = Slop.parse([]) { peep = self }
-    assert_same ret, peep
-    assert !equal?(peep)
-
-    peep = nil
-    ret = Slop.parse([]) { |a| peep = self }
-    assert !peep.equal?(ret)
-    assert_same peep, self
-
-    peep = nil
-    ret = Slop.parse([]) { |a, b| peep = self }
-    assert_same ret, peep
-    assert !equal?(peep)
-
-    peep = nil
-    ret = Slop.parse([]) { |a, *rest| peep = self }
-    assert_same ret, peep
-    assert !equal?(peep)
-  end
-
-  test "to_s do not break self" do
-    slop = Slop.new do
-      banner "foo"
-    end
-
-    assert_equal "foo", slop.banner
-    slop.to_s
-    assert_equal "foo", slop.banner
-  end
-
-  test "options with prefixed --no should not default to inverted behaviour unless intended" do
-    opts = Slop.new { on :bar }
-    opts.parse %w'--no-bar'
-    assert_equal false, opts[:bar]
-
-    opts = Slop.new { on 'no-bar' }
-    opts.parse %w'--no-bar'
-    assert_equal true, opts['no-bar']
-  end
-
-  test "method missing() is a private method" do
-    assert Slop.new.private_methods.map(&:to_sym).include?(:method_missing)
-  end
-
-  test "respond_to?() arity checker is similar of other objects" do
-    slop = Slop.new
-    obj = Object.new
-
-    assert_same obj.respond_to?(:__id__), slop.respond_to?(:__id__)
-    assert_same obj.respond_to?(:__id__, false), slop.respond_to?(:__id__, false)
-    assert_same obj.respond_to?(:__id__, true), slop.respond_to?(:__id__, true)
-
-    assert_raises ArgumentError do
-      slop.respond_to? :__id__, false, :INVALID_ARGUMENT
-    end
-  end
-
-  test "adding a runner" do
-    orun = proc { |r| assert_instance_of Slop, r }
-    arun = proc { |r| assert_equal ['foo', 'bar'], r }
-
-    Slop.parse(%w'foo --foo bar -v bar') do
-      on :v
-      on :foo=
-      run { |o, a| orun[o]; arun[a] }
-    end
-  end
-
-  test "ensure a runner does not execute when a help option is present" do
-    items = []
-    Slop.parse(%w'--help foo bar') do
-      run { |o, a| items.concat a }
-    end
-    assert_equal %w'--help foo bar', items
-    items.clear
-    temp_stdout do
-      assert_raises SystemExit do
-        Slop.parse(%w'--help foo bar', :help => true) do
-          run { |o, a| items.concat a }
-        end
-      end
-      assert_empty items
-    end
-  end
-
-  test "duplicate options should not exist, new options should replace old ones" do
-    i = nil
-    Slop.parse(%w'-v') do
-      on(:v) { i = 'first' }
-      on(:v) { i = 'second' }
-    end
-    assert_equal 'second', i
-  end
-
-  test "taking out the trash" do
-    args = []
-    opts = Slop.new { on :f, :foo }
-    opts.run { |_, a| args = a }
-    opts.parse! %w(--foo bar)
-    assert_equal %w(bar), args
-    opts.parse! %w(foo)
-    assert_equal %w(foo), args
-  end
-
-end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..8a7b470
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,6 @@
+$VERBOSE = true
+
+require 'slop'
+
+require 'minitest/autorun'
+require 'stringio'
diff --git a/test/types_test.rb b/test/types_test.rb
new file mode 100644
index 0000000..5950016
--- /dev/null
+++ b/test/types_test.rb
@@ -0,0 +1,120 @@
+require 'test_helper'
+
+describe Slop::BoolOption do
+  before do
+    @options  = Slop::Options.new
+    @verbose  = @options.bool "--verbose"
+    @quiet    = @options.bool "--quiet"
+    @inversed = @options.bool "--inversed", default: true
+    @result   = @options.parse %w(--verbose --no-inversed)
+  end
+
+  it "returns true if used" do
+    assert_equal true, @result[:verbose]
+  end
+
+  it "returns false if not used" do
+    assert_equal false, @result[:quiet]
+  end
+
+  it "can be inversed via --no- prefix" do
+    assert_equal false, @result[:inversed]
+  end
+end
+
+describe Slop::IntegerOption do
+  before do
+    @options = Slop::Options.new
+    @age     = @options.integer "--age"
+    @result  = @options.parse %w(--age 20)
+  end
+
+  it "returns the value as an integer" do
+    assert_equal 20, @result[:age]
+  end
+
+  it "returns nil for non-numbers by default" do
+    @result.parser.parse %w(--age hello)
+    assert_equal nil, @result[:age]
+  end
+end
+
+describe Slop::FloatOption do
+  before do
+    @options = Slop::Options.new
+    @apr     = @options.float "--apr"
+    @apr_value = 2.9
+    @result  = @options.parse %W(--apr #{@apr_value})
+  end
+
+  it "returns the value as a float" do
+    assert_equal @apr_value, @result[:apr]
+  end
+
+  it "returns nil for non-numbers by default" do
+    @result.parser.parse %w(--apr hello)
+    assert_equal nil, @result[:apr]
+  end
+end
+
+describe Slop::ArrayOption do
+  before do
+    @options = Slop::Options.new
+    @files   = @options.array "--files"
+    @delim   = @options.array "-d", delimiter: ":"
+    @limit   = @options.array "-l", limit: 2
+    @result  = @options.parse %w(--files foo.txt,bar.rb)
+  end
+
+  it "defaults to []" do
+    assert_equal [], @result[:d]
+  end
+
+  it "parses comma separated args" do
+    assert_equal %w(foo.txt bar.rb), @result[:files]
+  end
+
+  it "collects multiple option values" do
+    @result.parser.parse %w(--files foo.txt --files bar.rb)
+    assert_equal %w(foo.txt bar.rb), @result[:files]
+  end
+
+  it "can use a custom delimiter" do
+    @result.parser.parse %w(-d foo.txt:bar.rb)
+    assert_equal %w(foo.txt bar.rb), @result[:d]
+  end
+
+  it "can use a custom limit" do
+    @result.parser.parse %w(-l foo,bar,baz)
+    assert_equal ["foo", "bar,baz"], @result[:l]
+  end
+end
+
+describe Slop::NullOption do
+  before do
+    @options = Slop::Options.new
+    @version = @options.null('--version')
+    @result  = @options.parse %w(--version)
+  end
+
+  it 'has a return value of true' do
+    assert_equal true, @result[:version]
+  end
+
+  it 'is not included in to_hash' do
+    assert_equal({}, @result.to_hash)
+  end
+end
+
+describe Slop::RegexpOption do
+  before do
+    @options       = Slop::Options.new
+    @exclude       = @options.regexp "--exclude"
+    @exclude_value = "redirect|news"
+    @result        = @options.parse %W(--exclude #{@exclude_value})
+  end
+
+  it "returns the value as a Regexp" do
+    assert_equal Regexp.new(@exclude_value), @result[:exclude]
+  end
+end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-slop.git



More information about the Pkg-ruby-extras-commits mailing list