mirror of
https://github.com/chromebrew/chromebrew.git
synced 2026-01-06 22:24:12 -05:00
671 lines
17 KiB
Ruby
671 lines
17 KiB
Ruby
module Docopt
|
|
VERSION = '0.6.0'
|
|
end
|
|
module Docopt
|
|
class DocoptLanguageError < SyntaxError
|
|
end
|
|
|
|
class Exit < RuntimeError
|
|
def self.usage
|
|
@@usage
|
|
end
|
|
|
|
def self.set_usage(usage)
|
|
@@usage = usage ? usage : ''
|
|
end
|
|
|
|
def message
|
|
@@message
|
|
end
|
|
|
|
def initialize(message='')
|
|
@@message = (message + "\n" + @@usage).strip
|
|
end
|
|
end
|
|
|
|
class Pattern
|
|
attr_accessor :children
|
|
|
|
def ==(other)
|
|
return self.inspect == other.inspect
|
|
end
|
|
|
|
def to_str
|
|
return self.inspect
|
|
end
|
|
|
|
def dump
|
|
puts ::Docopt::dump_patterns(self)
|
|
end
|
|
|
|
def fix
|
|
fix_identities
|
|
fix_repeating_arguments
|
|
return self
|
|
end
|
|
|
|
def fix_identities(uniq=nil)
|
|
if not instance_variable_defined?(:@children)
|
|
return self
|
|
end
|
|
uniq ||= flat.uniq
|
|
|
|
@children.each_with_index do |c, i|
|
|
if not c.instance_variable_defined?(:@children)
|
|
if !uniq.include?(c)
|
|
raise RuntimeError
|
|
end
|
|
@children[i] = uniq[uniq.index(c)]
|
|
else
|
|
c.fix_identities(uniq)
|
|
end
|
|
end
|
|
end
|
|
|
|
def fix_repeating_arguments
|
|
either.children.map { |c| c.children }.each do |case_|
|
|
case_.select { |c| case_.count(c) > 1 }.each do |e|
|
|
if e.class == Argument or (e.class == Option and e.argcount > 0)
|
|
if e.value == nil
|
|
e.value = []
|
|
elsif e.value.class != Array
|
|
e.value = e.value.split
|
|
end
|
|
end
|
|
if e.class == Command or (e.class == Option and e.argcount == 0)
|
|
e.value = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
def either
|
|
ret = []
|
|
groups = [[self]]
|
|
while groups.count > 0
|
|
children = groups.shift
|
|
types = children.map { |c| c.class }
|
|
|
|
if types.include?(Either)
|
|
either = children.select { |c| c.class == Either }[0]
|
|
children.slice!(children.index(either))
|
|
for c in either.children
|
|
groups << [c] + children
|
|
end
|
|
elsif types.include?(Required)
|
|
required = children.select { |c| c.class == Required }[0]
|
|
children.slice!(children.index(required))
|
|
groups << required.children + children
|
|
|
|
elsif types.include?(Optional)
|
|
optional = children.select { |c| c.class == Optional }[0]
|
|
children.slice!(children.index(optional))
|
|
groups << optional.children + children
|
|
|
|
elsif types.include?(AnyOptions)
|
|
anyoptions = children.select { |c| c.class == AnyOptions }[0]
|
|
children.slice!(children.index(anyoptions))
|
|
groups << anyoptions.children + children
|
|
|
|
elsif types.include?(OneOrMore)
|
|
oneormore = children.select { |c| c.class == OneOrMore }[0]
|
|
children.slice!(children.index(oneormore))
|
|
groups << (oneormore.children * 2) + children
|
|
|
|
else
|
|
ret << children
|
|
end
|
|
end
|
|
|
|
args = ret.map { |e| Required.new(*e) }
|
|
return Either.new(*args)
|
|
end
|
|
end
|
|
|
|
|
|
class ChildPattern < Pattern
|
|
attr_accessor :name, :value
|
|
|
|
def initialize(name, value=nil)
|
|
@name = name
|
|
@value = value
|
|
end
|
|
|
|
def inspect()
|
|
"#{self.class.name}(#{self.name}, #{self.value})"
|
|
end
|
|
|
|
def flat(*types)
|
|
if types.empty? or types.include?(self.class)
|
|
[self]
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
|
|
def match(left, collected=nil)
|
|
collected ||= []
|
|
pos, match = self.single_match(left)
|
|
if match == nil
|
|
return [false, left, collected]
|
|
end
|
|
|
|
left_ = left.dup
|
|
left_.slice!(pos)
|
|
|
|
same_name = collected.select { |a| a.name == self.name }
|
|
if @value.is_a? Array or @value.is_a? Integer
|
|
if @value.is_a? Integer
|
|
increment = 1
|
|
else
|
|
increment = match.value.is_a?(String) ? [match.value] : match.value
|
|
end
|
|
if same_name.count == 0
|
|
match.value = increment
|
|
return [true, left_, collected + [match]]
|
|
end
|
|
same_name[0].value += increment
|
|
return [true, left_, collected]
|
|
end
|
|
return [true, left_, collected + [match]]
|
|
end
|
|
end
|
|
|
|
class ParentPattern < Pattern
|
|
attr_accessor :children
|
|
|
|
def initialize(*children)
|
|
@children = children
|
|
end
|
|
|
|
def inspect
|
|
childstr = self.children.map { |a| a.inspect }
|
|
return "#{self.class.name}(#{childstr.join(", ")})"
|
|
end
|
|
|
|
def flat(*types)
|
|
if types.include?(self.class)
|
|
[self]
|
|
else
|
|
self.children.map { |c| c.flat(*types) }.flatten
|
|
end
|
|
end
|
|
end
|
|
|
|
class Argument < ChildPattern
|
|
|
|
def single_match(left)
|
|
left.each_with_index do |p, n|
|
|
if p.class == Argument
|
|
return [n, Argument.new(self.name, p.value)]
|
|
end
|
|
end
|
|
return [nil, nil]
|
|
end
|
|
|
|
def self.parse(class_, source)
|
|
name = /(<\S*?>)/.match(source)[0]
|
|
value = /\[default: (.*)\]/i.match(source)
|
|
class_.new(name, (value ? value[0] : nil))
|
|
end
|
|
end
|
|
|
|
|
|
class Command < Argument
|
|
def initialize(name, value=false)
|
|
@name = name
|
|
@value = value
|
|
end
|
|
|
|
def single_match(left)
|
|
left.each_with_index do |p, n|
|
|
if p.class == Argument
|
|
if p.value == self.name
|
|
return n, Command.new(self.name, true)
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
return [nil, nil]
|
|
end
|
|
end
|
|
|
|
|
|
class Option < ChildPattern
|
|
attr_reader :short, :long
|
|
attr_accessor :argcount
|
|
|
|
def initialize(short=nil, long=nil, argcount=0, value=false)
|
|
unless [0, 1].include? argcount
|
|
raise RuntimeError
|
|
end
|
|
|
|
@short, @long = short, long
|
|
@argcount, @value = argcount, value
|
|
|
|
if value == false and argcount > 0
|
|
@value = nil
|
|
else
|
|
@value = value
|
|
end
|
|
end
|
|
|
|
def self.parse(option_description)
|
|
short, long, argcount, value = nil, nil, 0, false
|
|
options, _, description = option_description.strip.partition(' ')
|
|
|
|
options = options.gsub(',', ' ').gsub('=', ' ')
|
|
|
|
for s in options.split
|
|
if s.start_with?('--')
|
|
long = s
|
|
elsif s.start_with?('-')
|
|
short = s
|
|
else
|
|
argcount = 1
|
|
end
|
|
end
|
|
if argcount > 0
|
|
matched = description.scan(/\[default: (.*)\]/i)
|
|
value = matched[0][0] if matched.count > 0
|
|
end
|
|
new(short, long, argcount, value)
|
|
end
|
|
|
|
def single_match(left)
|
|
left.each_with_index do |p, n|
|
|
if self.name == p.name
|
|
return [n, p]
|
|
end
|
|
end
|
|
return [nil, nil]
|
|
end
|
|
|
|
def name
|
|
return self.long ? self.long : self.short
|
|
end
|
|
|
|
def inspect
|
|
return "Option(#{self.short}, #{self.long}, #{self.argcount}, #{self.value})"
|
|
end
|
|
end
|
|
|
|
class Required < ParentPattern
|
|
def match(left, collected=nil)
|
|
collected ||= []
|
|
l = left
|
|
c = collected
|
|
|
|
for p in self.children
|
|
matched, l, c = p.match(l, c)
|
|
if not matched
|
|
return [false, left, collected]
|
|
end
|
|
end
|
|
return [true, l, c]
|
|
end
|
|
end
|
|
|
|
class Optional < ParentPattern
|
|
def match(left, collected=nil)
|
|
collected ||= []
|
|
for p in self.children
|
|
m, left, collected = p.match(left, collected)
|
|
end
|
|
return [true, left, collected]
|
|
end
|
|
end
|
|
|
|
class AnyOptions < Optional
|
|
end
|
|
|
|
class OneOrMore < ParentPattern
|
|
def match(left, collected=nil)
|
|
if self.children.count != 1
|
|
raise RuntimeError
|
|
end
|
|
|
|
collected ||= []
|
|
l = left
|
|
c = collected
|
|
l_ = nil
|
|
matched = true
|
|
times = 0
|
|
while matched
|
|
# could it be that something didn't match but changed l or c?
|
|
matched, l, c = self.children[0].match(l, c)
|
|
times += (matched ? 1 : 0)
|
|
if l_ == l
|
|
break
|
|
end
|
|
l_ = l
|
|
end
|
|
if times >= 1
|
|
return [true, l, c]
|
|
end
|
|
return [false, left, collected]
|
|
end
|
|
end
|
|
|
|
class Either < ParentPattern
|
|
def match(left, collected=nil)
|
|
collected ||= []
|
|
outcomes = []
|
|
for p in self.children
|
|
matched, _, _ = outcome = p.match(left, collected)
|
|
if matched
|
|
outcomes << outcome
|
|
end
|
|
end
|
|
|
|
if outcomes.count > 0
|
|
return outcomes.min_by do |outcome|
|
|
outcome[1] == nil ? 0 : outcome[1].count
|
|
end
|
|
end
|
|
return [false, left, collected]
|
|
end
|
|
end
|
|
|
|
class TokenStream < Array
|
|
attr_reader :error
|
|
|
|
def initialize(source, error)
|
|
if !source
|
|
source = []
|
|
elsif source.class != ::Array
|
|
source = source.split
|
|
end
|
|
super(source)
|
|
@error = error
|
|
end
|
|
|
|
def move
|
|
return self.shift
|
|
end
|
|
|
|
def current
|
|
return self[0]
|
|
end
|
|
end
|
|
|
|
class << self
|
|
def parse_long(tokens, options)
|
|
long, eq, value = tokens.move().partition('=')
|
|
unless long.start_with?('--')
|
|
raise RuntimeError
|
|
end
|
|
value = (eq == value and eq == '') ? nil : value
|
|
|
|
similar = options.select { |o| o.long and o.long == long }
|
|
|
|
if tokens.error == Exit and similar == []
|
|
similar = options.select { |o| o.long and o.long.start_with?(long) }
|
|
end
|
|
|
|
if similar.count > 1
|
|
ostr = similar.map { |o| o.long }.join(', ')
|
|
raise tokens.error, "#{long} is not a unique prefix: #{ostr}?"
|
|
elsif similar.count < 1
|
|
argcount = (eq == '=' ? 1 : 0)
|
|
o = Option.new(nil, long, argcount)
|
|
options << o
|
|
if tokens.error == Exit
|
|
o = Option.new(nil, long, argcount, (argcount == 1 ? value : true))
|
|
end
|
|
else
|
|
s0 = similar[0]
|
|
o = Option.new(s0.short, s0.long, s0.argcount, s0.value)
|
|
if o.argcount == 0
|
|
if !value.nil?
|
|
raise tokens.error, "#{o.long} must not have an argument"
|
|
end
|
|
else
|
|
if value.nil?
|
|
if tokens.current().nil?
|
|
raise tokens.error, "#{o.long} requires argument"
|
|
end
|
|
value = tokens.move()
|
|
end
|
|
end
|
|
if tokens.error == Exit
|
|
o.value = (!value.nil? ? value : true)
|
|
end
|
|
end
|
|
return [o]
|
|
end
|
|
|
|
def parse_shorts(tokens, options)
|
|
token = tokens.move()
|
|
unless token.start_with?('-') && !token.start_with?('--')
|
|
raise RuntimeError
|
|
end
|
|
left = token[1..-1]
|
|
parsed = []
|
|
while left != ''
|
|
short, left = '-' + left[0], left[1..-1]
|
|
similar = options.select { |o| o.short == short }
|
|
if similar.count > 1
|
|
raise tokens.error, "#{short} is specified ambiguously #{similar.count} times"
|
|
elsif similar.count < 1
|
|
o = Option.new(short, nil, 0)
|
|
options << o
|
|
if tokens.error == Exit
|
|
o = Option.new(short, nil, 0, true)
|
|
end
|
|
else
|
|
s0 = similar[0]
|
|
o = Option.new(short, s0.long, s0.argcount, s0.value)
|
|
value = nil
|
|
if o.argcount != 0
|
|
if left == ''
|
|
if tokens.current().nil?
|
|
raise tokens.error, "#{short} requires argument"
|
|
end
|
|
value = tokens.move()
|
|
else
|
|
value = left
|
|
left = ''
|
|
end
|
|
end
|
|
if tokens.error == Exit
|
|
o.value = (!value.nil? ? value : true)
|
|
end
|
|
end
|
|
parsed << o
|
|
end
|
|
return parsed
|
|
end
|
|
|
|
|
|
def parse_pattern(source, options)
|
|
tokens = TokenStream.new(source.gsub(/([\[\]\(\)\|]|\.\.\.)/, ' \1 '), DocoptLanguageError)
|
|
|
|
result = parse_expr(tokens, options)
|
|
if tokens.current() != nil
|
|
raise tokens.error, "unexpected ending: #{tokens.join(" ")}"
|
|
end
|
|
return Required.new(*result)
|
|
end
|
|
|
|
|
|
def parse_expr(tokens, options)
|
|
seq = parse_seq(tokens, options)
|
|
if tokens.current() != '|'
|
|
return seq
|
|
end
|
|
result = seq.count > 1 ? [Required.new(*seq)] : seq
|
|
|
|
while tokens.current() == '|'
|
|
tokens.move()
|
|
seq = parse_seq(tokens, options)
|
|
result += seq.count > 1 ? [Required.new(*seq)] : seq
|
|
end
|
|
return result.count > 1 ? [Either.new(*result)] : result
|
|
end
|
|
|
|
def parse_seq(tokens, options)
|
|
result = []
|
|
stop = [nil, ']', ')', '|']
|
|
while !stop.include?(tokens.current)
|
|
atom = parse_atom(tokens, options)
|
|
if tokens.current() == '...'
|
|
atom = [OneOrMore.new(*atom)]
|
|
tokens.move()
|
|
end
|
|
result += atom
|
|
end
|
|
return result
|
|
end
|
|
|
|
def parse_atom(tokens, options)
|
|
token = tokens.current()
|
|
result = []
|
|
|
|
if ['(' , '['].include? token
|
|
tokens.move()
|
|
if token == '('
|
|
matching = ')'
|
|
pattern = Required
|
|
else
|
|
matching = ']'
|
|
pattern = Optional
|
|
end
|
|
result = pattern.new(*parse_expr(tokens, options))
|
|
if tokens.move() != matching
|
|
raise tokens.error, "unmatched '#{token}'"
|
|
end
|
|
return [result]
|
|
elsif token == 'options'
|
|
tokens.move()
|
|
return [AnyOptions.new]
|
|
elsif token.start_with?('--') and token != '--'
|
|
return parse_long(tokens, options)
|
|
elsif token.start_with?('-') and not ['-', '--'].include? token
|
|
return parse_shorts(tokens, options)
|
|
elsif token.start_with?('<') and token.end_with?('>') or (token.upcase == token && token.match(/[A-Z]/))
|
|
return [Argument.new(tokens.move())]
|
|
else
|
|
return [Command.new(tokens.move())]
|
|
end
|
|
end
|
|
|
|
def parse_argv(tokens, options, options_first=false)
|
|
parsed = []
|
|
while tokens.current() != nil
|
|
if tokens.current() == '--'
|
|
return parsed + tokens.map { |v| Argument.new(nil, v) }
|
|
elsif tokens.current().start_with?('--')
|
|
parsed += parse_long(tokens, options)
|
|
elsif tokens.current().start_with?('-') and tokens.current() != '-'
|
|
parsed += parse_shorts(tokens, options)
|
|
elsif options_first
|
|
return parsed + tokens.map { |v| Argument.new(nil, v) }
|
|
else
|
|
parsed << Argument.new(nil, tokens.move())
|
|
end
|
|
end
|
|
return parsed
|
|
end
|
|
|
|
def parse_defaults(doc)
|
|
split = doc.split(/^ *(<\S+?>|-\S+?)/).drop(1)
|
|
split = split.each_slice(2).reject { |pair| pair.count != 2 }.map { |s1, s2| s1 + s2 }
|
|
split.select { |s| s.start_with?('-') }.map { |s| Option.parse(s) }
|
|
end
|
|
|
|
def printable_usage(doc)
|
|
usage_split = doc.split(/([Uu][Ss][Aa][Gg][Ee]:)/)
|
|
if usage_split.count < 3
|
|
raise DocoptLanguageError, '"usage:" (case-insensitive) not found.'
|
|
end
|
|
if usage_split.count > 3
|
|
raise DocoptLanguageError, 'More than one "usage:" (case-insensitive).'
|
|
end
|
|
return usage_split.drop(1).join().split(/\n\s*\n/)[0].strip
|
|
end
|
|
|
|
def formal_usage(printable_usage)
|
|
pu = printable_usage.split().drop(1) # split and drop "usage:"
|
|
|
|
ret = []
|
|
for s in pu.drop(1)
|
|
if s == pu[0]
|
|
ret << ') | ('
|
|
else
|
|
ret << s
|
|
end
|
|
end
|
|
|
|
return '( ' + ret.join(' ') + ' )'
|
|
end
|
|
|
|
def dump_patterns(pattern, indent=0)
|
|
ws = " " * 4 * indent
|
|
out = ""
|
|
if pattern.class == Array
|
|
if pattern.count > 0
|
|
out << ws << "[\n"
|
|
for p in pattern
|
|
out << dump_patterns(p, indent+1).rstrip << "\n"
|
|
end
|
|
out << ws << "]\n"
|
|
else
|
|
out << ws << "[]\n"
|
|
end
|
|
|
|
elsif pattern.class.ancestors.include?(ParentPattern)
|
|
out << ws << pattern.class.name << "(\n"
|
|
for p in pattern.children
|
|
out << dump_patterns(p, indent+1).rstrip << "\n"
|
|
end
|
|
out << ws << ")\n"
|
|
|
|
else
|
|
out << ws << pattern.inspect
|
|
end
|
|
return out
|
|
end
|
|
|
|
def extras(help, version, options, doc)
|
|
if help and options.any? { |o| ['-h', '--help'].include?(o.name) && o.value }
|
|
Exit.set_usage(nil)
|
|
raise Exit, doc.strip
|
|
end
|
|
if version and options.any? { |o| o.name == '--version' && o.value }
|
|
Exit.set_usage(nil)
|
|
raise Exit, version
|
|
end
|
|
end
|
|
|
|
def docopt(doc, params={})
|
|
default = {:version => nil, :argv => nil, :help => true, :options_first => false}
|
|
params = default.merge(params)
|
|
params[:argv] = ARGV if !params[:argv]
|
|
|
|
Exit.set_usage(printable_usage(doc))
|
|
options = parse_defaults(doc)
|
|
pattern = parse_pattern(formal_usage(Exit.usage), options)
|
|
argv = parse_argv(TokenStream.new(params[:argv], Exit), options, params[:options_first])
|
|
pattern_options = pattern.flat(Option).uniq
|
|
pattern.flat(AnyOptions).each do |ao|
|
|
doc_options = parse_defaults(doc)
|
|
ao.children = doc_options.reject { |o| pattern_options.include?(o) }.uniq
|
|
end
|
|
extras(params[:help], params[:version], argv, doc)
|
|
|
|
matched, left, collected = pattern.fix().match(argv)
|
|
collected ||= []
|
|
|
|
if matched and (left.count == 0)
|
|
return Hash[(pattern.flat + collected).map { |a| [a.name, a.value] }]
|
|
end
|
|
raise Exit
|
|
end
|
|
end
|
|
end
|