Added a specs for flag & arg. As well as changed a few ways I as handling cli args and determining how they're to be passed to each flag
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
require "./cligen/exceptions"
|
||||||
require "./cligen/coercable"
|
require "./cligen/coercable"
|
||||||
require "./cligen/parsable"
|
require "./cligen/parsable"
|
||||||
require "./cligen/annotations"
|
require "./cligen/annotations"
|
||||||
|
|||||||
@@ -18,10 +18,25 @@ module CliGen
|
|||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.handle_command_raises(&block) : Nil
|
||||||
|
begin
|
||||||
|
block.call
|
||||||
|
rescue e : CliGen::RuntimeError
|
||||||
|
abort e.message
|
||||||
|
rescue e : CliGen::ConfigurationError
|
||||||
|
abort e.message
|
||||||
|
rescue e : CliGen::HelpRequestedError
|
||||||
|
puts e.message
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Convenience entry point; defaults to ARGV
|
# Convenience entry point; defaults to ARGV
|
||||||
def self.process(args : Array(String) = ARGV.to_a) : Nil
|
def self.process(args : Array(String) = ARGV.to_a) : Nil
|
||||||
generate if @@instance.nil?
|
generate if @@instance.nil?
|
||||||
|
handle_command_raises do
|
||||||
@@instance.not_nil!.process(args)
|
@@instance.not_nil!.process(args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+31
-7
@@ -24,36 +24,60 @@ module CliGen
|
|||||||
def initialize(@value, @index)
|
def initialize(@value, @index)
|
||||||
end
|
end
|
||||||
|
|
||||||
def flag? : Bool
|
def self.flag?(val : String) : Bool
|
||||||
if @value =~ CliGen::Regex::FLAG_REGEX
|
if val =~ CliGen::Regex::FLAG_REGEX
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def int? : Bool
|
def flag?(val : String = @value) : Bool
|
||||||
if @value =~ /^[[:digit:]]+$/
|
Arg.flag?(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.int?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::INT
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def float? : Bool
|
def int?(val : String = @value) : Bool
|
||||||
if @value =~ /^[[:digit:]]+(\.[[:digit:]]+)?$/
|
Arg.int?(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.uint?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::UINT
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def uint?(val : String = @value) : Bool
|
||||||
|
Arg.uint?(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.float?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::FLOAT
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def float?(val : String = @value) : Bool
|
||||||
|
Arg.float?(val)
|
||||||
|
end
|
||||||
|
|
||||||
# This serves as a trigger that tells the object that it has been processed
|
# This serves as a trigger that tells the object that it has been processed
|
||||||
#
|
#
|
||||||
# This will raise an exception if it is re-called after already having been
|
# This will raise an exception if it is re-called after already having been
|
||||||
# processed.
|
# processed.
|
||||||
def processed
|
def processed
|
||||||
raise "ERROR : CliGen::Arg(index: #{@index}, value: #{@value})#processed : This arg was re-processed" if @processed
|
raise CliGen::ArgReprocessedError.new("CliGen::Arg(index: #{@index}, value: #{@value})#processed : This arg was re-processed") if @processed
|
||||||
@processed = true
|
@processed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ module CliGen
|
|||||||
{% if def_setter %}
|
{% if def_setter %}
|
||||||
def {{variable.var}}= (value : {{type}})
|
def {{variable.var}}= (value : {{type}})
|
||||||
{% unless validation.nil? %}
|
{% unless validation.nil? %}
|
||||||
raise "ERROR : #{@type.name}##{@def.name} : Provided value #{value} is not passing validation" unless {{validation}}.call(value)
|
raise CliGen::ValidationError.new("#{@type.name}##{@def.name} : Provided value #{value} failed validation") unless {{validation}}.call(value)
|
||||||
{% end %}
|
{% end %}
|
||||||
@{{name}} = value
|
@{{name}} = value
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module CliGen
|
|||||||
flg.validate!
|
flg.validate!
|
||||||
@{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value!
|
@{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value!
|
||||||
else
|
else
|
||||||
raise "ERROR : {{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"?"
|
raise CliGen::FlagNotFoundError.new("{{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"")
|
||||||
end
|
end
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default_value %}
|
{% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default_value %}
|
||||||
|
|||||||
+33
-28
@@ -63,7 +63,7 @@ module CliGen
|
|||||||
message += "\nShort:\n%s" % short_duplicates.map { |f| "- #{f}" }.join("\n")
|
message += "\nShort:\n%s" % short_duplicates.map { |f| "- #{f}" }.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
raise error_buffer % [@name, message]
|
raise CliGen::DuplicateFlagError.new(error_buffer % [@name, message])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -75,6 +75,14 @@ module CliGen
|
|||||||
@flags.find{|f| f.short == short}
|
@flags.find{|f| f.short == short}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_flag_raises(&work)
|
||||||
|
begin
|
||||||
|
work.call
|
||||||
|
rescue e : CliGen::RuntimeError
|
||||||
|
abort e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def find_match(arg : String)
|
def find_match(arg : String)
|
||||||
if subcommand?(arg)
|
if subcommand?(arg)
|
||||||
return CliGen::MatchType::SubCommand
|
return CliGen::MatchType::SubCommand
|
||||||
@@ -172,7 +180,7 @@ module CliGen
|
|||||||
check_for_duplicates!(@flags)
|
check_for_duplicates!(@flags)
|
||||||
@commands.each(&.check!)
|
@commands.each(&.check!)
|
||||||
{% unless T == Nil %}
|
{% unless T == Nil %}
|
||||||
raise "ERROR : CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined" \
|
raise CliGen::MissingDispatchError.new("CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined") \
|
||||||
if subcommands.empty? && !{{T.has_method?(:main)}}
|
if subcommands.empty? && !{{T.has_method?(:main)}}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
@@ -199,35 +207,41 @@ module CliGen
|
|||||||
exit 0
|
exit 0
|
||||||
{% end %}
|
{% end %}
|
||||||
else
|
else
|
||||||
raise "ERROR : Couldn't find thing"
|
raise CliGen::UnknownCommandNodeError.new("CommandNode({{T}})#process : matched a BaseCommandNode that isn't a known CommandNode(T)")
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
passed_execution = true
|
passed_execution = true
|
||||||
|
|
||||||
when BaseFlag
|
when BaseFlag
|
||||||
|
# Ensuring we catch any exceptions to have them abort with the
|
||||||
|
# error message if caught
|
||||||
|
handle_flag_raises do
|
||||||
if match.requires_arg?
|
if match.requires_arg?
|
||||||
match.process(args.reject(&.processed?))
|
match.process(args.reject(&.processed?).take_while{|v| find_match(v.value) == CliGen::MatchType::NoMatch})
|
||||||
else
|
else
|
||||||
match.process
|
match.process
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
when MatchType::SubCommand
|
when MatchType::SubCommand
|
||||||
raise "ERROR : CommandNode({{T}})#process : Subcommand(#{matched_subcommand}) was already matched" if matched_subcommand
|
raise CliGen::InternalError.new("CommandNode({{T}})#process : subcommand '#{matched_subcommand}' was already matched — duplicate subcommand token") if matched_subcommand
|
||||||
matched_subcommand = arg.value
|
matched_subcommand = arg.value
|
||||||
|
|
||||||
when MatchType::Help
|
when MatchType::Help
|
||||||
abort help
|
raise CliGen::HelpRequestedError.new(help)
|
||||||
|
|
||||||
when MatchType::FlagWithArg
|
when MatchType::FlagWithArg
|
||||||
if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value)
|
if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value)
|
||||||
case flag_match = find_match(regex_match["flag"])
|
case flag_match = find_match(regex_match["flag"])
|
||||||
when BaseFlag
|
when BaseFlag
|
||||||
|
handle_flag_raises do
|
||||||
flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)])
|
flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)])
|
||||||
else
|
|
||||||
raise "ERROR : CommandNode(#{@name}).process : No flag matched '#{regex_match["flag"]}'"
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise "Oh good, you broke regex. How the hell did it match in find_match but not above? What the hell is going on"
|
raise CliGen::UnknownCommandNodeError.new("CommandNode(#{@name}).process : No flag matched '#{regex_match["flag"]}'")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise CliGen::RegexInvariantError.new("CommandNode(#{@name}).process : FLAG_WITH_ARG matched in find_match but failed on re-match — this is a framework bug")
|
||||||
end
|
end
|
||||||
|
|
||||||
when MatchType::FlagMultipleShort
|
when MatchType::FlagMultipleShort
|
||||||
@@ -236,43 +250,34 @@ module CliGen
|
|||||||
# If the next character in the series is a flag assume the remaining are flags as well.
|
# If the next character in the series is a flag assume the remaining are flags as well.
|
||||||
if flag?("-#{val[1]}")
|
if flag?("-#{val[1]}")
|
||||||
chars = val.chars
|
chars = val.chars
|
||||||
chars.map { |c| "-#{c}" }.each do |flag|
|
chars.map { |c| "-#{c}" }.each_with_index do |flag, index|
|
||||||
case match = find_match(flag)
|
case match = find_match(flag)
|
||||||
when BaseFlag
|
when BaseFlag
|
||||||
# If this is the last flag in the series let it process others
|
# If this is the last flag in the series let it process others
|
||||||
if flag == "-#{chars.last}"
|
handle_flag_raises do
|
||||||
|
## If this is the last flag provided in the clump do the thing
|
||||||
|
if index == chars.size - 1
|
||||||
if match.requires_arg?
|
if match.requires_arg?
|
||||||
match.process(args.reject(&.processed?))
|
match.process(args.reject(&.processed?).take_while{|v| find_match(v.value) == CliGen::MatchType::NoMatch})
|
||||||
else
|
else
|
||||||
match.process
|
match.process
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
abort "#{CliGen::APPNAME}: cannot bundle flag that requires an argument: #{flag}" if match.requires_arg?
|
raise CliGen::FlagBundleError.new("#{CliGen::APPNAME}: cannot bundle '#{flag}' — it requires an argument") if match.requires_arg?
|
||||||
match.process
|
match.process
|
||||||
end
|
end
|
||||||
|
end
|
||||||
when MatchType::NoMatch
|
when MatchType::NoMatch
|
||||||
abort "ERROR : CommandNode(#{@name}).process : No flag match for '#{flag}'"
|
raise CliGen::UnknownFlagError.new("#{CliGen::APPNAME}: unknown flag '#{flag}'")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# In this case we assume the characters following the flag is the argument
|
# In this case we assume the characters following the flag is the argument
|
||||||
flg = "-#{val[0]}"
|
raise CliGen::FlagArgumentError.new("#{CliGen::APPNAME}: inline flag arguments are not supported — did you mean '-#{val[0]} #{val[1..]}'?")
|
||||||
argument = Arg.new(value: val[1..], index: 0)
|
|
||||||
case match = find_match(flg)
|
|
||||||
when BaseFlag
|
|
||||||
if match.requires_arg?
|
|
||||||
match.process([argument])
|
|
||||||
else
|
|
||||||
abort "ERROR : CommandNode(#{@name}).process : Unknown characters proceeding a boolean flag(#{flg}) : #{val[1..]}"
|
|
||||||
end
|
|
||||||
when MatchType::NoMatch
|
|
||||||
abort "ERROR : CommandNode(#{@name}).process : No flag match for '#{arg.value}'"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
when MatchType::NoMatch
|
when MatchType::NoMatch
|
||||||
puts "ERROR : CommandNode(#{@name}).process : No command, subcommand or flag match for '#{arg.value}'"
|
raise CliGen::HelpRequestedError.new("#{CliGen::APPNAME}: unknown token '#{arg.value}'\n\n#{help}")
|
||||||
abort help
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
module CliGen
|
||||||
|
# Base for all CliGen exceptions
|
||||||
|
class Error < Exception; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Internal errors — framework invariant violations, should never reach users
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class InternalError < Error; end
|
||||||
|
|
||||||
|
# Arg#processed was called a second time on the same Arg
|
||||||
|
class ArgReprocessedError < InternalError; end
|
||||||
|
|
||||||
|
# A token matched FLAG_WITH_ARG in find_match but the regex failed on re-match
|
||||||
|
class RegexInvariantError < InternalError; end
|
||||||
|
|
||||||
|
# A BaseCommandNode was matched but couldn't be cast to any known CommandNode(T)
|
||||||
|
class UnknownCommandNodeError < InternalError; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Configuration errors — shard consumer wired something up incorrectly
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ConfigurationError < Error; end
|
||||||
|
|
||||||
|
# -h or --help was used as a flag short/long (reserved for internal help)
|
||||||
|
class ReservedFlagError < ConfigurationError; end
|
||||||
|
|
||||||
|
# Duplicate short or long flags detected during check!
|
||||||
|
class DuplicateFlagError < ConfigurationError; end
|
||||||
|
|
||||||
|
# A CommandNode(T) has no subcommands and no #main defined
|
||||||
|
class MissingDispatchError < ConfigurationError; end
|
||||||
|
|
||||||
|
# No flag was found in the handler for a Command ivar during initialize
|
||||||
|
class FlagNotFoundError < ConfigurationError; end
|
||||||
|
|
||||||
|
# A flag that requires an argument was processed with an empty argv
|
||||||
|
class FlagMissingArgumentError < ConfigurationError; end
|
||||||
|
|
||||||
|
# A Parsable type's parse_args did not mark any args as processed
|
||||||
|
class ParseableInvariantError < ConfigurationError; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Runtime errors — bad user input at the CLI level
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RuntimeError < Error; end
|
||||||
|
|
||||||
|
# A required flag was not provided and has no env var or default to fall back on
|
||||||
|
class MissingRequiredFlagError < RuntimeError; end
|
||||||
|
|
||||||
|
# A setter's validation proc rejected the provided value
|
||||||
|
class ValidationError < RuntimeError; end
|
||||||
|
|
||||||
|
# A flag token was provided where a value argument was expected
|
||||||
|
class FlagArgumentError < RuntimeError; end
|
||||||
|
|
||||||
|
# A provided value doesn't satisfy type or format requirements (wrong type, bad format, invalid bool/date string)
|
||||||
|
class InvalidFlagValueError < RuntimeError; end
|
||||||
|
|
||||||
|
# A provided value is not in the flag's allowed options list
|
||||||
|
class InvalidOptionError < RuntimeError; end
|
||||||
|
|
||||||
|
# An unrecognised flag token was encountered during parsing
|
||||||
|
class UnknownFlagError < RuntimeError; end
|
||||||
|
|
||||||
|
# A flag that requires an argument was included in a short-flag bundle
|
||||||
|
class FlagBundleError < RuntimeError; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Help signal — not an error; exit 0 after printing
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Raised when -h/--help is matched; carries the rendered help string
|
||||||
|
class HelpRequestedError < Error; end
|
||||||
|
end
|
||||||
+102
-40
@@ -67,10 +67,10 @@ module CliGen
|
|||||||
@on_match : Proc(Nil)? = nil,
|
@on_match : Proc(Nil)? = nil,
|
||||||
@format : ::Regex? = nil
|
@format : ::Regex? = nil
|
||||||
)
|
)
|
||||||
{% unless T.has_method? :to_s %}
|
{% unless T.class.has_method? :to_s %}
|
||||||
{% raise "ERROR : Flag(#{T}) : Error your flag type must have a to_s method" %}
|
{% raise "ERROR : Flag(#{T}) : Error your flag type must have a to_s method" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% if T <= Array %}
|
{% if T < Array %}
|
||||||
{% elem = T.type_vars.first %}
|
{% elem = T.type_vars.first %}
|
||||||
{% raise "ERROR : Flag(#{T}) : Error your flag subtype #{elem} must have a to_s method" unless elem.class.has_method? :to_s %}
|
{% raise "ERROR : Flag(#{T}) : Error your flag subtype #{elem} must have a to_s method" unless elem.class.has_method? :to_s %}
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -99,14 +99,14 @@ module CliGen
|
|||||||
|
|
||||||
def process(argv : Array(Arg) = [] of Arg) : Nil
|
def process(argv : Array(Arg) = [] of Arg) : Nil
|
||||||
if requires_arg?
|
if requires_arg?
|
||||||
raise "ERROR : Flag(#{T}, long: #{@long_key}) : Array requires an argument but provided array is empty" if argv.empty?
|
raise CliGen::FlagMissingArgumentError.new("Flag(#{T}, long: #{@long_key}) : requires an argument but provided array is empty") if argv.empty?
|
||||||
abort "ERROR : Flag(#{T}, long: #{@long_key}) : Provided argument was a flag (#{argv.first.value})" if argv.first.flag?
|
raise CliGen::FlagArgumentError.new("Flag(#{T}, long: #{@long_key}) : a flag token was provided where a value was expected (got: #{argv.first.value})") if argv.first.flag?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
{% if T == Bool %}
|
{% if T == Bool %}
|
||||||
@value = true
|
@value = true
|
||||||
{% elsif T <= Array %}
|
{% elsif T < Array %}
|
||||||
{% raise "ERROR : Flag(#{T}) : You cannot define multiple types of array entries" if T.type_vars.size > 1 %}
|
{% raise "ERROR : Flag(#{T}) : You cannot define multiple types of array entries" if T.type_vars.size > 1 %}
|
||||||
{% elem = T.type_vars.first %}
|
{% elem = T.type_vars.first %}
|
||||||
argv.each do |arg|
|
argv.each do |arg|
|
||||||
@@ -119,27 +119,54 @@ module CliGen
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
{% if elem == Int32 %}
|
{% if elem < Int %}
|
||||||
|
{% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
if arg.value.includes?(@delimiter)
|
if arg.value.includes?(@delimiter)
|
||||||
@value = (@value || [] of Int32) + arg.value.split(@delimiter).map(&.to_i32)
|
@value = (@value || T.new) + arg.value.split(@delimiter).map do |val|
|
||||||
|
val = val.strip
|
||||||
|
unless arg.{{int_case}}(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key}) : Provided arguemnt(#{arg.value}) was not an integer" unless arg.int?
|
unless arg.{{int_case}}
|
||||||
(@value ||= [] of Int32) << arg.value.to_i
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{arg.value}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
(@value ||= T.new) << {{elem}}.new(arg.value)
|
||||||
|
end
|
||||||
|
{% elsif elem < Float %}
|
||||||
|
if arg.value.includes?(@delimiter)
|
||||||
|
@value = (@value || T.new) + arg.value.split(@delimiter).map do |val|
|
||||||
|
val = val.strip
|
||||||
|
unless CliGen::Arg.float?(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
unless arg.float?
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{arg.value}' is not a float")
|
||||||
|
end
|
||||||
|
(@value ||= T.new) << {{elem}}.new(arg.value)
|
||||||
end
|
end
|
||||||
{% elsif elem == String %}
|
{% elsif elem == String %}
|
||||||
if arg.value.includes?(@delimiter)
|
if arg.value.includes?(@delimiter)
|
||||||
@value = (@value || [] of String) + arg.value.split(@delimiter).map{|v|
|
@value = (@value || [] of String) + arg.value.split(@delimiter).map { |v|
|
||||||
unless @format.nil?
|
unless @format.nil?
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key} ) : Provided arguemnt(#{v}) did not match a valid fromat \"#{@format.not_nil!.source}\"" unless v =~ @format
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{v}' does not match required format /#{@format.not_nil!.source}/") unless v =~ @format
|
||||||
end
|
end
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
if ! @format.nil? && arg.value !~ @format
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{arg.value}' does not match required format /#{@format.not_nil!.source}/")
|
||||||
|
end
|
||||||
(@value ||= [] of String) << arg.value
|
(@value ||= [] of String) << arg.value
|
||||||
end
|
end
|
||||||
{% elsif elem.class < CliGen::Coercable %}
|
{% elsif elem.class < CliGen::Coercable %}
|
||||||
if arg.value.includes?(@delimiter)
|
if arg.value.includes?(@delimiter)
|
||||||
@value = (@value || [] of {{elem}}) + arg.value.split(@delimiter).map{|i| {{elem}}.coerce(i)}
|
@value = (@value || [] of {{elem}}) + arg.value.split(@delimiter).map { |i| {{elem}}.coerce(i) }
|
||||||
else
|
else
|
||||||
@value = (@value || [] of {{elem}}) + [({{elem}}.coerce(arg.value))]
|
@value = (@value || [] of {{elem}}) + [({{elem}}.coerce(arg.value))]
|
||||||
end
|
end
|
||||||
@@ -148,16 +175,25 @@ module CliGen
|
|||||||
{% end %}
|
{% end %}
|
||||||
arg.processed
|
arg.processed
|
||||||
end
|
end
|
||||||
{% elsif T == Int32 %}
|
{% elsif T < Int %}
|
||||||
abort "ERROR : Flag(#{T}, long: #{@long_key}) : #{argv.first.value} is not an int" unless argv.first.int?
|
{% int_case = T.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
@value = argv.first.value.to_i
|
unless argv.first.{{int_case}}
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag(#{T}, long: #{@long_key}) : '#{argv.first.value}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
@value = T.new(argv.first.value)
|
||||||
|
argv.first.processed
|
||||||
|
{% elsif T < Float %}
|
||||||
|
unless argv.first.float?
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag(#{T}, long: #{@long_key}) : '#{argv.first.value}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
@value = T.new(argv.first.value)
|
||||||
argv.first.processed
|
argv.first.processed
|
||||||
{% elsif T == Time %}
|
{% elsif T == Time %}
|
||||||
@value = parse_time(argv.first.value)
|
@value = parse_time(argv.first.value)
|
||||||
argv.first.processed
|
argv.first.processed
|
||||||
{% elsif T == String %} # String
|
{% elsif T == String %} # String
|
||||||
unless @format.nil?
|
if ! @format.nil? && argv.first.value !~ @format
|
||||||
abort "ERROR : Flag(#{T}, long: #{@long_key}) : #{argv.first.value} does not match a correct format #{@format.not_nil!.source}" unless argv.first.value =~ @format
|
raise CliGen::InvalidFlagValueError.new("Flag(#{T}, long: #{@long_key}) : '#{argv.first.value}' does not match required format /#{@format.not_nil!.source}/")
|
||||||
end
|
end
|
||||||
|
|
||||||
@value = argv.first.value
|
@value = argv.first.value
|
||||||
@@ -167,7 +203,7 @@ module CliGen
|
|||||||
@value = T.parse_args(argv)
|
@value = T.parse_args(argv)
|
||||||
post_processed = argv.select(&.processed?)
|
post_processed = argv.select(&.processed?)
|
||||||
if processed == post_processed
|
if processed == post_processed
|
||||||
raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Your {{T}}#process_args did not mark processed args as processed. Please check your code"
|
raise CliGen::ParseableInvariantError.new("Flag({{T}}, long: #{@long_key})#process : {{T}}#parse_args did not mark any args as processed")
|
||||||
end
|
end
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "ERROR : Flag({{T}}#process : Generic Type #{T} is not supported. To add support you must extend with CliGen::Parsable & implement the class method" %}
|
{% raise "ERROR : Flag({{T}}#process : Generic Type #{T} is not supported. To add support you must extend with CliGen::Parsable & implement the class method" %}
|
||||||
@@ -189,7 +225,7 @@ module CliGen
|
|||||||
|
|
||||||
v ||= @default
|
v ||= @default
|
||||||
|
|
||||||
raise "#{CliGen::APPNAME}: required flag #{@long_key} was not provided" if v.nil?
|
raise CliGen::MissingRequiredFlagError.new("#{CliGen::APPNAME}: required flag #{@long_key} was not provided") if v.nil?
|
||||||
v.not_nil!
|
v.not_nil!
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -216,48 +252,66 @@ module CliGen
|
|||||||
if opts = @options
|
if opts = @options
|
||||||
{% if T < Array %}
|
{% if T < Array %}
|
||||||
v.each do |v2|
|
v.each do |v2|
|
||||||
abort "#{CliGen::APPNAME}: '#{v2}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.first.includes?(v2)
|
raise CliGen::InvalidOptionError.new("#{CliGen::APPNAME}: '#{v2}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})") unless opts.first.includes?(v2)
|
||||||
end
|
end
|
||||||
{% else %}
|
{% else %}
|
||||||
abort "#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.includes?(v)
|
raise CliGen::InvalidOptionError.new("#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})") unless opts.includes?(v)
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
if check = @validate
|
if check = @validate
|
||||||
abort "#{CliGen::APPNAME}: validation failed for #{@long_key} when testing #{v}" unless check.call(v)
|
raise CliGen::ValidationError.new("#{CliGen::APPNAME}: validation failed for #{@long_key} (got: #{v})") unless check.call(v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check! : Nil
|
def check! : Nil
|
||||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : -h is reserved for internal help usage" if @short == "-h"
|
raise CliGen::ReservedFlagError.new("Flag({{T}}, long: #{@long})#check! : -h is reserved for internal help") if @short == "-h"
|
||||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : --help is reserved for internal help usage" if @long_key == "--help"
|
raise CliGen::ReservedFlagError.new("Flag({{T}}, long: #{@long})#check! : --help is reserved for internal help") if @long_key == "--help"
|
||||||
end
|
end
|
||||||
|
|
||||||
private def coerce(raw : String) : T
|
private def coerce(raw : String) : T
|
||||||
{% if T == Bool %}
|
{% if T == Bool %}
|
||||||
case raw
|
case raw
|
||||||
when "t","true","1"
|
when /^(t|true|y|yes|1)$/i
|
||||||
true
|
true
|
||||||
when "f","false","0"
|
when /^(f|false|n|no|0)$/i
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{raw} is not a valid boolean identifier"
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid boolean (expected: t/f/true/false/y/n/yes/no/1/0)")
|
||||||
end
|
end
|
||||||
{% elsif T == Int32 %}
|
{% elsif T < Int %}
|
||||||
raw.to_i
|
{% int_case = T.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
unless CliGen::Arg.{{int_case}}(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
T.new(raw)
|
||||||
|
{% elsif T < Float %}
|
||||||
|
unless CliGen::Arg.float?(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
T.new(raw)
|
||||||
{% elsif T == Time %}
|
{% elsif T == Time %}
|
||||||
parse_time(raw)
|
parse_time(raw)
|
||||||
{% elsif T <= Array %}
|
{% elsif T < Array %}
|
||||||
if raw.includes?(@delimiter)
|
if raw.includes?(@delimiter)
|
||||||
{% elem = T.type_vars.first %}
|
{% elem = T.type_vars.first %}
|
||||||
raw.split(@delimiter).map do |val|
|
raw.split(@delimiter).map do |val|
|
||||||
unless @format.nil?
|
unless @format.nil?
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{val} does not match a valid format" unless val =~ @format
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' does not match required format /#{@format.not_nil!.source}/") unless val =~ @format
|
||||||
end
|
end
|
||||||
|
|
||||||
{% if elem == Int32 %}
|
{% if elem < Int %}
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{val} is not an integer" unless val =~ /^[[:digit:]]+$/
|
val = val.strip
|
||||||
val.to_i
|
{% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
unless CliGen::Arg.{{int_case}}(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
|
{% elsif elem < Float %}
|
||||||
|
unless CliGen::Arg.float?(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
{% elsif elem == String %}
|
{% elsif elem == String %}
|
||||||
val
|
val
|
||||||
{% elsif elem.class < CliGen::Coercable %}
|
{% elsif elem.class < CliGen::Coercable %}
|
||||||
@@ -267,12 +321,20 @@ module CliGen
|
|||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
unless @format.nil?
|
if ! @format.nil? && raw !~ @format
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{raw} does not match a valid format" unless raw =~ @format
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' does not match required format /#{@format.not_nil!.source}/")
|
||||||
end
|
end
|
||||||
{% if elem == Int32 %}
|
{% if elem < Int %}
|
||||||
abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{raw} is not an integer" unless raw =~ /^[[:digit:]]+$/
|
{% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
[ raw.to_i ]
|
unless CliGen::Arg.{{int_case}}(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
[ {{elem}}.new(raw) ]
|
||||||
|
{% elsif elem < Float %}
|
||||||
|
unless CliGen::Arg.float?(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
[ {{elem}}.new(raw) ]
|
||||||
{% elsif elem == String %}
|
{% elsif elem == String %}
|
||||||
[ raw ]
|
[ raw ]
|
||||||
{% elsif elem.class < CliGen::Coercable %}
|
{% elsif elem.class < CliGen::Coercable %}
|
||||||
@@ -295,7 +357,7 @@ module CliGen
|
|||||||
CliGen::Regex::INPUT_DATETIME_REGEX,
|
CliGen::Regex::INPUT_DATETIME_REGEX,
|
||||||
CliGen::Regex::INPUT_DATE_REGEX
|
CliGen::Regex::INPUT_DATE_REGEX
|
||||||
]
|
]
|
||||||
abort "#{CliGen::APPNAME}: invalid date/time format '#{raw}' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)" unless formats.any? { |f| raw.match(f) }
|
raise CliGen::InvalidFlagValueError.new("#{CliGen::APPNAME}: invalid date/time format '#{raw}' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)") unless formats.any? { |f| raw.match(f) }
|
||||||
if raw.match(formats[0])
|
if raw.match(formats[0])
|
||||||
Time.parse_local(raw, CliGen::Format::INPUT_DATETIME_FORMAT)
|
Time.parse_local(raw, CliGen::Format::INPUT_DATETIME_FORMAT)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ module CliGen::Regex
|
|||||||
|
|
||||||
INPUT_DATETIME_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/
|
INPUT_DATETIME_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/
|
||||||
INPUT_DATE_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
INPUT_DATE_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
||||||
|
|
||||||
|
FLOAT = /^[-+]?[[:digit:]]+(\.[[:digit:]]+)?$/
|
||||||
|
UINT = /^[[:digit:]]+$/
|
||||||
|
INT = /^[-+]?[[:digit:]]+$/
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user