From 78510bfffeff5c03097ff74079605839fcc68c87 Mon Sep 17 00:00:00 2001 From: "Kazushi (Jam) Marukawa" Date: Fri, 18 Aug 2017 08:15:25 +0900 Subject: [PATCH] Add docopt.rb --- lib/docopt.LICENSE | 22 ++ lib/docopt.rb | 670 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 692 insertions(+) create mode 100644 lib/docopt.LICENSE create mode 100644 lib/docopt.rb diff --git a/lib/docopt.LICENSE b/lib/docopt.LICENSE new file mode 100644 index 000000000..5fc4dd7db --- /dev/null +++ b/lib/docopt.LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Vladimir Keleshev + Blake Williams + Alex Speller + Nima Johari + +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. diff --git a/lib/docopt.rb b/lib/docopt.rb new file mode 100644 index 000000000..4afd5e781 --- /dev/null +++ b/lib/docopt.rb @@ -0,0 +1,670 @@ +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