Last active
January 24, 2019 14:44
-
-
Save paulsonkoly/6d92b32bdfa55bd7d6be7a96e83f93f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'active_support/inflector' | |
# Argument parsing | |
# My solution to http://codingdojo.org/kata/Args/ | |
module Args | |
# Error occured during argument parsing. {StandardError#message} should give | |
# a description about the error. | |
class ArgsError < StandardError; end | |
# An argument schema. | |
# | |
# Describes what arguments we take and what properties each argument have. | |
# | |
# @example | |
# schema = Args::Schema.new do |s| | |
# s.flag 'x' | |
# s.integer 'p' | |
# s.string 'h', default: 'blah' | |
# s.integer_list 't', default: [13] | |
# end | |
# | |
# schema | |
# # => #<Args::Schema:0x0000556b0562fe88 | |
# # @items=[ | |
# # #<Args::Schema::Item::Flag:0x0000556b0562fb18 @letter="x">, | |
# # #<Args::Schema::Item::Integer:0x0000556b0562f898 @letter="p", | |
# # @default=0>, | |
# # #<Args::Schema::Item::String:0x0000556b0562f5f0 @letter="h", | |
# # @default="blah">, | |
# # #<Args::Schema::Item::IntegerList:0x0000556b0562f168 @letter="t", | |
# # @default=[13]>]> | |
class Schema | |
# A single argument that may or may not have sub arguments | |
class Item | |
# @param letter [::String] the selector for the argument (the letter after | |
# the -) | |
def initialize(letter) | |
@letter = letter | |
end | |
# @!attribute [r] letter | |
# @return [::String] the letter for the argument | |
attr_reader :letter | |
alias_method :key, :letter | |
# Does our {letter} match the given letter | |
# @param letter [::String] the letter to match against | |
def match?(letter) | |
@letter == letter | |
end | |
# Single flag type {Item} | |
# | |
# Does not have any additional sub arguments. When present it's true when | |
# not present it's false. Defaults to false. | |
class Flag < Item | |
# @return [Boolean] false | |
def default; false; end | |
# @return [Boolean] true | |
def value(_token_list); true; end | |
# @return [::Integer] 1 | |
def token_count; 1; end | |
end | |
# Abstract class for {Item} with a single argument | |
# | |
# Sub classes have to implement #convert. Use {default} to define the | |
# default value. | |
class ArgumentItem < Item | |
# @!attribute [r] default | |
# @return [Object] the default value of the argument | |
attr_reader :default | |
# Read the value from the token_list | |
# @param token_list [[::String]] list of tokens produced by the parser | |
# @raise ArgsError if token_list doesn't have enough items | |
def value(token_list) | |
if token_list.count < token_count | |
raise ArgsError, "Premature end of tokens #{token_list}" | |
end | |
convert(token_list[1]) | |
end | |
# Count of tokens from a token_list this item is responsible for | |
# @return [::Integer] 2 | |
def token_count; 2; end | |
# Defines the initialize for the sub-classes using the default value | |
# provided | |
# @param value [Object] default value for the argument | |
def self.default(value) | |
define_method(:initialize) do |letter, default: value| | |
super(letter) | |
@default = default | |
end | |
end | |
end | |
# Integer argument with a default value of 0 | |
class Integer < ArgumentItem | |
# @!method initialize(letter, default: 0) | |
# @param default [::Integer] default value when the argument is not | |
# present | |
default 0 | |
# converts token to integer | |
# @param token [::String] token from the parser | |
# @return [::Integer] token converted | |
def convert(token) | |
token.to_i | |
end | |
end | |
# String argument with a default value of '' | |
class String < ArgumentItem | |
# @!method initialize(letter, default: '') | |
# @param default [::String] default value when the argument is not | |
# present | |
default '' | |
# converts token to string | |
# @param token [::String] token from the parser | |
# @return [::String] token unchanged | |
def convert(token) | |
token | |
end | |
end | |
# Integer list argument with a default value of [] | |
class IntegerList < ArgumentItem | |
# @!method initialize(letter, default: []) | |
# @param default [[::Integer]] default value when the argument is not | |
# present | |
default [] | |
# converts token to an integer list | |
# @param token [::String] token from the parser | |
# @return [[::Integer]] token converted to a list of Integers | |
def convert(token) | |
token.split(',').map(&:to_i) | |
end | |
end | |
end | |
def initialize | |
@items = [] | |
yield(self) if block_given? | |
end | |
# @!method flag(letter) | |
# Constructs an {Item::Flag} and registers it in self | |
# @see Item::Flag#initialize | |
# @!method string(letter, default: '') | |
# Constructs an {Item::String} and registers it in self | |
# @see Item::String#initialize | |
# @!method integer(letter, default: 0) | |
# Constructs an {Item::Integer} and registers it in self | |
# @see Item::Integer#initialize | |
# @!method integer_list(letter, default: []) | |
# Constructs an {Item::IntegerList} and registers it in self | |
# @see Item::IntegerList#initialize | |
%i[flag integer string integer_list].each do |sym| | |
define_method(sym) do |*args| | |
klass = Item.const_get(ActiveSupport::Inflector.classify(sym)) | |
@items << klass.new(*args) | |
end | |
end | |
# returns the matching {Item} registered for the prefix of token_list | |
# @param token_list [[::String]] the tokens to match | |
# @return [Item|nil] Item that matches | |
# @raise ArgsError if token_list doesn't start with a - and letter token | |
def match_token_list(token_list) | |
token = token_list[0] | |
unless token.match?(/\A-\w\z/) | |
raise ArgsError, "- and letter expected, got #{token}" | |
end | |
match(token[1]).tap do |item| | |
raise ArgsError, 'No matching item' if item.nil? | |
end | |
end | |
# returns matching {Item} registered for a letter | |
def match(letter) | |
@items.find { |item| item.match?(letter) } | |
end | |
end | |
end | |
module Args | |
# Command line arguments parser | |
# @see Schema example on constructing schemas | |
# @example | |
# parser = Args::Parser.new(schema) | |
# parser.parse('-x -p 10 -t 1,2,2') # => nil | |
# parser[?x] # => true | |
# parser[?p] # => 10 | |
# parser[?h] # => "blah" | |
# parser[?t] # => [1, 2, 2] | |
# parser.errors # => [] | |
class Parser | |
# @param schema [Schema] A valid schema with {Args::Schema::Item} items | |
# registered. | |
def initialize(schema) | |
@schema = schema | |
reset | |
end | |
# @!attribute [r] errors | |
# @return [[String]] List of errors that occured during parse | |
attr_reader :errors | |
# Parses the string. You can retrospect the result of the parse with | |
# {errors}, and {[]} | |
def parse(string) | |
reset | |
@token_list = tokenize(string) | |
parse_schema until @token_list.empty? | |
end | |
# the value of the given {Schema::Item} that is registered in the schema | |
# with letter | |
# @return [Object] The value (either default or if present converted from | |
# the input string). | |
def [](letter) | |
@values.fetch(letter, @schema.match(letter).default) | |
rescue NoMethodError | |
raise ArgsError, 'no such flag in schema' | |
end | |
private | |
def reset | |
@values = {} | |
@errors = [] | |
end | |
def tokenize(string) | |
@token_list = string.split(' ') # => ["-x", "-p", "10", "-t", "1,2,2"] | |
end | |
def parse_schema | |
item = @schema.match_token_list(@token_list) | |
@values[item.key] = item.value(@token_list) | |
@token_list.shift(item.token_count) | |
rescue ArgsError => error | |
@errors << error.message | |
@token_list.shift | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment