Permalink
Browse files

Initial checkin of Hermes

  • Loading branch information...
0 parents commit a8de418f67bf38061795e089a9a42735d2f47bdc @wycats wycats committed May 7, 2008
Showing with 561 additions and 0 deletions.
  1. +20 −0 LICENSE
  2. +61 −0 README.markdown
  3. +37 −0 Rakefile
  4. +238 −0 lib/getopt.rb
  5. +87 −0 lib/hermes.rb
  6. +14 −0 script/destroy
  7. +14 −0 script/generate
  8. +88 −0 spec/hermes_spec.rb
  9. +2 −0 spec/spec_helper.rb
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Yehuda Katz
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,61 @@
+hermes
+======
+
+Map options to a class. Simply create a class with the appropriate annotations, and have options automatically map
+to functions and parameters.
+
+Examples:
+
+ class MyApp
+ extend Hermes # [1]
+
+ map "-L" => :list # [2]
+
+ desc "install APP_NAME", "install one of the available apps" # [3]
+ method_options :force => :boolean # [4]
+ def install(name, opts)
+ ... code ...
+ if opts[:force]
+ # do something
+ end
+ end
+
+ desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
+ def list(search = "")
+ # list everything
+ end
+
+ end
+
+ MyApp.start
+
+Hermes automatically maps commands as follows:
+
+ app install name --force
+
+That gets converted to:
+
+ MyApp.new.install("name", :force => true)
+
+[1] Use `extend Hermes` to turn a class into an option mapper
+
+[2] Map additional non-valid identifiers to specific methods. In this case,
+ convert -L to :list
+
+[3] Describe the method immediately below. The first parameter is the usage information,
+ and the second parameter is the description.
+
+[4] Provide any additional options. These will be marshaled from -- and - params.
+ In this case, a --force and a -f option is added.
+
+Types for `method_options`
+--------------------------
+
+<dl>
+ <dt>:boolean</dt>
+ <dd>true if the option is passed</dd>
+ <dt>:required</dt>
+ <dd>A key/value option that MUST be provided</dd>
+ <dt>:optional</dt>
+ <dd>A key/value option that MAY be provided</dd>
+</dl>
@@ -0,0 +1,37 @@
+require 'rubygems'
+require 'rake/gempackagetask'
+
+GEM = "hermes"
+VERSION = "0.9.0"
+AUTHOR = "Yehuda Katz"
+EMAIL = "wycats@gmail.com"
+HOMEPAGE = "http://yehudakatz.com"
+SUMMARY = "A gem that maps options to a class"
+
+spec = Gem::Specification.new do |s|
+ s.name = GEM
+ s.version = VERSION
+ s.platform = Gem::Platform::RUBY
+ s.has_rdoc = true
+ s.extra_rdoc_files = ["README.markdown", "LICENSE"]
+ s.summary = SUMMARY
+ s.description = s.summary
+ s.author = AUTHOR
+ s.email = EMAIL
+ s.homepage = HOMEPAGE
+
+ # Uncomment this to add a dependency
+ # s.add_dependency "foo"
+
+ s.require_path = 'lib'
+ s.autorequire = GEM
+ s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,specs}/**/*")
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.gem_spec = spec
+end
+
+task :install => [:package] do
+ sh %{sudo gem install pkg/#{GEM}-#{VERSION}}
+end
@@ -0,0 +1,238 @@
+# The last time the Getopt gem was modified was August 2007, so it's safe to vendor (it does everything we need)
+
+module Getopt
+
+ REQUIRED = 0
+ BOOLEAN = 1
+ OPTIONAL = 2
+ INCREMENT = 3
+ NEGATABLE = 4
+ NUMERIC = 5
+
+ class Long
+ class Error < StandardError; end
+
+ VERSION = '1.3.6'
+
+ # Takes an array of switches. Each array consists of up to three
+ # elements that indicate the name and type of switch. Returns a hash
+ # containing each switch name, minus the '-', as a key. The value
+ # for each key depends on the type of switch and/or the value provided
+ # by the user.
+ #
+ # The long switch _must_ be provided. The short switch defaults to the
+ # first letter of the short switch. The default type is BOOLEAN.
+ #
+ # Example:
+ #
+ # opts = Getopt::Long.getopts(
+ # ["--debug"],
+ # ["--verbose", "-v"],
+ # ["--level", "-l", NUMERIC]
+ # )
+ #
+ # See the README file for more information.
+ #
+ def self.getopts(*switches)
+ if switches.empty?
+ raise ArgumentError, "no switches provided"
+ end
+
+ hash = {} # Hash returned to user
+ valid = [] # Tracks valid switches
+ types = {} # Tracks argument types
+ syns = {} # Tracks long and short arguments, or multiple shorts
+
+ # If a string is passed, split it and convert it to an array of arrays
+ if switches.first.kind_of?(String)
+ switches = switches.join.split
+ switches.map!{ |switch| switch = [switch] }
+ end
+
+ # Set our list of valid switches, and proper types for each switch
+ switches.each{ |switch|
+ valid.push(switch[0]) # Set valid long switches
+
+ # Set type for long switch, default to BOOLEAN.
+ if switch[1].kind_of?(Fixnum)
+ switch[2] = switch[1]
+ types[switch[0]] = switch[2]
+ switch[1] = switch[0][1..2]
+ else
+ switch[2] ||= BOOLEAN
+ types[switch[0]] = switch[2]
+ switch[1] ||= switch[0][1..2]
+ end
+
+ # Create synonym hash. Default to first char of long switch for
+ # short switch, e.g. "--verbose" creates a "-v" synonym. The same
+ # synonym can only be used once - first one wins.
+ syns[switch[0]] = switch[1] unless syns[switch[1]]
+ syns[switch[1]] = switch[0] unless syns[switch[1]]
+
+ switch[1].each{ |char|
+ types[char] = switch[2] # Set type for short switch
+ valid.push(char) # Set valid short switches
+ }
+
+ if ARGV.empty? && switch[2] == REQUIRED
+ raise Error, "no value provided for required argument '#{switch[0]}'"
+ end
+ }
+
+ re_long = /^(--\w+[-\w+]*)?$/
+ re_short = /^(-\w)$/
+ re_long_eq = /^(--\w+[-\w+]*)?=(.*?)$|(-\w?)=(.*?)$/
+ re_short_sq = /^(-\w)(\S+?)$/
+
+ ARGV.each_with_index{ |opt, index|
+
+ # Allow either -x -v or -xv style for single char args
+ if re_short_sq.match(opt)
+ chars = opt.split("")[1..-1].map{ |s| s = "-#{s}" }
+
+ chars.each_with_index{ |char, i|
+ unless valid.include?(char)
+ raise Error, "invalid switch '#{char}'"
+ end
+
+ # Grab the next arg if the switch takes a required arg
+ if types[char] == REQUIRED
+ # Deal with a argument squished up against switch
+ if chars[i+1]
+ arg = chars[i+1..-1].join.tr("-","")
+ ARGV.push(char, arg)
+ break
+ else
+ arg = ARGV.delete_at(index+1)
+ if arg.nil? || valid.include?(arg) # Minor cheat here
+ err = "no value provided for required argument '#{char}'"
+ raise Error, err
+ end
+ ARGV.push(char, arg)
+ end
+ elsif types[char] == OPTIONAL
+ if chars[i+1] && !valid.include?(chars[i+1])
+ arg = chars[i+1..-1].join.tr("-","")
+ ARGV.push(char, arg)
+ break
+ elsif
+ if ARGV[index+1] && !valid.include?(ARGV[index+1])
+ arg = ARGV.delete_at(index+1)
+ ARGV.push(char, arg)
+ end
+ else
+ ARGV.push(char)
+ end
+ else
+ ARGV.push(char)
+ end
+ }
+ next
+ end
+
+ if match = re_long.match(opt) || match = re_short.match(opt)
+ switch = match.captures.first
+ end
+
+ if match = re_long_eq.match(opt)
+ switch, value = match.captures.compact
+ ARGV.push(switch, value)
+ next
+ end
+
+ # Make sure that all the switches are valid. If 'switch' isn't
+ # defined at this point, it means an option was passed without
+ # a preceding switch, e.g. --option foo bar.
+ unless valid.include?(switch)
+ switch ||= opt
+ raise Error, "invalid switch '#{switch}'"
+ end
+
+ # Required arguments
+ if types[switch] == REQUIRED
+ nextval = ARGV[index+1]
+
+ # Make sure there's a value for mandatory arguments
+ if nextval.nil?
+ err = "no value provided for required argument '#{switch}'"
+ raise Error, err
+ end
+
+ # If there is a value, make sure it's not another switch
+ if valid.include?(nextval)
+ err = "cannot pass switch '#{nextval}' as an argument"
+ raise Error, err
+ end
+
+ # If the same option appears more than once, put the values
+ # in array.
+ if hash[switch]
+ hash[switch] = [hash[switch], nextval].flatten
+ else
+ hash[switch] = nextval
+ end
+ ARGV.delete_at(index+1)
+ end
+
+ # For boolean arguments set the switch's value to true.
+ if types[switch] == BOOLEAN
+ if hash.has_key?(switch)
+ raise Error, "boolean switch already set"
+ end
+ hash[switch] = true
+ end
+
+ # For increment arguments, set the switch's value to 0, or
+ # increment it by one if it already exists.
+ if types[switch] == INCREMENT
+ if hash.has_key?(switch)
+ hash[switch] += 1
+ else
+ hash[switch] = 1
+ end
+ end
+
+ # For optional argument, there may be an argument. If so, it
+ # cannot be another switch. If not, it is set to true.
+ if types[switch] == OPTIONAL
+ nextval = ARGV[index+1]
+ if valid.include?(nextval)
+ hash[switch] = true
+ else
+ hash[switch] = nextval
+ ARGV.delete_at(index+1)
+ end
+ end
+ }
+
+ # Set synonymous switches to the same value, e.g. if -t is a synonym
+ # for --test, and the user passes "--test", then set "-t" to the same
+ # value that "--test" was set to.
+ #
+ # This allows users to refer to the long or short switch and get
+ # the same value
+ hash.each{ |switch, val|
+ if syns.keys.include?(switch)
+ syns[switch].each{ |key|
+ hash[key] = val
+ }
+ end
+ }
+
+ # Get rid of leading "--" and "-" to make it easier to reference
+ hash.each{ |key, value|
+ if key[0,2] == '--'
+ nkey = key.sub('--', '')
+ else
+ nkey = key.sub('-', '')
+ end
+ hash.delete(key)
+ hash[nkey] = value
+ }
+
+ hash
+ end
+
+ end
+end
Oops, something went wrong.

0 comments on commit a8de418

Please sign in to comment.