Skip to content

Commit

Permalink
Initial checkin of Hermes
Browse files Browse the repository at this point in the history
  • Loading branch information
wycats committed May 7, 2008
0 parents commit a8de418
Show file tree
Hide file tree
Showing 9 changed files with 561 additions and 0 deletions.
20 changes: 20 additions & 0 deletions 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.
61 changes: 61 additions & 0 deletions README.markdown
@@ -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>
37 changes: 37 additions & 0 deletions Rakefile
@@ -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
238 changes: 238 additions & 0 deletions lib/getopt.rb
@@ -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

0 comments on commit a8de418

Please sign in to comment.